diff --git a/build.gradle b/build.gradle index c2396c6..a3f2f57 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,11 @@ dependencies { mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + implementation( + group: 'ca.spottedleaf', + name: 'concurrentutil', + version: '0.0.1-SNAPSHOT' + ) shadow( group: 'ca.spottedleaf', name: 'concurrentutil', diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java b/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java new file mode 100644 index 0000000..5188ed9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java @@ -0,0 +1,20 @@ +package ca.spottedleaf.moonrise.common.config; + +public final class PlaceholderConfig { + + public static double chunkLoadingBasic$playerMaxChunkSendRate = -1.0; + public static double chunkLoadingBasic$playerMaxChunkLoadRate = -1.0; + public static double chunkLoadingBasic$playerMaxChunkGenerateRate = -1.0; + + public static boolean chunkLoadingAdvanced$autoConfigSendDistance = true; + public static int chunkLoadingAdvanced$playerMaxConcurrentChunkLoads = 0; + public static int chunkLoadingAdvanced$playerMaxConcurrentChunkGenerates = 0; + + public static int autoSaveInterval = 60 * 5 * 20; // 5 mins + public static int maxAutoSaveChunksPerTick = 12; + + public static int chunkSystemIOThreads = -1; + public static int chunkSystemThreads = -1; + public static String chunkSystemGenParallelism = "default"; + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java new file mode 100644 index 0000000..ba68998 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java @@ -0,0 +1,129 @@ +package ca.spottedleaf.moonrise.common.list; + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import net.minecraft.world.entity.Entity; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +// list with O(1) remove & contains + +/** + * @author Spottedleaf + */ +public final class EntityList implements Iterable { + + protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); + { + this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + + protected static final Entity[] EMPTY_LIST = new Entity[0]; + + protected Entity[] entities = EMPTY_LIST; + protected int count; + + public int size() { + return this.count; + } + + public boolean contains(final Entity entity) { + return this.entityToIndex.containsKey(entity.getId()); + } + + public boolean remove(final Entity entity) { + final int index = this.entityToIndex.remove(entity.getId()); + if (index == Integer.MIN_VALUE) { + return false; + } + + // move the entity at the end to this index + final int endIndex = --this.count; + final Entity end = this.entities[endIndex]; + if (index != endIndex) { + // not empty after this call + this.entityToIndex.put(end.getId(), index); // update index + } + this.entities[index] = end; + this.entities[endIndex] = null; + + return true; + } + + public boolean add(final Entity entity) { + final int count = this.count; + final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count); + + if (currIndex != Integer.MIN_VALUE) { + return false; // already in this list + } + + Entity[] list = this.entities; + + if (list.length == count) { + // resize required + list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative + } + + list[count] = entity; + this.count = count + 1; + + return true; + } + + public Entity getChecked(final int index) { + if (index < 0 || index >= this.count) { + throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); + } + return this.entities[index]; + } + + public Entity getUnchecked(final int index) { + return this.entities[index]; + } + + public Entity[] getRawData() { + return this.entities; + } + + public void clear() { + this.entityToIndex.clear(); + Arrays.fill(this.entities, 0, this.count, null); + this.count = 0; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + Entity lastRet; + int current; + + @Override + public boolean hasNext() { + return this.current < EntityList.this.count; + } + + @Override + public Entity next() { + if (this.current >= EntityList.this.count) { + throw new NoSuchElementException(); + } + return this.lastRet = EntityList.this.entities[this.current++]; + } + + @Override + public void remove() { + final Entity lastRet = this.lastRet; + + if (lastRet == null) { + throw new IllegalStateException(); + } + this.lastRet = null; + + EntityList.this.remove(lastRet); + --this.current; + } + }; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java new file mode 100644 index 0000000..aa86882 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java @@ -0,0 +1,48 @@ +package ca.spottedleaf.moonrise.common.map; + +import it.unimi.dsi.fastutil.longs.Long2BooleanFunction; +import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap; + +public final class SynchronisedLong2BooleanMap { + private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap(); + private final int limit; + + public SynchronisedLong2BooleanMap(final int limit) { + this.limit = limit; + } + + // must hold lock on map + private void purgeEntries() { + while (this.map.size() > this.limit) { + this.map.removeLastBoolean(); + } + } + + public boolean remove(final long key) { + synchronized (this.map) { + return this.map.remove(key); + } + } + + // note: + public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) { + synchronized (this.map) { + if (this.map.containsKey(key)) { + return this.map.getAndMoveToFirst(key); + } + } + + final boolean put = ifAbsent.get(key); + + synchronized (this.map) { + if (this.map.containsKey(key)) { + return this.map.getAndMoveToFirst(key); + } + this.map.putAndMoveToFirst(key, put); + + this.purgeEntries(); + + return put; + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java new file mode 100644 index 0000000..dbb51af --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java @@ -0,0 +1,47 @@ +package ca.spottedleaf.moonrise.common.map; + +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import java.util.function.BiFunction; + +public final class SynchronisedLong2ObjectMap { + private final Long2ObjectLinkedOpenHashMap map = new Long2ObjectLinkedOpenHashMap<>(); + private final int limit; + + public SynchronisedLong2ObjectMap(final int limit) { + this.limit = limit; + } + + // must hold lock on map + private void purgeEntries() { + while (this.map.size() > this.limit) { + this.map.removeLast(); + } + } + + public V get(final long key) { + synchronized (this.map) { + return this.map.getAndMoveToFirst(key); + } + } + + public V put(final long key, final V value) { + synchronized (this.map) { + final V ret = this.map.putAndMoveToFirst(key, value); + this.purgeEntries(); + return ret; + } + } + + public V compute(final long key, final BiFunction remappingFunction) { + synchronized (this.map) { + // first, compute the value - if one is added, it will be at the last entry + this.map.compute(key, remappingFunction); + // move the entry to first, just in case it was added at last + final V ret = this.map.getAndMoveToFirst(key); + // now purge the last entries + this.purgeEntries(); + + return ret; + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java new file mode 100644 index 0000000..9c0eff9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java @@ -0,0 +1,75 @@ +package ca.spottedleaf.moonrise.common.misc; + +public final class AllocatingRateLimiter { + + // max difference granularity in ns + private final long maxGranularity; + + private double allocation = 0.0; + private long lastAllocationUpdate; + // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error) + // over any time period using take regardless of the number of take calls or the intervals between the take calls + // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3 + private double takeCarry = 0.0; + private long lastTakeUpdate; + + public AllocatingRateLimiter(final long maxGranularity) { + this.maxGranularity = maxGranularity; + } + + public void reset(final long time) { + this.allocation = 0.0; + this.lastAllocationUpdate = time; + this.takeCarry = 0.0; + this.lastTakeUpdate = time; + } + + // rate in units/s, and time in ns + public void tickAllocation(final long time, final double rate, final double maxAllocation) { + final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate); + this.lastAllocationUpdate = time; + + this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D)); + } + + public long previewAllocation(final long time, final double rate, final long maxTake) { + if (maxTake < 1L) { + return 0L; + } + + final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate); + + // note: abs(takeCarry) <= 1.0 + final double take = Math.min( + Math.min((double)maxTake - this.takeCarry, this.allocation), + rate * (diff*1.0E-9) + ); + + return (long)Math.floor(this.takeCarry + take); + } + + // rate in units/s, and time in ns + public long takeAllocation(final long time, final double rate, final long maxTake) { + if (maxTake < 1L) { + return 0L; + } + + double ret = this.takeCarry; + final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate); + this.lastTakeUpdate = time; + + // note: abs(takeCarry) <= 1.0 + final double take = Math.min( + Math.min((double)maxTake - this.takeCarry, this.allocation), + rate * (diff*1.0E-9) + ); + + ret += take; + this.allocation -= take; + + final long retInteger = (long)Math.floor(ret); + this.takeCarry = ret - (double)retInteger; + + return retInteger; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java new file mode 100644 index 0000000..61f7024 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java @@ -0,0 +1,232 @@ +package ca.spottedleaf.moonrise.common.misc; + +import ca.spottedleaf.concurrentutil.util.IntegerUtil; + +public abstract class SingleUserAreaMap { + + private static final int NOT_SET = Integer.MIN_VALUE; + + private final T parameter; + private int lastChunkX = NOT_SET; + private int lastChunkZ = NOT_SET; + private int distance = NOT_SET; + + public SingleUserAreaMap(final T parameter) { + this.parameter = parameter; + } + + /* math sign function except 0 returns 1 */ + protected static int sign(int val) { + return 1 | (val >> (Integer.SIZE - 1)); + } + + protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ); + + protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ); + + private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) { + final int maxX = chunkX + distance; + final int maxZ = chunkZ + distance; + + for (int cx = chunkX - distance; cx <= maxX; ++cx) { + for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { + this.addCallback(parameter, cx, cz); + } + } + } + + private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) { + final int maxX = chunkX + distance; + final int maxZ = chunkZ + distance; + + for (int cx = chunkX - distance; cx <= maxX; ++cx) { + for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { + this.removeCallback(parameter, cx, cz); + } + } + } + + public final boolean add(final int chunkX, final int chunkZ, final int distance) { + if (distance < 0) { + throw new IllegalArgumentException(Integer.toString(distance)); + } + if (this.lastChunkX != NOT_SET) { + return false; + } + this.lastChunkX = chunkX; + this.lastChunkZ = chunkZ; + this.distance = distance; + + this.addToNew(this.parameter, chunkX, chunkZ, distance); + + return true; + } + + public final boolean update(final int toX, final int toZ, final int newViewDistance) { + if (newViewDistance < 0) { + throw new IllegalArgumentException(Integer.toString(newViewDistance)); + } + final int fromX = this.lastChunkX; + final int fromZ = this.lastChunkZ; + final int oldViewDistance = this.distance; + if (fromX == NOT_SET) { + return false; + } + + this.lastChunkX = toX; + this.lastChunkZ = toZ; + this.distance = newViewDistance; + + final T parameter = this.parameter; + + + final int dx = toX - fromX; + final int dz = toZ - fromZ; + + final int totalX = IntegerUtil.branchlessAbs(fromX - toX); + final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); + + if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { + // teleported + this.removeFromOld(parameter, fromX, fromZ, oldViewDistance); + this.addToNew(parameter, toX, toZ, newViewDistance); + return true; + } + + if (oldViewDistance != newViewDistance) { + // remove loop + + final int oldMinX = fromX - oldViewDistance; + final int oldMinZ = fromZ - oldViewDistance; + final int oldMaxX = fromX + oldViewDistance; + final int oldMaxZ = fromZ + oldViewDistance; + for (int currX = oldMinX; currX <= oldMaxX; ++currX) { + for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { + + // only remove if we're outside the new view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + // add loop + + final int newMinX = toX - newViewDistance; + final int newMinZ = toZ - newViewDistance; + final int newMaxX = toX + newViewDistance; + final int newMaxZ = toZ + newViewDistance; + for (int currX = newMinX; currX <= newMaxX; ++currX) { + for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { + + // only add if we're outside the old view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { + this.addCallback(parameter, currX, currZ); + } + } + } + + return true; + } + + // x axis is width + // z axis is height + // right refers to the x axis of where we moved + // top refers to the z axis of where we moved + + // same view distance + + // used for relative positioning + final int up = sign(dz); // 1 if dz >= 0, -1 otherwise + final int right = sign(dx); // 1 if dx >= 0, -1 otherwise + + // The area excluded by overlapping the two view distance squares creates four rectangles: + // Two on the left, and two on the right. The ones on the left we consider the "removed" section + // and on the right the "added" section. + // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually + // exclusive to the regions they surround. + + // 4 points of the rectangle + int maxX; // exclusive + int minX; // inclusive + int maxZ; // exclusive + int minZ; // inclusive + + if (dx != 0) { + // handle right addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = fromX + (oldViewDistance * right) + right; // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addCallback(parameter, currX, currZ); + } + } + } + + if (dz != 0) { + // handle up addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = toX - (oldViewDistance * right); // inclusive + maxZ = toZ + (oldViewDistance * up) + up; // exclusive + minZ = fromZ + (oldViewDistance * up) + up; // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addCallback(parameter, currX, currZ); + } + } + } + + if (dx != 0) { + // handle left removal + + maxX = toX - (oldViewDistance * right); // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + if (dz != 0) { + // handle down removal + + maxX = fromX + (oldViewDistance * right) + right; // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = toZ - (oldViewDistance * up); // exclusive + minZ = fromZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeCallback(parameter, currX, currZ); + } + } + } + + return true; + } + + public final boolean remove() { + final int chunkX = this.lastChunkX; + final int chunkZ = this.lastChunkZ; + final int distance = this.distance; + if (chunkX == NOT_SET) { + return false; + } + + this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET; + + this.removeFromOld(this.parameter, chunkX, chunkZ, distance); + + return true; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/real_dumb_shit/HolderCompletableFuture.java b/src/main/java/ca/spottedleaf/moonrise/common/real_dumb_shit/HolderCompletableFuture.java new file mode 100644 index 0000000..aa423bf --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/real_dumb_shit/HolderCompletableFuture.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.common.real_dumb_shit; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class HolderCompletableFuture extends CompletableFuture { + + public final List toExecute = new ArrayList<>(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java new file mode 100644 index 0000000..1cf32d7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.common.util; + +public final class MoonriseConstants { + + public static final int MAX_VIEW_DISTANCE = 32; + + private MoonriseConstants() {} + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java new file mode 100644 index 0000000..b3bac1b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java @@ -0,0 +1,139 @@ +package ca.spottedleaf.moonrise.common.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +public class TickThread extends Thread { + + private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class); + + /** + * @deprecated + */ + @Deprecated + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final Entity entity, final String reason) { + if (!isTickThreadFor(entity)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) { + if (!isTickThreadFor(world, aabb)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { + if (!isTickThreadFor(world, blockX, blockZ)) { + LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); + + public TickThread(final String name) { + this(null, name); + } + + public TickThread(final Runnable run, final String name) { + this(run, name, ID_GENERATOR.incrementAndGet()); + } + + private TickThread(final Runnable run, final String name, final int id) { + super(run, name); + this.id = id; + } + + public static TickThread getCurrentTickThread() { + return (TickThread)Thread.currentThread(); + } + + public static boolean isTickThread() { + return Thread.currentThread() instanceof TickThread; + } + + public static boolean isShutdownThread() { + return false; + } + + public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { + return isTickThread(); + } + + public static boolean isTickThreadFor(final Entity entity) { + return isTickThread(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java index 21a3525..8e30701 100644 --- a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java @@ -1,5 +1,6 @@ package ca.spottedleaf.moonrise.common.util; +import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelHeightAccessor; public final class WorldUtil { @@ -40,6 +41,13 @@ public final class WorldUtil { return (getMaxSection(world) << 4) | 15; } + public static String getWorldName(final Level world) { + if (world == null) { + return "null world"; + } + return world.dimension().toString(); + } + private WorldUtil() { throw new RuntimeException(); } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkGeneratorMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkGeneratorMixin.java new file mode 100644 index 0000000..21a234b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkGeneratorMixin.java @@ -0,0 +1,72 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +@Mixin(ChunkGenerator.class) +public abstract class ChunkGeneratorMixin { + + /** + * @reason Pass the supplier to the mixin below so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Redirect( + method = "createBiomes", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture passSupplier(Supplier supplier, Executor executor) { + return (CompletableFuture)CompletableFuture.completedFuture(supplier); + } + + /** + * @reason Retrieve the supplier from the mixin above so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Inject( + method = "createBiomes", + cancellable = true, + at = @At( + value = "RETURN" + ) + ) + private void unpackSupplier(Executor executor, RandomState randomState, Blender blender, + StructureManager structureManager, ChunkAccess chunkAccess, + CallbackInfoReturnable> cir) { + cir.setReturnValue( + CompletableFuture.supplyAsync(((CompletableFuture>)(CompletableFuture)cir.getReturnValue()).join(), executor) + ); + } + + /** + * @reason Bypass thread checks on sync load by using syncLoadNonFull + * @author Spottedleaf + */ + @Redirect( + method = "getStructureGeneratingAt", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/LevelReader;getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;)Lnet/minecraft/world/level/chunk/ChunkAccess;" + ) + ) + private static ChunkAccess redirectToNonSyncLoad(final LevelReader instance, final int x, final int z, + final ChunkStatus toStatus) { + return ((ChunkSystemLevelReader)instance).moonrise$syncLoadNonFull(x, z, toStatus); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkHolderMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkHolderMixin.java new file mode 100644 index 0000000..d537dd0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkHolderMixin.java @@ -0,0 +1,504 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import com.mojang.datafixers.util.Pair; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ChunkHolder.class) +public abstract class ChunkHolderMixin implements ChunkSystemChunkHolder { + + @Shadow + @Final + private ChunkPos pos; + + @Shadow + @Final + private ChunkHolder.PlayerProvider playerProvider; + + @Shadow + @Final + public static CompletableFuture> UNLOADED_CHUNK_FUTURE; + + + @Unique + private NewChunkHolder newChunkHolder; + + @Unique + private ReferenceList playersSentChunkTo; + + @Unique + private ChunkMap getChunkMap() { + return (ChunkMap)this.playerProvider; + } + + @Override + public final NewChunkHolder moonrise$getRealChunkHolder() { + return this.newChunkHolder; + } + + @Override + public final void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder) { + this.newChunkHolder = newChunkHolder; + } + + @Override + public final void moonrise$addReceivedChunk(final ServerPlayer player) { + if (!this.playersSentChunkTo.add(player)) { + throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); + } + } + + @Override + public final void moonrise$removeReceivedChunk(final ServerPlayer player) { + if (!this.playersSentChunkTo.remove(player)) { + throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); + } + } + + @Override + public final boolean moonrise$hasChunkBeenSent() { + return this.playersSentChunkTo.size() != 0; + } + + @Override + public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) { + return this.playersSentChunkTo.contains(to); + } + + @Override + public final List moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { + final List ret = new ArrayList<>(); + final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked(); + for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { + final ServerPlayer player = raw[i]; + if (onlyOnWatchDistanceEdge && !((ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { + continue; + } + ret.add(player); + } + + return ret; + } + + @Unique + private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0]; + + /** + * @reason Initialise our fields + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initFields(final CallbackInfo ci) { + this.playersSentChunkTo = new ReferenceList<>(EMPTY_PLAYER_ARRAY, 0); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getFutureIfPresentUnchecked(final ChunkStatus chunkStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getFutureIfPresent(final ChunkStatus chunkStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getTickingChunkFuture() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getEntityTickingChunkFuture() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getFullChunkFuture() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new chunk holder + * @author Spottedleaf + */ + @Overwrite + public LevelChunk getTickingChunk() { + if (this.newChunkHolder.isTickingReady()) { + if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { + return levelChunk; + } // else: race condition: chunk unload + } + return null; + } + + /** + * @reason Chunk system is not built on futures anymore, and I am pretty sure this is a disgusting hack for a problem + * that doesn't even exist. + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture getChunkSendSyncFuture() { + throw new UnsupportedOperationException(); + } + + @Unique + private boolean isRadiusLoaded(final int radius) { + final ChunkHolderManager manager = ((ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler() + .chunkHolderManager; + final ChunkPos pos = this.pos; + final int chunkX = pos.x; + final int chunkZ = pos.z; + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ); + + if (holder == null || !holder.isFullChunkReady()) { + return false; + } + } + } + + return true; + } + + /** + * @reason Chunk sending may now occur for non-ticking chunks, provided that both the 1 radius neighbours are FULL + * and post-processing is ran. + * @author Spottedleaf + */ + @Overwrite + public LevelChunk getChunkToSend() { + final LevelChunk ret = this.moonrise$getFullChunk(); + if (ret != null && this.isRadiusLoaded(1)) { + return ret; + } + return null; + } + + @Override + public final LevelChunk moonrise$getFullChunk() { + if (this.newChunkHolder.isFullChunkReady()) { + if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { + return levelChunk; + } // else: race condition: chunk unload + } + return null; + } + + /** + * @reason Route to new chunk holder + * @author Spottedleaf + */ + @Overwrite + public ChunkStatus getLastAvailableStatus() { + final NewChunkHolder.ChunkCompletion lastCompletion = this.newChunkHolder.getLastChunkCompletion(); + return lastCompletion == null ? null : lastCompletion.genStatus(); + } + + /** + * @reason Route to new chunk holder + * @author Spottedleaf + */ + @Overwrite + public ChunkAccess getLastAvailable() { + final NewChunkHolder.ChunkCompletion lastCompletion = this.newChunkHolder.getLastChunkCompletion(); + return lastCompletion == null ? null : lastCompletion.chunk(); + } + + /** + * @reason Chunk system is not built on futures anymore, unloading is now checked via {@link NewChunkHolder#isSafeToUnload()} + * while holding chunk system locks. + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture getChunkToSave() { + throw new UnsupportedOperationException(); + } + + /** + * @reason need to reroute getTickingChunk to getChunkToSend, as we do not bring all sent chunks to ticking + * @author Spottedleaf + */ + @Redirect( + method = "blockChanged", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkHolder;getTickingChunk()Lnet/minecraft/world/level/chunk/LevelChunk;") + ) + private LevelChunk redirectBlockUpdate(final ChunkHolder instance) { + if (this.playersSentChunkTo.size() == 0) { + // no players to sent to, so don't need to update anything + return null; + } + return this.getChunkToSend(); + } + + /** + * @reason Need to reroute getFutureIfPresent to new chunk system call + * @author Spottedleaf + */ + @Redirect( + method = "sectionLightChanged", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkHolder;getFutureIfPresent(Lnet/minecraft/world/level/chunk/status/ChunkStatus;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture> redirectLightUpdate(final ChunkHolder instance, + final ChunkStatus chunkStatus) { + final NewChunkHolder.ChunkCompletion chunkCompletion = this.newChunkHolder.getLastChunkCompletion(); + if (chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(ChunkStatus.INITIALIZE_LIGHT)) { + return UNLOADED_CHUNK_FUTURE; + } + + return CompletableFuture.completedFuture(ChunkResult.of(chunkCompletion.chunk())); + } + + /** + * @reason need to reroute getTickingChunk to getChunkToSend, as we do not bring all sent chunks to ticking + * @author Spottedleaf + */ + @Redirect( + method = "sectionLightChanged", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkHolder;getTickingChunk()Lnet/minecraft/world/level/chunk/LevelChunk;" + ) + ) + private LevelChunk redirectLightUpdate(final ChunkHolder instance) { + return this.getChunkToSend(); + } + + /** + * @reason Redirect player retrieval to the sent player list, as we do not maintain the Vanilla hook + * @author Spottedleaf + */ + @Redirect( + method = "broadcastChanges", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkHolder$PlayerProvider;getPlayers(Lnet/minecraft/world/level/ChunkPos;Z)Ljava/util/List;") + ) + private List redirectPlayerRetrieval(final ChunkHolder.PlayerProvider instance, final ChunkPos chunkPos, + final boolean onlyOnWatchDistanceEdge) { + return this.moonrise$getPlayers(onlyOnWatchDistanceEdge); + } + + /** + * @reason Chunk system is not built on futures anymore, use {@link ChunkTaskScheduler} + * schedule methods to await for a chunk load + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getOrScheduleFuture(final ChunkStatus chunkStatus, + final ChunkMap chunkMap) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use ticket levels to prevent chunk unloading. + * @author Spottedleaf + */ + @Overwrite + public void addSaveDependency(final String string, final CompletableFuture completableFuture) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, use ticket levels to prevent chunk unloading. + * @author Spottedleaf + */ + @Overwrite + public void updateChunkToSave(CompletableFuture> completableFuture, + final String string) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore, and I am pretty sure this is a disgusting hack for a problem + * that doesn't even exist. + * @author Spottedleaf + */ + @Overwrite + public void addSendDependency(final CompletableFuture completableFuture) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new chunk holder + * @author Spottedleaf + */ + @Overwrite + public FullChunkStatus getFullStatus() { + return this.newChunkHolder.getChunkStatus(); + } + + /** + * @reason Route to new chunk holder + * @author Spottedleaf + */ + @Overwrite + public int getTicketLevel() { + return this.newChunkHolder.getTicketLevel(); + } + + /** + * @reason Set chunk priority instead in the new chunk system + * @author Spottedleaf + * @see ChunkTaskScheduler + */ + @Overwrite + public int getQueueLevel() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Set chunk priority instead in the new chunk system + * @author Spottedleaf + * @see ChunkTaskScheduler + */ + @Overwrite + public void setQueueLevel(int i) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Use ticket system to control ticket levels + * @author Spottedleaf + * @see net.minecraft.server.level.ServerChunkCache#addRegionTicket(TicketType, ChunkPos, int, Object) + */ + @Overwrite + public void setTicketLevel(int i) { + // don't throw, this is called during construction of ChunkHolder + } + + /** + * @reason Chunk system is not built on futures anymore + * @author Spottedleaf + */ + @Overwrite + public void scheduleFullChunkPromotion(final ChunkMap chunkMap, + final CompletableFuture> completableFuture, + final Executor executor, final FullChunkStatus fullChunkStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore + * @author Spottedleaf + */ + @Overwrite + public void demoteFullChunk(final ChunkMap chunkMap, final FullChunkStatus fullChunkStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system hooks for ticket level updating now in {@link NewChunkHolder#processTicketLevelUpdate(List, List)} + * @author Spottedleaf + */ + @Overwrite + public void updateFutures(final ChunkMap chunkMap, final Executor executor) { + throw new UnsupportedOperationException(); + } + + /** + * @reason New chunk system has no equivalent, as chunks should be saved according to their dirty flag to ensure + * that all unsaved data is not lost. + * @author Spottedleaf + */ + @Overwrite + public boolean wasAccessibleSinceLastSave() { + throw new UnsupportedOperationException(); + } + + /** + * @reason New chunk system has no equivalent, as chunks should be saved according to their dirty flag to ensure + * that all unsaved data is not lost. + * @author Spottedleaf + */ + @Overwrite + public void refreshAccessibility() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore + * @author Spottedleaf + */ + @Overwrite + public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system is not built on futures anymore + * @author Spottedleaf + */ + @Overwrite + public List>>> getAllFutures() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMap$DistanceManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMap$DistanceManagerMixin.java new file mode 100644 index 0000000..db4d1af --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMap$DistanceManagerMixin.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager; +import net.minecraft.server.level.ChunkMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import java.util.concurrent.Executor; + +@Mixin(ChunkMap.DistanceManager.class) +public abstract class ChunkMap$DistanceManagerMixin extends net.minecraft.server.level.DistanceManager implements ChunkSystemDistanceManager { + + @Shadow + @Final + ChunkMap field_17443; + + protected ChunkMap$DistanceManagerMixin(Executor executor, Executor executor2) { + super(executor, executor2); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Override + @Overwrite + public boolean isChunkToRemove(final long pos) { + throw new UnsupportedOperationException(); + } + + @Override + public final ChunkMap moonrise$getChunkMap() { + return this.field_17443; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMapMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMapMixin.java new file mode 100644 index 0000000..e27baf9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkMapMixin.java @@ -0,0 +1,561 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.util.MoonriseConstants; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import com.mojang.datafixers.DataFixer; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StreamTagVisitor; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; +import net.minecraft.server.level.ChunkTrackingView; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.ChunkScanAccess; +import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.BooleanSupplier; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; + +@Mixin(ChunkMap.class) +public abstract class ChunkMapMixin extends ChunkStorage implements ChunkHolder.PlayerProvider { + + @Shadow + @Final + public ServerLevel level; + + @Shadow + private Long2ObjectLinkedOpenHashMap updatingChunkMap; + + @Shadow + private volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; + + @Shadow + private ChunkTaskPriorityQueueSorter queueSorter; + + @Shadow + private ProcessorHandle> worldgenMailbox; + + @Shadow + private ProcessorHandle> mainThreadMailbox; + + @Shadow + private int serverViewDistance; + + public ChunkMapMixin(RegionStorageInfo regionStorageInfo, Path path, DataFixer dataFixer, boolean bl) { + super(regionStorageInfo, path, dataFixer, bl); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void constructor(final CallbackInfo ci) { + // intentionally destroy old chunk system hooks + this.updatingChunkMap = null; + this.visibleChunkMap = null; + this.queueSorter = null; + this.worldgenMailbox = null; + this.mainThreadMailbox = null; + } + + /** + * @reason Route to new chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public boolean isChunkTracked(final ServerPlayer player, final int chunkX, final int chunkZ) { + return ((ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); + } + + /** + * @reason Route to new chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public boolean isChunkOnTrackedBorder(final ServerPlayer player, final int chunkX, final int chunkZ) { + return ((ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); + } + + /** + * @reason Route to new chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public ChunkHolder getUpdatingChunkIfPresent(final long pos) { + final NewChunkHolder holder = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); + return holder == null ? null : holder.vanillaChunkHolder; + } + + /** + * @reason Route to new chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public ChunkHolder getVisibleChunkIfPresent(final long pos) { + final NewChunkHolder holder = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); + return holder == null ? null : holder.vanillaChunkHolder; + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public IntSupplier getChunkQueueLevel(final long pos) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture>> getChunkRangeFuture(final ChunkHolder centerChunk, + final int margin, + final IntFunction distanceToStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture>> prepareEntityTickingChunk(final ChunkHolder chunk) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public ChunkHolder updateChunkScheduling(final long pos, final int level, final ChunkHolder holder, + final int newLevel) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Override + @Overwrite + public void close() throws IOException { + throw new UnsupportedOperationException("Use ServerChunkCache#close"); + } + + /** + * @reason Route to new chunk system and handle close-save operations + * @author Spottedleaf + */ + @Overwrite + public void saveAllChunks(final boolean flush) { + final boolean shutdown = ((ChunkSystemServerLevel)this.level).moonrise$isMarkedClosing(); + + if (!shutdown) { + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks( + flush, false, false + ); + } else { + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close( + true, true + ); + } + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public boolean hasWork() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new chunk unloading code + * @author Spottedleaf + */ + @Overwrite + public void processUnloads(final BooleanSupplier shouldKeepTicking) { + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void scheduleUnload(final long pos, final ChunkHolder holder) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Replaced by concurrent map, removing the need for this logic. + * A side-note of this logic is that expensive map copying is no longer performed. + * @author Spottedleaf + * @see ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager#chunkHolders + */ + @Overwrite + public boolean promoteChunkMap() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> schedule(final ChunkHolder holder, final ChunkStatus requiredStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> scheduleChunkLoad(final ChunkPos pos) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> scheduleChunkGeneration(final ChunkHolder holder, + final ChunkStatus requiredStatus) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> protoChunkToFullChunk(final ChunkHolder chunkHolder, + final ChunkAccess chunkAccess) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> prepareTickingChunk(final ChunkHolder holder) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void onChunkReadyToSend(final LevelChunk chunk) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> prepareAccessibleChunk(final ChunkHolder holder) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + * @see NewChunkHolder#save(boolean) + */ + @Overwrite + public boolean saveChunkIfNeeded(final ChunkHolder chunkHolder) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + * @see NewChunkHolder#save(boolean) + */ + @Overwrite + public boolean save(final ChunkAccess chunk) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public boolean isExistingChunkFull(final ChunkPos pos) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new player chunk loader + * @author Spottedleaf + */ + @Overwrite + public void setServerViewDistance(final int watchDistance) { + final int clamped = Mth.clamp(watchDistance, 2, MoonriseConstants.MAX_VIEW_DISTANCE); + if (clamped == this.serverViewDistance) { + return; + } + + this.serverViewDistance = clamped; + ((ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1); + } + + /** + * @reason Route to new player chunk loader + * @author Spottedleaf + */ + @Overwrite + public int getPlayerViewDistance(final ServerPlayer player) { + return ChunkSystem.getSendViewDistance(player); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void markChunkPendingToSend(final ServerPlayer player, final ChunkPos pos) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public static void markChunkPendingToSend(final ServerPlayer player, final LevelChunk chunk) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public static void dropChunk(final ServerPlayer player, final ChunkPos pos) { + + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void dumpChunks(final Writer writer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture> read(final ChunkPos pos) { + if (!RegionFileIOThread.isRegionFileThread()) { + try { + return CompletableFuture.completedFuture( + Optional.ofNullable( + RegionFileIOThread.loadData( + this.level, pos.x, pos.z, RegionFileIOThread.RegionFileType.CHUNK_DATA, + RegionFileIOThread.getIOBlockingPriorityForCurrentThread() + ) + ) + ); + } catch (final Throwable thr) { + return CompletableFuture.failedFuture(thr); + } + } + return super.read(pos); + } + + @Override + public CompletableFuture write(final ChunkPos pos, final CompoundTag tag) { + if (!RegionFileIOThread.isRegionFileThread()) { + RegionFileIOThread.scheduleSave( + this.level, pos.x, pos.z, tag, + RegionFileIOThread.RegionFileType.CHUNK_DATA); + return null; + } + super.write(pos, tag); + return null; + } + + @Override + public void flushWorker() { + RegionFileIOThread.flush(); + } + + /** + * @reason New player chunk loader handles this, and redirect to the distance map add + * @author Spottedleaf + */ + @Redirect( + method = "updatePlayerStatus", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkMap;updateChunkTracking(Lnet/minecraft/server/level/ServerPlayer;)V" + ) + ) + private void avoidUpdateChunkTrackingInUpdate(final ChunkMap instance, final ServerPlayer serverPlayer) { + ChunkSystem.addPlayerToDistanceMaps(this.level, serverPlayer); + } + + /** + * @reason updateChunkTracking is not needed, the player chunk loader has its own tick hook elsewhere + * @author Spottedleaf + * @see RegionizedPlayerChunkLoader#tick() + */ + @Redirect( + method = "tick()V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkMap;updateChunkTracking(Lnet/minecraft/server/level/ServerPlayer;)V" + ) + ) + private void skipChunkTrackingInTick(final ChunkMap instance, final ServerPlayer serverPlayer) {} + + /** + * @reason New player chunk loader handles this, and redirect to the distance map remove + * @author Spottedleaf + */ + @Redirect( + method = "updatePlayerStatus", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkMap;applyChunkTrackingView(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/server/level/ChunkTrackingView;)V" + ) + ) + private void avoidApplyChunkTrackingViewInUpdate(final ChunkMap instance, final ServerPlayer serverPlayer, + final ChunkTrackingView chunkTrackingView) { + ChunkSystem.removePlayerFromDistanceMaps(this.level, serverPlayer); + } + + /** + * Hook into move call so that we can run callbacks on chunk position change + * @author Spottedleaf + */ + @Inject( + method = "move", + at = @At( + value = "RETURN" + ) + ) + private void updateMapsHook(final ServerPlayer player, final CallbackInfo ci) { + ChunkSystem.updateMaps(this.level, player); + } + + /** + * @reason New player chunk loader handles this + * @author Spottedleaf + */ + @Redirect( + method = "move", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkMap;updateChunkTracking(Lnet/minecraft/server/level/ServerPlayer;)V" + ) + ) + private void avoidSetChunkTrackingViewInMove(final ChunkMap instance, final ServerPlayer serverPlayer) {} + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void updateChunkTracking(final ServerPlayer player) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public void applyChunkTrackingView(final ServerPlayer player, final ChunkTrackingView chunkFilter) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new player chunk loader + * @author Spottedleaf + */ + @Override + @Overwrite + public List getPlayers(final ChunkPos chunkPos, final boolean onlyOnWatchDistanceEdge) { + final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong()); + if (holder == null) { + return new ArrayList<>(); + } else { + return ((ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge); + } + } + + /** + * @reason See {@link ChunkHolderMixin#addSendDependency(CompletableFuture)} + * @author Spottedleaf + */ + @Overwrite + public void waitForLightBeforeSending(final ChunkPos centerPos, final int radius) {} + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public int size() { + return ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public Iterable getChunks() { + return ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHoldersIterable(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkSerializerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkSerializerMixin.java new file mode 100644 index 0000000..1748bf5 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkSerializerMixin.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ChunkSerializer.class) +public abstract class ChunkSerializerMixin { + + /** + * @reason Chunk system handles this during full transition + * @author Spottedleaf + * @see ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask + */ + @Redirect( + method = "read", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;checkConsistencyWithBlocks(Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/LevelChunkSection;)V" + ) + ) + private static void skipConsistencyCheck(PoiManager instance, SectionPos sectionPos, LevelChunkSection levelChunkSection) {} + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStatusMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStatusMixin.java new file mode 100644 index 0000000..9349138 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStatusMixin.java @@ -0,0 +1,117 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStatusTasks; +import net.minecraft.world.level.chunk.status.ChunkType; +import net.minecraft.world.level.levelgen.Heightmap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.EnumSet; +import java.util.concurrent.atomic.AtomicBoolean; + +@Mixin(ChunkStatus.class) +public abstract class ChunkStatusMixin implements ChunkSystemChunkStatus { + + @Unique + private boolean isParallelCapable; + + @Unique + private boolean emptyLoadTask; + + @Unique + private int writeRadius; + + @Unique + private int loadRadius; + + @Unique + private ChunkStatus nextStatus; + + @Unique + private AtomicBoolean warnedAboutNoImmediateComplete; + + @Override + public final boolean moonrise$isParallelCapable() { + return this.isParallelCapable; + } + + @Override + public final void moonrise$setParallelCapable(final boolean value) { + this.isParallelCapable = value; + } + + @Override + public final int moonrise$getWriteRadius() { + return this.writeRadius; + } + + @Override + public final void moonrise$setWriteRadius(final int value) { + this.writeRadius = value; + } + + @Override + public final int moonrise$getLoadRadius() { + return this.loadRadius; + } + + @Override + public final void moonrise$setLoadRadius(final int value) { + this.loadRadius = value; + } + + @Override + public final ChunkStatus moonrise$getNextStatus() { + return this.nextStatus; + } + + @Override + public final boolean moonrise$isEmptyLoadStatus() { + return this.emptyLoadTask; + } + + @Override + public void moonrise$setEmptyLoadStatus(final boolean value) { + this.emptyLoadTask = value; + } + + @Override + public final boolean moonrise$isEmptyGenStatus() { + return (Object)this == ChunkStatus.EMPTY; + } + + @Override + public final AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() { + return this.warnedAboutNoImmediateComplete; + } + + /** + * @reason Initialise default values for fields and nextStatus + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initFields(ChunkStatus prevStatus, int i, boolean bl, EnumSet enumSet, ChunkType chunkType, + ChunkStatus.GenerationTask generationTask, ChunkStatus.LoadingTask loadingTask, + CallbackInfo ci) { + this.isParallelCapable = false; + this.writeRadius = -1; + this.loadRadius = 0; + this.nextStatus = (ChunkStatus)(Object)this; + if (prevStatus != null) { + ((ChunkStatusMixin)(Object)prevStatus).nextStatus = (ChunkStatus)(Object)this; + } + this.warnedAboutNoImmediateComplete = new AtomicBoolean(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStorageMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStorageMixin.java new file mode 100644 index 0000000..b9737f7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ChunkStorageMixin.java @@ -0,0 +1,191 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.ChunkScanAccess; +import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.IOWorker; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Mixin(ChunkStorage.class) +public abstract class ChunkStorageMixin implements ChunkSystemChunkStorage, AutoCloseable { + + @Shadow + private IOWorker worker; + + @Unique + private static final Logger LOGGER = LogUtils.getLogger(); + + @Unique + private RegionFileStorage storage; + + /** + * @reason Destroy old IO worker field after retrieving region storage from it + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.storage = this.worker.storage; + this.worker = null; + } + + @Override + public final RegionFileStorage moonrise$getRegionStorage() { + return this.storage; + } + + /** + * @reason The code using this method only uses it to avoid retrieving possibly empty biome blending data. There is + * no actual cost to retrieve this data, but there is an obvious significant penalty to loading the NBT data + * from disk directly to check (even _if_ cached). + * @author Spottedleaf + */ + @Overwrite + public boolean isOldChunkAround(final ChunkPos pos, final int radius) { + return true; + } + + + /** + * @reason Legacy data is accessed by multiple threads, and so it should be synchronised correctly. + * The initialisation code is oddly initialised correctly, but not the actual accesses after. + * @author Spottedleaf + */ + @Redirect( + method = "upgradeChunkTag", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler;updateFromLegacy(Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;" + ) + ) + private CompoundTag synchroniseLegacyDataUpgrade(final LegacyStructureDataHandler instance, final CompoundTag compoundTag) { + synchronized (instance) { + return instance.updateFromLegacy(compoundTag); + } + } + + /** + * @reason Redirect to use the raw storage. It is expected that {@link net.minecraft.server.level.ChunkMap} + * overrides to route to the RegionFile IO thread, as ChunkStorage may be initialised directly when + * forceUpgrading. The IO threads are not capable of servicing requests during forceUpgrading, as a + * world is not initialised. + * @author Spottedleaf + */ + @Redirect( + method = "read", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/IOWorker;loadAsync(Lnet/minecraft/world/level/ChunkPos;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture> redirectLoad(final IOWorker instance, final ChunkPos chunkPos) { + try { + return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos))); + } catch (final Throwable throwable) { + return CompletableFuture.failedFuture(throwable); + } + } + + /** + * @reason Redirect to use the raw storage. It is expected that {@link net.minecraft.server.level.ChunkMap} + * overrides to route to the RegionFile IO thread, as ChunkStorage may be initialised directly when + * forceUpgrading. The IO threads are not capable of servicing requests during forceUpgrading, as a + * world is not initialised. + * @author Spottedleaf + */ + @Redirect( + method = "write", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/IOWorker;store(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/nbt/CompoundTag;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture redirectWrite(final IOWorker instance, final ChunkPos chunkPos, + final CompoundTag compoundTag) { + try { + this.storage.write(chunkPos, compoundTag); + return CompletableFuture.completedFuture(null); + } catch (final Throwable throwable) { + return CompletableFuture.failedFuture(throwable); + } + } + + /** + * @reason Legacy data is accessed by multiple threads, and so it should be synchronised correctly. + * The initialisation code is oddly initialised correctly, but not the actual accesses after. + * @author Spottedleaf + */ + @Redirect( + method = "handleLegacyStructureIndex", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler;removeIndex(J)V" + ) + ) + private void synchroniseLegacyDataWrite(final LegacyStructureDataHandler instance, + final long pos) { + synchronized (instance) { + instance.removeIndex(pos); + } + } + + /** + * @reason Redirect to flush the storage directly + * @author Spottedleaf + */ + @Overwrite + public void flushWorker() { + try { + this.storage.flush(); + } catch (final IOException ex) { + LOGGER.error("Failed to flush chunk storage", ex); + } + } + + /** + * @reason Redirect to close the storage directly + * @author Spottedleaf + */ + @Override + @Overwrite + public void close() throws Exception { + this.storage.close(); + } + + /** + * @reason Redirect to access the storage directly + * @author Spottedleaf + */ + @Overwrite + public ChunkScanAccess chunkScanner() { + // TODO ChunkMap implementation? + return (chunkPos, streamTagVisitor) -> { + try { + this.storage.scanChunk(chunkPos, streamTagVisitor); + return java.util.concurrent.CompletableFuture.completedFuture(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ClientLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ClientLevelMixin.java new file mode 100644 index 0000000..b8aba16 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ClientLevelMixin.java @@ -0,0 +1,143 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client.ClientEntityLookup; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.TransientEntitySectionManager; +import net.minecraft.world.level.storage.WritableLevelData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.function.Supplier; + +@Mixin(ClientLevel.class) +public abstract class ClientLevelMixin extends Level implements ChunkSystemLevel { + + @Shadow + private TransientEntitySectionManager entityStorage; + + protected ClientLevelMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + /** + * @reason Initialise fields / destroy entity manager state + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(ClientPacketListener clientPacketListener, ClientLevel.ClientLevelData clientLevelData, + ResourceKey resourceKey, Holder holder, int i, int j, Supplier supplier, + LevelRenderer levelRenderer, boolean bl, long l, CallbackInfo ci) { + this.entityStorage = null; + + this.moonrise$setEntityLookup(new ClientEntityLookup(this, ((ClientLevel)(Object)this).new EntityCallbacks())); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Overwrite + public int getEntityCount() { + return this.moonrise$getEntityLookup().getEntityCount(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "addEntity", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/TransientEntitySectionManager;addEntity(Lnet/minecraft/world/level/entity/EntityAccess;)V" + ) + ) + private void addEntityHook(final TransientEntitySectionManager instance, final T entityAccess) { + this.moonrise$getEntityLookup().addNewEntity((Entity)entityAccess); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/TransientEntitySectionManager;getEntityGetter()Lnet/minecraft/world/level/entity/LevelEntityGetter;" + ) + ) + private LevelEntityGetter redirectGetEntities(final TransientEntitySectionManager instance) { + return this.moonrise$getEntityLookup(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "gatherChunkSourceStats", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/TransientEntitySectionManager;gatherStats()Ljava/lang/String;" + ) + ) + private String redirectGatherChunkSourceStats(final TransientEntitySectionManager instance) { + return this.moonrise$getEntityLookup().getDebugInfo(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "unload", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/TransientEntitySectionManager;stopTicking(Lnet/minecraft/world/level/ChunkPos;)V" + ) + ) + private void chunkUnloadHook(final TransientEntitySectionManager instance, + final ChunkPos pos) { + ((ClientEntityLookup)this.moonrise$getEntityLookup()).markNonTicking(pos.toLong()); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "onChunkLoaded", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/TransientEntitySectionManager;startTicking(Lnet/minecraft/world/level/ChunkPos;)V" + ) + ) + private void chunkLoadHook(final TransientEntitySectionManager instance, final ChunkPos pos) { + ((ClientEntityLookup)this.moonrise$getEntityLookup()).markTicking(pos.toLong()); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/DistanceManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/DistanceManagerMixin.java new file mode 100644 index 0000000..5d147ce --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/DistanceManagerMixin.java @@ -0,0 +1,342 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.level.TickingTracker; +import net.minecraft.util.SortedArraySet; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Set; +import java.util.concurrent.Executor; + +@Mixin(DistanceManager.class) +public abstract class DistanceManagerMixin implements ChunkSystemDistanceManager { + + @Shadow + Long2ObjectOpenHashMap>> tickets; + + @Shadow + private DistanceManager.ChunkTicketTracker ticketTracker; + + @Shadow + private TickingTracker tickingTicketsTracker; + + @Shadow + private DistanceManager.PlayerTicketTracker playerTicketManager; + + @Shadow + Set chunksToUpdateFutures; + + @Shadow + ChunkTaskPriorityQueueSorter ticketThrottler; + + @Shadow + ProcessorHandle> ticketThrottlerInput; + + @Shadow + ProcessorHandle ticketThrottlerReleaser; + + @Shadow + LongSet ticketsToRelease; + + @Shadow + Executor mainThreadExecutor; + + @Shadow + @Final + private DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter; + + @Shadow + private int simulationDistance; + + @Override + public ChunkMap moonrise$getChunkMap() { + throw new AbstractMethodError(); + } + + /** + * @reason Destroy old chunk system state to prevent it from being used + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void destroyFields(final Executor executor, final Executor executor2, + final CallbackInfo ci) { + this.tickets = null; + this.ticketTracker = null; + this.tickingTicketsTracker = null; + this.playerTicketManager = null; + this.chunksToUpdateFutures = null; + this.ticketThrottler = null; + this.ticketThrottlerInput = null; + this.ticketThrottlerReleaser = null; + this.ticketsToRelease = null; + this.mainThreadExecutor = null; + this.simulationDistance = -1; + } + + @Unique + private ChunkHolderManager getChunkHolderManager() { + return ((ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager; + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void purgeStaleTickets() { + this.getChunkHolderManager().tick(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean runAllUpdates(final ChunkMap chunkStorage) { + return this.getChunkHolderManager().processTicketUpdates(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void addTicket(final long pos, final Ticket ticket) { + this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), pos, ticket.getTicketLevel(), ticket.key); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void removeTicket(final long pos, final Ticket ticket) { + this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), pos, ticket.getTicketLevel(), ticket.key); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void addRegionTicket(final TicketType type, final ChunkPos pos, final int radius, final T identifier) { + this.getChunkHolderManager().addTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - radius, identifier); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void removeRegionTicket(final TicketType type, final ChunkPos pos, final int radius, final T identifier) { + this.getChunkHolderManager().removeTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - radius, identifier); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void updateChunkForced(final ChunkPos pos, final boolean forced) { + if (forced) { + this.getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } else { + this.getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } + } + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "addPlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager$PlayerTicketTracker;update(JIZ)V" + ) + ) + private void skipTickingTicketTrackerAdd(final DistanceManager.PlayerTicketTracker instance, final long l, + final int i, final boolean b) {} + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "addPlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/TickingTracker;addTicket(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V" + ) + ) + private void skipTickingTicketTrackerAdd(final TickingTracker instance, final TicketType ticketType, + final ChunkPos chunkPos, final int i, final T object) {} + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "addPlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager;getPlayerTicketLevel()I" + ) + ) + private int skipTicketLevelAdd(final DistanceManager instance) { + return 0; + } + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "removePlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager$PlayerTicketTracker;update(JIZ)V" + ) + ) + private void skipTickingTicketTrackerRemove(final DistanceManager.PlayerTicketTracker instance, final long l, + final int i, final boolean b) {} + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "removePlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/TickingTracker;removeTicket(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V" + ) + ) + private void skipTickingTicketTrackerRemove(final TickingTracker instance, final TicketType ticketType, + final ChunkPos chunkPos, final int i, final T object) {} + + /** + * @reason Remove old chunk system hooks + * @author Spottedleaf + */ + @Redirect( + method = "removePlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager;getPlayerTicketLevel()I" + ) + ) + private int skipTicketLevelRemove(final DistanceManager instance) { + return 0; + } + + /** + * @reason Destroy old chunk system hooks + * @author Spottedleaf + */ + @Overwrite + public int getPlayerTicketLevel() { + throw new UnsupportedOperationException(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean inEntityTickingRange(final long pos) { + final NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(pos); + return chunkHolder != null && chunkHolder.isEntityTickingReady(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean inBlockTickingRange(final long pos) { + final NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(pos); + return chunkHolder != null && chunkHolder.isTickingReady(); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public String getTicketDebugString(final long pos) { + return this.getChunkHolderManager().getTicketDebugString(pos); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void updatePlayerTickets(final int viewDistance) { + this.moonrise$getChunkMap().setServerViewDistance(viewDistance); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public void updateSimulationDistance(final int simulationDistance) { + ((ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(simulationDistance); + } + + /** + * @reason Route to new chunk system + * @author Spottedleaf + */ + @Overwrite + public String getDebugStatus() { + return "No DistanceManager stats available"; + } + + /** + * @reason This hack is not required anymore, see {@link MinecraftServerMixin} + * @author Spottedleaf + */ + @Overwrite + public void removeTicketsOnClosing() {} + + /** + * @reason This hack is not required anymore, see {@link MinecraftServerMixin} + * @author Spottedleaf + */ + @Overwrite + public boolean hasTickets() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityGetterMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityGetterMixin.java new file mode 100644 index 0000000..f176d39 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityGetterMixin.java @@ -0,0 +1,22 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.EntityGetter; +import net.minecraft.world.phys.AABB; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(EntityGetter.class) +public interface EntityGetterMixin extends ChunkSystemEntityGetter { + + @Shadow + List getEntities(Entity entity, AABB aABB, Predicate predicate); + + @Override + default List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + return this.getEntities(entity, box, predicate); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityMixin.java new file mode 100644 index 0000000..ffa142d --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityMixin.java @@ -0,0 +1,215 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import com.google.common.collect.ImmutableList; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +@Mixin(Entity.class) +public abstract class EntityMixin implements ChunkSystemEntity { + + @Shadow + private ImmutableList passengers; + + @Shadow + protected abstract Stream getIndirectPassengersStream(); + + @Shadow + @Final + private static Logger LOGGER; + + @Shadow + private Level level; + + @Shadow + @Nullable + private Entity.RemovalReason removalReason; + + + @Unique + private final boolean isHardColliding = this.moonrise$isHardCollidingUncached(); + + @Unique + private FullChunkStatus chunkStatus; + + @Unique + private int sectionX; + + @Unique + private int sectionY; + + @Unique + private int sectionZ; + + @Unique + private boolean updatingSectionStatus; + + @Override + public final boolean moonrise$isHardColliding() { + return this.isHardColliding; + } + + @Override + public final FullChunkStatus moonrise$getChunkStatus() { + return this.chunkStatus; + } + + @Override + public final void moonrise$setChunkStatus(final FullChunkStatus status) { + this.chunkStatus = status; + } + + @Override + public final int moonrise$getSectionX() { + return this.sectionX; + } + + @Override + public final void moonrise$setSectionX(final int x) { + this.sectionX = x; + } + + @Override + public final int moonrise$getSectionY() { + return this.sectionY; + } + + @Override + public final void moonrise$setSectionY(final int y) { + this.sectionY = y; + } + + @Override + public final int moonrise$getSectionZ() { + return this.sectionZ; + } + + @Override + public final void moonrise$setSectionZ(final int z) { + this.sectionZ = z; + } + + @Override + public final boolean moonrise$isUpdatingSectionStatus() { + return this.updatingSectionStatus; + } + + @Override + public final void moonrise$setUpdatingSectionStatus(final boolean to) { + this.updatingSectionStatus = to; + } + + @Override + public final boolean moonrise$hasAnyPlayerPassengers() { + if (this.passengers.isEmpty()) { + return false; + } + return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player); + } + + /** + * @reason Initialise fields + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.sectionX = this.sectionY = this.sectionZ = Integer.MIN_VALUE; + } + + /** + * @reason Stop bad mods from moving entities during section status updates, which otherwise would cause CMEs + * @author Spottedleaf + */ + @Inject( + method = "setPosRaw", + cancellable = true, + at = @At( + value = "HEAD" + ) + ) + private void checkUpdatingStatusPoi(final double x, final double y, final double z, final CallbackInfo ci) { + if (this.updatingSectionStatus) { + LOGGER.error( + "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z) + + " since it is processing a section status update", new Throwable() + ); + ci.cancel(); + return; + } + } + + /** + * @reason Stop bad mods from removing entities during section status updates, which otherwise would cause CMEs + * @author Spottedleaf + */ + @Inject( + method = "setRemoved", + cancellable = true, + at = @At( + value = "HEAD" + ) + ) + private void checkCanRemove(final CallbackInfo ci) { + if (!((ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) { + LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable()); + ci.cancel(); + return; + } + } + + /** + * @reason Don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) + * @author Spottedleaf + */ + @Redirect( + method = "setRemoved", + at = @At( + value = "INVOKE", + target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V" + ) + ) + private void avoidDismountOnUnload(final List instance, final Consumer consumer) { + if (this.removalReason == Entity.RemovalReason.UNLOADED_TO_CHUNK) { + return; + } + + instance.forEach(consumer); + } + + /** + * @reason We should not save entities with any player passengers + * @author Spottedleaf + */ + @Redirect( + method = "shouldBeSaved", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;hasExactlyOnePlayerPassenger()Z" + ) + ) + private boolean properlyCheckPlayers(final Entity instance) { + return ((ChunkSystemEntity)instance).moonrise$hasAnyPlayerPassengers(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityTickListMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityTickListMixin.java new file mode 100644 index 0000000..d2c97a3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/EntityTickListMixin.java @@ -0,0 +1,127 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; +import ca.spottedleaf.moonrise.common.util.TickThread; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.function.Consumer; + +@Mixin(EntityTickList.class) +public abstract class EntityTickListMixin { + + @Shadow + private Int2ObjectMap active; + + @Shadow + private Int2ObjectMap passive; + + @Unique + private IteratorSafeOrderedReferenceSet entities; + + /** + * @reason Initialise new fields and destroy old state + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.active = null; + this.passive = null; + this.entities = new IteratorSafeOrderedReferenceSet<>(); + } + + + /** + * @reason Do not delay removals + * @author Spottedleaf + */ + @Overwrite + public void ensureActiveIsNotIterated() {} + + /** + * @reason Route to new entity list + * @author Spottedleaf + */ + @Redirect( + method = "add", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;put(ILjava/lang/Object;)Ljava/lang/Object;" + ) + ) + private V hookAdd(final Int2ObjectMap instance, final int key, final V value) { + this.entities.add((Entity)value); + return null; + } + + /** + * @reason Route to new entity list + * @author Spottedleaf + */ + @Inject( + method = "remove", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;remove(I)Ljava/lang/Object;" + ) + ) + private void hookRemove(final Entity entity, final CallbackInfo ci) { + this.entities.remove(entity); + } + + + /** + * @reason Avoid NPE on accessing old state + * @author Spottedleaf + */ + @Redirect( + method = "remove", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;remove(I)Ljava/lang/Object;" + ) + ) + private V hookRemoveAvoidNPE(final Int2ObjectMap instance, final int key) { + return null; + } + + /** + * @reason Route to new entity list + * @author Spottedleaf + */ + @Overwrite + public boolean contains(Entity entity) { + return this.entities.contains(entity); + } + + /** + * @reason Route to new entity list + * @author Spottedleaf + */ + @Overwrite + public void forEach(final Consumer action) { + // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... + // (by dfl iterator() is configured to not iterate over new entries) + final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); + try { + while (iterator.hasNext()) { + action.accept(iterator.next()); + } + } finally { + iterator.finishedIterating(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkMixin.java new file mode 100644 index 0000000..136103d --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkMixin.java @@ -0,0 +1,89 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks; +import net.minecraft.core.Registry; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelChunk.class) +public abstract class LevelChunkMixin extends ChunkAccess implements ChunkSystemLevelChunk { + + @Shadow + @Final + private LevelChunkTicks blockTicks; + + @Shadow + @Final + private LevelChunkTicks fluidTicks; + + @Shadow + @Final + Level level; + + public LevelChunkMixin(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, Registry registry, long l, @Nullable LevelChunkSection[] levelChunkSections, @Nullable BlendingData blendingData) { + super(chunkPos, upgradeData, levelHeightAccessor, registry, l, levelChunkSections, blendingData); + } + + @Unique + private boolean postProcessingDone; + + @Override + public final boolean moonrise$isPostProcessingDone() { + return this.postProcessingDone; + } + + /** + * @reason Hook to set {@link #postProcessingDone} to {@code true} when post-processing completes to avoid invoking + * this function many times by the player chunk loader. + * @author Spottedlef + */ + @Inject( + method = "postProcessGeneration", + at = @At( + value = "RETURN" + ) + ) + private void finishPostProcessing(final CallbackInfo ci) { + this.postProcessingDone = true; + } + + // add support for dirty scheduled chunk ticks + @Override + public boolean isUnsaved() { + final long gameTime = this.level.getGameTime(); + if (((ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) + || ((ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { + return true; + } + + return super.isUnsaved(); + } + + @Override + public void setUnsaved(final boolean needsSaving) { + if (!needsSaving) { + ((ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty(); + ((ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty(); + } + super.setUnsaved(needsSaving); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkTicksMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkTicksMixin.java new file mode 100644 index 0000000..8213464 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelChunkTicksMixin.java @@ -0,0 +1,148 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.ticks.LevelChunkTicks; +import net.minecraft.world.ticks.SavedTick; +import net.minecraft.world.ticks.ScheduledTick; +import net.minecraft.world.ticks.SerializableTickContainer; +import net.minecraft.world.ticks.TickContainerAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.List; +import java.util.Queue; +import java.util.function.Function; + +@Mixin(LevelChunkTicks.class) +public abstract class LevelChunkTicksMixin implements ChunkSystemLevelChunkTicks, SerializableTickContainer, TickContainerAccess { + + @Shadow + @Final + private Queue> tickQueue; + + @Shadow + private List> pendingTicks; + + /* + * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks + * and the last saved tick is not equal to the current tick + */ + /* + * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a + * bit out of scope for the chunk system + */ + + + @Unique + private boolean dirty; + + @Unique + private long lastSaved; + + /** + * @reason Hook to init fields + * @author Spottedleaf + */ + @Inject( + method = "()V", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.lastSaved = Long.MIN_VALUE; + } + + @Override + public final boolean moonrise$isDirty(final long tick) { + return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); + } + + @Override + public final void moonrise$clearDirty() { + this.dirty = false; + } + + /** + * @reason Set dirty when a scheduled tick is removed + * @author Spottedleaf + */ + @Inject( + method = "poll", + at = @At( + value = "INVOKE", + target = "Ljava/util/Set;remove(Ljava/lang/Object;)Z" + ) + ) + private void pollHook(final CallbackInfoReturnable> cir) { + this.dirty = true; + } + + /** + * @reason Set dirty when a tick is scheduled + * @author Spottedleaf + */ + @Inject( + method = "schedule", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/ticks/LevelChunkTicks;scheduleUnchecked(Lnet/minecraft/world/ticks/ScheduledTick;)V" + ) + ) + private void scheduleHook(final CallbackInfo ci) { + this.dirty = true; + } + + /** + * @reason Set dirty when a tick is removed + * @author Spottedleaf + */ + @Inject( + method = "removeIf", + at = @At( + value = "INVOKE", + target = "Ljava/util/Iterator;remove()V" + ) + ) + private void removeHook(final CallbackInfo ci) { + this.dirty = true; + } + + /** + * @reason Update last save tick + * @author Spottedleaf + */ + @Inject( + method = "save(JLjava/util/function/Function;)Lnet/minecraft/nbt/ListTag;", + at = @At( + value = "HEAD" + ) + ) + private void saveHook(final long time, final Function idFunction, final CallbackInfoReturnable cir) { + this.lastSaved = time; + } + + /** + * @reason Update last save to current tick when first unpacking the chunk data + * @author Spottedleaf + */ + @Inject( + method = "unpack", + at = @At( + value = "HEAD" + ) + ) + private void unpackHook(final long tick, final CallbackInfo ci) { + if (this.pendingTicks == null) { + return; + } + + this.lastSaved = tick; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelMixin.java new file mode 100644 index 0000000..b4a0167 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelMixin.java @@ -0,0 +1,210 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup; +import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(Level.class) +public abstract class LevelMixin implements ChunkSystemLevel, ChunkSystemEntityGetter, LevelAccessor, AutoCloseable { + + @Shadow + public abstract ProfilerFiller getProfiler(); + + + @Unique + private EntityLookup entityLookup; + + @Override + public final EntityLookup moonrise$getEntityLookup() { + return this.entityLookup; + } + + @Override + public void moonrise$setEntityLookup(final EntityLookup entityLookup) { + if (this.entityLookup != null && !(this.entityLookup instanceof DefaultEntityLookup)) { + throw new IllegalStateException("Entity lookup already initialised"); + } + this.entityLookup = entityLookup; + } + + /** + * @reason Default initialise entity lookup incase mods extend Level + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.entityLookup = new DefaultEntityLookup((Level)(Object)this); + } + + /** + * @reason Route to faster lookup + * @author Spottedleaf + */ + @Overwrite + @Override + public List getEntities(final Entity entity, final AABB boundingBox, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entity, boundingBox, ret, predicate); + + return ret; + } + + /** + * @reason Route to faster lookup + * @author Spottedleaf + */ + @Overwrite + public void getEntities(final EntityTypeTest entityTypeTest, + final AABB boundingBox, final Predicate predicate, + final List into, final int maxCount) { + this.getProfiler().incrementCounter("getEntities"); + + if (entityTypeTest instanceof EntityType byType) { + if (maxCount != Integer.MAX_VALUE) { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount); + return; + } else { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate); + return; + } + } + + if (entityTypeTest == null) { + if (maxCount != Integer.MAX_VALUE) { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount); + return; + } else { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate); + return; + } + } + + final Class base = entityTypeTest.getBaseClass(); + + final Predicate modifiedPredicate; + if (predicate == null) { + modifiedPredicate = (final T obj) -> { + return entityTypeTest.tryCast(obj) != null; + }; + } else { + modifiedPredicate = (final Entity obj) -> { + final T casted = entityTypeTest.tryCast(obj); + if (casted == null) { + return false; + } + + return predicate.test(casted); + }; + } + + if (base == null || base == Entity.class) { + if (maxCount != Integer.MAX_VALUE) { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + return; + } else { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); + return; + } + } else { + if (maxCount != Integer.MAX_VALUE) { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); + return; + } else { + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); + return; + } + } + } + + /** + * Route to faster lookup + * @author Spottedleaf + */ + @Override + public final List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate); + + return ret; + } + + /** + * Route to faster lookup + * @author Spottedleaf + */ + @Override + public final List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + final List ret = new ArrayList<>(); + + ((ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate); + + return ret; + } + + @Override + public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { + return this.getChunkSource().getChunk(chunkX, chunkZ, false); + } + + @Override + public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { + return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false); + } + + @Override + public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) { + return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false); + } + + /** + * @reason Allow block updates in non-ticking chunks, as new chunk system sends non-ticking chunks to clients + * @author Spottedleaf + */ + @Redirect( + method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/FullChunkStatus;isOrAfter(Lnet/minecraft/server/level/FullChunkStatus;)Z" + ) + ) + private boolean sendUpdatesForFullChunks(final FullChunkStatus instance, + final FullChunkStatus fullChunkStatus) { + + return instance.isOrAfter(FullChunkStatus.FULL); + } + + // TODO: Thread.currentThread() != this.thread to TickThread? +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelReaderMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelReaderMixin.java new file mode 100644 index 0000000..4e43d2b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/LevelReaderMixin.java @@ -0,0 +1,24 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.CollisionGetter; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.SignalGetter; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(LevelReader.class) +public interface LevelReaderMixin extends ChunkSystemLevelReader, BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource { + + @Override + public default ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { + if (status == null || status.isOrAfter(ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status.toString()); + } + return ((LevelReader)this).getChunk(chunkX, chunkZ, status, true); + } + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/MinecraftServerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/MinecraftServerMixin.java new file mode 100644 index 0000000..da42c68 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/MinecraftServerMixin.java @@ -0,0 +1,167 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer; +import net.minecraft.commands.CommandSource; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerInfo; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin extends ReentrantBlockableEventLoop implements ChunkSystemMinecraftServer, ServerInfo, CommandSource, AutoCloseable { + + @Shadow + public abstract Iterable getAllLevels(); + + @Shadow + public abstract boolean saveAllChunks(boolean bl, boolean bl2, boolean bl3); + + + public MinecraftServerMixin(String string) { + super(string); + } + + @Unique + private volatile Throwable chunkSystemCrash; + + @Override + public final void moonrise$setChunkSystemCrash(final Throwable throwable) { + this.chunkSystemCrash = throwable; + } + + /** + * @reason Force response to chunk system crash + * @author Spottedleaf + */ + @Inject( + method = "runServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", + shift = At.Shift.AFTER + ) + ) + private void hookChunkSystemCrash(final CallbackInfo ci) { + final Throwable crash = this.chunkSystemCrash; + if (crash != null) { + this.chunkSystemCrash = null; + throw new RuntimeException("Chunk system crash propagated to tick()", crash); + } + } + + /** + * @reason Initialise chunk system threads hook + * @author Spottedleaf + */ + @Inject( + method = "spin", + at = @At( + value = "HEAD" + ) + ) + private static void initHook(Function function, CallbackInfoReturnable cir) { + // TODO better place? + ChunkTaskScheduler.init(); + } + + /** + * @reason Make server thread an instance of TickThread for thread checks + * @author Spottedleaf + */ + @Redirect( + method = "spin", + at = @At( + value = "NEW", + target = "(Ljava/lang/Runnable;Ljava/lang/String;)Ljava/lang/Thread;" + ) + ) + private static Thread createTickThread(final Runnable target, final String name) { + return new TickThread(target, name); + } + + + /** + * @reason Close logic is re-written so that we do not wait for tasks to complete and unload everything + * but rather we halt all task processing and then save. + * The reason this is done is that the server may not be in a state where the chunk system can + * complete its tasks, which would prevent the saving of any data. The new close logic will ensure + * that if the system is deadlocked that both a full save will occur and that the server will halt. + * @author Spottedleaf + */ + @Redirect( + method = "stopServer", + at = @At( + value = "INVOKE", + target = "Ljava/util/stream/Stream;anyMatch(Ljava/util/function/Predicate;)Z", + ordinal = 0 + ) + ) + private boolean doNotWaitChunkSystemShutdown(final Stream instance, final Predicate predicate) { + return false; + } + + /** + * @reason Mark all ServerLevel instances as being closed so that saveAllChunks can perform a close-save operation. + * Additionally, sets force = true, so that the save call is guaranteed to be converted into a close-save call. + * @author Spottedleaf + */ + @Redirect( + method = "stopServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;saveAllChunks(ZZZ)Z" + ) + ) + private boolean markClosed(final MinecraftServer instance, boolean bl, boolean bl2, boolean bl3) { + for (final ServerLevel world : this.getAllLevels()) { + ((ChunkSystemServerLevel)world).moonrise$setMarkedClosing(true); + } + // !log, flush, force + return this.saveAllChunks(false, true, true); + } + + /** + * @reason Close is handled above + * @author Spottedleaf + */ + @Redirect( + method = "stopServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;close()V", + ordinal = 0 + ) + ) + private void noOpClose(final ServerLevel instance) {} + + /** + * @reason Halt regionfile threads after everything is closed + * @author Spottedleaf + */ + @Inject( + method = "stopServer", + at = @At( + value = "RETURN" + ) + ) + private void closeIOThreads(final CallbackInfo ci) { + // TODO reinit code needs to be put somewhere + RegionFileIOThread.deinit(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/NoiseBasedChunkGeneratorMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/NoiseBasedChunkGeneratorMixin.java new file mode 100644 index 0000000..5126b38 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/NoiseBasedChunkGeneratorMixin.java @@ -0,0 +1,104 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.real_dumb_shit.HolderCompletableFuture; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +@Mixin(NoiseBasedChunkGenerator.class) +public abstract class NoiseBasedChunkGeneratorMixin { + + /** + * @reason Pass the supplier to the mixin below so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Redirect( + method = "createBiomes", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture passSupplierBiomes(Supplier supplier, Executor executor) { + return (CompletableFuture)CompletableFuture.completedFuture(supplier); + } + + /** + * @reason Retrieve the supplier from the mixin above so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Inject( + method = "createBiomes", + cancellable = true, + at = @At( + value = "RETURN" + ) + ) + private void unpackSupplierBiomes(Executor executor, RandomState randomState, Blender blender, + StructureManager structureManager, ChunkAccess chunkAccess, + CallbackInfoReturnable> cir) { + cir.setReturnValue( + CompletableFuture.supplyAsync(((CompletableFuture>)(CompletableFuture)cir.getReturnValue()).join(), executor) + ); + } + + + /** + * @reason Pass the executor tasks to the mixin below so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Redirect( + method = "fillFromNoise", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture passSupplierNoise(Supplier supplier, Executor executor) { + final HolderCompletableFuture ret = new HolderCompletableFuture<>(); + + ret.toExecute.add(() -> { + try { + ret.complete(supplier.get()); + } catch (final Throwable throwable) { + ret.completeExceptionally(throwable); + } + }); + + return ret; + } + + /** + * @reason Retrieve the executor tasks from the mixin above so that we can change the executor to the parameter provided + * @author Spottedleaf + */ + @Redirect( + method = "fillFromNoise", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;whenCompleteAsync(Ljava/util/function/BiConsumer;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture unpackSupplierNoise(final CompletableFuture instance, final BiConsumer action, + final Executor executor) { + final HolderCompletableFuture casted = (HolderCompletableFuture)instance; + + for (final Runnable run : casted.toExecute) { + executor.execute(run); + } + + // note: executor is the parameter we want + return instance.whenCompleteAsync(action, executor); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/OptionsMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/OptionsMixin.java new file mode 100644 index 0000000..ada63c4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/OptionsMixin.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.util.MoonriseConstants; +import net.minecraft.client.Options; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(Options.class) +public abstract class OptionsMixin { + + /** + * @reason Allow higher view distances + * @author Spottedleaf + */ + @ModifyConstant( + method = "", + constant = @Constant( + intValue = 32, ordinal = 1 + ) + ) + private int replaceViewDistanceConstant(final int constant) { + return MoonriseConstants.MAX_VIEW_DISTANCE; + } + + /** + * @reason Allow higher view distances + * @author Spottedleaf + */ + @ModifyConstant( + method = "", + constant = @Constant( + intValue = 32, ordinal = 2 + ) + ) + private int replaceSimulationDistanceConstant(final int constant) { + return MoonriseConstants.MAX_VIEW_DISTANCE; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PlayerListMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PlayerListMixin.java new file mode 100644 index 0000000..48dfd00 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PlayerListMixin.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerList.class) +public abstract class PlayerListMixin { + + /** + * @reason Mark the player as "real", which enables chunk loading + * @author Spottedleaf + */ + @Inject( + method = "placeNewPlayer", + at = @At( + value = "HEAD" + ) + ) + private void initRealPlayer(final Connection connection, final ServerPlayer serverPlayer, + final CommonListenerCookie commonListenerCookie, final CallbackInfo ci) { + ((ChunkSystemServerPlayer)serverPlayer).moonrise$setRealPlayer(true); + } + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiManagerMixin.java new file mode 100644 index 0000000..8e9a465 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiManagerMixin.java @@ -0,0 +1,278 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Codec; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@Mixin(PoiManager.class) +public abstract class PoiManagerMixin extends SectionStorage implements ChunkSystemPoiManager { + @Shadow + abstract boolean isVillageCenter(long l); + + @Shadow + public abstract void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection); + + public PoiManagerMixin(SimpleRegionStorage simpleRegionStorage, Function> function, Function function2, RegistryAccess registryAccess, LevelHeightAccessor levelHeightAccessor) { + super(simpleRegionStorage, function, function2, registryAccess, levelHeightAccessor); + } + + @Unique + private ServerLevel world; + + // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes + // to support poi unloading + @Unique + private Delayed26WayDistancePropagator3D villageDistanceTracker; + + @Unique + private static final int POI_DATA_SOURCE = 7; + + @Unique + private static int convertBetweenLevels(final int level) { + return POI_DATA_SOURCE - level; + } + + @Unique + private void updateDistanceTracking(long section) { + if (this.isVillageCenter(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } + } + + /** + * @reason Initialise fields + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(RegionStorageInfo regionStorageInfo, Path path, DataFixer dataFixer, boolean bl, RegistryAccess registryAccess, + LevelHeightAccessor levelHeightAccessor, CallbackInfo ci) { + this.world = (ServerLevel)levelHeightAccessor; + this.villageDistanceTracker = new Delayed26WayDistancePropagator3D(); + } + + /** + * @reason Replace vanilla tracker + * @author Spottedleaf + */ + @Overwrite + public int sectionsToVillage(final SectionPos pos) { + this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util + return convertBetweenLevels(this.villageDistanceTracker.getLevel(CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util + } + + /** + * @reason Replace vanilla tracker and avoid superclass poi data writing (which is now handled by chunk autosave) + * @author Spottedleaf + */ + @Overwrite + public void tick(final BooleanSupplier shouldKeepTicking) { + this.villageDistanceTracker.propagateUpdates(); + } + + /** + * @reason Replace vanilla tracker, mark poi chunk as dirty + * @author Spottedleaf + */ + @Override + @Overwrite + public void setDirty(final long pos) { + final int chunkX = CoordinateUtils.getChunkSectionX(pos); + final int chunkZ = CoordinateUtils.getChunkSectionZ(pos); + final ChunkHolderManager manager = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + final PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false); + if (chunk != null) { + chunk.setDirty(true); + } + this.updateDistanceTracking(pos); + } + + /** + * @reason Replace vanilla tracker + * @author Spottedleaf + */ + @Override + @Overwrite + public void onSectionLoad(final long pos) { + this.updateDistanceTracking(pos); + } + + @Override + public Optional get(final long pos) { + final int chunkX = CoordinateUtils.getChunkSectionX(pos); + final int chunkY = CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = CoordinateUtils.getChunkSectionZ(pos); + + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ChunkHolderManager manager = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + final PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + + return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); + } + + @Override + public Optional getOrLoad(final long pos) { + final int chunkX = CoordinateUtils.getChunkSectionX(pos); + final int chunkY = CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = CoordinateUtils.getChunkSectionZ(pos); + + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ChunkHolderManager manager = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + + if (chunkY >= WorldUtil.getMinSection(this.world) && chunkY <= WorldUtil.getMaxSection(this.world)) { + final PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + if (ret != null) { + return ret.getSectionForVanilla(chunkY); + } else { + return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); + } + } + // retain vanilla behavior: do not load section if out of bounds! + return Optional.empty(); + } + + @Override + protected PoiSection getOrCreate(final long pos) { + final int chunkX = CoordinateUtils.getChunkSectionX(pos); + final int chunkY = CoordinateUtils.getChunkSectionY(pos); + final int chunkZ = CoordinateUtils.getChunkSectionZ(pos); + + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); + + final ChunkHolderManager manager = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager; + + final PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); + if (ret != null) { + return ret.getOrCreateSection(chunkY); + } else { + return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); + } + } + + @Override + public final ServerLevel moonrise$getWorld() { + return this.world; + } + + @Override + public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); + for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) { + final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ); + this.updateDistanceTracking(sectionPos); + } + } + + @Override + public final void moonrise$loadInPoiChunk(final PoiChunk poiChunk) { + final int chunkX = poiChunk.chunkX; + final int chunkZ = poiChunk.chunkZ; + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); + for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) { + final PoiSection section = poiChunk.getSection(sectionY); + if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) { + this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); + } + } + } + + @Override + public final void moonrise$checkConsistency(final ChunkAccess chunk) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + final int minY = WorldUtil.getMinSection(chunk); + final int maxY = WorldUtil.getMaxSection(chunk); + final LevelChunkSection[] sections = chunk.getSections(); + for (int section = minY; section <= maxY; ++section) { + this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]); + } + } + + /** + * @reason The loaded field is unused, so adding entries needlessly consumes memory. + * @author Spottedleaf + */ + @Redirect( + method = "ensureLoadedAndValid", + at = @At( + value = "INVOKE", + target = "Ljava/util/stream/Stream;filter(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;", + ordinal = 1 + ) + ) + private Stream skipLoadedSet(final Stream instance, final Predicate predicate) { + return instance; + } + + @Override + public final void moonrise$close() throws IOException {} + + @Override + public final CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws IOException { + if (!RegionFileIOThread.isRegionFileThread()) { + return RegionFileIOThread.loadData( + this.world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, + RegionFileIOThread.getIOBlockingPriorityForCurrentThread() + ); + } + return this.moonrise$getRegionStorage().read(new ChunkPos(chunkX, chunkZ)); + } + + @Override + public final void moonrise$write(final int chunkX, final int chunkZ, final CompoundTag data) throws IOException { + if (!RegionFileIOThread.isRegionFileThread()) { + RegionFileIOThread.scheduleSave(this.world, chunkX, chunkZ, data, RegionFileIOThread.RegionFileType.POI_DATA); + return; + } + this.moonrise$getRegionStorage().write(new ChunkPos(chunkX, chunkZ), data); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiSectionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiSectionMixin.java new file mode 100644 index 0000000..2ba9a64 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/PoiSectionMixin.java @@ -0,0 +1,62 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@Mixin(PoiSection.class) +public abstract class PoiSectionMixin implements ChunkSystemPoiSection { + + @Shadow + private boolean isValid; + + @Shadow + @Final + private Short2ObjectMap records; + + @Shadow + @Final + public Map, Set> byType; + + + @Unique + private Optional noAllocOptional; + + /** + * @reason Initialise fields + * @author Spottedleaf + */ + @Inject( + method = "(Ljava/lang/Runnable;ZLjava/util/List;)V", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.noAllocOptional = Optional.of((PoiSection)(Object)this); + } + + @Override + public final boolean moonrise$isEmpty() { + return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); + } + + @Override + public final Optional moonrise$asOptional() { + return this.noAllocOptional; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileMixin.java new file mode 100644 index 0000000..020bd5d --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileMixin.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import net.minecraft.world.level.chunk.storage.RegionFile; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(RegionFile.class) +public abstract class RegionFileMixin { + + // TODO can't really add synchronized to methods, can we? + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileStorageMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileStorageMixin.java new file mode 100644 index 0000000..3c69e62 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/RegionFileStorageMixin.java @@ -0,0 +1,306 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import net.minecraft.FileUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StreamTagVisitor; +import net.minecraft.util.ExceptionCollector; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +@Mixin(RegionFileStorage.class) +public abstract class RegionFileStorageMixin implements ChunkSystemRegionFileStorage, AutoCloseable { + + @Shadow + @Final + private Long2ObjectLinkedOpenHashMap regionCache; + + @Shadow + @Final + private static int MAX_CACHE_SIZE; + + @Shadow + @Final + private Path folder; + + @Shadow + @Final + private boolean sync; + + @Shadow + @Final + private RegionStorageInfo info; + + + @Unique + private static final int REGION_SHIFT = 5; + + @Unique + private static final int MAX_NON_EXISTING_CACHE = 1024 * 64; + + @Unique + private final LongLinkedOpenHashSet nonExistingRegionFiles = new LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1); + + @Unique + private static String getRegionFileName(final int chunkX, final int chunkZ) { + return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca"; + } + + @Unique + private boolean doesRegionFilePossiblyExist(final long position) { + synchronized (this.nonExistingRegionFiles) { + if (this.nonExistingRegionFiles.contains(position)) { + this.nonExistingRegionFiles.addAndMoveToFirst(position); + return false; + } + return true; + } + } + + @Unique + private void createRegionFile(final long position) { + synchronized (this.nonExistingRegionFiles) { + this.nonExistingRegionFiles.remove(position); + } + } + + @Unique + private void markNonExisting(final long position) { + synchronized (this.nonExistingRegionFiles) { + if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) { + while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) { + this.nonExistingRegionFiles.removeLastLong(); + } + } + } + } + + @Override + public final boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ) { + return !this.doesRegionFilePossiblyExist(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); + } + + @Override + public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { + return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); + } + + @Override + public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { + final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + + RegionFile ret = this.regionCache.getAndMoveToFirst(key); + if (ret != null) { + return ret; + } + + if (!this.doesRegionFilePossiblyExist(key)) { + return null; + } + + if (this.regionCache.size() >= MAX_CACHE_SIZE) { + this.regionCache.removeLast().close(); + } + + final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ)); + + if (!Files.exists(regionPath)) { + this.markNonExisting(key); + return null; + } + + this.createRegionFile(key); + + FileUtil.createDirectoriesSafe(this.folder); + + ret = new RegionFile(this.info, regionPath, this.folder, this.sync); + + this.regionCache.putAndMoveToFirst(key, ret); + + return ret; + } + + /** + * @reason Make this method thread-safe, and add in support for storing when regionfiles do not exist + * @author Spottedleaf + */ + @Overwrite + public final RegionFile getRegionFile(final ChunkPos chunkPos) throws IOException { + synchronized (this) { + final long key = ChunkPos.asLong(chunkPos.x >> REGION_SHIFT, chunkPos.z >> REGION_SHIFT); + + RegionFile ret = this.regionCache.getAndMoveToFirst(key); + if (ret != null) { + return ret; + } + + if (this.regionCache.size() >= MAX_CACHE_SIZE) { + this.regionCache.removeLast().close(); + } + + final Path regionPath = this.folder.resolve(getRegionFileName(chunkPos.x, chunkPos.z)); + + this.createRegionFile(key); + + FileUtil.createDirectoriesSafe(this.folder); + + ret = new RegionFile(this.info, regionPath, this.folder, this.sync); + + this.regionCache.putAndMoveToFirst(key, ret); + + return ret; + } + } + + /** + * @reason Make this method thread-safe + * @author Spottedleaf + */ + @Override + @Overwrite + public void close() throws IOException { + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); + for (final RegionFile regionFile : this.regionCache.values()) { + try { + regionFile.close(); + } catch (final IOException ex) { + exceptionCollector.add(ex); + } + } + + exceptionCollector.throwIfPresent(); + } + } + + /** + * @reason Make this method thread-safe + * @author Spottedleaf + */ + @Overwrite + public void flush() throws IOException { + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); + for (final RegionFile regionFile : this.regionCache.values()) { + try { + regionFile.flush(); + } catch (final IOException ex) { + exceptionCollector.add(ex); + } + } + + exceptionCollector.throwIfPresent(); + } + } + + /** + * @reason Avoid creating RegionFiles on read when they do not exist + * @author Spottedleaf + */ + @Redirect( + method = "read", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/RegionFileStorage;getRegionFile(Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/world/level/chunk/storage/RegionFile;" + ) + ) + private RegionFile avoidCreatingReadRegionFile(final RegionFileStorage instance, final ChunkPos chunkPos) throws IOException { + return ((RegionFileStorageMixin)(Object)instance).moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z); + } + + /** + * @reason Avoid creating RegionFiles on read when they do not exist, this hook is required to exit early when + * the RegionFile does not exist. + * @author Spottedleaf + */ + @Inject( + method = "read", + cancellable = true, + locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/RegionFile;getChunkDataInputStream(Lnet/minecraft/world/level/ChunkPos;)Ljava/io/DataInputStream;" + ) + ) + private void avoidCreatingReadRegionFileExit(final ChunkPos chunkPos, final CallbackInfoReturnable cir, + final RegionFile regionFile) { + if (regionFile == null) { + cir.setReturnValue(null); + return; + } + } + + /** + * @reason Avoid creating RegionFiles on scan when they do not exist + * @author Spottedleaf + */ + @Redirect( + method = "scanChunk", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/RegionFileStorage;getRegionFile(Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/world/level/chunk/storage/RegionFile;" + ) + ) + private RegionFile avoidCreatingScanRegionFile(final RegionFileStorage instance, final ChunkPos chunkPos) throws IOException { + return ((RegionFileStorageMixin)(Object)instance).moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z); + } + + /** + * @reason Avoid creating RegionFiles on scan when they do not exist, this hook is required to exit early when + * the RegionFile does not exist. + * @author Spottedleaf + */ + @Inject( + method = "scanChunk", + cancellable = true, + locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/RegionFile;getChunkDataInputStream(Lnet/minecraft/world/level/ChunkPos;)Ljava/io/DataInputStream;" + ) + ) + private void avoidCreatingScanRegionFileExit(final ChunkPos chunkPos, final StreamTagVisitor streamTagVisitor, + final CallbackInfo ci, final RegionFile regionFile) { + if (regionFile == null) { + ci.cancel(); + return; + } + } + + /** + * @reason Avoid creating RegionFiles on write when the input value is null (indicating a delete operation) + * @author Spottedleaf + */ + @Inject( + method = "write", + cancellable = true, + at = @At( + value = "HEAD" + ) + ) + private void avoidCreatingWriteRegionFile(final ChunkPos chunkPos, final CompoundTag compoundTag, final CallbackInfo ci) throws IOException { + if (compoundTag == null && this.moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z) == null) { + ci.cancel(); + return; + } + // double reading the RegionFile is fine, as the result is cached + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SectionStorageMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SectionStorageMixin.java new file mode 100644 index 0000000..279ede4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SectionStorageMixin.java @@ -0,0 +1,116 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin implements ChunkSystemSectionStorage, AutoCloseable { + + @Shadow + private SimpleRegionStorage simpleRegionStorage; + + @Shadow + @Final + private static Logger LOGGER; + + + @Unique + private RegionFileStorage storage; + + @Override + public final RegionFileStorage moonrise$getRegionStorage() { + return this.storage; + } + + /** + * @reason Retrieve storage from IOWorker, and then nuke it + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.storage = this.simpleRegionStorage.worker.storage; + this.simpleRegionStorage = null; + } + + /** + * @reason Route to new chunk system hook + * @author Spottedleaf + */ + @Overwrite + public final CompletableFuture> tryRead(final ChunkPos pos) { + try { + return CompletableFuture.completedFuture(Optional.ofNullable(this.moonrise$read(pos.x, pos.z))); + } catch (final Throwable thr) { + return CompletableFuture.failedFuture(thr); + } + } + + /** + * @reason Destroy old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public void readColumn(final ChunkPos pos, final RegistryOps ops, final CompoundTag data) { + throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); + } + + /** + * @reason Route to new chunk system hook + * @author Spottedleaf + */ + @Redirect( + method = "writeColumn(Lnet/minecraft/world/level/ChunkPos;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/SimpleRegionStorage;write(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/nbt/CompoundTag;)Ljava/util/concurrent/CompletableFuture;" + ) + ) + private CompletableFuture redirectWrite(final SimpleRegionStorage instance, final ChunkPos pos, + final CompoundTag tag) { + try { + this.moonrise$write(pos.x, pos.z, tag); + } catch (final IOException ex) { + LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex); + } + return null; + } + + /** + * @reason Route to new chunk system hook + * @author Spottedleaf + */ + @Redirect( + method = "close", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/storage/SimpleRegionStorage;close()V" + ) + ) + private void redirectClose(final SimpleRegionStorage instance) throws IOException { + this.moonrise$close(); + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCache$MainThreadExecutorMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCache$MainThreadExecutorMixin.java new file mode 100644 index 0000000..b0fd650 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCache$MainThreadExecutorMixin.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.util.thread.BlockableEventLoop; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ServerChunkCache.MainThreadExecutor.class) +public abstract class ServerChunkCache$MainThreadExecutorMixin extends BlockableEventLoop { + + @Shadow + @Final + ServerChunkCache field_18810; + + protected ServerChunkCache$MainThreadExecutorMixin(String string) { + super(string); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public boolean pollTask() { + final ServerChunkCache serverChunkCache = this.field_18810; + if (serverChunkCache.runDistanceManagerUpdates()) { + return true; + } else { + return super.pollTask() | ((ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCacheMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCacheMixin.java new file mode 100644 index 0000000..b8cbca6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerChunkCacheMixin.java @@ -0,0 +1,254 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LightChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheMixin extends ChunkSource { + + @Shadow + @Final + public ServerChunkCache.MainThreadExecutor mainThreadProcessor; + + + @Shadow + @Final + public ServerLevel level; + + + @Unique + private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { + final ChunkTaskScheduler chunkTaskScheduler = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final CompletableFuture completable = new CompletableFuture<>(); + chunkTaskScheduler.scheduleChunkLoad( + chunkX, chunkZ, toStatus, true, PrioritisedExecutor.Priority.BLOCKING, + completable::complete + ); + + if (TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) { + this.mainThreadProcessor.managedBlock(completable::isDone); + } + + final ChunkAccess ret = completable.join(); + if (ret == null) { + throw new IllegalStateException("Chunk not loaded when requested"); + } + + return ret; + } + + /** + * @reason Optimise impl and support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public ChunkAccess getChunk(final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean load) { + final ChunkTaskScheduler chunkTaskScheduler = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; + + final NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (toStatus == ChunkStatus.FULL) { + if (currentChunk != null && currentChunk.isFullChunkReady() && (currentChunk.getCurrentChunk() instanceof LevelChunk fullChunk)) { + return fullChunk; + } else if (!load) { + return null; + } + return this.syncLoad(chunkX, chunkZ, toStatus); + } else { + final NewChunkHolder.ChunkCompletion lastCompletion; + if (currentChunk != null && (lastCompletion = currentChunk.getLastChunkCompletion()) != null && + lastCompletion.genStatus().isOrAfter(toStatus)) { + return lastCompletion.chunk(); + } else if (!load) { + return null; + } + return this.syncLoad(chunkX, chunkZ, toStatus); + } + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public LevelChunk getChunkNow(final int chunkX, final int chunkZ) { + return ((ChunkSystemServerLevel)this.level).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public boolean hasChunk(final int chunkX, final int chunkZ) { + return this.getChunkNow(chunkX, chunkZ) != null; + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Overwrite + public CompletableFuture> getChunkFutureMainThread(final int chunkX, final int chunkZ, + final ChunkStatus toStatus, + final boolean create) { + TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); + + final int minLevel = ChunkLevel.byStatus(toStatus); + final NewChunkHolder chunkHolder = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); + + final boolean needsFullScheduling = toStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); + + if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { + return ChunkHolder.UNLOADED_CHUNK_FUTURE; + } + + final NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion(); + if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(toStatus)) { + // schedule + CompletableFuture> ret = new CompletableFuture<>(); + Consumer complete = (ChunkAccess chunk) -> { + if (chunk == null) { + ret.complete(ChunkHolder.UNLOADED_CHUNK); + } else { + ret.complete(ChunkResult.of(chunk)); + } + }; + + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( + chunkX, chunkZ, toStatus, true, + PrioritisedExecutor.Priority.HIGHER, + complete + ); + + return ret; + } else { + // can return now + return CompletableFuture.completedFuture(ChunkResult.of(chunkCompletion.chunk())); + } + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public LightChunk getChunkForLighting(final int chunkX, final int chunkZ) { + final NewChunkHolder newChunkHolder = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); + if (newChunkHolder == null) { + return null; + } + final NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); + if (lastCompletion == null || !lastCompletion.genStatus().isOrAfter(ChunkStatus.INITIALIZE_LIGHT)) { + return null; + } + return lastCompletion.chunk(); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean runDistanceManagerUpdates() { + return ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean isPositionTicking(final long los) { + final NewChunkHolder newChunkHolder = ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(los); + return newChunkHolder != null && newChunkHolder.isTickingReady(); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public void close() throws IOException { + ((ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true); + } + + /** + * @reason Add hook to tick player chunk loader + * @author Spottedleaf + */ + @Inject( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerChunkCache;tickChunks()V" + ) + ) + private void tickHook(final CallbackInfo ci) { + ((ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); + } + + /** + * @reason Support new chunk system + * @author Spottedleaf + */ + @Overwrite + public void getFullChunk(final long pos, final Consumer consumer) { + final LevelChunk fullChunk = this.getChunkNow(CoordinateUtils.getChunkX(pos), CoordinateUtils.getChunkZ(pos)); + if (fullChunk != null) { + consumer.accept(fullChunk); + } + } + + /** + * @reason Do not run distance manager updates on save. They are not required to run in the new chunk system. + * Additionally, distance manager updates may not complete if some error has occurred in the propagator + * code or there is deadlock. Thus, on shutdown we want to avoid stalling. + * @author Spottedleaf + */ + @Redirect( + method = "save", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerChunkCache;runDistanceManagerUpdates()Z" + ) + ) + private boolean skipSaveTicketUpdates(final ServerChunkCache instance) { + return false; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerLevelMixin.java new file mode 100644 index 0000000..ac9beed --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerLevelMixin.java @@ -0,0 +1,604 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController; +import ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController; +import ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.RandomSequences; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.CustomSpawner; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.WritableLevelData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@Mixin(ServerLevel.class) +public abstract class ServerLevelMixin extends Level implements ChunkSystemServerLevel, ChunkSystemLevelReader, WorldGenLevel { + + @Shadow + private PersistentEntitySectionManager entityManager; + + protected ServerLevelMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + @Unique + private boolean markedClosing; + + @Unique + private final RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new RegionizedPlayerChunkLoader.ViewDistanceHolder(); + + @Unique + private final RegionizedPlayerChunkLoader chunkLoader = new RegionizedPlayerChunkLoader((ServerLevel)(Object)this); + + @Unique + private EntityDataController entityDataController; + + @Unique + private PoiDataController poiDataController; + + @Unique + private ChunkDataController chunkDataController; + + @Unique + private ChunkTaskScheduler chunkTaskScheduler; + + /** + * @reason Initialise fields / destroy entity manager state + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(MinecraftServer minecraftServer, Executor executor, + LevelStorageSource.LevelStorageAccess levelStorageAccess, ServerLevelData serverLevelData, + ResourceKey resourceKey, LevelStem levelStem, ChunkProgressListener chunkProgressListener, + boolean bl, long l, List list, boolean bl2, RandomSequences randomSequences, + CallbackInfo ci) { + this.entityManager = null; + + this.entityDataController = new EntityDataController( + new EntityDataController.EntityRegionFileStorage( + new RegionStorageInfo(levelStorageAccess.getLevelId(), resourceKey, "entities"), + levelStorageAccess.getDimensionPath(resourceKey).resolve("entities"), + minecraftServer.forceSynchronousWrites() + ) + ); + this.poiDataController = new PoiDataController((ServerLevel)(Object)this); + this.chunkDataController = new ChunkDataController((ServerLevel)(Object)this); + this.moonrise$setEntityLookup(new ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks())); + this.chunkTaskScheduler = new ChunkTaskScheduler((ServerLevel)(Object)this, ChunkTaskScheduler.workerThreads); + } + + @Override + public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { + final NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (!newChunkHolder.isFullChunkReady()) { + return null; + } + + if (newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { + return levelChunk; + } + // race condition: chunk unloaded, only happens off-main + return null; + } + + @Override + public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { + final NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (newChunkHolder == null) { + return null; + } + final NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); + return lastCompletion == null ? null : lastCompletion.chunk(); + } + + @Override + public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) { + final NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); + if (newChunkHolder == null) { + return null; + } + final NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); + return lastCompletion == null || !lastCompletion.genStatus().isOrAfter(leastStatus) ? null : lastCompletion.chunk(); + } + + @Override + public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { + return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status); + } + + @Override + public final ChunkTaskScheduler moonrise$getChunkTaskScheduler() { + return this.chunkTaskScheduler; + } + + @Override + public final RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() { + return this.chunkDataController; + } + + @Override + public final RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() { + return this.poiDataController; + } + + @Override + public final RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() { + return this.entityDataController; + } + + @Override + public final int moonrise$getRegionChunkShift() { + // current default in Folia + // note that there is no actual regionizing taking place in Moonrise... + return 2; + } + + @Override + public final boolean moonrise$isMarkedClosing() { + return this.markedClosing; + } + + @Override + public final void moonrise$setMarkedClosing(final boolean value) { + this.markedClosing = value; + } + + @Override + public final RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() { + return this.chunkLoader; + } + + @Override + public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final PrioritisedExecutor.Priority priority, + final Consumer> onLoad) { + this.moonrise$loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + priority, onLoad + ); + } + + @Override + public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, + final Consumer> onLoad) { + this.moonrise$loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + chunkStatus, priority, onLoad + ); + } + + @Override + public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final PrioritisedExecutor.Priority priority, + final Consumer> onLoad) { + this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, ChunkStatus.FULL, priority, onLoad); + } + + @Override + public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, + final Consumer> onLoad) { + final ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler(); + final ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; + + final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); + final AtomicInteger loadedChunks = new AtomicInteger(); + final Long holderIdentifier = ChunkTaskScheduler.getNextChunkLoadId(); + final int ticketLevel = ChunkTaskScheduler.getTicketLevel(chunkStatus); + + final List ret = new ArrayList<>(requiredChunks); + + final Consumer consumer = (final ChunkAccess chunk) -> { + if (chunk != null) { + synchronized (ret) { + ret.add(chunk); + } + chunkHolderManager.addTicketAtLevel(ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier); + } + if (loadedChunks.incrementAndGet() == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + final ChunkPos chunkPos = ret.get(i).getPos(); + + chunkHolderManager.removeTicketAtLevel(ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier); + } + } + } + }; + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer); + } + } + } + + @Override + public final RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() { + return this.viewDistanceHolder; + } + + /** + * @reason Entities are guaranteed to be ticking in the new chunk system + * @author Spottedleaf + */ + @Redirect( + method = "method_31420", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager;inEntityTickingRange(J)Z" + ) + ) + private boolean shortCircuitTickCheck(final DistanceManager instance, final long chunk) { + return true; + } + + /** + * @reason This logic is handled by the chunk system + * @author Spottedleaf + */ + @Redirect( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;tick()V" + ) + ) + private void redirectEntityManagerTick(final PersistentEntitySectionManager instance) {} + + /** + * @reason Optimise implementation and route to new chunk system + * @author Spottedleaf + */ + @Override + @Overwrite + public boolean shouldTickBlocksAt(final long chunkPos) { + final NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); + return holder != null && holder.isTickingReady(); + } + + /** + * @reason saveAll handled by ServerChunkCache#save + * @author Spottedleaf + */ + @Redirect( + method = "save", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;saveAll()V" + ) + ) + private void redirectSaveAll(final PersistentEntitySectionManager instance) {} + + /** + * @reason autoSave handled by ServerChunkCache#save + * @author Spottedleaf + */ + @Redirect( + method = "save", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;autoSave()V" + ) + ) + private void redirectAutoSave(final PersistentEntitySectionManager instance) {} + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "addPlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;addNewEntity(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private boolean redirectAddPlayerEntity(final PersistentEntitySectionManager instance, final T entity) { + return this.moonrise$getEntityLookup().addNewEntity((Entity)entity); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "addEntity", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;addNewEntity(Lnet/minecraft/world/level/entity/EntityAccess;)Z" + ) + ) + private boolean redirectAddEntityEntity(final PersistentEntitySectionManager instance, final T entity) { + return this.moonrise$getEntityLookup().addNewEntity((Entity)entity); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Overwrite + public boolean tryAddFreshEntityWithPassengers(Entity entity) { + final Stream stream = entity.getSelfAndPassengers().map(Entity::getUUID); + if (stream.anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { + return false; + } else { + this.addFreshEntityWithPassengers(entity); + return true; + } + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "saveDebugReport", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;gatherStats()Ljava/lang/String;" + ) + ) + private String redirectDebugStats(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup().getDebugInfo(); + } + + /** + * @reason dumpChunks not implemented + * @author Spottedleaf + */ + @Redirect( + method = "saveDebugReport", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ChunkMap;dumpChunks(Ljava/io/Writer;)V" + ) + ) + private void redirectChunkMapDebug(final ChunkMap instance, final Writer writer) {} + + /** + * @reason dumpSections not implemented + * @author Spottedleaf + */ + @Redirect( + method = "saveDebugReport", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;dumpSections(Ljava/io/Writer;)V" + ) + ) + private void redirectEntityManagerDebug(final PersistentEntitySectionManager instance, final Writer writer) {} + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "getWatchdogStats", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;gatherStats()Ljava/lang/String;" + ) + ) + private String redirectWatchdogStats1(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup().getDebugInfo(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "getWatchdogStats", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;getEntityGetter()Lnet/minecraft/world/level/entity/LevelEntityGetter;" + ) + ) + private LevelEntityGetter redirectWatchdogStats2(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;getEntityGetter()Lnet/minecraft/world/level/entity/LevelEntityGetter;" + ) + ) + private LevelEntityGetter redirectGetEntities(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "addLegacyChunkEntities", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;addLegacyChunkEntities(Ljava/util/stream/Stream;)V" + ) + ) + private void redirectLegacyChunkEntities(final PersistentEntitySectionManager instance, + final Stream stream) { + this.moonrise$getEntityLookup().addLegacyChunkEntities(stream.toList(), null); // TODO + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "addWorldGenChunkEntities", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;addWorldGenChunkEntities(Ljava/util/stream/Stream;)V" + ) + ) + private void redirectWorldGenChunkEntities(final PersistentEntitySectionManager instance, + final Stream stream) { + this.moonrise$getEntityLookup().addWorldGenChunkEntities(stream.toList(), null); // TODO + } + + /** + * @reason Level close now handles this + * @author Spottedleaf + */ + @Redirect( + method = "close", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;close()V" + ) + ) + private void redirectClose(final PersistentEntitySectionManager instance) {} + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "gatherChunkSourceStats", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;gatherStats()Ljava/lang/String;" + ) + ) + private String redirectGatherChunkSourceStats(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup().getDebugInfo(); + } + + /** + * @reason Redirect to chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean areEntitiesLoaded(final long chunkPos) { + // chunk loading guarantees entity loading + return this.moonrise$getAnyChunkIfLoaded(CoordinateUtils.getChunkX(chunkPos), CoordinateUtils.getChunkZ(chunkPos)) != null; + } + + /** + * @reason Redirect to chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean isPositionTickingWithEntitiesLoaded(final long chunkPos) { + final NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); + // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded + return chunkHolder != null && chunkHolder.isTickingReady(); + } + + /** + * @reason Redirect to chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean isPositionEntityTicking(BlockPos pos) { + final NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(pos)); + return chunkHolder != null && chunkHolder.isEntityTickingReady(); + } + + /** + * @reason Redirect to chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean isNaturalSpawningAllowed(final BlockPos pos) { + final NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(pos)); + return chunkHolder != null && chunkHolder.isEntityTickingReady(); + } + + /** + * @reason Redirect to chunk system + * @author Spottedleaf + */ + @Overwrite + public boolean isNaturalSpawningAllowed(final ChunkPos pos) { + final NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(CoordinateUtils.getChunkKey(pos)); + return chunkHolder != null && chunkHolder.isEntityTickingReady(); + } + + /** + * @reason Redirect to new entity manager + * @author Spottedleaf + */ + @Redirect( + method = "method_54438", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/PersistentEntitySectionManager;count()I" + ) + ) + private int redirectCrashCount(final PersistentEntitySectionManager instance) { + return this.moonrise$getEntityLookup().getEntityCount(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerPlayerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerPlayerMixin.java new file mode 100644 index 0000000..f989fdc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/ServerPlayerMixin.java @@ -0,0 +1,69 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerMixin extends Player implements ChunkSystemServerPlayer { + public ServerPlayerMixin(Level level, BlockPos blockPos, float f, GameProfile gameProfile) { + super(level, blockPos, f, gameProfile); + } + + @Unique + private boolean isRealPlayer; + + @Unique + private RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; + + @Unique + private RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder; + + /** + * @reason Initialise fields + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void init(final CallbackInfo ci) { + this.viewDistanceHolder = new RegionizedPlayerChunkLoader.ViewDistanceHolder(); + } + + @Override + public final boolean moonrise$isRealPlayer() { + return this.isRealPlayer; + } + + @Override + public final void moonrise$setRealPlayer(final boolean real) { + this.isRealPlayer = real; + } + + @Override + public final RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() { + return this.chunkLoader; + } + + @Override + public final void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) { + this.chunkLoader = loader; + } + + @Override + public final RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() { + return this.viewDistanceHolder; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SortedArraySetMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SortedArraySetMixin.java new file mode 100644 index 0000000..5c56922 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/SortedArraySetMixin.java @@ -0,0 +1,110 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet; +import net.minecraft.util.SortedArraySet; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Predicate; + +@Mixin(SortedArraySet.class) +public abstract class SortedArraySetMixin extends AbstractSet implements ChunkSystemSortedArraySet { + + @Shadow + int size; + + @Shadow + T[] contents; + + @Shadow + @Final + private Comparator comparator; + + @Shadow + protected abstract int findIndex(T object); + + @Shadow + private static int getInsertionPosition(int i) { + return 0; + } + + @Shadow + protected abstract void addInternal(T object, int i); + + @Shadow + abstract void removeInternal(int i); + + + @Override + public final boolean removeIf(final Predicate filter) { + // prev. impl used an iterator, which could be n^2 and creates garbage + int i = 0; + final int len = this.size; + final T[] backingArray = this.contents; + + for (;;) { + if (i >= len) { + return false; + } + if (!filter.test(backingArray[i])) { + ++i; + continue; + } + break; + } + + // we only want to write back to backingArray if we really need to + + int lastIndex = i; // this is where new elements are shifted to + + for (; i < len; ++i) { + final T curr = backingArray[i]; + if (!filter.test(curr)) { // if test throws we're screwed + backingArray[lastIndex++] = curr; + } + } + + // cleanup end + Arrays.fill(backingArray, lastIndex, len, null); + this.size = lastIndex; + return true; + } + + @Override + public final T moonrise$replace(final T object) { + final int index = this.findIndex(object); + if (index >= 0) { + final T old = this.contents[index]; + this.contents[index] = object; + return old; + } else { + this.addInternal(object, getInsertionPosition(index)); + return object; + } + } + + @Override + public final T moonrise$removeAndGet(final T object) { + int i = this.findIndex(object); + if (i >= 0) { + final T ret = this.contents[i]; + this.removeInternal(i); + return ret; + } else { + return null; + } + } + + @Override + public final SortedArraySet moonrise$copy() { + final SortedArraySet ret = SortedArraySet.create(this.comparator, 0); + + ((SortedArraySetMixin)(Object)ret).size = this.size; + ((SortedArraySetMixin)(Object)ret).contents = Arrays.copyOf(this.contents, this.size); + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/StructureCheckMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/StructureCheckMixin.java new file mode 100644 index 0000000..9d4f6ed --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/StructureCheckMixin.java @@ -0,0 +1,150 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap; +import ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap; +import com.mojang.datafixers.DataFixer; +import it.unimi.dsi.fastutil.longs.Long2BooleanMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.storage.ChunkScanAccess; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureCheck; +import net.minecraft.world.level.levelgen.structure.StructureCheckResult; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(StructureCheck.class) +public abstract class StructureCheckMixin { + + @Shadow + private Long2ObjectMap> loadedChunks; + + @Shadow + private Map featureChecks; + + @Shadow + protected abstract boolean canCreateStructure(ChunkPos chunkPos, Structure structure); + + @Shadow + private static Object2IntMap deduplicateEmptyMap(Object2IntMap object2IntMap) { + return null; + } + + + // make sure to purge entries from the maps to prevent memory leaks + @Unique + private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + @Unique + private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + @Unique + private final SynchronisedLong2ObjectMap> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); + @Unique + private final ConcurrentHashMap featureChecksSafe = new ConcurrentHashMap<>(); + + /** + * @reason Initialise fields and destroy old state + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(ChunkScanAccess chunkScanAccess, RegistryAccess registryAccess, + StructureTemplateManager structureTemplateManager, ResourceKey resourceKey, + ChunkGenerator chunkGenerator, RandomState randomState, LevelHeightAccessor levelHeightAccessor, + BiomeSource biomeSource, long l, DataFixer dataFixer, CallbackInfo ci) { + this.loadedChunks = null; + this.featureChecks = null; + } + + /** + * @reason Redirect to new map + * @author Spottedleaf + */ + @Redirect( + method = "checkStart", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;get(J)Ljava/lang/Object;" + ) + ) + private V redirectCachedGet(final Long2ObjectMap instance, final long pos) { + return (V)this.loadedChunksSafe.get(pos); + } + + /** + * @reason Redirect to new map + * @author Spottedleaf + */ + @Inject( + method = "checkStart", + cancellable = true, + at = @At( + value = "INVOKE", + target = "Ljava/util/Map;computeIfAbsent(Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;" + ) + ) + private void redirectUncached(final ChunkPos pos, final Structure structure, final StructurePlacement structurePlacement, + final boolean bl, final CallbackInfoReturnable cir) { + final boolean ret = this.featureChecksSafe + .computeIfAbsent(structure, structure2 -> new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) + .getOrCompute(pos.toLong(), chunkPos -> this.canCreateStructure(pos, structure)); + cir.setReturnValue(!ret ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED); + } + + /** + * @reason Redirect to new map + * @author Spottedleaf + */ + @Overwrite + public void storeFullResults(final long pos, final Object2IntMap referencesByStructure) { + this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure)); + // once we insert into loadedChunks, we don't really need to be very careful about removing everything + // from this map, as everything that checks this map uses loadedChunks first + // so, one way or another it's a race condition that doesn't matter + for (SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) { + value.remove(pos); + } + } + + /** + * @reason Redirect to new map + * @author Spottedleaf + */ + @Overwrite + public void incrementReference(final ChunkPos pos, final Structure structure) { + this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system - synchronise this class + // make this COW so that we do not mutate state that may be currently in use + if (referencesByStructure == null) { + referencesByStructure = new Object2IntOpenHashMap<>(); + } else { + referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure); + } + // Paper end - rewrite chunk system - synchronise this class + + referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1); + return referencesByStructure; + }); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/TicketMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/TicketMixin.java new file mode 100644 index 0000000..40e8629 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/chunk_system/TicketMixin.java @@ -0,0 +1,68 @@ +package ca.spottedleaf.moonrise.mixin.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Ticket.class) +public abstract class TicketMixin implements ChunkSystemTicket, Comparable> { + + @Shadow + @Final + private TicketType type; + + @Shadow + @Final + private int ticketLevel; + + @Shadow + @Final + public T key; + + + @Unique + private long removeDelay; + + @Override + public final long moonrise$getRemoveDelay() { + return this.removeDelay; + } + + @Override + public final void moonrise$setRemoveDelay(final long removeDelay) { + this.removeDelay = removeDelay; + } + + /** + * @reason Change debug to include remove delay + * @author Spottedleaf + */ + @Overwrite + @Override + public String toString() { + return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; + } + + /** + * @reason Remove old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public void setCreatedTick(final long tickCreated) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Remove old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public boolean timedOut(final long currentTick) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java index a96387b..dee15f0 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityGetterMixin.java @@ -1,9 +1,9 @@ package ca.spottedleaf.moonrise.mixin.collisions; +import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; -import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.EntityGetter; import net.minecraft.world.phys.AABB; @@ -17,7 +17,7 @@ import java.util.List; import java.util.function.Predicate; @Mixin(EntityGetter.class) -public interface EntityGetterMixin extends CollisionEntityGetter { +public interface EntityGetterMixin { @Shadow List getEntities(final Entity entity, final AABB box, final Predicate predicate); @@ -44,10 +44,10 @@ public interface EntityGetterMixin extends CollisionEntityGetter { box = box.inflate(-CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON, -CollisionUtil.COLLISION_EPSILON); final List entities; - if (entity != null && ((CollisionEntity)entity).moonrise$isHardColliding()) { + if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { entities = this.getEntities(entity, box, null); } else { - entities = this.moonrise$getHardCollidingEntities(entity, box, null); + entities = ((ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null); } final List ret = new ArrayList<>(Math.min(25, entities.size())); @@ -67,11 +67,6 @@ public interface EntityGetterMixin extends CollisionEntityGetter { return ret; } - @Override - default List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { - return this.getEntities(entity, box, predicate); - } - /** * @reason Use faster intersection checks * @author Spottedleaf diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java index a1aab1a..4407278 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/EntityMixin.java @@ -3,7 +3,6 @@ package ca.spottedleaf.moonrise.mixin.collisions; import ca.spottedleaf.moonrise.common.util.CoordinateUtils; import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; -import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; import ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall; import net.minecraft.CrashReport; @@ -36,7 +35,7 @@ import java.util.List; import java.util.stream.Stream; @Mixin(Entity.class) -public abstract class EntityMixin implements CollisionEntity { +public abstract class EntityMixin { @Shadow private Level level; @@ -79,6 +78,7 @@ public abstract class EntityMixin implements CollisionEntity { @Shadow public boolean wasOnFire; + @Shadow public boolean isInPowderSnow; @@ -91,14 +91,6 @@ public abstract class EntityMixin implements CollisionEntity { @Shadow protected abstract void onInsideBlock(BlockState blockState); - @Unique - private final boolean isHardColliding = this.moonrise$isHardCollidingUncached(); - - @Override - public final boolean moonrise$isHardColliding() { - return this.isHardColliding; - } - /** * @author Spottedleaf * @reason Optimise entire method diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java index ab1c546..cb20761 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java @@ -5,15 +5,12 @@ import ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk; import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; -import ca.spottedleaf.moonrise.patches.collisions.slices.EntityLookup; -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.util.Mth; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; @@ -24,7 +21,6 @@ import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; import net.minecraft.world.level.chunk.status.ChunkStatus; -import net.minecraft.world.level.entity.EntityTypeTest; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; import net.minecraft.world.phys.AABB; @@ -34,7 +30,6 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -43,10 +38,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.Predicate; @Mixin(Level.class) -public abstract class LevelMixin implements CollisionLevel, CollisionEntityGetter, LevelAccessor, AutoCloseable { +public abstract class LevelMixin implements CollisionLevel, LevelAccessor, AutoCloseable { @Shadow public abstract ProfilerFiller getProfiler(); @@ -56,20 +50,12 @@ public abstract class LevelMixin implements CollisionLevel, CollisionEntityGette - @Unique - private final EntityLookup collisionLookup = new EntityLookup((Level)(Object)this); - @Unique private int minSection; @Unique private int maxSection; - @Override - public final EntityLookup moonrise$getCollisionLookup() { - return this.collisionLookup; - } - @Override public final int moonrise$getMinSection() { return this.minSection; @@ -95,115 +81,6 @@ public abstract class LevelMixin implements CollisionLevel, CollisionEntityGette this.maxSection = WorldUtil.getMaxSection(this); } - /** - * @reason Route to faster lookup - * @author Spottedleaf - */ - @Overwrite - @Override - public List getEntities(final Entity entity, final AABB boundingBox, final Predicate predicate) { - this.getProfiler().incrementCounter("getEntities"); - final List ret = new ArrayList<>(); - - this.collisionLookup.getEntities(entity, boundingBox, ret, predicate); - - return ret; - } - - /** - * @reason Route to faster lookup - * @author Spottedleaf - */ - @Overwrite - public void getEntities(final EntityTypeTest entityTypeTest, - final AABB boundingBox, final Predicate predicate, - final List into, final int maxCount) { - this.getProfiler().incrementCounter("getEntities"); - if (entityTypeTest instanceof EntityType byType) { - if (maxCount != Integer.MAX_VALUE) { - this.collisionLookup.getEntities(byType, boundingBox, into, predicate, maxCount); - return; - } else { - this.collisionLookup.getEntities(byType, boundingBox, into, predicate); - return; - } - } - - if (entityTypeTest == null) { - if (maxCount != Integer.MAX_VALUE) { - this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount); - return; - } else { - this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate); - return; - } - } - - final Class base = entityTypeTest.getBaseClass(); - - final Predicate modifiedPredicate; - if (predicate == null) { - modifiedPredicate = (final T obj) -> { - return entityTypeTest.tryCast(obj) != null; - }; - } else { - modifiedPredicate = (final Entity obj) -> { - final T casted = entityTypeTest.tryCast(obj); - if (casted == null) { - return false; - } - - return predicate.test(casted); - }; - } - - if (base == null || base == Entity.class) { - if (maxCount != Integer.MAX_VALUE) { - this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); - return; - } else { - this.collisionLookup.getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate); - return; - } - } else { - if (maxCount != Integer.MAX_VALUE) { - this.collisionLookup.getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount); - return; - } else { - this.collisionLookup.getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate); - return; - } - } - } - - /** - * Route to faster lookup - * @author Spottedleaf - */ - @Override - public final List getEntitiesOfClass(final Class entityClass, final AABB boundingBox, final Predicate predicate) { - this.getProfiler().incrementCounter("getEntities"); - final List ret = new ArrayList<>(); - - this.collisionLookup.getEntities(entityClass, null, boundingBox, ret, predicate); - - return ret; - } - - /** - * Route to faster lookup - * @author Spottedleaf - */ - @Override - public final List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate) { - this.getProfiler().incrementCounter("getEntities"); - final List ret = new ArrayList<>(); - - this.collisionLookup.getHardCollidingEntities(entity, box, ret, predicate); - - return ret; - } - /** * Route to faster lookup. * See {@link EntityGetterMixin#isUnobstructed(Entity, VoxelShape)} for expected behavior diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java deleted file mode 100644 index 17d4d7c..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerCallbackMixin.java +++ /dev/null @@ -1,59 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.collisions; - -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.entity.EntityAccess; -import net.minecraft.world.level.entity.PersistentEntitySectionManager; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(PersistentEntitySectionManager.Callback.class) -public abstract class PersistentEntitySectionManagerCallbackMixin { - - @Shadow - @Final - private T entity; - - @Shadow - private long currentSectionKey; - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "onMove", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" - ) - ) - private void changeSections(final CallbackInfo ci) { - final Entity entity = (Entity)this.entity; - - final long currentChunk = this.currentSectionKey; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().moveEntity(entity, currentChunk); - } - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "onRemove", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" - ) - ) - private void onRemoved(final CallbackInfo ci) { - final Entity entity = (Entity)this.entity; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().removeEntity(entity); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java deleted file mode 100644 index 8f38d1d..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/PersistentEntitySectionManagerMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.collisions; - -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.entity.EntityAccess; -import net.minecraft.world.level.entity.PersistentEntitySectionManager; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(PersistentEntitySectionManager.class) -public abstract class PersistentEntitySectionManagerMixin { - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "addEntity", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;add(Lnet/minecraft/world/level/entity/EntityAccess;)V" - ) - ) - private void addEntity(final T entityAccess, final boolean onDisk, final CallbackInfoReturnable cir) { - final Entity entity = (Entity)entityAccess; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().addEntity(entity); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java index dd3d102..ce18d61 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ServerEntityMixin.java @@ -1,6 +1,6 @@ package ca.spottedleaf.moonrise.mixin.collisions; -import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; import net.minecraft.server.level.ServerEntity; import net.minecraft.world.entity.Entity; import org.spongepowered.asm.mixin.Final; @@ -32,7 +32,7 @@ public abstract class ServerEntityMixin { ) ) private void forceHardCollideTeleport(final CallbackInfo ci) { - if (((CollisionEntity)this.entity).moonrise$isHardColliding()) { + if (((ChunkSystemEntity)this.entity).moonrise$isHardColliding()) { this.teleportDelay = 9999; } } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java deleted file mode 100644 index 48cc50b..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerCallbackMixin.java +++ /dev/null @@ -1,59 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.collisions; - -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.entity.EntityAccess; -import net.minecraft.world.level.entity.TransientEntitySectionManager; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(TransientEntitySectionManager.Callback.class) -public abstract class TransientEntitySectionManagerCallbackMixin { - - @Shadow - @Final - private T entity; - - @Shadow - private long currentSectionKey; - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "onMove", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" - ) - ) - private void changeSections(final CallbackInfo ci) { - final Entity entity = (Entity)this.entity; - - final long currentChunk = this.currentSectionKey; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().moveEntity(entity, currentChunk); - } - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "onRemove", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;remove(Lnet/minecraft/world/level/entity/EntityAccess;)Z" - ) - ) - private void onRemoved(final CallbackInfo ci) { - final Entity entity = (Entity)this.entity; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().removeEntity(entity); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java deleted file mode 100644 index b67acdc..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/TransientEntitySectionManagerMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.collisions; - -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.entity.EntityAccess; -import net.minecraft.world.level.entity.TransientEntitySectionManager; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(TransientEntitySectionManager.class) -public abstract class TransientEntitySectionManagerMixin { - - /** - * @reason Hook into our entity slices - * @author Spottedleaf - */ - @Inject( - method = "addEntity", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/level/entity/EntitySection;add(Lnet/minecraft/world/level/entity/EntityAccess;)V" - ) - ) - private void addEntity(final T entityAccess, final CallbackInfo ci) { - final Entity entity = (Entity)entityAccess; - - ((CollisionLevel)entity.level()).moonrise$getCollisionLookup().addEntity(entity); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java index cc0b832..dd6908f 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/LevelLightEngineMixin.java @@ -225,6 +225,9 @@ public abstract class LevelLightEngineMixin implements LightEventListener, StarL } break; } + default: { + throw new IllegalStateException("Unknown light type: " + lightType); + } } } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java index 0cbb578..c73ec09 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/lightengine/ThreadedLevelLightEngineMixin.java @@ -1,15 +1,14 @@ package ca.spottedleaf.moonrise.mixin.starlight.lightengine; -import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; -import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; -import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.util.thread.ProcessorMailbox; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.chunk.ChunkAccess; @@ -18,39 +17,37 @@ import net.minecraft.world.level.chunk.LightChunkGetter; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.lighting.LevelLightEngine; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.IntSupplier; import java.util.function.Supplier; @Mixin(ThreadedLevelLightEngine.class) public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine implements StarLightLightingProvider { - @Final @Shadow - private ChunkMap chunkMap; - - @Final - @Shadow - private static Logger LOGGER; + private ProcessorMailbox taskMailbox; @Shadow - public abstract void tryScheduleUpdate(); + private ProcessorHandle> sorterMailbox; public ThreadedLevelLightEngineMixin(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight) { super(chunkProvider, hasBlockLight, hasSkyLight); } @Unique - private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + private final AtomicLong chunkWorkCounter = new AtomicLong(); @Unique private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, - final Supplier runnable) { + final Supplier supplier) { final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ); @@ -60,58 +57,78 @@ public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine imp return; } - if (center.getStatus() != ChunkStatus.FULL) { - // do not keep chunk loaded, we are probably in a gen thread - // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) - runnable.get(); - return; - } + final StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get(); - if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { - // ticket logic is not safe to run off-main, re-schedule - world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { - this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); - }); - return; - } - - final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); - - final StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); - - if (updateFuture == null) { + if (scheduledTask == null) { // not scheduled return; } - if (updateFuture.isTicketAdded) { + if (!scheduledTask.markTicketAdded()) { // ticket already added return; } - updateFuture.isTicketAdded = true; - final int references = this.chunksBeingWorkedOn.addTo(key, 1); - if (references == 0) { - final ChunkPos pos = new ChunkPos(chunkX, chunkZ); - world.getChunkSource().addRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); - } + final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement()); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); - updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> { - final int newReferences = this.chunksBeingWorkedOn.get(key); - if (newReferences == 1) { - this.chunksBeingWorkedOn.remove(key); - final ChunkPos pos = new ChunkPos(chunkX, chunkZ); - world.getChunkSource().removeRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); - } else { - this.chunksBeingWorkedOn.put(key, newReferences - 1); - } - }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { - if (thr != null) { - LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); - } + scheduledTask.queueOrRunTask(() -> { + world.getChunkSource().removeRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); }); } + /** + * @reason Destroy old chunk system hook + * @author Spottedleaf + */ + @Inject( + method = "", + at = @At( + value = "RETURN" + ) + ) + private void initHook(final CallbackInfo ci) { + this.taskMailbox = null; + this.sorterMailbox = null; + } + + /** + * @reason Destroy old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public void addTask(final int x, final int z, final ThreadedLevelLightEngine.TaskType type, + final Runnable task) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Destroy old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public void addTask(final int x, final int z, final IntSupplier ticketLevelSupplier, + final ThreadedLevelLightEngine.TaskType type, final Runnable task) { + throw new UnsupportedOperationException(); + } + + /** + * @reason Chunk system schedules light tasks immediately + * @author Spottedleaf + */ + @Overwrite + public void tryScheduleUpdate() {} + + /** + * @reason Destroy old chunk system hook + * @author Spottedleaf + */ + @Overwrite + public void runUpdate() { + throw new UnsupportedOperationException(); + } + /** * @reason Redirect scheduling call away from the vanilla light engine, as well as enforce * that chunk neighbours are loaded before the processing can occur @@ -121,7 +138,7 @@ public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine imp public void checkBlock(final BlockPos pos) { final BlockPos posCopy = pos.immutable(); this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { - return this.starlight$getLightEngine().blockChange(posCopy); + return ThreadedLevelLightEngineMixin.this.starlight$getLightEngine().blockChange(posCopy); }); } @@ -141,7 +158,7 @@ public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine imp @Overwrite public void updateSectionStatus(final SectionPos pos, final boolean notReady) { this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { - return this.starlight$getLightEngine().sectionChange(pos, notReady); + return ThreadedLevelLightEngineMixin.this.starlight$getLightEngine().sectionChange(pos, notReady); }); } @@ -192,36 +209,11 @@ public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine imp } /** - * @reason Route to new logic to either light or just load the data + * @reason Chunk system patch replaces the vanilla scheduling entirely * @author Spottedleaf */ @Overwrite public CompletableFuture lightChunk(final ChunkAccess chunk, final boolean lit) { - final ChunkPos chunkPos = chunk.getPos(); - - return CompletableFuture.supplyAsync(() -> { - final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); - if (!lit) { - chunk.setLightCorrect(false); - this.starlight$getLightEngine().lightChunk(chunk, emptySections); - chunk.setLightCorrect(true); - } else { - this.starlight$getLightEngine().forceLoadInChunk(chunk, emptySections); - // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have - // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should - // catch what we miss here. - this.starlight$getLightEngine().checkChunkEdges(chunkPos.x, chunkPos.z); - } - - this.chunkMap.releaseLightTicket(chunkPos); - return chunk; - }, (runnable) -> { - this.starlight$getLightEngine().scheduleChunkLight(chunkPos, runnable); - this.tryScheduleUpdate(); - }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { - if (throwable != null) { - LOGGER.error("Failed to light chunk " + chunkPos, throwable); - } - }); + throw new UnsupportedOperationException(); } } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java index d292cc8..0089b40 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/multiplayer/ClientPacketListenerMixin.java @@ -22,7 +22,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -@Mixin(value = ClientPacketListener.class, priority = 1001) +@Mixin(ClientPacketListener.class) public abstract class ClientPacketListenerMixin implements ClientGamePacketListener { /* diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java deleted file mode 100644 index 25aa9a4..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ClientLevelMixin.java +++ /dev/null @@ -1,38 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.starlight.world; - -import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; -import net.minecraft.client.multiplayer.ClientChunkCache; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.core.Holder; -import net.minecraft.core.RegistryAccess; -import net.minecraft.resources.ResourceKey; -import net.minecraft.util.profiling.ProfilerFiller; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.dimension.DimensionType; -import net.minecraft.world.level.storage.WritableLevelData; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import java.util.function.Supplier; - -@Mixin(ClientLevel.class) -public abstract class ClientLevelMixin extends Level implements StarlightWorld { - - protected ClientLevelMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { - super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); - } - - @Shadow - public abstract ClientChunkCache getChunkSource(); - - @Override - public final LevelChunk starlight$getChunkAtImmediately(final int chunkX, final int chunkZ) { - return this.getChunkSource().getChunk(chunkX, chunkZ, false); - } - - @Override - public final ChunkAccess starlight$getAnyChunkImmediately(final int chunkX, final int chunkZ) { - return this.getChunkSource().getChunk(chunkX, chunkZ, false); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java deleted file mode 100644 index 77b6b2d..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/LevelMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.starlight.world; - -import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.LevelAccessor; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.status.ChunkStatus; -import net.minecraft.world.level.chunk.LevelChunk; -import org.spongepowered.asm.mixin.Mixin; - -@Mixin(Level.class) -public abstract class LevelMixin implements LevelAccessor, AutoCloseable, StarlightWorld { - - @Override - public LevelChunk starlight$getChunkAtImmediately(final int chunkX, final int chunkZ) { - return this.getChunkSource().getChunk(chunkX, chunkZ, false); - } - - @Override - public ChunkAccess starlight$getAnyChunkImmediately(final int chunkX, final int chunkZ) { - return this.getChunkSource().getChunk(chunkX, chunkX, ChunkStatus.EMPTY, false); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java deleted file mode 100644 index 56ef4e4..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/starlight/world/ServerWorldMixin.java +++ /dev/null @@ -1,58 +0,0 @@ -package ca.spottedleaf.moonrise.mixin.starlight.world; - -import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; -import net.minecraft.core.Holder; -import net.minecraft.core.RegistryAccess; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.level.ChunkHolder; -import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.ChunkResult; -import net.minecraft.server.level.ServerChunkCache; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.util.profiling.ProfilerFiller; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.WorldGenLevel; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.status.ChunkStatus; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.dimension.DimensionType; -import net.minecraft.world.level.storage.WritableLevelData; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import java.util.function.Supplier; - -@Mixin(ServerLevel.class) -public abstract class ServerWorldMixin extends Level implements WorldGenLevel, StarlightWorld { - - @Shadow - @Final - private ServerChunkCache chunkSource; - - protected ServerWorldMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) { - super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); - } - - @Override - public final LevelChunk starlight$getChunkAtImmediately(final int chunkX, final int chunkZ) { - final ChunkMap storage = this.chunkSource.chunkMap; - final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - if (holder == null) { - return null; - } - - final ChunkResult result = holder.getFutureIfPresentUnchecked(ChunkStatus.FULL).getNow(null); - - return result == null ? null : (LevelChunk)result.orElse(null); - } - - @Override - public final ChunkAccess starlight$getAnyChunkImmediately(final int chunkX, final int chunkZ) { - final ChunkMap storage = this.chunkSource.chunkMap; - final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); - - return holder == null ? null : holder.getLastAvailable(); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java new file mode 100644 index 0000000..100d4ce --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java @@ -0,0 +1,139 @@ +package ca.spottedleaf.moonrise.patches.chunk_system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import com.mojang.logging.LogUtils; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.slf4j.Logger; +import java.util.List; +import java.util.function.Consumer; + +public final class ChunkSystem { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { + scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority); + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, + final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); + } + + // Paper - rewrite chunk system + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } + + public static List getVisibleChunkHolders(final ServerLevel level) { + return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); + } + + public static List getUpdatingChunkHolders(final ServerLevel level) { + return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); + } + + public static int getVisibleChunkHolderCount(final ServerLevel level) { + return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); + } + + public static int getUpdatingChunkHolderCount(final ServerLevel level) { + return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); + } + + public static boolean hasAnyChunkHolders(final ServerLevel level) { + return getUpdatingChunkHolderCount(level) != 0; + } + + public static void onEntityPreAdd(final ServerLevel level, final Entity entity) { + + } + + public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { + + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { + + } + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + chunk.postProcessGeneration(); + } + ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); + ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { + return null; + } + + public static int getSendViewDistance(final ServerPlayer player) { + return RegionizedPlayerChunkLoader.getAPISendViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { + return RegionizedPlayerChunkLoader.getLoadViewDistance(player); + } + + public static int getTickViewDistance(final ServerPlayer player) { + return RegionizedPlayerChunkLoader.getAPITickViewDistance(player); + } + + public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) { + ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player); + } + + public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) { + ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player); + } + + public static void updateMaps(final ServerLevel world, final ServerPlayer player) { + ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player); + } + + private ChunkSystem() {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java new file mode 100644 index 0000000..49160a3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java @@ -0,0 +1,38 @@ +package ca.spottedleaf.moonrise.patches.chunk_system; + +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.DataFixTypes; + +public final class ChunkSystemConverters { + + // See SectionStorage#getVersion + private static final int DEFAULT_POI_DATA_VERSION = 1945; + + private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1; + + private static int getCurrentVersion() { + return SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + } + + private static int getDataVersion(final CompoundTag data, final int dfl) { + return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC) + ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG); + } + + public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION); + + return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) { + final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION); + + return DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); + } + + private ChunkSystemConverters() {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java new file mode 100644 index 0000000..7c614ac --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.patches.chunk_system; + +import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; + +public final class ChunkSystemFeatures { + + public static boolean supportsAsyncChunkSave() { + // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write + // additionally, there may be mods hooking into the write() call which may not be thread-safe to call + return false; + } + + public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) { + throw new UnsupportedOperationException(); + } + + public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) { + throw new UnsupportedOperationException(); + } + + public static boolean forceNoSave(final ChunkAccess chunk) { + // support for CB chunk mustNotSave + return false; + } + + public static boolean supportsAsyncChunkDeserialization() { + // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods + // hooking into ChunkSerializer#read() are thread-safe to call + return false; + } + + private ChunkSystemFeatures() {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java new file mode 100644 index 0000000..becd1c6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.async_save; + +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +public record AsyncChunkSaveData( + Tag blockTickList, // non-null if we had to go to the server's tick list + Tag fluidTickList, // non-null if we had to go to the server's tick list + ListTag blockEntities, + long worldTime +) {} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java new file mode 100644 index 0000000..2c27985 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java @@ -0,0 +1,39 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.entity; + +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.monster.Shulker; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.Boat; + +public interface ChunkSystemEntity { + + public boolean moonrise$isHardColliding(); + + // for mods to override + public default boolean moonrise$isHardCollidingUncached() { + return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith(); + } + + public FullChunkStatus moonrise$getChunkStatus(); + + public void moonrise$setChunkStatus(final FullChunkStatus status); + + public int moonrise$getSectionX(); + + public void moonrise$setSectionX(final int x); + + public int moonrise$getSectionY(); + + public void moonrise$setSectionY(final int y); + + public int moonrise$getSectionZ(); + + public void moonrise$setSectionZ(final int z); + + public boolean moonrise$isUpdatingSectionStatus(); + + public void moonrise$setUpdatingSectionStatus(final boolean to); + + public boolean moonrise$hasAnyPlayerPassengers(); +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java new file mode 100644 index 0000000..73df26b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java @@ -0,0 +1,14 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io; + +import net.minecraft.world.level.chunk.storage.RegionFile; +import java.io.IOException; + +public interface ChunkSystemRegionFileStorage { + + public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); + + public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); + + public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java new file mode 100644 index 0000000..9a2f9e4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java @@ -0,0 +1,1295 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; +import ca.spottedleaf.concurrentutil.function.BiLong1Function; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.lang.invoke.VarHandle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Prioritised RegionFile I/O executor, responsible for all RegionFile access. + *

+ * All functions provided are MT-Safe, however certain ordering constraints are recommended: + *

  • + * Chunk saves may not occur for unloaded chunks. + *
  • + *
  • + * Tasks must be scheduled on the chunk scheduler thread. + *
  • + * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. + *

    + */ +public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class); + + /** + * The kinds of region files controlled by the region file thread. Add more when needed, and ensure + * getControllerFor is updated. + */ + public static enum RegionFileType { + CHUNK_DATA, + POI_DATA, + ENTITY_DATA; + } + + private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); + + public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { + switch (type) { + case CHUNK_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); + case POI_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); + case ENTITY_DATA: + return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); + default: + throw new IllegalStateException("Unknown controller type " + type); + } + } + + /** + * Collects regionfile data for a certain chunk. + */ + public static final class RegionFileData { + + private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; + private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; + private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; + + /** + * Sets the result associated with the specified regionfile type. Note that + * results can only be set once per regionfile type. + * + * @param type The regionfile type. + * @param data The result to set. + */ + public void setData(final RegionFileType type, final CompoundTag data) { + final int index = type.ordinal(); + + if (this.hasResult[index]) { + throw new IllegalArgumentException("Result already exists for type " + type); + } + this.hasResult[index] = true; + this.data[index] = data; + } + + /** + * Sets the result associated with the specified regionfile type. Note that + * results can only be set once per regionfile type. + * + * @param type The regionfile type. + * @param throwable The result to set. + */ + public void setThrowable(final RegionFileType type, final Throwable throwable) { + final int index = type.ordinal(); + + if (this.hasResult[index]) { + throw new IllegalArgumentException("Result already exists for type " + type); + } + this.hasResult[index] = true; + this.throwables[index] = throwable; + } + + /** + * Returns whether there is a result for the specified regionfile type. + * + * @param type Specified regionfile type. + * + * @return Whether a result exists for {@code type}. + */ + public boolean hasResult(final RegionFileType type) { + return this.hasResult[type.ordinal()]; + } + + /** + * Returns the data result for the regionfile type. + * + * @param type Specified regionfile type. + * + * @throws IllegalArgumentException If the result has not been set for {@code type}. + * @return The data result for the specified type. If the result is a {@code Throwable}, + * then returns {@code null}. + */ + public CompoundTag getData(final RegionFileType type) { + final int index = type.ordinal(); + + if (!this.hasResult[index]) { + throw new IllegalArgumentException("Result does not exist for type " + type); + } + + return this.data[index]; + } + + /** + * Returns the throwable result for the regionfile type. + * + * @param type Specified regionfile type. + * + * @throws IllegalArgumentException If the result has not been set for {@code type}. + * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, + * then returns {@code null}. + */ + public Throwable getThrowable(final RegionFileType type) { + final int index = type.ordinal(); + + if (!this.hasResult[index]) { + throw new IllegalArgumentException("Result does not exist for type " + type); + } + + return this.throwables[index]; + } + } + + private static final Object INIT_LOCK = new Object(); + + static RegionFileIOThread[] threads; + + /* needs to be consistent given a set of parameters */ + static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + if (threads == null) { + throw new IllegalStateException("Threads not initialised"); + } + + final int regionX = chunkX >> 5; + final int regionZ = chunkZ >> 5; + final int typeOffset = type.ordinal(); + + return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; + } + + /** + * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. + * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. + * + * @param wait Whether to wait until all tasks have completed. + */ + public static void close(final boolean wait) { + for (int i = 0, len = threads.length; i < len; ++i) { + threads[i].close(false, true); + } + if (wait) { + RegionFileIOThread.flush(); + } + } + + public static long[] getExecutedTasks() { + final long[] ret = new long[threads.length]; + for (int i = 0, len = threads.length; i < len; ++i) { + ret[i] = threads[i].getTotalTasksExecuted(); + } + + return ret; + } + + public static long[] getTasksScheduled() { + final long[] ret = new long[threads.length]; + for (int i = 0, len = threads.length; i < len; ++i) { + ret[i] = threads[i].getTotalTasksScheduled(); + } + return ret; + } + + public static void flush() { + for (int i = 0, len = threads.length; i < len; ++i) { + threads[i].waitUntilAllExecuted(); + } + } + + public static void flushRegionStorages(final ServerLevel world) throws IOException { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + getControllerFor(world, type).getCache().flush(); + } + } + + public static void partialFlush(final int totalTasksRemaining) { + long failures = 1L; // start out at 0.25ms + + for (;;) { + final long[] executed = getExecutedTasks(); + final long[] scheduled = getTasksScheduled(); + + long sum = 0; + for (int i = 0; i < executed.length; ++i) { + sum += scheduled[i] - executed[i]; + } + + if (sum <= totalTasksRemaining) { + break; + } + + failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms + } + } + + /** + * Inits the executor with the specified number of threads. + * + * @param threads Specified number of threads. + */ + public static void init(final int threads) { + synchronized (INIT_LOCK) { + if (RegionFileIOThread.threads != null) { + throw new IllegalStateException("Already initialised threads"); + } + + RegionFileIOThread.threads = new RegionFileIOThread[threads]; + + for (int i = 0; i < threads; ++i) { + RegionFileIOThread.threads[i] = new RegionFileIOThread(i); + RegionFileIOThread.threads[i].start(); + } + } + } + + public static void deinit() { + if (false) { + // TODO does this cause issues with mods? how to implement + close(true); + synchronized (INIT_LOCK) { + RegionFileIOThread.threads = null; + } + } else { RegionFileIOThread.flush(); } + } + + private RegionFileIOThread(final int threadNumber) { + super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time + this.setName("RegionFile I/O Thread #" + threadNumber); + this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us + this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { + LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); + }); + } + + /** + * Returns whether the current thread is a regionfile I/O executor. + * @return Whether the current thread is a regionfile I/O executor. + */ + public static boolean isRegionFileThread() { + return Thread.currentThread() instanceof RegionFileIOThread; + } + + /** + * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid + * dumb plugins from taking away priority from threads we consider crucial. + * @return The priroity to use with blocking I/O on the current thread. + */ + public static Priority getIOBlockingPriorityForCurrentThread() { + if (TickThread.isTickThread()) { + return Priority.BLOCKING; + } + return Priority.HIGHEST; + } + + /** + * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type. + * Note that this does not copy the result, so do not modify the result returned. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * + * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. + */ + public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); + } + + CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + final ChunkDataController taskController = getControllerFor(world, type); + final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task == null) { + return null; + } + + final CompoundTag ret = task.inProgressWrite; + + return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; + } + + /** + * Returns the priority for the specified regionfile type for the specified chunk. + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @return The priority for the chunk + */ + public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + return thread.getPriorityInternal(world, chunkX, chunkZ, type); + } + + Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { + final ChunkDataController taskController = getControllerFor(world, type); + final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task == null) { + return Priority.COMPLETING; + } + + return task.prioritisedTask.getPriority(); + } + + /** + * Sets the priority for all regionfile types for the specified chunk. Note that great care should + * be taken using this method, as there can be multiple tasks tied to the same chunk that want different + * priorities. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should + * be taken using this method, as there can be multiple tasks tied to the same chunk that want different + * priorities. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); + } + + void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final ChunkDataController taskController = getControllerFor(world, type); + final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.prioritisedTask.setPriority(priority); + } + } + + /** + * Raises the priority for all regionfile types for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Raises the priority for the specified regionfile type for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #lowerPriority(ServerLevel, int, int, Priority) + * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); + } + + void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final ChunkDataController taskController = getControllerFor(world, type); + final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.prioritisedTask.raisePriority(priority); + } + } + + /** + * Lowers the priority for all regionfile types for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, + final Priority priority) { + for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { + RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); + } + } + + /** + * Lowers the priority for the specified regionfile type for the specified chunk. + * + * @param world Specified world. + * @param chunkX Specified chunk x. + * @param chunkZ Specified chunk z. + * @param type Specified regionfile type. + * @param priority New priority. + * + * @see #raisePriority(ServerLevel, int, int, Priority) + * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) + * @see #setPriority(ServerLevel, int, int, Priority) + * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) + */ + public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); + } + + void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) { + final ChunkDataController taskController = getControllerFor(world, type); + final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (task != null) { + task.prioritisedTask.lowerPriority(priority); + } + } + + /** + * Schedules the chunk data to be written asynchronously. + *

    + * Impl notes: + *

    + *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param data Chunk's data + * @param type The regionfile type to write to. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, + final RegionFileType type) { + RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); + } + + /** + * Schedules the chunk data to be written asynchronously. + *

    + * Impl notes: + *

    + *
  • + * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means + * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • + * Writes may be called concurrently, although only the "later" write will go through. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param data Chunk's data + * @param type The regionfile type to write to. + * @param priority The minimum priority to schedule at. + * + * @throws IllegalStateException If the file io thread has shutdown. + */ + public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, + final RegionFileType type, final Priority priority) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); + } + + void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, + final RegionFileType type, final Priority priority) { + final ChunkDataController taskController = getControllerFor(world, type); + + final boolean[] created = new boolean[1]; + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> { + if (taskRunning == null || taskRunning.failedWrite) { + // no task is scheduled or the previous write failed - meaning we need to overwrite it + + // create task + final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); + newTask.inProgressWrite = data; + created[0] = true; + + return newTask; + } + + taskRunning.inProgressWrite = data; + + return taskRunning; + }); + + if (created[0]) { + task.prioritisedTask.queue(); + } else { + task.prioritisedTask.raisePriority(priority); + } + } + + /** + * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call + * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + */ + public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock) { + return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); + } + + /** + * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call + * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param priority The minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + */ + public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final Priority priority) { + return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); + } + + /** + * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and + * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param types The regionfile type(s) to load. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final RegionFileType... types) { + return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); + } + + /** + * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and + * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} + * for single load. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param types The regionfile type(s) to load. + * @param priority The minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) + * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, + final Consumer onComplete, final boolean intendingToBlock, + final Priority priority, final RegionFileType... types) { + if (types == null) { + throw new NullPointerException("Types cannot be null"); + } + if (types.length == 0) { + throw new IllegalArgumentException("Types cannot be empty"); + } + + final RegionFileData ret = new RegionFileData(); + + final Cancellable[] reads = new CancellableRead[types.length]; + final AtomicInteger completions = new AtomicInteger(); + final int expectedCompletions = types.length; + + for (int i = 0; i < expectedCompletions; ++i) { + final RegionFileType type = types[i]; + reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, + (final CompoundTag data, final Throwable throwable) -> { + if (throwable != null) { + ret.setThrowable(type, throwable); + } else { + ret.setData(type, data); + } + + if (completions.incrementAndGet() == expectedCompletions) { + onComplete.accept(ret); + } + }, intendingToBlock, priority); + } + + return new CancellableReads(reads); + } + + /** + * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call + * {@code onComplete}. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileType type, final BiConsumer onComplete, + final boolean intendingToBlock) { + return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); + } + + /** + * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call + * {@code onComplete}. + *

    + * Impl notes: + *

    + *
  • + * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may + * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of + * data is undefined behaviour, and can cause deadlock. + *
  • + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param onComplete Consumer to execute once this task has completed + * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost + * of this call. + * @param priority Minimum priority to load the data at. + * + * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. + * + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) + * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) + * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) + */ + public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileType type, final BiConsumer onComplete, + final boolean intendingToBlock, final Priority priority) { + final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); + return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); + } + + private static Boolean doesRegionFileExist(final int chunkX, final int chunkZ, final boolean intendingToBlock, + final ChunkDataController taskController) { + final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + if (intendingToBlock) { + return taskController.computeForRegionFile(chunkX, chunkZ, true, (final RegionFile file) -> { + if (file == null) { // null if no regionfile exists + return Boolean.FALSE; + } + + return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; + }); + } else { + // first check if the region file for sure does not exist + if (taskController.doesRegionFileNotExist(chunkX, chunkZ)) { + return Boolean.FALSE; + } // else: it either exists or is not known, fall back to checking the loaded region file + + return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final RegionFile file) -> { + if (file == null) { // null if not loaded + // not sure at this point, let the I/O thread figure it out + return Boolean.TRUE; + } + + return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; + }); + } + } + + Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileType type, final BiConsumer onComplete, + final boolean intendingToBlock, final Priority priority) { + final ChunkDataController taskController = getControllerFor(world, type); + + final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final BiLong1Function compute = (final long keyInMap, final ChunkDataTask running) -> { + if (running == null) { + // not scheduled + + if (callbackInfo.regionFileCalculation == null) { + // caller will compute this outside of compute(), to avoid holding the bin lock + callbackInfo.needsRegionFileTest = true; + return null; + } + + if (callbackInfo.regionFileCalculation == Boolean.FALSE) { + // not on disk + callbackInfo.data = null; + callbackInfo.throwable = null; + callbackInfo.completeNow = true; + return null; + } + + // set up task + final ChunkDataTask newTask = new ChunkDataTask( + world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority + ); + newTask.inProgressRead = new InProgressRead(); + newTask.inProgressRead.addToAsyncWaiters(onComplete); + + callbackInfo.tasksNeedsScheduling = true; + return newTask; + } + + final CompoundTag pendingWrite = running.inProgressWrite; + + if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { + // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations + if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { + callbackInfo.data = running.inProgressRead.value; + callbackInfo.throwable = running.inProgressRead.throwable; + callbackInfo.completeNow = true; + } + return running; + } + + // at this stage we have to use the in progress write's data to avoid an order issue + callbackInfo.data = pendingWrite; + callbackInfo.throwable = null; + callbackInfo.completeNow = true; + return running; + }; + + ChunkDataTask curr = taskController.tasks.get(key); + if (curr == null) { + callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); + } + ChunkDataTask ret = taskController.tasks.compute(key, compute); + if (callbackInfo.needsRegionFileTest) { + // curr isn't null but when we went into compute() it was + callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); + // now it should be fine + ret = taskController.tasks.compute(key, compute); + } + + // needs to be scheduled + if (callbackInfo.tasksNeedsScheduling) { + ret.prioritisedTask.queue(); + } else if (callbackInfo.completeNow) { + try { + onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); + } catch (final Throwable thr) { + LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); + } + } else { + // we're waiting on a task we didn't schedule, so raise its priority to what we want + ret.prioritisedTask.raisePriority(priority); + } + + return new CancellableRead(onComplete, ret); + } + + /** + * Schedules a load task to be executed asynchronously, and blocks on that task. + * + * @param world Chunk's world + * @param chunkX Chunk's x coordinate + * @param chunkZ Chunk's z coordinate + * @param type Regionfile type + * @param priority Minimum priority to load the data at. + * + * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. + * + * @throws IOException If the load fails for any reason + */ + public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, + final Priority priority) throws IOException { + final CompletableFuture ret = new CompletableFuture<>(); + + RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { + if (thr != null) { + ret.completeExceptionally(thr); + } else { + ret.complete(compound); + } + }, true, priority); + + try { + return ret.join(); + } catch (final CompletionException ex) { + throw new IOException(ex); + } + } + + private static final class ImmediateCallbackCompletion { + + public CompoundTag data; + public Throwable throwable; + public boolean completeNow; + public boolean tasksNeedsScheduling; + public boolean needsRegionFileTest; + public Boolean regionFileCalculation; + + } + + private static final class CancellableRead implements Cancellable { + + private BiConsumer callback; + private ChunkDataTask task; + + CancellableRead(final BiConsumer callback, final ChunkDataTask task) { + this.callback = callback; + this.task = task; + } + + @Override + public boolean cancel() { + final BiConsumer callback = this.callback; + final ChunkDataTask task = this.task; + + if (callback == null || task == null) { + return false; + } + + this.callback = null; + this.task = null; + + final InProgressRead read = task.inProgressRead; + + // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) + return read != null && read.cancel(callback); + } + } + + private static final class CancellableReads implements Cancellable { + + private Cancellable[] reads; + + private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); + + CancellableReads(final Cancellable[] reads) { + this.reads = reads; + } + + @Override + public boolean cancel() { + final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); + + if (reads == null) { + return false; + } + + boolean ret = false; + + for (final Cancellable read : reads) { + ret |= read.cancel(); + } + + return ret; + } + } + + private static final class InProgressRead { + + private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); + + private CompoundTag value; + private Throwable throwable; + private MultiThreadedQueue> callbacks = new MultiThreadedQueue<>(); + + public boolean hasNoWaiters() { + return this.callbacks.isEmpty(); + } + + public boolean addToAsyncWaiters(final BiConsumer callback) { + return this.callbacks.add(callback); + } + + public boolean cancel(final BiConsumer callback) { + return this.callbacks.remove(callback); + } + + public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) { + this.value = value; + this.throwable = throwable; + + BiConsumer consumer; + while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { + try { + consumer.accept(value == null ? null : value.copy(), throwable); + } catch (final Throwable thr) { + LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); + } + } + } + } + + public static abstract class ChunkDataController { + + // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. + private final ConcurrentLong2ReferenceChainedHashTable tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f); + + public final RegionFileType type; + + public ChunkDataController(final RegionFileType type) { + this.type = type; + } + + public abstract RegionFileStorage getCache(); + + public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; + + public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; + + public boolean hasTasks() { + return !this.tasks.isEmpty(); + } + + public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) { + return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ); + } + + public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { + final RegionFileStorage cache = this.getCache(); + final RegionFile regionFile; + synchronized (cache) { + try { + if (existingOnly) { + regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ); + } else { + regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ)); + } + } catch (final IOException ex) { + throw new RuntimeException(ex); + } + + return function.apply(regionFile); + } + } + + public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { + final RegionFileStorage cache = this.getCache(); + final RegionFile regionFile; + + synchronized (cache) { + regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ); + + return function.apply(regionFile); + } + } + } + + private static final class ChunkDataTask implements Runnable { + + private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class); + + private InProgressRead inProgressRead; + private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release + + private boolean failedWrite; + + private final ServerLevel world; + private final int chunkX; + private final int chunkZ; + private final ChunkDataController taskController; + + private final PrioritisedTask prioritisedTask; + + /* + * IO thread will perform reads before writes for a given chunk x and z + * + * How reads/writes are scheduled: + * + * If read is scheduled while scheduling write, take no special action and just schedule write + * If read is scheduled while scheduling read and no write is scheduled, chain the read task + * + * + * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) + * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data + * + * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however + * it fails to properly propagate write failures thanks to writes overwriting each other + */ + + public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController, + final PrioritisedExecutor executor, final Priority priority) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.taskController = taskController; + this.prioritisedTask = executor.createTask(this, priority); + } + + @Override + public String toString() { + return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ + + ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); + } + + @Override + public void run() { + final InProgressRead read = this.inProgressRead; + final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); + + if (read != null) { + final boolean[] canRead = new boolean[] { true }; + + if (read.hasNoWaiters()) { + // cancelled read? go to task controller to confirm + final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkDataTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + + if (!read.hasNoWaiters()) { + return valueInMap; + } else { + canRead[0] = false; + } + + return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; + }); + + if (inMap == null) { + // read is cancelled - and no write pending, so we're done + return; + } + // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - + // the readers will just use the in progress write, so the value in canRead is good to use without + // further synchronisation. + } + + if (canRead[0]) { + CompoundTag compound = null; + Throwable throwable = null; + + try { + compound = this.taskController.readData(this.chunkX, this.chunkZ); + } catch (final Throwable thr) { + throwable = thr; + LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); + } + read.complete(this, compound, throwable); + } + } + + CompoundTag write = this.inProgressWrite; + + if (write == NOTHING_TO_WRITE) { + final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkDataTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; + }); + + if (inMap == null) { + return; // set the task value to null, indicating we're done + } // else: inProgressWrite changed, so now we have something to write + } + + for (;;) { + write = this.inProgressWrite; + final CompoundTag dataWritten = write; + + boolean failedWrite = false; + + try { + this.taskController.writeData(this.chunkX, this.chunkZ, write); + } catch (final Throwable thr) { + // TODO implement this? + /*if (thr instanceof RegionFileStorage.RegionFileSizeException) { + final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); + LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); + } else */{ + failedWrite = thr instanceof IOException; + LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); + } + } + + final boolean finalFailWrite = failedWrite; + final boolean[] done = new boolean[] { false }; + + this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { + if (valueInMap == null) { + throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); + } + if (valueInMap != ChunkDataTask.this) { + throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); + } + if (valueInMap.inProgressWrite == dataWritten) { + valueInMap.failedWrite = finalFailWrite; + done[0] = true; + // keep the data in map if we failed the write so we can try to prevent data loss + return finalFailWrite ? valueInMap : null; + } + // different data than expected, means we need to retry write + return valueInMap; + }); + + if (done[0]) { + return; + } + + // fetch & write new data + continue; + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java new file mode 100644 index 0000000..c35e0c2 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java @@ -0,0 +1,56 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public final class ChunkDataController extends RegionFileIOThread.ChunkDataController { + + private final ServerLevel world; + + public ChunkDataController(final ServerLevel world) { + super(RegionFileIOThread.RegionFileType.CHUNK_DATA); + this.world = world; + } + + @Override + public RegionFileStorage getCache() { + return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage(); + } + + @Override + public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + final CompletableFuture future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); + + try { + if (future != null) { + // rets non-null when sync writing (i.e. future should be completed here) + future.join(); + } + } catch (final CompletionException ex) { + if (ex.getCause() instanceof IOException ioException) { + throw ioException; + } + throw ex; + } + } + + @Override + public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { + try { + return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null); + } catch (final CompletionException ex) { + if (ex.getCause() instanceof IOException ioException) { + throw ioException; + } + throw ex; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java new file mode 100644 index 0000000..fdd189e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java @@ -0,0 +1,55 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.EntityStorage; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import java.io.IOException; +import java.nio.file.Path; + +public final class EntityDataController extends RegionFileIOThread.ChunkDataController { + + private final EntityRegionFileStorage storage; + + public EntityDataController(final EntityRegionFileStorage storage) { + super(RegionFileIOThread.RegionFileType.ENTITY_DATA); + this.storage = storage; + } + + @Override + public RegionFileStorage getCache() { + return this.storage; + } + + @Override + public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + this.storage.write(new ChunkPos(chunkX, chunkZ), compound); + } + + @Override + public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { + return this.storage.read(new ChunkPos(chunkX, chunkZ)); + } + + public static final class EntityRegionFileStorage extends RegionFileStorage { + + public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory, + final boolean dsync) { + super(regionStorageInfo, directory, dsync); + } + + @Override + public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException { + final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); + if (nbtPos != null && !pos.equals(nbtPos)) { + throw new IllegalArgumentException( + "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() + + " but compound says coordinate is " + nbtPos + " for world: " + this + ); + } + super.write(pos, nbt); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java new file mode 100644 index 0000000..af867f8 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java @@ -0,0 +1,33 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; + +public final class PoiDataController extends RegionFileIOThread.ChunkDataController { + + private final ServerLevel world; + + public PoiDataController(final ServerLevel world) { + super(RegionFileIOThread.RegionFileType.POI_DATA); + this.world = world; + } + + @Override + public RegionFileStorage getCache() { + return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage(); + } + + @Override + public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { + ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound); + } + + @Override + public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { + return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java new file mode 100644 index 0000000..eab0994 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java @@ -0,0 +1,20 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface ChunkSystemLevel { + + public EntityLookup moonrise$getEntityLookup(); + + public void moonrise$setEntityLookup(final EntityLookup entityLookup); + + public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ); + + public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ); + + public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java new file mode 100644 index 0000000..0b58701 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface ChunkSystemLevelReader { + + public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java new file mode 100644 index 0000000..a31c392 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java @@ -0,0 +1,49 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.util.List; +import java.util.function.Consumer; + +public interface ChunkSystemServerLevel extends ChunkSystemLevel { + + public ChunkTaskScheduler moonrise$getChunkTaskScheduler(); + + public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController(); + + public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController(); + + public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController(); + + public int moonrise$getRegionChunkShift(); + + public boolean moonrise$isMarkedClosing(); + + public void moonrise$setMarkedClosing(final boolean value); + + public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader(); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final PrioritisedExecutor.Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, + final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final PrioritisedExecutor.Priority priority, + final Consumer> onLoad); + + public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, + final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, + final Consumer> onLoad); + + public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java new file mode 100644 index 0000000..7d049d7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java @@ -0,0 +1,26 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; +import java.util.List; + +public interface ChunkSystemChunkHolder { + + public NewChunkHolder moonrise$getRealChunkHolder(); + + public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder); + + public void moonrise$addReceivedChunk(final ServerPlayer player); + + public void moonrise$removeReceivedChunk(final ServerPlayer player); + + public boolean moonrise$hasChunkBeenSent(); + + public boolean moonrise$hasChunkBeenSent(final ServerPlayer to); + + public List moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge); + + public LevelChunk moonrise$getFullChunk(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java new file mode 100644 index 0000000..fb6a6c7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java @@ -0,0 +1,30 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.util.concurrent.atomic.AtomicBoolean; + +public interface ChunkSystemChunkStatus { + + public boolean moonrise$isParallelCapable(); + + public void moonrise$setParallelCapable(final boolean value); + + public int moonrise$getWriteRadius(); + + public void moonrise$setWriteRadius(final int value); + + public int moonrise$getLoadRadius(); + + public void moonrise$setLoadRadius(final int value); + + public ChunkStatus moonrise$getNextStatus(); + + public boolean moonrise$isEmptyLoadStatus(); + + public void moonrise$setEmptyLoadStatus(final boolean value); + + public boolean moonrise$isEmptyGenStatus(); + + public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java new file mode 100644 index 0000000..883fe64 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +import net.minecraft.server.level.ChunkMap; + +public interface ChunkSystemDistanceManager { + + public ChunkMap moonrise$getChunkMap(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java new file mode 100644 index 0000000..755b08d --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; + +public interface ChunkSystemLevelChunk { + + public boolean moonrise$isPostProcessingDone(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java similarity index 72% rename from src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java rename to src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java index f4d1b61..560bd4d 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/ChunkEntitySlices.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java @@ -1,18 +1,27 @@ -package ca.spottedleaf.moonrise.patches.collisions.slices; +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; -import ca.spottedleaf.moonrise.common.list.ReferenceList; -import ca.spottedleaf.moonrise.common.util.WorldUtil; -import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.common.list.EntityList; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import com.google.common.collect.ImmutableList; import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.boss.EnderDragonPart; import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.EntityStorage; import net.minecraft.world.level.entity.Visibility; import net.minecraft.world.phys.AABB; +import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -20,46 +29,205 @@ import java.util.function.Predicate; public final class ChunkEntitySlices { - protected final int minSection; - protected final int maxSection; + public final int minSection; + public final int maxSection; public final int chunkX; public final int chunkZ; - protected final Level world; + public final Level world; - protected final EntityCollectionBySection allEntities; - protected final EntityCollectionBySection hardCollidingEntities; - protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByType; - protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; - protected final ReferenceList entities = new ReferenceList<>(); + private final EntityCollectionBySection allEntities; + private final EntityCollectionBySection hardCollidingEntities; + private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; + private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByType; + private final EntityList entities = new EntityList(); - public Visibility sectionVisibility = Visibility.TRACKED; // TODO + public FullChunkStatus status; - public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ) { // inclusive, inclusive - this.minSection = WorldUtil.getMinSection(world); - this.maxSection = WorldUtil.getMaxSection(world); + private boolean isTransient; + + public boolean isTransient() { + return this.isTransient; + } + + public void setTransient(final boolean value) { + this.isTransient = value; + } + + public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status, + final int minSection, final int maxSection) { // inclusive, inclusive + this.minSection = minSection; + this.maxSection = maxSection; this.chunkX = chunkX; this.chunkZ = chunkZ; this.world = world; this.allEntities = new EntityCollectionBySection(this); this.hardCollidingEntities = new EntityCollectionBySection(this); - this.entitiesByType = new Reference2ObjectOpenHashMap<>(); this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); + this.entitiesByType = new Reference2ObjectOpenHashMap<>(); + + this.status = status; + } + + public static List readEntities(final ServerLevel world, final CompoundTag compoundTag) { + // TODO check this and below on update for format changes + return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world).collect(ImmutableList.toImmutableList()); + } + + // Paper start - rewrite chunk system + public static void copyEntities(final CompoundTag from, final CompoundTag into) { + if (from == null) { + return; + } + final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND); + if (entitiesFrom == null || entitiesFrom.isEmpty()) { + return; + } + + final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND); + into.put("Entities", entitiesInto); // this is in case into doesn't have any entities + entitiesInto.addAll(0, entitiesFrom); + } + + public static CompoundTag saveEntityChunk(final List entities, final ChunkPos chunkPos, final ServerLevel world) { + return saveEntityChunk0(entities, chunkPos, world, false); + } + + public static CompoundTag saveEntityChunk0(final List entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) { + if (!force && entities.isEmpty()) { + return null; + } + + final ListTag entitiesTag = new ListTag(); + for (final Entity entity : entities) { + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + entitiesTag.add(compoundTag); + } + } + final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag()); + ret.put("Entities", entitiesTag); + EntityStorage.writeChunkPos(ret, chunkPos); + + return !force && entitiesTag.isEmpty() ? null : ret; + } + + public CompoundTag save() { + final int len = this.entities.size(); + if (len == 0) { + return null; + } + + final Entity[] rawData = this.entities.getRawData(); + final List collectedEntities = new ArrayList<>(len); + for (int i = 0; i < len; ++i) { + final Entity entity = rawData[i]; + if (entity.shouldBeSaved()) { + collectedEntities.add(entity); + } + } + + if (collectedEntities.isEmpty()) { + return null; + } + + return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world); + } + + // returns true if this chunk has transient entities remaining + public boolean unload() { + final int len = this.entities.size(); + final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); + + for (int i = 0; i < len; ++i) { + final Entity entity = collectedEntities[i]; + if (entity.isRemoved()) { + // removed by us below + continue; + } + if (entity.shouldBeSaved()) { + entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK); + if (entity.isVehicle()) { + // we cannot assume that these entities are contained within this chunk, because entities can + // desync - so we need to remove them all + for (final Entity passenger : entity.getIndirectPassengers()) { + passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK); + } + } + } + } + + return this.entities.size() != 0; + } + + private List getAllEntities() { + final int len = this.entities.size(); + if (len == 0) { + return new ArrayList<>(); + } + + final Entity[] rawData = this.entities.getRawData(); + final List collectedEntities = new ArrayList<>(len); + for (int i = 0; i < len; ++i) { + collectedEntities.add(rawData[i]); + } + + return collectedEntities; } public boolean isEmpty() { return this.entities.size() == 0; } + public void mergeInto(final ChunkEntitySlices slices) { + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY()); + } + } + + private boolean preventStatusUpdates; + public boolean startPreventingStatusUpdates() { + final boolean ret = this.preventStatusUpdates; + this.preventStatusUpdates = true; + return ret; + } + + public boolean isPreventingStatusUpdates() { + return this.preventStatusUpdates; + } + + public void stopPreventingStatusUpdates(final boolean prev) { + this.preventStatusUpdates = prev; + } + + public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { + this.status = status; + + final Entity[] entities = this.entities.getRawData(); + + for (int i = 0, size = this.entities.size(); i < size; ++i) { + final Entity entity = entities[i]; + + final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status); + final Visibility newVisibility = EntityLookup.getEntityStatus(entity); + + lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); + } + } + public boolean addEntity(final Entity entity, final int chunkSection) { if (!this.entities.add(entity)) { return false; } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status); final int sectionIndex = chunkSection - this.minSection; this.allEntities.addEntity(entity, sectionIndex); - if (((CollisionEntity)entity).moonrise$isHardColliding()) { + if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { this.hardCollidingEntities.addEntity(entity, sectionIndex); } @@ -87,11 +255,12 @@ public final class ChunkEntitySlices { if (!this.entities.remove(entity)) { return false; } + ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null); final int sectionIndex = chunkSection - this.minSection; this.allEntities.removeEntity(entity, sectionIndex); - if (((CollisionEntity)entity).moonrise$isHardColliding()) { + if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { this.hardCollidingEntities.removeEntity(entity, sectionIndex); } @@ -198,13 +367,13 @@ public final class ChunkEntitySlices { } } - protected static final class BasicEntityList { + private static final class BasicEntityList { - protected static final Entity[] EMPTY = new Entity[0]; - protected static final int DEFAULT_CAPACITY = 4; + private static final Entity[] EMPTY = new Entity[0]; + private static final int DEFAULT_CAPACITY = 4; - protected E[] storage; - protected int size; + private E[] storage; + private int size; public BasicEntityList() { this(0); @@ -274,19 +443,17 @@ public final class ChunkEntitySlices { } } - protected static final class EntityCollectionBySection { + private static final class EntityCollectionBySection { - protected final ChunkEntitySlices manager; - protected final long[] nonEmptyBitset; - protected final BasicEntityList[] entitiesBySection; - protected int count; + private final ChunkEntitySlices slices; + private final BasicEntityList[] entitiesBySection; + private int count; - public EntityCollectionBySection(final ChunkEntitySlices manager) { - this.manager = manager; + public EntityCollectionBySection(final ChunkEntitySlices slices) { + this.slices = slices; - final int sectionCount = manager.maxSection - manager.minSection + 1; + final int sectionCount = slices.maxSection - slices.minSection + 1; - this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE this.entitiesBySection = new BasicEntityList[sectionCount]; } @@ -299,7 +466,6 @@ public final class ChunkEntitySlices { if (list == null) { this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); - this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); } list.add(entity); @@ -317,7 +483,6 @@ public final class ChunkEntitySlices { if (list.isEmpty()) { this.entitiesBySection[sectionIndex] = null; - this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); } } @@ -326,8 +491,8 @@ public final class ChunkEntitySlices { return; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -365,8 +530,8 @@ public final class ChunkEntitySlices { return false; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -409,8 +574,8 @@ public final class ChunkEntitySlices { return; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -460,8 +625,8 @@ public final class ChunkEntitySlices { return false; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -519,8 +684,8 @@ public final class ChunkEntitySlices { return; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -570,8 +735,8 @@ public final class ChunkEntitySlices { return false; } - final int minSection = this.manager.minSection; - final int maxSection = this.manager.maxSection; + final int minSection = this.slices.minSection; + final int maxSection = this.slices.maxSection; final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); @@ -623,4 +788,4 @@ public final class ChunkEntitySlices { return false; } } -} +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java new file mode 100644 index 0000000..3a8c192 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java @@ -0,0 +1,1044 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; + +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.moonrise.common.list.EntityList; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.util.AbortableIterationConsumer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.EntityInLevelCallback; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.entity.LevelCallback; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.Visibility; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public abstract class EntityLookup implements LevelEntityGetter { + + private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class); + + protected static final int REGION_SHIFT = 5; + protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; + protected static final int REGION_SIZE = 1 << REGION_SHIFT; + + public final Level world; + + protected final SWMRLong2ObjectHashTable regions = new SWMRLong2ObjectHashTable<>(128, 0.5f); + + protected final int minSection; // inclusive + protected final int maxSection; // inclusive + protected final LevelCallback worldCallback; + + protected final ConcurrentLong2ReferenceChainedHashTable entityById = new ConcurrentLong2ReferenceChainedHashTable<>(); + protected final ConcurrentHashMap entityByUUID = new ConcurrentHashMap<>(); + protected final EntityList accessibleEntities = new EntityList(); + + public EntityLookup(final Level world, final LevelCallback worldCallback) { + this.world = world; + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + this.worldCallback = worldCallback; + } + + protected abstract Boolean blockTicketUpdates(); + + protected abstract void setBlockTicketUpdates(final Boolean value); + + protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason); + + protected abstract void checkThread(final Entity entity, final String reason); + + protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk); + + protected abstract void onEmptySlices(final int chunkX, final int chunkZ); + + private static Entity maskNonAccessible(final Entity entity) { + if (entity == null) { + return null; + } + final Visibility visibility = EntityLookup.getEntityStatus(entity); + return visibility.isAccessible() ? entity : null; + } + + @Override + public Entity get(final int id) { + return maskNonAccessible(this.entityById.get((long)id)); + } + + @Override + public Entity get(final UUID id) { + return maskNonAccessible(this.entityByUUID.get(id)); + } + + public boolean hasEntity(final UUID uuid) { + return this.get(uuid) != null; + } + + public String getDebugInfo() { + return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size(); + } + + protected static final class ArrayIterable implements Iterable { + + private final T[] array; + private final int off; + private final int length; + + public ArrayIterable(final T[] array, final int off, final int length) { + this.array = array; + this.off = off; + this.length = length; + if (length > array.length) { + throw new IllegalArgumentException("Length must be no greater-than the array length"); + } + } + + @Override + public Iterator iterator() { + return new ArrayIterator<>(this.array, this.off, this.length); + } + + protected static final class ArrayIterator implements Iterator { + + private final T[] array; + private int off; + private final int length; + + public ArrayIterator(final T[] array, final int off, final int length) { + this.array = array; + this.off = off; + this.length = length; + } + + @Override + public boolean hasNext() { + return this.off < this.length; + } + + @Override + public T next() { + if (this.off >= this.length) { + throw new NoSuchElementException(); + } + return this.array[this.off++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + } + + @Override + public Iterable getAll() { + synchronized (this.accessibleEntities) { + final int len = this.accessibleEntities.size(); + final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class); + + Objects.checkFromToIndex(0, len, cpy.length); + + return new ArrayIterable<>(cpy, 0, len); + } + } + + public int getEntityCount() { + synchronized (this.accessibleEntities) { + return this.accessibleEntities.size(); + } + } + + public Entity[] getAllCopy() { + synchronized (this.accessibleEntities) { + return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); + } + } + + @Override + public void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { + for (final Iterator iterator = this.entityById.valueIterator(); iterator.hasNext();) { + final Entity entity = iterator.next(); + final Visibility visibility = EntityLookup.getEntityStatus(entity); + if (!visibility.isAccessible()) { + continue; + } + final U casted = filter.tryCast(entity); + if (casted != null && action.accept(casted).shouldAbort()) { + break; + } + } + } + + @Override + public void get(final AABB box, final Consumer action) { + List entities = new ArrayList<>(); + this.getEntitiesWithoutDragonParts(null, box, entities, null); + for (int i = 0, len = entities.size(); i < len; ++i) { + action.accept(entities.get(i)); + } + } + + @Override + public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { + List entities = new ArrayList<>(); + this.getEntitiesWithoutDragonParts(null, box, entities, null); + for (int i = 0, len = entities.size(); i < len; ++i) { + final U casted = filter.tryCast(entities.get(i)); + if (casted != null && action.accept(casted).shouldAbort()) { + break; + } + } + } + + public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, + final boolean created, final boolean destroyed) { + this.checkThread(entity, "Entity status change must only happen on the main thread"); + + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + // recursive status update + LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); + return; + } + + final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); + + if (entityStatusUpdateBefore) { + LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); + return; + } + + try { + final Boolean ticketBlockBefore = this.blockTicketUpdates(); + try { + ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true); + try { + if (created) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onCreated(entity); + } + } + + if (oldVisibility == newVisibility) { + if (moved && newVisibility.isAccessible()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onSectionChange(entity); + } + } + return; + } + + if (newVisibility.ordinal() > oldVisibility.ordinal()) { + // status upgrade + if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { + synchronized (this.accessibleEntities) { + this.accessibleEntities.add(entity); + } + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTrackingStart(entity); + } + } + + if (!oldVisibility.isTicking() && newVisibility.isTicking()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTickingStart(entity); + } + } + } else { + // status downgrade + if (oldVisibility.isTicking() && !newVisibility.isTicking()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTickingEnd(entity); + } + } + + if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { + synchronized (this.accessibleEntities) { + this.accessibleEntities.remove(entity); + } + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onTrackingEnd(entity); + } + } + } + + if (moved && newVisibility.isAccessible()) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onSectionChange(entity); + } + } + + if (destroyed) { + if (EntityLookup.this.worldCallback != null) { + EntityLookup.this.worldCallback.onDestroyed(entity); + } + } + } finally { + ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false); + } + } finally { + this.setBlockTicketUpdates(ticketBlockBefore); + } + } finally { + if (slices != null) { + slices.stopPreventingStatusUpdates(false); + } + } + } + + public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) { + this.getChunk(x, z).updateStatus(newStatus, this); + } + + public void addLegacyChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, true); + } + + public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, true); + } + + public void addWorldGenChunkEntities(final List entities, final ChunkPos forChunk) { + this.addEntityChunk(entities, forChunk, false); + } + + protected void addRecursivelySafe(final Entity root, final boolean fromDisk) { + if (!this.addEntity(root, fromDisk)) { + // possible we are a passenger, and so should dismount from any valid entity in the world + root.stopRiding(); + return; + } + for (final Entity passenger : root.getPassengers()) { + this.addRecursivelySafe(passenger, fromDisk); + } + } + + protected void addEntityChunk(final List entities, final ChunkPos forChunk, final boolean fromDisk) { + for (int i = 0, len = entities.size(); i < len; ++i) { + final Entity entity = entities.get(i); + if (entity.isPassenger()) { + continue; + } + + if (forChunk != null && !entity.chunkPosition().equals(forChunk)) { + LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk); + // can't set removed here, as we may not own the chunk position + // skip the entity + continue; + } + + final Vec3 rootPosition = entity.position(); + + // always adjust positions before adding passengers in case plugins access the entity, and so that + // they are added to the right entity chunk + for (final Entity passenger : entity.getIndirectPassengers()) { + if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) { + passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z); + } + } + + this.addRecursivelySafe(entity, fromDisk); + } + } + + public boolean addNewEntity(final Entity entity) { + return this.addEntity(entity, false); + } + + public static Visibility getEntityStatus(final Entity entity) { + if (entity.isAlwaysTicking()) { + return Visibility.TICKING; + } + final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus(); + return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); + } + + protected boolean addEntity(final Entity entity, final boolean fromDisk) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); + final int sectionZ = pos.getZ() >> 4; + this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread"); + + if (entity.isRemoved()) { + LOGGER.warn("Refusing to add removed entity: " + entity); + return false; + } + + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); + return false; + } + + Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity); + if (currentlyMapped != null) { + LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity); + return false; + } + + currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity); + if (currentlyMapped != null) { + // need to remove mapping for id + this.entityById.remove((long)entity.getId(), entity); + LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity); + return false; + } + + ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX); + ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ); + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + if (!slices.addEntity(entity, sectionY)) { + LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); + } + + entity.setLevelCallback(new EntityCallback(entity)); + + this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); + + return true; + } + + public boolean canRemoveEntity(final Entity entity) { + if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { + return false; + } + + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); + return slices == null || !slices.isPreventingStatusUpdates(); + } + + protected void removeEntity(final Entity entity) { + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main"); + if (!entity.isRemoved()) { + throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); + } + final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); + // all entities should be in a chunk + if (slices == null) { + LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); + } else { + if (slices.isPreventingStatusUpdates()) { + throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates"); + } + if (!slices.removeEntity(entity, sectionY)) { + LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); + } + } + ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE); + ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE); + + + Entity currentlyMapped; + if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) { + LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped); + } + + Entity[] currentlyMappedArr = new Entity[1]; + + // need reference equality + this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> { + currentlyMappedArr[0] = valueInMap; + if (valueInMap != entity) { + return valueInMap; + } + return null; + }); + + if (currentlyMappedArr[0] != entity) { + LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]); + } + + if (slices != null && slices.isEmpty()) { + this.onEmptySlices(sectionX, sectionZ); + } + } + + protected ChunkEntitySlices moveEntity(final Entity entity) { + // ensure we own the entity + this.checkThread(entity, "Cannot move entity off-main"); + + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); + final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ(); + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; + final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); + final int newSectionZ = newPos.getZ() >> 4; + + if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) { + return null; + } + + // ensure the new section is owned by this tick thread + this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main"); + + // ensure the old section is owned by this tick thread + this.checkThread(sectionX, sectionZ, "Cannot move entity off-main"); + + final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ); + final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); + + if (!old.removeEntity(entity, sectionY)) { + LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section"); + } + + if (!slices.addEntity(entity, newSectionY)) { + LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); + } + + ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX); + ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY); + ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ); + + if (old.isEmpty()) { + this.onEmptySlices(sectionX, sectionZ); + } + + return slices; + } + + public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(except, box, into, predicate); + } + } + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getHardCollidingEntities(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(type, box, (List)into, (Predicate)predicate); + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + chunk.getEntities(clazz, except, box, into, predicate); + } + } + } + } + } + + //////// Limited //////// + + public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, + final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate, final int maxCount) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { + continue; + } + + if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) { + return; + } + } + } + } + } + } + + public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main"); + synchronized (this) { + final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); + if (curr != null) { + this.removeChunk(chunkX, chunkZ); + + curr.mergeInto(slices); + + this.addChunk(chunkX, chunkZ, slices); + } else { + this.addChunk(chunkX, chunkZ, slices); + } + } + } + + public void entitySectionUnload(final int chunkX, final int chunkZ) { + this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main"); + this.removeChunk(chunkX, chunkZ); + } + + public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + if (region == null) { + return null; + } + + return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); + } + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + ChunkEntitySlices ret; + if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { + return this.createEntityChunk(chunkX, chunkZ, true); + } + + return ret; + } + + public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { + final long key = CoordinateUtils.getChunkKey(regionX, regionZ); + + return this.regions.get(key); + } + + protected synchronized void removeChunk(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + final ChunkSlicesRegion region = this.regions.get(key); + final int remaining = region.remove(relIndex); + + if (remaining == 0) { + this.regions.remove(key); + } + } + + public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkSlicesRegion region = this.regions.get(key); + if (region != null) { + region.add(relIndex, slices); + } else { + region = new ChunkSlicesRegion(); + region.add(relIndex, slices); + this.regions.put(key, region); + } + } + + public static final class ChunkSlicesRegion { + + private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; + private int sliceCount; + + public ChunkEntitySlices get(final int index) { + return this.slices[index]; + } + + public int remove(final int index) { + final ChunkEntitySlices slices = this.slices[index]; + if (slices == null) { + throw new IllegalStateException(); + } + + this.slices[index] = null; + + return --this.sliceCount; + } + + public void add(final int index, final ChunkEntitySlices slices) { + final ChunkEntitySlices curr = this.slices[index]; + if (curr != null) { + throw new IllegalStateException(); + } + + this.slices[index] = slices; + + ++this.sliceCount; + } + } + + protected final class EntityCallback implements EntityInLevelCallback { + + public final Entity entity; + + public EntityCallback(final Entity entity) { + this.entity = entity; + } + + @Override + public void onMove() { + final Entity entity = this.entity; + final Visibility oldVisibility = getEntityStatus(entity); + final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); + if (newSlices == null) { + // no new section, so didn't change sections + return; + } + final Visibility newVisibility = getEntityStatus(entity); + + EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); + } + + @Override + public void onRemove(final Entity.RemovalReason reason) { + final Entity entity = this.entity; + EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system + final Visibility tickingState = EntityLookup.getEntityStatus(entity); + + EntityLookup.this.removeEntity(entity); + + EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); + + this.entity.setLevelCallback(NoOpCallback.INSTANCE); + } + } + + protected static final class NoOpCallback implements EntityInLevelCallback { + + public static final NoOpCallback INSTANCE = new NoOpCallback(); + + @Override + public void onMove() {} + + @Override + public void onRemove(final Entity.RemovalReason reason) {} + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java new file mode 100644 index 0000000..fc4ea13 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java @@ -0,0 +1,81 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.LevelCallback; + +public final class ClientEntityLookup extends EntityLookup { + + private final LongOpenHashSet tickingChunks = new LongOpenHashSet(); + + public ClientEntityLookup(final Level world, final LevelCallback worldCallback) { + super(world, worldCallback); + } + + @Override + protected Boolean blockTicketUpdates() { + // not present on client + return null; + } + + @Override + protected void setBlockTicketUpdates(Boolean value) { + // not present on client + } + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) { + // TODO implement? + } + + @Override + protected void checkThread(final Entity entity, final String reason) { + // TODO implement? + } + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, + ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + this.removeChunk(chunkX, chunkZ); + } + + public void markTicking(final long pos) { + if (this.tickingChunks.add(pos)) { + final int chunkX = CoordinateUtils.getChunkX(pos); + final int chunkZ = CoordinateUtils.getChunkZ(pos); + if (this.getChunk(chunkX, chunkZ) != null) { + this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING); + } + } + } + + public void markNonTicking(final long pos) { + if (this.tickingChunks.remove(pos)) { + final int chunkX = CoordinateUtils.getChunkX(pos); + final int chunkZ = CoordinateUtils.getChunkZ(pos); + if (this.getChunk(chunkX, chunkZ) != null) { + this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL); + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java new file mode 100644 index 0000000..a9b0e8e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java @@ -0,0 +1,72 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.LevelCallback; + +public final class DefaultEntityLookup extends EntityLookup { + public DefaultEntityLookup(final Level world) { + super(world, new DefaultLevelCallback()); + } + + @Override + protected Boolean blockTicketUpdates() { + return null; + } + + @Override + protected void setBlockTicketUpdates(final Boolean value) {} + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) {} + + @Override + protected void checkThread(final Entity entity, final String reason) {} + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + final ChunkEntitySlices ret = new ChunkEntitySlices( + this.world, chunkX, chunkZ, FullChunkStatus.FULL, + WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + // note: not handled by superclass + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + this.removeChunk(chunkX, chunkZ); + } + + protected static final class DefaultLevelCallback implements LevelCallback { + + @Override + public void onCreated(final Entity entity) {} + + @Override + public void onDestroyed(final Entity entity) {} + + @Override + public void onTickingStart(final Entity entity) {} + + @Override + public void onTickingEnd(final Entity entity) {} + + @Override + public void onTrackingStart(final Entity entity) {} + + @Override + public void onTrackingEnd(final Entity entity) {} + + @Override + public void onSectionChange(final Entity entity) {} + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java new file mode 100644 index 0000000..a6f4caf --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java @@ -0,0 +1,51 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.LevelCallback; + +public final class ServerEntityLookup extends EntityLookup { + + private final ServerLevel serverWorld; + + public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { + super(world, worldCallback); + this.serverWorld = world; + } + + @Override + protected Boolean blockTicketUpdates() { + return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates(); + } + + @Override + protected void setBlockTicketUpdates(final Boolean value) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value); + } + + @Override + protected void checkThread(final int chunkX, final int chunkZ, final String reason) { + TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason); + } + + @Override + protected void checkThread(final Entity entity, final String reason) { + TickThread.ensureTickThread(entity, reason); + } + + @Override + protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + // loadInEntityChunk will call addChunk for us + return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager + .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk); + } + + @Override + protected void onEmptySlices(final int chunkX, final int chunkZ) { + // entity slices unloading is managed by ticket levels in chunk system + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java new file mode 100644 index 0000000..458d1fc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java @@ -0,0 +1,17 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; + +public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage { + + public ServerLevel moonrise$getWorld(); + + public void moonrise$onUnload(final long coordinate); + + public void moonrise$loadInPoiChunk(final PoiChunk poiChunk); + + public void moonrise$checkConsistency(final ChunkAccess chunk); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java new file mode 100644 index 0000000..89b956b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java @@ -0,0 +1,12 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import java.util.Optional; + +public interface ChunkSystemPoiSection { + + public boolean moonrise$isEmpty(); + + public Optional moonrise$asOptional(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java new file mode 100644 index 0000000..fd35e4d --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java @@ -0,0 +1,212 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Optional; + +public final class PoiChunk { + + private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class); + + public final ServerLevel world; + public final int chunkX; + public final int chunkZ; + public final int minSection; + public final int maxSection; + + private final PoiSection[] sections; + + private boolean isDirty; + private boolean loaded; + + public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) { + this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]); + } + + public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.minSection = minSection; + this.maxSection = maxSection; + this.sections = sections; + if (this.sections.length != (maxSection - minSection + 1)) { + throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length); + } + } + + public void load() { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main"); + if (this.loaded) { + return; + } + this.loaded = true; + ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this); + } + + public boolean isLoaded() { + return this.loaded; + } + + public boolean isEmpty() { + for (final PoiSection section : this.sections) { + if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) { + return false; + } + } + + return true; + } + + public PoiSection getOrCreateSection(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + final int idx = chunkY - this.minSection; + final PoiSection ret = this.sections[idx]; + if (ret != null) { + return ret; + } + + final PoiManager poiManager = this.world.getPoiManager(); + final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ); + + return this.sections[idx] = new PoiSection(() -> { + poiManager.setDirty(key); + }); + } + throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]"); + } + + public PoiSection getSection(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + return this.sections[chunkY - this.minSection]; + } + return null; + } + + public Optional getSectionForVanilla(final int chunkY) { + if (chunkY >= this.minSection && chunkY <= this.maxSection) { + final PoiSection ret = this.sections[chunkY - this.minSection]; + return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional(); + } + return Optional.empty(); + } + + public boolean isDirty() { + return this.isDirty; + } + + public void setDirty(final boolean dirty) { + this.isDirty = dirty; + } + + // returns null if empty + public CompoundTag save() { + final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess()); + + final CompoundTag ret = new CompoundTag(); + final CompoundTag sections = new CompoundTag(); + ret.put("Sections", sections); + + ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + + final ServerLevel world = this.world; + final PoiManager poiManager = world.getPoiManager(); + final int chunkX = this.chunkX; + final int chunkZ = this.chunkZ; + + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final PoiSection section = this.sections[sectionY - this.minSection]; + if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) { + continue; + } + + final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); + // codecs are honestly such a fucking disaster. What the fuck is this trash? + final Codec codec = PoiSection.codec(() -> { + poiManager.setDirty(key); + }); + + final DataResult serializedResult = codec.encodeStart(registryOps, section); + final int finalSectionY = sectionY; + final Tag serialized = serializedResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); + }).orElse(null); + if (serialized == null) { + // failed, should be logged from the resultOrPartial + continue; + } + + sections.put(Integer.toString(sectionY), serialized); + } + + return sections.isEmpty() ? null : ret; + } + + public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { + final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); + ret.loaded = true; + return ret; + } + + public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { + final PoiChunk ret = empty(world, chunkX, chunkZ); + + final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess()); + + final CompoundTag sections = data.getCompound("Sections"); + + if (sections.isEmpty()) { + // nothing to parse + return ret; + } + + final PoiManager poiManager = world.getPoiManager(); + + boolean readAnything = false; + + for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { + final String key = Integer.toString(sectionY); + if (!sections.contains(key)) { + continue; + } + + final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); + // codecs are honestly such a fucking disaster. What the fuck is this trash? + final Codec codec = PoiSection.codec(() -> { + poiManager.setDirty(coordinateKey); + }); + + final CompoundTag section = sections.getCompound(key); + final DataResult deserializeResult = codec.parse(registryOps, section); + final int finalSectionY = sectionY; + final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> { + LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); + }).orElse(null); + + if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) { + // completely empty, no point in storing this + continue; + } + + readAnything = true; + ret.sections[sectionY - ret.minSection] = deserialized; + } + + ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java new file mode 100644 index 0000000..3f5edb7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java @@ -0,0 +1,21 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.level.storage; + +import com.mojang.serialization.Dynamic; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface ChunkSystemSectionStorage { + + public CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws IOException; + + public void moonrise$write(final int chunkX, final int chunkZ, final CompoundTag data) throws IOException; + + public RegionFileStorage moonrise$getRegionStorage(); + + public void moonrise$close() throws IOException; + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java new file mode 100644 index 0000000..003a857 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.player; + +public interface ChunkSystemServerPlayer { + + public boolean moonrise$isRealPlayer(); + + public void moonrise$setRealPlayer(final boolean real); + + public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader(); + + public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader); + + public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java new file mode 100644 index 0000000..5977e8f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -0,0 +1,1046 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.player; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; +import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; +import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; +import net.minecraft.server.level.ChunkTrackingView; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.network.PlayerChunkSender; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +public class RegionizedPlayerChunkLoader { + + public static final TicketType PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); + public static final TicketType PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); + + public static final int MIN_VIEW_DISTANCE = 2; + public static final int MAX_VIEW_DISTANCE = 32; + + public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL; + public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY); + public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL; + + public static class ViewDistanceHolder { + + private volatile ViewDistances viewDistances; + private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class); + + public ViewDistanceHolder() { + VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1)); + } + + public ViewDistances getViewDistances() { + return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this); + } + + public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) { + return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update); + } + + public void updateViewDistance(final Function update) { + int failures = 0; + for (ViewDistances curr = this.getViewDistances();;) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) { + return; + } + ++failures; + } + } + + public void setTickViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setTickViewDistance(distance); + }); + } + + public void setLoadViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setLoadViewDistance(distance); + }); + } + + public void setSendViewDistance(final int distance) { + this.updateViewDistance((final ViewDistances param) -> { + return param.setTickViewDistance(distance); + }); + } + } + + public static final record ViewDistances( + int tickViewDistance, + int loadViewDistance, + int sendViewDistance + ) { + public ViewDistances setTickViewDistance(final int distance) { + return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); + } + + public ViewDistances setLoadViewDistance(final int distance) { + return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); + } + + public ViewDistances setSendViewDistance(final int distance) { + return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); + } + } + + public static int getAPITickViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance(); + } + return data.lastTickDistance; + } + + public static int getAPIViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance(); + } + // view distance = load distance + 1 + return data.lastLoadDistance - 1; + } + + public static int getLoadViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance(); + } + // view distance = load distance + 1 + return data.lastLoadDistance - 1; + } + + public static int getAPISendViewDistance(final ServerPlayer player) { + final ServerLevel level = player.serverLevel(); + final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (data == null) { + return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance(); + } + return data.lastSendDistance; + } + + private final ServerLevel world; + + public RegionizedPlayerChunkLoader(final ServerLevel world) { + this.world = world; + } + + public void addPlayer(final ServerPlayer player) { + TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); + if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { + return; + } + + if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) { + throw new IllegalStateException("Player is already added to player chunk loader"); + } + + final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); + + ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader); + loader.add(); + } + + public void updatePlayer(final ServerPlayer player) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader != null) { + loader.update(); + } + } + + public void removePlayer(final ServerPlayer player) { + TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); + if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { + return; + } + + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + + if (loader == null) { + return; + } + + loader.remove(); + ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null); + } + + public void setSendDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance); + } + + public void setLoadDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance); + } + + public void setTickDistance(final int distance) { + ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance); + } + + // Note: follow the player chunk loader so everything stays consistent... + public int getAPITickDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); + return tickViewDistance; + } + + public int getAPIViewDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); + final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); + + // loadDistance = api view distance + 1 + return loadDistance - 1; + } + + public int getAPISendViewDistance() { + final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); + final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); + final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( + loadDistance, -1, -1, distances.sendViewDistance + ); + + return sendViewDistance; + } + + public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { + return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); + } + + public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null) { + return false; + } + + return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null) { + return false; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { + return true; + } + } + } + + return false; + } + + public void tick() { + TickThread.ensureTickThread("Cannot tick player chunk loader async"); + long currTime = System.nanoTime(); + for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null || loader.removed || loader.world != this.world) { + // not our problem anymore + continue; + } + loader.update(); // can't invoke plugin logic + loader.updateQueues(currTime); + } + } + + public static final class PlayerChunkLoaderData { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + private final long id = ID_GENERATOR.incrementAndGet(); + private final Long idBoxed = Long.valueOf(this.id); + + private static final long MAX_RATE = 10_000L; + + private final ServerPlayer player; + private final ServerLevel world; + + private int lastChunkX = Integer.MIN_VALUE; + private int lastChunkZ = Integer.MIN_VALUE; + + private int lastSendDistance = Integer.MIN_VALUE; + private int lastLoadDistance = Integer.MIN_VALUE; + private int lastTickDistance = Integer.MIN_VALUE; + + private int lastSentChunkCenterX = Integer.MIN_VALUE; + private int lastSentChunkCenterZ = Integer.MIN_VALUE; + + private int lastSentChunkRadius = Integer.MIN_VALUE; + private int lastSentSimulationDistance = Integer.MIN_VALUE; + + private boolean canGenerateChunks = true; + + private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); + private final LongOpenHashSet sentChunks = new LongOpenHashSet(); + + private static final byte CHUNK_TICKET_STAGE_NONE = 0; + private static final byte CHUNK_TICKET_STAGE_LOADING = 1; + private static final byte CHUNK_TICKET_STAGE_LOADED = 2; + private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; + private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; + private static final byte CHUNK_TICKET_STAGE_TICK = 5; + private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { + ChunkHolderManager.MAX_TICKET_LEVEL + 1, + LOADED_TICKET_LEVEL, + LOADED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + GENERATED_TICKET_LEVEL, + TICK_TICKET_LEVEL + }; + private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); + { + this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); + } + + // rate limiting + private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); + private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); + + // queues + private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { + final int c1x = CoordinateUtils.getChunkX(c1); + final int c1z = CoordinateUtils.getChunkZ(c1); + + final int c2x = CoordinateUtils.getChunkX(c2); + final int c2z = CoordinateUtils.getChunkZ(c2); + + final int centerX = PlayerChunkLoaderData.this.lastChunkX; + final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; + + return Integer.compare( + Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), + Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) + ); + }; + private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + + private volatile boolean removed; + + public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { + this.world = world; + this.player = player; + } + + private void flushDelayedTicketOps() { + if (this.delayedTicketOps.isEmpty()) { + return; + } + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps); + this.delayedTicketOps.clear(); + } + + private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { + this.delayedTicketOps.addLast(op); + } + + private void sendChunk(final int chunkX, final int chunkZ) { + if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); + PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ)); + return; + } + throw new IllegalStateException(); + } + + private void sendUnloadChunk(final int chunkX, final int chunkZ) { + if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { + return; + } + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { + // Note: Check PlayerChunkSender#dropChunk for other logic + // Note: drop isAlive() check so that chunks properly unload client-side when the player dies + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); + this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); + } + + private final SingleUserAreaMap broadcastMap = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we only care about remove + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + parameter.sendUnloadChunk(chunkX, chunkZ); + } + }; + private final SingleUserAreaMap loadTicketCleanup = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we only care about remove + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final byte ticketStage = parameter.chunkTicketStage.remove(chunk); + final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; + if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { + return; + } + + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + PLAYER_TICKET_DELAYED, level, parameter.idBoxed, + PLAYER_TICKET, level, parameter.idBoxed + )); + } + }; + private final SingleUserAreaMap tickMap = new SingleUserAreaMap<>(this) { + @Override + protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + // do nothing, we will detect ticking chunks when we try to load them + } + + @Override + protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at + // the tick stage it was deemed in range for loading. Thus, we need to move it to generated + if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { + return; + } + + // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that + // the level is kept for a short period of time + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( + chunk, + PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed, + PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed + )); + // keep chunk at new generated level + parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( + chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed + )); + } + }; + + private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, + final int sendRadius) { + // expect sendRadius to be = 1 + target viewable radius + return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); + } + + private static int getClientViewDistance(final ServerPlayer player) { + final Integer vd = player.requestedViewDistance(); + return vd == null ? -1 : Math.max(0, vd.intValue()); + } + + private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) { + return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance; + } + + private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, + final int worldLoadViewDistance) { + return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); + } + + private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, + final int playerSendViewDistance, final int worldSendViewDistance) { + return Math.min( + loadViewDistance - 1, + playerSendViewDistance < 0 ? (!PlaceholderConfig.chunkLoadingAdvanced$autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance + ); + } + + private Packet updateClientChunkRadius(final int radius) { + this.lastSentChunkRadius = radius; + return new ClientboundSetChunkCacheRadiusPacket(radius); + } + + private Packet updateClientSimulationDistance(final int distance) { + this.lastSentSimulationDistance = distance; + return new ClientboundSetSimulationDistancePacket(distance); + } + + private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { + this.lastSentChunkCenterX = chunkX; + this.lastSentChunkCenterZ = chunkZ; + return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); + } + + private boolean canPlayerGenerateChunks() { + return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); + } + + private double getMaxChunkLoadRate() { + final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkLoadRate; + + return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkGenRate() { + final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkGenerateRate; + + return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private double getMaxChunkSendRate() { + final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkSendRate; + + return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); + } + + private long getMaxChunkLoads() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); + long configLimit = PlaceholderConfig.chunkLoadingAdvanced$playerMaxConcurrentChunkLoads; + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); + } else if (configLimit < 0L) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.loadingQueue.size(); + + return configLimit; + } + + private long getMaxChunkGenerates() { + final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); + long configLimit = PlaceholderConfig.chunkLoadingAdvanced$playerMaxConcurrentChunkGenerates; + if (configLimit == 0L) { + // by default, only allow 1/5th of the chunks in the view distance to be concurrently active + configLimit = Math.max(5L, radiusChunks / 5L); + } else if (configLimit < 0L) { + configLimit = Integer.MAX_VALUE; + } // else: use the value configured + configLimit = configLimit - this.generatingQueue.size(); + + return configLimit; + } + + private boolean wantChunkSent(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded( + this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance + ); + } + + private boolean wantChunkTicked(final int chunkX, final int chunkZ) { + final int dx = this.lastChunkX - chunkX; + final int dz = this.lastChunkZ - chunkZ; + return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; + } + + private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) { + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ); + final byte stage = this.chunkTicketStage.get(neighbour); + + if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { + return false; + } + } + } + + return true; + } + + void updateQueues(final long time) { + TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); + if (this.removed) { + throw new IllegalStateException("Ticking removed player chunk loader"); + } + // update rate limits + final double loadRate = this.getMaxChunkLoadRate(); + final double genRate = this.getMaxChunkGenRate(); + final double sendRate = this.getMaxChunkSendRate(); + + this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate); + this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate); + this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate); + + // try to progress chunk loads + while (!this.loadingQueue.isEmpty()) { + final long pendingLoadChunk = this.loadingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); + final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + // chunk has loaded, so we can take it out of the queue + this.loadingQueue.dequeueLong(); + + // try to move to generate queue + final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); + if (prev != CHUNK_TICKET_STAGE_LOADING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); + } + + if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { + this.genQueue.enqueue(pendingLoadChunk); + } // else: don't want to generate, so just leave it loaded + } + + // try to push more chunk loads + final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads()))); + final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads); + if (maxLoadsThisTick > 0) { + final LongArrayList chunks = new LongArrayList(maxLoadsThisTick); + for (int i = 0; i < maxLoadsThisTick; ++i) { + final long chunk = this.loadQueue.dequeueLong(); + final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); + if (prev != CHUNK_TICKET_STAGE_NONE) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addOp( + chunk, + PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + chunks.add(chunk); + this.loadingQueue.enqueue(chunk); + } + + // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false + this.flushDelayedTicketOps(); + // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk + // load - only generate ticket levels start anything, but they start generation... + // propagate levels + // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); + + if (this.removed) { + // process ticket updates may invoke plugin logic, which may remove this player + return; + } + + for (int i = 0; i < maxLoadsThisTick; ++i) { + final long queuedLoadChunk = chunks.getLong(i); + final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); + final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad( + queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null + ); + if (this.removed) { + return; + } + } + } + + // try to progress chunk generations + while (!this.generatingQueue.isEmpty()) { + final long pendingGenChunk = this.generatingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); + final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ); + if (pending == null) { + // nothing to do here + break; + } + + // chunk has generated, so we can take it out of queue + this.generatingQueue.dequeueLong(); + + final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); + if (prev != CHUNK_TICKET_STAGE_GENERATING) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); + } + + // try to move to send queue + if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { + this.sendQueue.enqueue(pendingGenChunk); + } + // try to move to tick queue + if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { + this.tickingQueue.enqueue(pendingGenChunk); + } + } + + // try to push more chunk generations + final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates()))); + // preview the allocations, as we may not actually utilise all of them + final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens); + long ratedGensThisTick = 0L; + while (!this.genQueue.isEmpty()) { + final long chunkKey = this.genQueue.firstLong(); + final int chunkX = CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); + final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); + if (chunk.getStatus() != ChunkStatus.FULL) { + // only rate limit actual generations + if ((ratedGensThisTick + 1L) > maxGensThisTick) { + break; + } + ++ratedGensThisTick; + } + + this.genQueue.dequeueLong(); + + final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING); + if (prev != CHUNK_TICKET_STAGE_LOADED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); + } + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + chunkKey, + PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, + PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed + ) + ); + this.generatingQueue.enqueue(chunkKey); + } + // take the allocations we actually used + this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick); + + // try to pull ticking chunks + while (!this.tickingQueue.isEmpty()) { + final long pendingTicking = this.tickingQueue.firstLong(); + final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); + final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); + + if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ, + ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) { + break; + } + + // only gets here if all neighbours were marked as generated or ticking themselves + this.tickingQueue.dequeueLong(); + this.pushDelayedTicketOp( + ChunkHolderManager.TicketOperation.addAndRemove( + pendingTicking, + PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, + PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed + ) + ); + // note: there is no queue to add after ticking + final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); + if (prev != CHUNK_TICKET_STAGE_GENERATED) { + throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); + } + } + + // try to pull sending chunks + final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends + final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); + // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it + for (int i = 0; i < maxSendsThisTick; ++i) { + final long pendingSend = this.sendQueue.firstLong(); + final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); + final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); + final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ); + if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) { + // nothing to do + // the target chunk may not be owned by this region, but this should be resolved in the future + break; + } + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + // not yet post-processed, need to do this so that tile entities can properly be sent to clients + chunk.postProcessGeneration(); + // check if there was any recursive action + if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { + return; + } // else: good to dequeue and send, fall through + } + this.sendQueue.dequeueLong(); + + this.sendChunk(pendingSendX, pendingSendZ); + + if (this.removed) { + // sendChunk may invoke plugin logic + return; + } + } + + this.flushDelayedTicketOps(); + } + + void add() { + TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Adding removed player chunk loader"); + } + final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); + final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + final int chunkX = this.player.chunkPosition().x; + final int chunkZ = this.player.chunkPosition().z; + + final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + // TODO check PlayerList diff in paper chunk system patch + // send view distances + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + + // add to distance maps + this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1); + this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); + this.tickMap.add(chunkX, chunkZ, tickViewDistance); + + // update chunk center + this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); + + // reset limiters, they will start at a zero allocation + final long time = System.nanoTime(); + this.chunkLoadTicketLimiter.reset(time); + this.chunkGenerateTicketLimiter.reset(time); + this.chunkSendLimiter.reset(time); + + // now we can update + this.update(); + } + + private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { + return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ)); + } + + private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { + final BelowZeroRetrogen belowZeroRetrogen; + // see PortalForcer#findPortalAround + return chunkAccess != null && ( + chunkAccess.getStatus() == ChunkStatus.FULL || + ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN)) + ); + } + + void update() { + TickThread.ensureTickThread(this.player, "Cannot update player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Updating removed player chunk loader"); + } + final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); + final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); + + final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); + // load view cannot be less-than tick view + 1 + final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); + // send view cannot be greater-than load view + final int clientViewDistance = getClientViewDistance(this.player); + final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); + + final ChunkPos playerPos = this.player.chunkPosition(); + final boolean canGenerateChunks = this.canPlayerGenerateChunks(); + final int currentChunkX = playerPos.x; + final int currentChunkZ = playerPos.z; + + final int prevChunkX = this.lastChunkX; + final int prevChunkZ = this.lastChunkZ; + + if ( + // has view distance stayed the same? + sendViewDistance == this.lastSendDistance + && loadViewDistance == this.lastLoadDistance + && tickViewDistance == this.lastTickDistance + + // has our chunk stayed the same? + && prevChunkX == currentChunkX + && prevChunkZ == currentChunkZ + + // can we still generate chunks? + && this.canGenerateChunks == canGenerateChunks + ) { + // nothing we care about changed, so we're not re-calculating + return; + } + + // update distance maps + this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1); + this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); + this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); + if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) { + throw new IllegalStateException(); + } + + // update VDs for client + // this should be after the distance map updates, as they will send unload packets + if (this.lastSentChunkRadius != sendViewDistance) { + this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); + } + if (this.lastSentSimulationDistance != tickViewDistance) { + this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); + } + + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + this.lastChunkX = currentChunkX; + this.lastChunkZ = currentChunkZ; + this.lastSendDistance = sendViewDistance; + this.lastLoadDistance = loadViewDistance; + this.lastTickDistance = tickViewDistance; + this.canGenerateChunks = canGenerateChunks; + + // +1 since we need to load chunks +1 around the load view distance... + final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1); + // the iteration order is by increasing manhattan distance - so, we do NOT need to + // sort anything in the queue! + for (final long deltaChunk : toIterate) { + final int dx = CoordinateUtils.getChunkX(deltaChunk); + final int dz = CoordinateUtils.getChunkZ(deltaChunk); + final int chunkX = dx + currentChunkX; + final int chunkZ = dz + currentChunkZ; + final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + final int manhattanDistance = Math.abs(dx) + Math.abs(dz); + + // since chunk sending is not by radius alone, we need an extra check here to account for + // everything <= sendDistance + // Note: Vanilla may want to send chunks outside the send view distance, so we do need + // the dist <= view check + final boolean sendChunk = (squareDistance <= (sendViewDistance + 1)) + && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); + final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); + + if (!sendChunk && sentChunk) { + // have sent the chunk, but don't want it anymore + // unload it now + this.sendUnloadChunkRaw(chunkX, chunkZ); + } + + final byte stage = this.chunkTicketStage.get(chunk); + switch (stage) { + case CHUNK_TICKET_STAGE_NONE: { + // we want the chunk to be at least loaded + this.loadQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADING: { + this.loadingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_LOADED: { + if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { + this.genQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_GENERATING: { + this.generatingQueue.enqueue(chunk); + break; + } + case CHUNK_TICKET_STAGE_GENERATED: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + if (squareDistance <= tickViewDistance) { + this.tickingQueue.enqueue(chunk); + } + break; + } + case CHUNK_TICKET_STAGE_TICK: { + if (sendChunk && !sentChunk) { + this.sendQueue.enqueue(chunk); + } + break; + } + default: { + throw new IllegalStateException("Unknown stage: " + stage); + } + } + } + + // update the chunk center + // this must be done last so that the client does not ignore any of our unload chunk packets above + if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { + this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); + } + + this.flushDelayedTicketOps(); + } + + void remove() { + TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); + if (this.removed) { + throw new IllegalStateException("Removing removed player chunk loader"); + } + this.removed = true; + // sends the chunk unload packets + this.broadcastMap.remove(); + // cleans up loading/generating tickets + this.loadTicketCleanup.remove(); + // cleans up ticking tickets + this.tickMap.remove(); + + // purge queues + this.sendQueue.clear(); + this.tickingQueue.clear(); + this.generatingQueue.clear(); + this.genQueue.clear(); + this.loadingQueue.clear(); + this.loadQueue.clear(); + + // flush ticket changes + this.flushDelayedTicketOps(); + + // now all tickets should be removed, which is all of our external state + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java new file mode 100644 index 0000000..bc07e71 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java @@ -0,0 +1,140 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.queue; + +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +public final class ChunkUnloadQueue { + + public final int coordinateShift; + private final AtomicLong orderGenerator = new AtomicLong(); + private final ConcurrentLong2ReferenceChainedHashTable unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>(); + + /* + * Note: write operations do not occur in parallel for any given section. + * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly + */ + + public ChunkUnloadQueue(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} + + public List retrieveForAllRegions() { + final List ret = new ArrayList<>(); + + for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { + final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next(); + final long key = entry.getKey(); + final UnloadSection section = entry.getValue(); + final int sectionX = CoordinateUtils.getChunkX(key); + final int sectionZ = CoordinateUtils.getChunkZ(key); + + ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); + } + + ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { + return Long.compare(s1.order, s2.order); + }); + + return ret; + } + + public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { + return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ)); + } + + public UnloadSection removeSection(final int sectionX, final int sectionZ) { + return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ)); + } + + // write operation + public boolean addChunk(final int chunkX, final int chunkZ) { + // write operations do not occur in parallel for a given section + final int shift = this.coordinateShift; + final int sectionX = chunkX >> shift; + final int sectionZ = chunkZ >> shift; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + UnloadSection section = this.unloadSections.get(chunkKey); + if (section == null) { + section = new UnloadSection(this.orderGenerator.getAndIncrement()); + this.unloadSections.put(chunkKey, section); + } + + return section.chunks.add(chunkKey); + } + + // write operation + public boolean removeChunk(final int chunkX, final int chunkZ) { + // write operations do not occur in parallel for a given section + final int shift = this.coordinateShift; + final int sectionX = chunkX >> shift; + final int sectionZ = chunkZ >> shift; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final UnloadSection section = this.unloadSections.get(chunkKey); + + if (section == null) { + return false; + } + + if (!section.chunks.remove(chunkKey)) { + return false; + } + + if (section.chunks.isEmpty()) { + this.unloadSections.remove(chunkKey); + } + + return true; + } + + public JsonElement toDebugJson() { + final JsonArray ret = new JsonArray(); + + for (final SectionToUnload section : this.retrieveForAllRegions()) { + final JsonObject sectionJson = new JsonObject(); + ret.add(sectionJson); + + sectionJson.addProperty("sectionX", section.sectionX()); + sectionJson.addProperty("sectionZ", section.sectionX()); + sectionJson.addProperty("order", section.order()); + + final JsonArray coordinates = new JsonArray(); + sectionJson.add("coordinates", coordinates); + + final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); + for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final JsonObject coordinateJson = new JsonObject(); + coordinates.add(coordinateJson); + + coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); + coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); + } + } + + return ret; + } + + public static final class UnloadSection { + + public final long order; + public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); + + public UnloadSection(final long order) { + this.order = order; + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java new file mode 100644 index 0000000..7811982 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java @@ -0,0 +1,1423 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.SortedArraySet; +import net.minecraft.util.Unit; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.PrimitiveIterator; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Predicate; + +public final class ChunkHolderManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class); + + public static final int FULL_LOADED_TICKET_LEVEL = 33; + public static final int BLOCK_TICKING_TICKET_LEVEL = 32; + public static final int ENTITY_TICKING_TICKET_LEVEL = 31; + public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive + + public static final TicketType UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20); + + private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; + private static final long PROBE_MARKER = Long.MIN_VALUE + 1; + public final ReentrantAreaLock ticketLockArea; + + private final ConcurrentLong2ReferenceChainedHashTable>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); + private final ConcurrentLong2ReferenceChainedHashTable sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); + final ChunkUnloadQueue unloadQueue; + + private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); + private final ServerLevel world; + private final ChunkTaskScheduler taskScheduler; + private long currentTick; + + private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); + private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { + if (c1 == c2) { + return 0; + } + + final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + + if (saveTickCompare != 0) { + return saveTickCompare; + } + + final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); + final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + + if (coord1 == coord2) { + throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + } + + return Long.compare(coord1, coord2); + }); + + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + this.world = world; + this.taskScheduler = taskScheduler; + this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); + this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift()); + } + + public boolean processTicketUpdates(final int posX, final int posZ) { + final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; + final int ticketMask = (1 << ticketShift) - 1; + final List scheduledTasks = new ArrayList<>(); + final List changedFullStatus = new ArrayList<>(); + final boolean ret; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + ((posX >> ticketShift) - 1) << ticketShift, + ((posZ >> ticketShift) - 1) << ticketShift, + (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, + (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask + ); + try { + ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + this.addChangedStatuses(changedFullStatus); + + for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { + scheduledTasks.get(i).schedule(); + } + + return ret; + } + + private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List scheduledTasks, + final List changedFullStatus) { + return this.ticketLevelPropagator.performUpdate( + sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus + ); + } + + public List getOldChunkHolders() { + final List ret = new ArrayList<>(this.chunkHolders.size() + 1); + for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { + ret.add(iterator.next().vanillaChunkHolder); + } + return ret; + } + + public List getChunkHolders() { + final List ret = new ArrayList<>(this.chunkHolders.size() + 1); + for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { + ret.add(iterator.next()); + } + return ret; + } + + public int size() { + return this.chunkHolders.size(); + } + + // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks + public Iterable getOldChunkHoldersIterable() { + return new Iterable() { + @Override + public Iterator iterator() { + final Iterator iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public ChunkHolder next() { + return iterator.next().vanillaChunkHolder; + } + }; + } + }; + } + + public void close(final boolean save, final boolean halt) { + TickThread.ensureTickThread("Closing world off-main"); + if (halt) { + LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); + if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); + } else { + LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + } + + if (save) { + this.saveAllChunks(true, true, true); + } + + boolean hasTasks = false; + for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { + if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) { + hasTasks = true; + break; + } + } + if (hasTasks) { + RegionFileIOThread.flush(); + } + + // kill regionfile cache + for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { + try { + RegionFileIOThread.getControllerFor(this.world, type).getCache().close(); + } catch (final IOException ex) { + LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } + } + } + + void ensureInAutosave(final NewChunkHolder holder) { + if (!this.autoSaveQueue.contains(holder)) { + holder.lastAutoSave = this.currentTick; + this.autoSaveQueue.add(holder); + } + } + + public void autoSave() { + final List reschedule = new ArrayList<>(); + final long currentTick = this.currentTick; + final long maxSaveTime = currentTick - (long)PlaceholderConfig.autoSaveInterval; + for (int autoSaved = 0; autoSaved < (long)PlaceholderConfig.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { + final NewChunkHolder holder = this.autoSaveQueue.first(); + + if (holder.lastAutoSave > maxSaveTime) { + break; + } + + this.autoSaveQueue.remove(holder); + + holder.lastAutoSave = currentTick; + if (holder.save(false) != null) { + ++autoSaved; + } + + if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { + reschedule.add(holder); + } + } + + for (final NewChunkHolder holder : reschedule) { + if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { + this.autoSaveQueue.add(holder); + } + } + } + + public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { + final List holders = this.getChunkHolders(); + + if (logProgress) { + LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + + final DecimalFormat format = new DecimalFormat("#0.00"); + + int saved = 0; + + long start = System.nanoTime(); + long lastLog = start; + boolean needsFlush = false; + final int flushInterval = 50; + + int savedChunk = 0; + int savedEntity = 0; + int savedPoi = 0; + + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); + try { + final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); + if (saveStat != null) { + ++saved; + needsFlush = flush; + if (saveStat.savedChunk()) { + ++savedChunk; + } + if (saveStat.savedEntityChunk()) { + ++savedEntity; + } + if (saveStat.savedPoiChunk()) { + ++savedPoi; + } + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); + } + if (needsFlush && (saved % flushInterval) == 0) { + needsFlush = false; + RegionFileIOThread.partialFlush(flushInterval / 2); + } + if (logProgress) { + final long currTime = System.nanoTime(); + if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { + lastLog = currTime; + LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'"); + } + } + } + if (flush) { + RegionFileIOThread.flush(); + try { + RegionFileIOThread.flushRegionStorages(this.world); + } catch (final IOException ex) { + LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); + } + } + if (logProgress) { + LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"); + } + } + + private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { + @Override + protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { + // first the necessary chunkholders must be created, so just update the ticket levels + for (final Iterator iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { + final Long2ByteMap.Entry entry = iterator.next(); + final long key = entry.getLongKey(); + final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); + + NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); + if (current == null && newLevel > MAX_TICKET_LEVEL) { + // not loaded and it shouldn't be loaded! + iterator.remove(); + continue; + } + + final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); + if (currentLevel == newLevel) { + // nothing to do + iterator.remove(); + continue; + } + + if (current == null) { + // must create + current = ChunkHolderManager.this.createChunkHolder(key); + ChunkHolderManager.this.chunkHolders.put(key, current); + current.updateTicketLevel(newLevel); + } else { + current.updateTicketLevel(newLevel); + } + } + } + + @Override + protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, + final List changedFullStatus) { + final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); + CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); + try { + for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { + final long key = iterator.nextLong(); + final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); + + if (current == null) { + throw new IllegalStateException("Expected chunk holder to be created"); + } + + current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); + } + } finally { + CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); + } + } + }; + // function for converting between ticket levels and propagator levels and vice versa + // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects + // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator + // and the levels we get out of the propagator + + public static int convertBetweenTicketLevels(final int level) { + return ChunkLevel.MAX_LEVEL - level + 1; + } + + public String getTicketDebugString(final long coordinate) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); + try { + final SortedArraySet> tickets = this.tickets.get(coordinate); + + return tickets != null ? tickets.first().toString() : "no_ticket"; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public Long2ObjectOpenHashMap>> getTicketsCopy() { + final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>(); + final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); + for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { + final long coord = iterator.nextLong(); + sections.computeIfAbsent( + CoordinateUtils.getChunkKey( + CoordinateUtils.getChunkX(coord) >> sectionShift, + CoordinateUtils.getChunkZ(coord) >> sectionShift + ), + (final long keyInMap) -> { + return new LongArrayList(); + } + ).add(coord); + } + + for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + final LongArrayList coordinates = entry.getValue(); + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + try { + for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { + final long coord = iterator2.nextLong(); + final SortedArraySet> tickets = this.tickets.get(coord); + if (tickets == null) { + // removed before we acquired lock + continue; + } + ret.put(coord, ((ChunkSystemSortedArraySet>)tickets).moonrise$copy()); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + return ret; + } + + protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { + if (ticketLevel > ChunkLevel.MAX_LEVEL) { + this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); + } else { + this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); + } + } + + private static int getTicketLevelAt(SortedArraySet> tickets) { + return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; + } + + public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, + final T identifier) { + return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); + } + + public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, + final T identifier) { + return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); + } + + private void addExpireCount(final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + final long sectionKey = CoordinateUtils.getChunkKey( + chunkX >> sectionShift, + chunkZ >> sectionShift + ); + + this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> { + return new Long2IntOpenHashMap(); + }).addTo(chunkKey, 1); + } + + private void removeExpireCount(final int chunkX, final int chunkZ) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + final long sectionKey = CoordinateUtils.getChunkKey( + chunkX >> sectionShift, + chunkZ >> sectionShift + ); + + final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); + final int prevCount = removeCounts.addTo(chunkKey, -1); + + if (prevCount == 1) { + removeCounts.remove(chunkKey); + if (removeCounts.isEmpty()) { + this.sectionToChunkToExpireCount.remove(sectionKey); + } + } + } + + // supposed to return true if the ticket was added and did not replace another + // but, we always return false if the ticket cannot be added + public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { + return this.addTicketAtLevel(type, chunk, level, identifier, true); + } + + boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { + final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; + if (level > MAX_TICKET_LEVEL) { + return false; + } + + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + final Ticket ticket = new Ticket<>(type, level, identifier); + ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); + + final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; + try { + final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { + return SortedArraySet.create(4); + }); + + final int levelBefore = getTicketLevelAt(ticketsAtChunk); + final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); + final int levelAfter = getTicketLevelAt(ticketsAtChunk); + + if (current != ticket) { + final long oldRemoveDelay = ((ChunkSystemTicket)(Object)current).moonrise$getRemoveDelay(); + if (removeDelay != oldRemoveDelay) { + if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { + this.removeExpireCount(chunkX, chunkZ); + } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { + // since old != new, we have that NO_TIMEOUT_MARKER != new + this.addExpireCount(chunkX, chunkZ); + } + } + } else { + if (removeDelay != NO_TIMEOUT_MARKER) { + this.addExpireCount(chunkX, chunkZ); + } + } + + if (levelBefore != levelAfter) { + this.updateTicketLevel(chunk, levelAfter); + } + + return current == ticket; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { + return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); + } + + public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { + return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); + } + + public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { + return this.removeTicketAtLevel(type, chunk, level, identifier, true); + } + + boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { + if (level > MAX_TICKET_LEVEL) { + return false; + } + + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + final Ticket probe = new Ticket<>(type, level, identifier); + + final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; + try { + final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); + if (ticketsAtChunk == null) { + return false; + } + + final int oldLevel = getTicketLevelAt(ticketsAtChunk); + final Ticket ticket = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$removeAndGet(probe); + + if (ticket == null) { + return false; + } + + final int newLevel = getTicketLevelAt(ticketsAtChunk); + // we should not change the ticket levels while the target region may be ticking + if (oldLevel != newLevel) { + final Ticket unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); + ((ChunkSystemTicket)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout)); + if (ticketsAtChunk.add(unknownTicket)) { + this.addExpireCount(chunkX, chunkZ); + } else { + throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); + } + } + + final long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); + if (removeDelay != NO_TIMEOUT_MARKER) { + this.removeExpireCount(chunkX, chunkZ); + } + + return true; + } finally { + if (ticketLock != null) { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + // atomic with respect to all add/remove/addandremove ticket calls for the given chunk + public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); + try { + this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); + this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + // atomic with respect to all add/remove/addandremove ticket calls for the given chunk + public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); + try { + if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { + this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); + return true; + } + return false; + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { + if (ticketLevel > MAX_TICKET_LEVEL) { + return; + } + + final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>(); + final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); + for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { + final long coord = iterator.nextLong(); + sections.computeIfAbsent( + CoordinateUtils.getChunkKey( + CoordinateUtils.getChunkX(coord) >> sectionShift, + CoordinateUtils.getChunkZ(coord) >> sectionShift + ), + (final long keyInMap) -> { + return new LongArrayList(); + } + ).add(coord); + } + + for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + final LongArrayList coordinates = entry.getValue(); + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + try { + for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { + final long coord = iterator2.nextLong(); + this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + } + + public void tick() { + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + + final Predicate> expireNow = (final Ticket ticket) -> { + long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); + if (removeDelay == NO_TIMEOUT_MARKER) { + return false; + } + --removeDelay; + ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); + return removeDelay <= 0L; + }; + + for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { + final long sectionKey = iterator.nextLong(); + + if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { + // removed concurrently + continue; + } + + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift + ); + + try { + final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey); + if (chunkToExpireCount == null) { + // lost to some race + continue; + } + + for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { + final Long2IntMap.Entry entry = iterator1.next(); + + final long chunkKey = entry.getLongKey(); + final int expireCount = entry.getIntValue(); + + final SortedArraySet> tickets = this.tickets.get(chunkKey); + final int levelBefore = getTicketLevelAt(tickets); + + final int sizeBefore = tickets.size(); + tickets.removeIf(expireNow); + final int sizeAfter = tickets.size(); + final int levelAfter = getTicketLevelAt(tickets); + + if (tickets.isEmpty()) { + this.tickets.remove(chunkKey); + } + if (levelBefore != levelAfter) { + this.updateTicketLevel(chunkKey, levelAfter); + } + + final int newExpireCount = expireCount - (sizeBefore - sizeAfter); + + if (newExpireCount == expireCount) { + continue; + } + + if (newExpireCount != 0) { + entry.setValue(newExpireCount); + } else { + iterator1.remove(); + } + } + + if (chunkToExpireCount.isEmpty()) { + this.sectionToChunkToExpireCount.remove(sectionKey); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + } + + this.processTicketUpdates(); + } + + public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { + return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + public NewChunkHolder getChunkHolder(final long position) { + return this.chunkHolders.get(position); + } + + public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.raisePriority(priority); + } + } + + public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.setPriority(priority); + } + } + + public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); + if (chunkHolder != null) { + chunkHolder.lowerPriority(priority); + } + } + + private NewChunkHolder createChunkHolder(final long position) { + final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); + + ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); + + return ret; + } + + // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure + // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, + // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate + private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { + return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } + + private NewChunkHolder getOrCreateChunkHolder(final long position) { + final int chunkX = CoordinateUtils.getChunkX(position); + final int chunkZ = CoordinateUtils.getChunkZ(position); + + if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { + throw new IllegalStateException("Must hold ticket level update lock!"); + } + if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { + throw new IllegalStateException("Must hold scheduler lock!!"); + } + + // we could just acquire these locks, but... + // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns + + NewChunkHolder current = this.chunkHolders.get(position); + if (current != null) { + return current; + } + + current = this.createChunkHolder(position); + this.chunkHolders.put(position, current); + + + return current; + } + + public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); + ChunkEntitySlices ret; + + NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); + if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { + return ret; + } + + final AtomicBoolean isCompleted = new AtomicBoolean(); + final Thread waiter = Thread.currentThread(); + final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId(); + NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); + try { + this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); + try { + current = this.getOrCreateChunkHolder(chunkX, chunkZ); + if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { + this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + return ret; + } + + if (!transientChunk) { + if (current.isEntityChunkNBTLoaded()) { + isCompleted.setPlain(true); + } else { + loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> { + isCompleted.set(true); + LockSupport.unpark(waiter); + }); + final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); + + if (entityLoad != null) { + entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); + } + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + if (loadTask != null) { + loadTask.schedule(); + } + + if (!transientChunk) { + // Note: no need to busy wait on the chunk queue, entity load will complete off-main + boolean interrupted = false; + while (!isCompleted.get()) { + interrupted |= Thread.interrupted(); + LockSupport.park(); + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + // now that the entity data is loaded, we can load it into the world + + ret = current.loadInEntityChunk(transientChunk); + + this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); + + return ret; + } + + public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { + final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); + if (holder != null) { + final PoiChunk ret = holder.getPoiChunk(); + return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; + } + return null; + } + + public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { + TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); + PoiChunk ret; + + NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); + if (current != null && (ret = current.getPoiChunk()) != null) { + ret.load(); + return ret; + } + + final AtomicReference completed = new AtomicReference<>(); + final AtomicBoolean isCompleted = new AtomicBoolean(); + final Thread waiter = Thread.currentThread(); + final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId(); + NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); + try { + this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); + final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); + try { + current = this.getOrCreateChunkHolder(chunkX, chunkZ); + if (null == (ret = current.getPoiChunk())) { + loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> { + completed.setPlain(result.left()); + isCompleted.set(true); + LockSupport.unpark(waiter); + }); + final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + if (loadTask != null) { + loadTask.schedule(); + + // Note: no need to busy wait on the chunk queue, poi load will complete off-main + + boolean interrupted = false; + while (!isCompleted.get()) { + interrupted |= Thread.interrupted(); + LockSupport.park(); + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + + ret = completed.getPlain(); + } // else: became loaded during the scheduling attempt, need to ensure load() is invoked + + ret.load(); + + this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); + + return ret; + } + + void addChangedStatuses(final List changedFullStatus) { + if (changedFullStatus.isEmpty()) { + return; + } + if (!TickThread.isTickThread()) { + this.taskScheduler.scheduleChunkTask(() -> { + final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + + ChunkHolderManager.this.processPendingFullUpdate(); + }, PrioritisedExecutor.Priority.HIGHEST); + } else { + final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + } + } + + private void removeChunkHolder(final NewChunkHolder holder) { + holder.markUnloaded(); + this.autoSaveQueue.remove(holder); + ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); + this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); + + } + + // note: never call while inside the chunk system, this will absolutely break everything + public void processUnloads() { + TickThread.ensureTickThread("Cannot unload chunks off-main"); + + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot unload chunks recursively"); + } + final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift + final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); + int unloadCountTentative = 0; + for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { + final ChunkUnloadQueue.UnloadSection section + = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); + + if (section == null) { + // removed concurrently + continue; + } + + // technically reading the size field is unsafe, and it may be incorrect. + // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible + // for chunks to never unload or not unload fast enough. + unloadCountTentative += section.chunks.size(); + } + + if (unloadCountTentative <= 0) { + // no work to do + return; + } + + // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed. + this.processTicketUpdates(); + + final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); + int processedCount = 0; + + for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { + final List stage1 = new ArrayList<>(); + final List stage2 = new ArrayList<>(); + + final int sectionLowerX = sectionRef.sectionX() << sectionShift; + final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; + + // stage 1: set up for stage 2 while holding critical locks + ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ChunkUnloadQueue.UnloadSection section + = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); + + if (section == null) { + // removed concurrently + continue; + } + + // collect the holders to run stage 1 on + final int sectionCount = section.chunks.size(); + + if ((sectionCount + processedCount) <= toUnloadCount) { + // we can just drain the entire section + + for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { + final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); + if (holder == null) { + throw new IllegalStateException(); + } + stage1.add(holder); + } + + // remove section + this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); + } else { + // processedCount + len = toUnloadCount + // we cannot drain the entire section + for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { + final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); + if (holder == null) { + throw new IllegalStateException(); + } + stage1.add(holder); + } + } + + // run stage 1 + for (int i = 0, len = stage1.size(); i < len; ++i) { + final NewChunkHolder chunkHolder = stage1.get(i); + chunkHolder.removeFromUnloadQueue(); + if (chunkHolder.isSafeToUnload() != null) { + LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); + continue; + } + final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); + if (state == null) { + // can unload immediately + this.removeChunkHolder(chunkHolder); + continue; + } + stage2.add(state); + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(scheduleLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 + final List stage3 = new ArrayList<>(stage2.size()); + + final Boolean before = this.blockTicketUpdates(); + try { + for (int i = 0, len = stage2.size(); i < len; ++i) { + final NewChunkHolder.UnloadState state = stage2.get(i); + final NewChunkHolder holder = state.holder(); + + holder.unloadStage2(state); + stage3.add(holder); + } + } finally { + this.unblockTicketUpdates(before); + } + + // stage 3: actually attempt to remove the chunk holders + ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); + try { + final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); + try { + for (int i = 0, len = stage3.size(); i < len; ++i) { + final NewChunkHolder holder = stage3.get(i); + + if (holder.unloadStage3()) { + this.removeChunkHolder(holder); + } else { + // add cooldown so the next unload check is not immediately next tick + this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); + } + } + } finally { + this.taskScheduler.schedulingLockArea.unlock(scheduleLock); + } + } finally { + this.ticketLockArea.unlock(ticketLock); + } + + processedCount += stage1.size(); + + if (processedCount >= toUnloadCount) { + break; + } + } + } + + public enum TicketOperationType { + ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE + } + + public static record TicketOperation ( + TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier, + TicketType ticketType2, int ticketLevel2, V identifier2 + ) { + + private TicketOperation(TicketOperationType op, long chunkCoord, + TicketType ticketType, int ticketLevel, T identifier) { + this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); + } + + public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { + return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); + } + + public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { + return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); + } + + public static TicketOperation addIfRemovedOp(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + + public static TicketOperation addAndRemove(final long chunk, + final TicketType addType, final int addLevel, final T addIdentifier, + final TicketType removeType, final int removeLevel, final V removeIdentifier) { + return new TicketOperation<>( + TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, + removeType, removeLevel, removeIdentifier + ); + } + } + + private boolean processTicketOp(TicketOperation operation) { + boolean ret = false; + switch (operation.op) { + case ADD: { + ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); + break; + } + case REMOVE: { + ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); + break; + } + case ADD_IF_REMOVED: { + ret |= this.addIfRemovedTicket( + operation.chunkCoord, + operation.ticketType, operation.ticketLevel, operation.identifier, + operation.ticketType2, operation.ticketLevel2, operation.identifier2 + ); + break; + } + case ADD_AND_REMOVE: { + ret = true; + this.addAndRemoveTickets( + operation.chunkCoord, + operation.ticketType, operation.ticketLevel, operation.identifier, + operation.ticketType2, operation.ticketLevel2, operation.identifier2 + ); + break; + } + } + + return ret; + } + + public void performTicketUpdates(final Collection> operations) { + for (final TicketOperation operation : operations) { + this.processTicketOp(operation); + } + } + + private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { + return Boolean.FALSE; + }); + + public Boolean blockTicketUpdates() { + final Boolean ret = BLOCK_TICKET_UPDATES.get(); + BLOCK_TICKET_UPDATES.set(Boolean.TRUE); + return ret; + } + + public void unblockTicketUpdates(final Boolean before) { + BLOCK_TICKET_UPDATES.set(before); + } + + public boolean processTicketUpdates() { + return this.processTicketUpdates(true, null); + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); + + static List getCurrentTicketUpdateScheduling() { + return CURRENT_TICKET_UPDATE_SCHEDULING.get(); + } + + private boolean processTicketUpdates(final boolean processFullUpdates, List scheduledTasks) { + TickThread.ensureTickThread("Cannot process ticket levels off-main"); + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); + } + + List changedFullStatus = null; + + final boolean isTickThread = TickThread.isTickThread(); + + boolean ret = false; + final boolean canProcessFullUpdates = processFullUpdates & isTickThread; + final boolean canProcessScheduling = scheduledTasks == null; + + if (this.ticketLevelPropagator.hasPendingUpdates()) { + if (scheduledTasks == null) { + scheduledTasks = new ArrayList<>(); + } + changedFullStatus = new ArrayList<>(); + + ret |= this.ticketLevelPropagator.performUpdates( + this.ticketLockArea, this.taskScheduler.schedulingLockArea, + scheduledTasks, changedFullStatus + ); + } + + if (changedFullStatus != null) { + this.addChangedStatuses(changedFullStatus); + } + + if (canProcessScheduling && scheduledTasks != null) { + for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { + scheduledTasks.get(i).schedule(); + } + } + + if (canProcessFullUpdates) { + ret |= this.processPendingFullUpdate(); + } + + return ret; + } + + // only call on tick thread + private boolean processPendingFullUpdate() { + final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + + boolean ret = false; + + final List changedFullStatus = new ArrayList<>(); + + NewChunkHolder holder; + while ((holder = pendingFullLoadUpdate.poll()) != null) { + ret |= holder.handleFullStatusChange(changedFullStatus); + + if (!changedFullStatus.isEmpty()) { + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { + pendingFullLoadUpdate.add(changedFullStatus.get(i)); + } + changedFullStatus.clear(); + } + } + + return ret; + } + + public JsonObject getDebugJson() { + final JsonObject ret = new JsonObject(); + + ret.addProperty("lock_shift", Integer.valueOf(this.taskScheduler.getChunkSystemLockShift())); + ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); + ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift())); + + ret.add("unload_queue", this.unloadQueue.toDebugJson()); + + final JsonArray holders = new JsonArray(); + ret.add("chunkholders", holders); + + for (final NewChunkHolder holder : this.getChunkHolders()) { + holders.add(holder.getDebugJson()); + } + + /* TODO + final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); + ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); + + for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { + final long tick = tickEntry.getLongKey(); + final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); + + final JsonObject tickJson = new JsonObject(); + removeTickToChunkExpireTicketCount.add(tickJson); + + tickJson.addProperty("tick", Long.valueOf(tick)); + + final JsonArray tickEntries = new JsonArray(); + tickJson.add("entries", tickEntries); + + for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { + final long coordinate = entry.getLongKey(); + final int count = entry.getIntValue(); + + final JsonObject entryJson = new JsonObject(); + tickEntries.add(entryJson); + + entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); + entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + entryJson.addProperty("count", Integer.valueOf(count)); + } + } + + final JsonArray allTicketsJson = new JsonArray(); + ret.add("tickets", allTicketsJson); + + for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { + final long coordinate = coordinateTickets.getLongKey(); + final SortedArraySet> tickets = coordinateTickets.getValue(); + + final JsonObject coordinateJson = new JsonObject(); + allTicketsJson.add(coordinateJson); + + coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); + coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + + final JsonArray ticketsSerialized = new JsonArray(); + coordinateJson.add("tickets", ticketsSerialized); + + for (final Ticket ticket : tickets) { + final JsonObject ticketSerialized = new JsonObject(); + ticketsSerialized.add(ticketSerialized); + + ticketSerialized.addProperty("type", ticket.getType().toString()); + ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); + ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); + ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); + } + } + */ + + return ret; + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java new file mode 100644 index 0000000..20cc91a --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -0,0 +1,919 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask; +import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer; +import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public final class ChunkTaskScheduler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class); + + static int newChunkSystemIOThreads; + static int newChunkSystemWorkerThreads; + static int newChunkSystemGenParallelism; + static int newChunkSystemLoadParallelism; + + public static PrioritisedThreadPool workerThreads; + + private static boolean initialised = false; + + public static void init() { + if (initialised) { + return; + } + initialised = true; + newChunkSystemIOThreads = PlaceholderConfig.chunkSystemIOThreads; + newChunkSystemWorkerThreads = PlaceholderConfig.chunkSystemThreads; + if (newChunkSystemIOThreads < 0) { + newChunkSystemIOThreads = 1; + } else { + newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); + } + int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; + if (defaultWorkerThreads <= 4) { + defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; + } else { + defaultWorkerThreads = defaultWorkerThreads / 2; + } + defaultWorkerThreads = Integer.getInteger("Moonrise.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); + + if (newChunkSystemWorkerThreads < 0) { + newChunkSystemWorkerThreads = defaultWorkerThreads; + } else { + newChunkSystemWorkerThreads = Math.max(1, newChunkSystemWorkerThreads); + } + + String newChunkSystemGenParallelism = PlaceholderConfig.chunkSystemGenParallelism; + if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { + newChunkSystemGenParallelism = "true"; + } + boolean useParallelGen; + if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") + || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { + useParallelGen = true; + } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") + || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { + useParallelGen = false; + } else { + throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); + } + + ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? newChunkSystemWorkerThreads : 1; + ChunkTaskScheduler.newChunkSystemLoadParallelism = newChunkSystemWorkerThreads; + + RegionFileIOThread.init(newChunkSystemIOThreads); + workerThreads = new PrioritisedThreadPool( + "Paper Chunk System Worker Pool", newChunkSystemWorkerThreads, + (final Thread thread, final Integer id) -> { + thread.setPriority(Thread.NORM_PRIORITY - 2); + thread.setName("Moonrise Chunk System Worker #" + id.intValue()); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(final Thread thread, final Throwable throwable) { + LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); + } + }); + }, (long)(20.0e6)); // 20ms + + LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + newChunkSystemWorkerThreads + " worker threads, and gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenParallelism + " threads"); + } + + public static final TicketType CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); + private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong(); + + public static Long getNextChunkLoadId() { + return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo); + private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong(); + + public static Long getNextNonFullLoadId() { + return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo); + private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong(); + + public static Long getNextEntityLoadId() { + return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement()); + } + + public static final TicketType POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo); + private static final AtomicLong POI_LOAD_IDS = new AtomicLong(); + + public static Long getNextPoiLoadId() { + return Long.valueOf(POI_LOAD_IDS.getAndIncrement()); + } + + + public static int getTicketLevel(final ChunkStatus status) { + return ChunkHolderManager.FULL_LOADED_TICKET_LEVEL + ChunkStatus.getDistance(status); + } + + public final ServerLevel world; + public final PrioritisedThreadPool workers; + public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; + public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; + private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; + public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; + + private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); + + public final ChunkHolderManager chunkHolderManager; + + static { + ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1); + ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2); + ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0); + ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0); + + ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true); + ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true); + + /* + It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as + a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, + for a status to be parallel safe it must not read the block data from its neighbours. + */ + final List parallelCapableStatus = Arrays.asList( + // No-op executor. + ChunkStatus.EMPTY, + + // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. + // Does not touch neighbour chunks. + ChunkStatus.STRUCTURE_STARTS, + + // Surprisingly this is parallel capable. It is simply reading the already-created structure starts + // into the structure references for the chunk. So while it reads from it neighbours, its neighbours + // will not change, even if executed in parallel. + ChunkStatus.STRUCTURE_REFERENCES, + + // Safe. Mojang runs it in parallel as well. + ChunkStatus.BIOMES, + + // Safe. Mojang runs it in parallel as well. + ChunkStatus.NOISE, + + // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is + // completely thread-safe. + ChunkStatus.SURFACE, + + // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. + ChunkStatus.CARVERS, + + // FEATURES is not parallel safe. It writes to neighbours. + + // no-op executor + ChunkStatus.INITIALIZE_LIGHT + + // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. + + // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. + // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - + // see the task margin. + // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever + // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not + // safe to execute this in parallel. + // SPAWN + + // FULL is executed on main. + ); + + for (final ChunkStatus status : parallelCapableStatus) { + ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true); + } + } + + private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; + private static final int[] MAX_ACCESS_RADIUS_TABLE = new int[ACCESS_RADIUS_TABLE.length]; + static { + Arrays.fill(ACCESS_RADIUS_TABLE, -1); + } + + private static int getAccessRadius0(final ChunkStatus genStatus) { + if (genStatus == ChunkStatus.EMPTY) { + return 0; + } + + final int radius = Math.max(((ChunkSystemChunkStatus)genStatus).moonrise$getLoadRadius(), genStatus.getRange()); + int maxRange = radius; + + for (int dist = 1; dist <= radius; ++dist) { + final ChunkStatus requiredNeighbourStatus = ChunkStatus.getStatusAroundFullChunk(ChunkStatus.getDistance(genStatus) + dist); + final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; + if (rad == -1) { + throw new IllegalStateException(); + } + + maxRange = Math.max(maxRange, dist + rad); + } + + return maxRange; + } + + private static int maxAccessRadius; + + static { + final List statuses = ChunkStatus.getStatusList(); + for (int i = 0, len = statuses.size(); i < len; ++i) { + ACCESS_RADIUS_TABLE[i] = getAccessRadius0(statuses.get(i)); + } + int max = 0; + for (int i = 0, len = statuses.size(); i < len; ++i) { + MAX_ACCESS_RADIUS_TABLE[i] = max = Math.max(ACCESS_RADIUS_TABLE[i], max); + } + maxAccessRadius = max; + } + + public static int getMaxAccessRadius() { + return maxAccessRadius; + } + + public static int getAccessRadius(final ChunkStatus genStatus) { + return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; + } + + public static int getAccessRadius(final FullChunkStatus status) { + return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); + } + + + public final ReentrantAreaLock schedulingLockArea; + private final int lockShift; + + public final int getChunkSystemLockShift() { + return this.lockShift; + } + + public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { + this.world = world; + this.workers = workers; + // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift + // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections + // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning + // the entire section + // we just take the max, as we want the smallest shift that satisfies these properties + this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); + this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); + + final String worldName = WorldUtil.getWorldName(world); + this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism)); + this.radiusAwareGenExecutor = + newChunkSystemGenParallelism <= 1 ? this.parallelGenExecutor : workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, newChunkSystemGenParallelism); + this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism); + this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenParallelism)); + this.chunkHolderManager = new ChunkHolderManager(world, this); + } + + private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); + + public static Object stringIfNull(final Object obj) { + return obj == null ? "null" : obj; + } + + public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map objectsOfInterest, final Throwable thr) { + final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); + + if (this.failedChunkSystem.getAndSet(true)) { + return; + } + + final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); + + CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); + crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); + crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); + crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); + + crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); + for (final Map.Entry entry : objectsOfInterest.entrySet()) { + if (entry.getValue() instanceof Throwable thrObject) { + crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); + } else { + crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); + } + } + + final Runnable crash = () -> { + throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); + }; + + // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions + this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); + // so, make the main thread pick it up + ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); + } + + public boolean executeMainThreadTask() { + TickThread.ensureTickThread("Cannot execute main thread task off-main"); + return this.mainThreadExecutor.executeTask(); + } + + public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + this.chunkHolderManager.raisePriority(x, z, priority); + } + + public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + this.chunkHolderManager.setPriority(x, z, priority); + } + + public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { + this.chunkHolderManager.lowerPriority(x, z, priority); + } + + public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { + if (!TickThread.isTickThread()) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + final int accessRadius = getAccessRadius(toStatus); + if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); + } + if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk loading recursively"); + } + + if (toStatus == FullChunkStatus.INACCESSIBLE) { + throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); + } + + final int minLevel = 33 - (toStatus.ordinal() - 1); + final Long chunkReference = addTicket ? getNextChunkLoadId() : null; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + if (addTicket) { + this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + this.chunkHolderManager.processTicketUpdates(); + } + + final Consumer loadCallback = (final LevelChunk chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } finally { + if (addTicket) { + ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + } + } + }; + + final boolean scheduled; + final LevelChunk chunk; + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + scheduled = false; + chunk = null; + } else { + final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); + if (currStatus.isOrAfter(toStatus)) { + scheduled = false; + chunk = (LevelChunk)chunkHolder.getCurrentChunk(); + } else { + scheduled = true; + chunk = null; + + final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final NewChunkHolder neighbour = + (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); + if (neighbour != null) { + neighbour.raisePriority(priority); + } + } + } + + // ticket level should schedule for us + chunkHolder.addFullStatusConsumer(toStatus, loadCallback); + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + if (!scheduled) { + // couldn't schedule + try { + loadCallback.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk full status callback", thr); + } + } + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + if (gen) { + this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + return; + } + this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { + if (chunk == null) { + onComplete.accept(null); + } else { + if (chunk.getStatus().isOrAfter(toStatus)) { + this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } else { + onComplete.accept(null); + } + } + }); + } + + // only appropriate to use with syncLoadNonFull + public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final PrioritisedExecutor.Priority priority) { + final int accessRadius = getAccessRadius(toStatus); + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); + final List tasks = new ArrayList<>(); + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + return false; + } else { + final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); + if (genStatus != null && genStatus.isOrAfter(toStatus)) { + return true; + } else { + chunkHolder.raisePriority(priority); + + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + + return true; + } + + // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only + // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock + // between regions + public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { + if (status == null || status.isOrAfter(ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status); + } + ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); + if (loaded != null) { + return loaded; + } + + final Long ticketId = getNextNonFullLoadId(); + final int ticketLevel = getTicketLevel(status); + this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); + this.chunkHolderManager.processTicketUpdates(); + + this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING); + + // we could do a simple spinwait here, since we do not need to process tasks while performing this load + // but we process tasks only because it's a better use of the time spent + this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> { + return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null; + }); + + loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); + + this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); + + if (loaded == null) { + throw new IllegalStateException("Expected chunk to be loaded for status " + status); + } + + return loaded; + } + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + if (!TickThread.isTickThread()) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + final int accessRadius = getAccessRadius(toStatus); + if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); + } + if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { + throw new IllegalStateException("Cannot schedule chunk loading recursively"); + } + + if (toStatus == ChunkStatus.FULL) { + this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); + return; + } + + final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); + final Long chunkReference = addTicket ? getNextChunkLoadId() : null; + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + if (addTicket) { + this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + this.chunkHolderManager.processTicketUpdates(); + } + + final Consumer loadCallback = (final ChunkAccess chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } finally { + if (addTicket) { + ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); + } + } + }; + + final List tasks = new ArrayList<>(); + + final boolean scheduled; + final ChunkAccess chunk; + final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + scheduled = false; + chunk = null; + } else { + final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); + if (genStatus != null && genStatus.isOrAfter(toStatus)) { + scheduled = false; + chunk = chunkHolder.getCurrentChunk(); + } else { + scheduled = true; + chunk = null; + chunkHolder.raisePriority(priority); + + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } + chunkHolder.addStatusConsumer(toStatus, loadCallback); + } + } + } finally { + this.schedulingLockArea.unlock(schedulingLock); + } + } finally { + this.chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + + if (!scheduled) { + // couldn't schedule + try { + loadCallback.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + } + + private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, + final NewChunkHolder chunkHolder, final List neighbours, + final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { + if (toStatus == ChunkStatus.EMPTY) { + return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); + } + if (toStatus == ChunkStatus.LIGHT) { + return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); + } + if (toStatus == ChunkStatus.FULL) { + return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); + } + + return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); + } + + ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, + final List allTasks) { + return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)); + } + + // rets new task scheduled for the _specified_ chunk + // note: this must hold the scheduling lock + // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done + // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! + private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, + final NewChunkHolder chunkHolder, final List allTasks, + final PrioritisedExecutor.Priority minPriority) { + if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { + throw new IllegalStateException("Not holding scheduling lock"); + } + + if (chunkHolder.hasGenerationTask()) { + chunkHolder.upgradeGenTarget(targetStatus); + return null; + } + + final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max( + minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); + final ChunkAccess chunk = chunkHolder.getCurrentChunk(); + + if (currentGenStatus == null) { + // not yet loaded + final ChunkProgressionTask task = this.createTask( + chunkX, chunkZ, chunk, chunkHolder, Collections.emptyList(), ChunkStatus.EMPTY, requestedPriority + ); + + allTasks.add(task); + + final List chunkHolderNeighbours = new ArrayList<>(1); + chunkHolderNeighbours.add(chunkHolder); + + chunkHolder.setGenerationTarget(targetStatus); + chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); + + return task; + } + + if (currentGenStatus.isOrAfter(targetStatus)) { + // nothing to do + return null; + } + + // we know for sure now that we want to schedule _something_, so set the target + chunkHolder.setGenerationTarget(targetStatus); + + final ChunkStatus chunkRealStatus = chunk.getStatus(); + final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus(); + + // if this chunk has already generated up to or past the specified status, then we don't + // need the neighbours AT ALL. + final int neighbourReadRadius = chunkRealStatus.isOrAfter(toStatus) ? ((ChunkSystemChunkStatus)toStatus).moonrise$getLoadRadius() : toStatus.getRange(); + + boolean unGeneratedNeighbours = false; + + if (neighbourReadRadius > 0) { + final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; + for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) { + final int x = CoordinateUtils.getChunkX(pos); + final int z = CoordinateUtils.getChunkZ(pos); + final int radius = Math.max(Math.abs(x), Math.abs(z)); + final ChunkStatus requiredNeighbourStatus = chunkMap.getDependencyStatus(toStatus, radius); + + unGeneratedNeighbours |= this.checkNeighbour( + chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority + ); + } + } + + if (unGeneratedNeighbours) { + // can't schedule, but neighbour completion will schedule for us when they're ALL done + + // propagate our priority to neighbours + chunkHolder.recalculateNeighbourPriorities(); + return null; + } + + // need to gather neighbours + + final List neighbours; + final List chunkHolderNeighbours; + if (neighbourReadRadius <= 0) { + neighbours = new ArrayList<>(1); + chunkHolderNeighbours = new ArrayList<>(1); + neighbours.add(chunk); + chunkHolderNeighbours.add(chunkHolder); + } else { + // the iteration order is _very_ important, as all generation statuses expect a certain order such that: + // chunkAtRelative = neighbours.get(relX + relZ * (2 * radius + 1)) + neighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); + chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); + for (int dz = -neighbourReadRadius; dz <= neighbourReadRadius; ++dz) { + for (int dx = -neighbourReadRadius; dx <= neighbourReadRadius; ++dx) { + final NewChunkHolder holder = (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); + neighbours.add(holder.getChunkForNeighbourAccess()); + chunkHolderNeighbours.add(holder); + } + } + } + + final ChunkProgressionTask task = this.createTask( + chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, + chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + allTasks.add(task); + + chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); + + return task; + } + + // rets true if the neighbour is not at the required status, false otherwise + private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, + final List tasks, final PrioritisedExecutor.Priority minPriority) { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + + if (chunkHolder == null) { + throw new IllegalStateException("Missing chunkholder when required"); + } + + final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); + if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { + return false; + } + + if (chunkHolder.hasFailedGeneration()) { + return true; + } + + center.addGenerationBlockingNeighbour(chunkHolder); + chunkHolder.addWaitingNeighbour(center, requiredStatus); + + if (chunkHolder.upgradeGenTarget(requiredStatus)) { + return true; + } + + // not at status required, so we need to schedule its generation + this.schedule( + chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority + ); + + return true; + } + + /** + * @deprecated Chunk tasks must be tied to coordinates in the future + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { + return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); + } + + /** + * @deprecated Chunk tasks must be tied to coordinates in the future + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { + return this.mainThreadExecutor.queueRunnable(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { + return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { + return this.mainThreadExecutor.createTask(run, priority); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { + return this.mainThreadExecutor.queueRunnable(run); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { + return this.mainThreadExecutor.queueRunnable(run, priority); + } + + public boolean halt(final boolean sync, final long maxWaitNS) { + this.radiusAwareGenExecutor.halt(); + this.parallelGenExecutor.halt(); + this.loadExecutor.halt(); + final long time = System.nanoTime(); + if (sync) { + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + if ( + !this.radiusAwareGenExecutor.isActive() && + !this.parallelGenExecutor.isActive() && + !this.loadExecutor.isActive() + ) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack + + public static final class ChunkInfo { + + public final int chunkX; + public final int chunkZ; + public final ServerLevel world; + + public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + } + + @Override + public String toString() { + return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']"; + } + } + + public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { + synchronized (WAITING_CHUNKS) { + WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); + } + } + + public static void popChunkWait() { + synchronized (WAITING_CHUNKS) { + WAITING_CHUNKS.pop(); + } + } + + public static ChunkInfo[] getChunkInfos() { + synchronized (WAITING_CHUNKS) { + return WAITING_CHUNKS.toArray(new ChunkInfo[0]); + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java new file mode 100644 index 0000000..9debd78 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java @@ -0,0 +1,2016 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; +import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import net.minecraft.world.level.chunk.storage.EntityStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class NewChunkHolder { + + private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class); + + public final ServerLevel world; + public final int chunkX; + public final int chunkZ; + + public final ChunkTaskScheduler scheduler; + + // load/unload state + + // chunk data state + + private ChunkEntitySlices entityChunk; + // entity chunk that is loaded, but not yet deserialized + private CompoundTag pendingEntityChunk; + + ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); + final CompoundTag entityChunk; + final ChunkEntitySlices ret; + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { + return this.entityChunk; + } + final CompoundTag pendingEntityChunk = this.pendingEntityChunk; + if (!transientChunk && pendingEntityChunk == null) { + throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); + } + + if (this.entityChunk == null) { + ret = this.entityChunk = new ChunkEntitySlices( + this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), + WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) + ); + + ret.setTransient(transientChunk); + + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); + } else { + // transientChunk = false here + ret = this.entityChunk; + this.entityChunk.setTransient(false); + } + + if (!transientChunk) { + this.pendingEntityChunk = null; + entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; + } else { + entityChunk = null; + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (!transientChunk) { + if (entityChunk != null) { + final List entities = ChunkEntitySlices.readEntities(this.world, entityChunk); + + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); + } + } + + return ret; + } + + // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ + // been read from disk + private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); + + private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; + // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, + // then the task is rescheduled + private List entityDataLoadTaskWaiters; + + public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { + return this.entityDataLoadTask; + } + + // must hold schedule lock for the two below functions + + // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized + // or added into the world (or even into entityChunk) + public boolean isEntityChunkNBTLoaded() { + return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; + } + + private void completeEntityLoad(final GenericDataLoadTask.TaskResult result) { + final List completeWaiters; + ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; + boolean scheduleEntityTask = false; + ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + final List waiters = this.entityDataLoadTaskWaiters; + this.entityDataLoadTask = null; + if (result != null) { + this.entityDataLoadTaskWaiters = null; + this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); + if (result.right() != null) { + LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); + } + + for (final GenericDataLoadTaskCallback callback : waiters) { + callback.markCompleted(); + } + + completeWaiters = waiters; + } else { + // cancelled + completeWaiters = null; + + // need to re-schedule? + if (waiters.isEmpty()) { + this.entityDataLoadTaskWaiters = null; + // no tasks to schedule _for_ + } else { + entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + entityDataLoadTask.addCallback(this::completeEntityLoad); + // need one schedule() per waiter + for (final GenericDataLoadTaskCallback callback : waiters) { + scheduleEntityTask |= entityDataLoadTask.schedule(true); + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (scheduleEntityTask) { + entityDataLoadTask.scheduleNow(); + } + + // avoid holding the scheduling lock while completing + if (completeWaiters != null) { + for (final GenericDataLoadTaskCallback callback : completeWaiters) { + callback.acceptCompleted(result); + } + } + + schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held + // however, when the consumer is invoked, it will hold the schedule lock + public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer> consumer) { + if (this.isEntityChunkNBTLoaded()) { + throw new IllegalStateException("Cannot load entity data, it is already loaded"); + } + // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! + if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { + throw new IllegalStateException("Must hold scheduling lock"); + } + + final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); + + if (this.entityDataLoadTask == null) { + this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + this.entityDataLoadTask.addCallback(this::completeEntityLoad); + this.entityDataLoadTaskWaiters = new ArrayList<>(); + } + this.entityDataLoadTaskWaiters.add(ret); + if (this.entityDataLoadTask.schedule(true)) { + ret.schedule = this.entityDataLoadTask; + } + this.checkUnload(); + + return ret; + } + + private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { + + public EntityDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { + super(consumer, chunkHolder); + } + + @Override + void internalCancel() { + this.chunkHolder.entityDataLoadTaskWaiters.remove(this); + this.chunkHolder.entityDataLoadTask.cancel(); + } + } + + private PoiChunk poiChunk; + + private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; + // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, + // then the task is rescheduled + private List poiDataLoadTaskWaiters; + + public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { + return this.poiDataLoadTask; + } + + // must hold schedule lock for the two below functions + + public boolean isPoiChunkLoaded() { + return this.poiChunk != null; + } + + private void completePoiLoad(final GenericDataLoadTask.TaskResult result) { + final List completeWaiters; + ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; + boolean schedulePoiTask = false; + ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + final List waiters = this.poiDataLoadTaskWaiters; + this.poiDataLoadTask = null; + if (result != null) { + this.poiDataLoadTaskWaiters = null; + this.poiChunk = result.left(); + if (result.right() != null) { + LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); + } + + for (final GenericDataLoadTaskCallback callback : waiters) { + callback.markCompleted(); + } + + completeWaiters = waiters; + } else { + // cancelled + completeWaiters = null; + + // need to re-schedule? + if (waiters.isEmpty()) { + this.poiDataLoadTaskWaiters = null; + // no tasks to schedule _for_ + } else { + poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + poiDataLoadTask.addCallback(this::completePoiLoad); + // need one schedule() per waiter + for (final GenericDataLoadTaskCallback callback : waiters) { + schedulePoiTask |= poiDataLoadTask.schedule(true); + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (schedulePoiTask) { + poiDataLoadTask.scheduleNow(); + } + + // avoid holding the scheduling lock while completing + if (completeWaiters != null) { + for (final GenericDataLoadTaskCallback callback : completeWaiters) { + callback.acceptCompleted(result); + } + } + schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held + // however, when the consumer is invoked, it will hold the schedule lock + public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer> consumer) { + if (this.isPoiChunkLoaded()) { + throw new IllegalStateException("Cannot load poi data, it is already loaded"); + } + // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! + if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { + throw new IllegalStateException("Must hold scheduling lock"); + } + + final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); + + if (this.poiDataLoadTask == null) { + this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( + this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) + ); + this.poiDataLoadTask.addCallback(this::completePoiLoad); + this.poiDataLoadTaskWaiters = new ArrayList<>(); + } + this.poiDataLoadTaskWaiters.add(ret); + if (this.poiDataLoadTask.schedule(true)) { + ret.schedule = this.poiDataLoadTask; + } + this.checkUnload(); + + return ret; + } + + private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { + + public PoiDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { + super(consumer, chunkHolder); + } + + @Override + void internalCancel() { + this.chunkHolder.poiDataLoadTaskWaiters.remove(this); + this.chunkHolder.poiDataLoadTask.cancel(); + } + } + + public static abstract class GenericDataLoadTaskCallback implements Cancellable { + + protected final Consumer> consumer; + protected final NewChunkHolder chunkHolder; + protected boolean completed; + protected GenericDataLoadTask schedule; + protected final AtomicBoolean scheduled = new AtomicBoolean(); + + public GenericDataLoadTaskCallback(final Consumer> consumer, + final NewChunkHolder chunkHolder) { + this.consumer = consumer; + this.chunkHolder = chunkHolder; + } + + public void schedule() { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("Double calling schedule()"); + } + if (this.schedule != null) { + this.schedule.scheduleNow(); + this.schedule = null; + } + } + + boolean isCompleted() { + return this.completed; + } + + // must hold scheduling lock + private boolean setCompleted() { + if (this.completed) { + return false; + } + return this.completed = true; + } + + // must hold scheduling lock + void markCompleted() { + if (this.completed) { + throw new IllegalStateException("May not be completed here"); + } + this.completed = true; + } + + void acceptCompleted(final GenericDataLoadTask.TaskResult result) { + if (result != null) { + if (this.completed) { + this.consumer.accept(result); + } else { + throw new IllegalStateException("Cannot be uncompleted at this point"); + } + } else { + throw new NullPointerException("Result cannot be null (cancelled)"); + } + } + + // holds scheduling lock + abstract void internalCancel(); + + @Override + public boolean cancel() { + final NewChunkHolder holder = this.chunkHolder; + final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); + try { + if (!this.completed) { + this.completed = true; + this.internalCancel(); + return true; + } + return false; + } finally { + holder.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + } + + private ChunkAccess currentChunk; + + // generation status state + + /** + * Current status the chunk has been brought up to by the chunk system. null indicates no work at all + */ + private ChunkStatus currentGenStatus; + + // This allows unsynchronised access to the chunk and last gen status + private volatile ChunkCompletion lastChunkCompletion; + + public ChunkCompletion getLastChunkCompletion() { + return this.lastChunkCompletion; + } + + public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; + + /** + * The target final chunk status the chunk system will bring the chunk to. + */ + private ChunkStatus requestedGenStatus; + + private ChunkProgressionTask generationTask; + private ChunkStatus generationTaskStatus; + + /** + * contains the neighbours that this chunk generation is blocking on + */ + private final ReferenceLinkedOpenHashSet neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); + + /** + * map of ChunkHolder -> Required Status for this chunk + */ + private final Reference2ObjectLinkedOpenHashMap neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); + + public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { + this.neighboursBlockingGenTask.add(neighbour); + } + + public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { + final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); + this.neighboursWaitingForUs.put(neighbour, requiredStatus); + if (wasEmpty) { + this.checkUnload(); + } + } + + // priority state + + // the target priority for this chunk to generate at + private PrioritisedExecutor.Priority priority = null; + private boolean priorityLocked; + + // the priority neighbouring chunks have requested this chunk generate at + private PrioritisedExecutor.Priority neighbourRequestedPriority = null; + + public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) { + final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority; + final PrioritisedExecutor.Priority us = this.priority; + + if (neighbour == null) { + return us == null ? dfl : us; + } + if (us == null) { + return dfl; + } + + return PrioritisedExecutor.Priority.max(us, neighbour); + } + + private void recalculateNeighbourRequestedPriority() { + if (this.neighboursWaitingForUs.isEmpty()) { + this.neighbourRequestedPriority = null; + return; + } + + PrioritisedExecutor.Priority max = null; + + for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { + final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null); + if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { + max = neighbourPriority; + } + } + + final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); + this.neighbourRequestedPriority = max; + final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); + + if (current == next) { + return; + } + + // our effective priority has changed, so change our task + if (this.generationTask != null) { + this.generationTask.setPriority(next); + } + + // now propagate this to our neighbours + this.recalculateNeighbourPriorities(); + } + + public void recalculateNeighbourPriorities() { + for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { + holder.recalculateNeighbourRequestedPriority(); + } + } + + // must hold scheduling lock + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (this.priority == null || this.priority.isHigherOrEqualPriority(priority)) { + return; + } + this.setPriority(priority); + } + + private void lockPriority() { + this.priority = null; + this.priorityLocked = true; + } + + // must hold scheduling lock + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (this.priorityLocked) { + return; + } + final PrioritisedExecutor.Priority old = this.getEffectivePriority(null); + this.priority = priority; + final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); + + if (old != newPriority) { + if (this.generationTask != null) { + this.generationTask.setPriority(newPriority); + } + } + + this.recalculateNeighbourPriorities(); + } + + // must hold scheduling lock + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (this.priority == null || this.priority.isLowerOrEqualPriority(priority)) { + return; + } + this.setPriority(priority); + } + + // error handling state + private ChunkStatus failedGenStatus; + private Throwable genTaskException; + private Thread genTaskFailedThread; + + private boolean failedLightUpdate; + + public void failedLightUpdate() { + this.failedLightUpdate = true; + } + + public boolean hasFailedGeneration() { + return this.genTaskException != null; + } + + // ticket level state + private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; + + public int getTicketLevel() { + return this.currentTicketLevel; + } + + public final ChunkHolder vanillaChunkHolder; + + public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.scheduler = scheduler; + this.vanillaChunkHolder = new ChunkHolder( + new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world, + world.getLightEngine(), null, world.getChunkSource().chunkMap + ); + ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this); + } + + private ImposterProtoChunk wrappedChunkForNeighbour; + + // holds scheduling lock + public ChunkAccess getChunkForNeighbourAccess() { + // Vanilla overrides the status futures with an imposter chunk to prevent writes to full chunks + // But we don't store per-status futures, so we need this hack + if (this.wrappedChunkForNeighbour != null) { + return this.wrappedChunkForNeighbour; + } + final ChunkAccess ret = this.currentChunk; + return ret instanceof LevelChunk fullChunk ? this.wrappedChunkForNeighbour = new ImposterProtoChunk(fullChunk, false) : ret; + } + + public ChunkAccess getCurrentChunk() { + return this.currentChunk; + } + + int getCurrentTicketLevel() { + return this.currentTicketLevel; + } + + void updateTicketLevel(final int toLevel) { + this.currentTicketLevel = toLevel; + } + + private int totalNeighboursUsingThisChunk = 0; + + // holds schedule lock + public void addNeighbourUsingChunk() { + final int now = ++this.totalNeighboursUsingThisChunk; + + if (now == 1) { + this.checkUnload(); + } + } + + // holds schedule lock + public void removeNeighbourUsingChunk() { + final int now = --this.totalNeighboursUsingThisChunk; + + if (now == 0) { + this.checkUnload(); + } + + if (now < 0) { + throw new IllegalStateException("Neighbours using this chunk cannot be negative"); + } + } + + // must hold scheduling lock + // returns string reason for why chunk should remain loaded, null otherwise + public final String isSafeToUnload() { + // is ticket level below threshold? + if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { + return "ticket_level"; + } + + // are we being used by another chunk for generation? + if (this.totalNeighboursUsingThisChunk != 0) { + return "neighbours_generating"; + } + + // are we going to be used by another chunk for generation? + if (!this.neighboursWaitingForUs.isEmpty()) { + return "neighbours_waiting"; + } + + // chunk must be marked inaccessible (i.e. unloaded to plugins) + if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { + return "fullchunkstatus"; + } + + // are we currently generating anything, or have requested generation? + if (this.generationTask != null) { + return "generating"; + } + if (this.requestedGenStatus != null) { + return "requested_generation"; + } + + // entity data requested? + if (this.entityDataLoadTask != null) { + return "entity_data_requested"; + } + + // poi data requested? + if (this.poiDataLoadTask != null) { + return "poi_data_requested"; + } + + // are we pending serialization? + if (this.entityDataUnload != null) { + return "entity_serialization"; + } + if (this.poiDataUnload != null) { + return "poi_serialization"; + } + if (this.chunkDataUnload != null) { + return "chunk_serialization"; + } + + // Note: light tasks do not need a check, as they add a ticket. + + // nothing is using this chunk, so it should be unloaded + return null; + } + + /** Unloaded from chunk map */ + private boolean unloaded; + + void markUnloaded() { + this.unloaded = true; + } + + private boolean inUnloadQueue = false; + + void removeFromUnloadQueue() { + this.inUnloadQueue = false; + } + + // must hold scheduling lock + private void checkUnload() { + if (this.unloaded) { + return; + } + if (this.isSafeToUnload() == null) { + // ensure in unload queue + if (!this.inUnloadQueue) { + this.inUnloadQueue = true; + this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); + } + } else { + // ensure not in unload queue + if (this.inUnloadQueue) { + this.inUnloadQueue = false; + this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); + } + } + } + + static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; + + // note: these are completed with null to indicate that no write occurred + // they are also completed with null to indicate a null write occurred + private UnloadTask chunkDataUnload; + private UnloadTask entityDataUnload; + private UnloadTask poiDataUnload; + + public static final record UnloadTask(Completable completable, DelayedPrioritisedTask task) {} + + public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { + switch (type) { + case CHUNK_DATA: + return this.chunkDataUnload; + case ENTITY_DATA: + return this.entityDataUnload; + case POI_DATA: + return this.poiDataUnload; + default: + throw new IllegalStateException("Unknown regionfile type " + type); + } + } + + private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) { + switch (type) { + case CHUNK_DATA: { + this.chunkDataUnload = null; + return; + } + case ENTITY_DATA: { + this.entityDataUnload = null; + return; + } + case POI_DATA: { + this.poiDataUnload = null; + return; + } + default: + throw new IllegalStateException("Unknown regionfile type " + type); + } + } + + private UnloadState unloadState; + + // holds schedule lock + UnloadState unloadStage1() { + // because we hold the scheduling lock, we cannot actually unload anything + // so, what we do here instead is to null this chunk's state and setup the unload tasks + // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which + // we do not hold the lock) c + final ChunkAccess chunk = this.currentChunk; + final ChunkEntitySlices entityChunk = this.entityChunk; + final PoiChunk poiChunk = this.poiChunk; + // chunk state + this.currentChunk = null; + this.currentGenStatus = null; + this.wrappedChunkForNeighbour = null; + this.lastChunkCompletion = null; + // entity chunk state + this.entityChunk = null; + this.pendingEntityChunk = null; + + // poi chunk state + this.poiChunk = null; + + // priority state + this.priorityLocked = false; + + if (chunk != null) { + this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); + } + if (poiChunk != null) { + this.poiDataUnload = new UnloadTask(new Completable<>(), null); + } + if (entityChunk != null) { + this.entityDataUnload = new UnloadTask(new Completable<>(), null); + } + + return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; + } + + // data is null if failed or does not need to be saved + void completeAsyncUnloadDataSave(final RegionFileIOThread.RegionFileType type, final CompoundTag data) { + if (data != null) { + RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); + } + + this.getUnloadTask(type).completable().complete(data); + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + // can only write to these fields while holding the schedule lock + this.removeUnloadTask(type); + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + void unloadStage2(final UnloadState state) { + this.unloadState = null; + final ChunkAccess chunk = state.chunk(); + final ChunkEntitySlices entityChunk = state.entityChunk(); + final PoiChunk poiChunk = state.poiChunk(); + + final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk); + + // unload chunk data + if (chunk != null) { + if (chunk instanceof LevelChunk levelChunk) { + levelChunk.setLoaded(false); + } + + if (!shouldLevelChunkNotSave) { + this.saveChunk(chunk, true); + } else { + this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); + } + + if (chunk instanceof LevelChunk levelChunk) { + this.world.unload(levelChunk); + } + } + + // unload entity data + if (entityChunk != null) { + this.saveEntities(entityChunk, true); + // yes this is a hack to pass the compound tag through... + final CompoundTag lastEntityUnload = this.lastEntityUnload; + this.lastEntityUnload = null; + + if (entityChunk.unload()) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + entityChunk.setTransient(true); + this.entityChunk = entityChunk; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } else { + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); + } + // we need to delay the callback until after determining transience, otherwise a potential loader could + // set entityChunk before we do + this.entityDataUnload.completable().complete(lastEntityUnload); + } + + // unload poi data + if (poiChunk != null) { + if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { + this.savePOI(poiChunk, true); + } else { + this.poiDataUnload.completable().complete(null); + } + + if (poiChunk.isLoaded()) { + ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); + } + } + } + + boolean unloadStage3() { + // can only write to these while holding the schedule lock, and we instantly complete them in stage2 + this.poiDataUnload = null; + this.entityDataUnload = null; + + // we need to check if anything has been loaded in the meantime (or if we have transient entities) + if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { + return false; + } + + return this.isSafeToUnload() == null; + } + + private void cancelGenTask() { + if (this.generationTask != null) { + this.generationTask.cancel(); + } else { + // otherwise, we are blocking on neighbours, so remove them + if (!this.neighboursBlockingGenTask.isEmpty()) { + for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { + if (neighbour.neighboursWaitingForUs.remove(this) == null) { + throw new IllegalStateException("Corrupt state"); + } + if (neighbour.neighboursWaitingForUs.isEmpty()) { + neighbour.checkUnload(); + } + } + this.neighboursBlockingGenTask.clear(); + this.checkUnload(); + } + } + } + + // holds: ticket level update lock + // holds: schedule lock + public void processTicketLevelUpdate(final List scheduledTasks, final List changedLoadStatus) { + final int oldLevel = this.oldTicketLevel; + final int newLevel = this.currentTicketLevel; + + if (oldLevel == newLevel) { + return; + } + + this.oldTicketLevel = newLevel; + + final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); + final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); + final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; + final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; + + final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); + final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); + + // check for cancellations from downgrading ticket level + if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { + // note: cancel() may invoke onChunkGenComplete synchronously here + if (newUnloaded) { + // need to cancel all tasks + // note: requested status must be set to null here before cancellation, to indicate to the + // completion logic that we do not want rescheduling to occur + this.requestedGenStatus = null; + this.cancelGenTask(); + } else { + final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus(); + final ChunkStatus currentRequestedStatus = this.requestedGenStatus; + + if (currentRequestedStatus.isOrAfter(toCancel)) { + // we do have to cancel something here + // clamp requested status to the maximum + if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { + // already generated to status, so we must cancel + this.requestedGenStatus = null; + this.cancelGenTask(); + } else { + // not generated to status, so we may have to cancel + // note: gen task is always 1 status above current gen status if not null + this.requestedGenStatus = maxGenerationStatusNew; + if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { + // TOOD is this even possible? i don't think so + throw new IllegalStateException("?????"); + } + } + } + } + } + + if (oldState != newState) { + if (newState.isOrAfter(oldState)) { + // status upgrade + if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { + // may need to schedule full load + if (this.currentGenStatus != ChunkStatus.FULL) { + if (this.requestedGenStatus != null) { + this.requestedGenStatus = ChunkStatus.FULL; + } else { + this.scheduler.schedule( + this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks + ); + } + } + } + } else { + // status downgrade + if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); + } + + if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); + } + + if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { + this.completeFullStatusConsumers(FullChunkStatus.FULL, null); + } + } + + if (this.updatePendingStatus()) { + changedLoadStatus.add(this); + } + } + + if (oldUnloaded != newUnloaded) { + this.checkUnload(); + } + } + + static final int NEIGHBOUR_RADIUS = 2; + private long fullNeighbourChunksLoadedBitset; + + private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { + // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) + // optimised variant of the above by moving some of the ops to compile time + return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); + } + public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { + return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; + } + + // returns true if this chunk changed pending full status + // must hold scheduling lock + public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { + final int index = getFullNeighbourIndex(relativeX, relativeZ); + this.fullNeighbourChunksLoadedBitset |= (1L << index); + return this.updatePendingStatus(); + } + + // returns true if this chunk changed pending full status + // must hold scheduling lock + public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { + final int index = getFullNeighbourIndex(relativeX, relativeZ); + this.fullNeighbourChunksLoadedBitset &= ~(1L << index); + return this.updatePendingStatus(); + } + + private static long getLoadedMask(final int radius) { + long mask = 0L; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + mask |= (1L << getFullNeighbourIndex(dx, dz)); + } + } + + return mask; + } + + private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0); + private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1); + private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2); + + public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { + switch (radius) { + case 0: { + return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0; + } + case 1: { + return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1; + } + case 2: { + return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2; + } + + default: { + throw new IllegalArgumentException("Radius not recognized: " + radius); + } + } + } + + // only updated while holding scheduling lock + private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE; + // updated while holding no locks, but adds a ticket before to prevent pending status from dropping + // so, current will never update to a value higher than pending + private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE; + + public FullChunkStatus getChunkStatus() { + // no volatile access, access off-main is considered racey anyways + return this.currentFullChunkStatus; + } + + public boolean isEntityTickingReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); + } + + public boolean isTickingReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); + } + + public boolean isFullChunkReady() { + return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); + } + + private static FullChunkStatus getStatusForBitset(final long bitset) { + if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) { + return FullChunkStatus.ENTITY_TICKING; + } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) { + return FullChunkStatus.BLOCK_TICKING; + } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) { + return FullChunkStatus.FULL; + } else { + return FullChunkStatus.INACCESSIBLE; + } + } + + // must hold scheduling lock + // returns whether the pending status was changed + private boolean updatePendingStatus() { + final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock + + FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); + if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { + // the bitset is only for chunks that have gone through the status updater + // but here we are ready to go to FULL + pending = FullChunkStatus.FULL; + } + + if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel + // cannot set above ticket level + pending = byTicketLevel; + } + + if (this.pendingFullChunkStatus == pending) { + return false; + } + + this.pendingFullChunkStatus = pending; + + return true; + } + + private void onFullChunkLoadChange(final boolean loaded, final List changedFullStatus) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); + try { + for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { + for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { + final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); + if (loaded) { + if (holder.setNeighbourFullLoaded(-dx, -dz)) { + changedFullStatus.add(holder); + } + } else { + if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { + changedFullStatus.add(holder); + } + } + } + } + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + + private void changeEntityChunkStatus(final FullChunkStatus toStatus) { + ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); + } + + private boolean processingFullStatus = false; + + private void updateCurrentState(final FullChunkStatus to) { + this.currentFullChunkStatus = to; + } + + // only to be called on the main thread, no locks need to be held + public boolean handleFullStatusChange(final List changedFullStatus) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); + + boolean ret = false; + + if (this.processingFullStatus) { + // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status + return ret; + } + + this.processingFullStatus = true; + try { + for (;;) { + // check if we have any remaining work to do + + // we do not need to hold the scheduling lock to read pending, as changes to pending + // will queue a status update + + final FullChunkStatus pending = this.pendingFullChunkStatus; + FullChunkStatus current = this.currentFullChunkStatus; + + if (pending == current) { + if (pending == FullChunkStatus.INACCESSIBLE) { + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + this.checkUnload(); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + } + return ret; + } + + ret = true; + + // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we + // do not need to consider cases where the ticket level may decrease during this call by asynchronous + // ticket changes + + // chunks cannot downgrade state while status is pending a change + // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE + final LevelChunk chunk = (LevelChunk)this.currentChunk; + + // Note: we assume that only load/unload contain plugin logic + // plugin logic is anything stupid enough to possibly change the chunk status while it is already + // being changed (i.e during load it is possible it will try to set to full ticking) + // in order to allow this change, we also need this plugin logic to be contained strictly after all + // of the chunk system load callbacks are invoked + if (pending.isOrAfter(current)) { + // state upgrade + if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) { + this.updateCurrentState(FullChunkStatus.FULL); + this.scheduler.chunkHolderManager.ensureInAutosave(this); + this.changeEntityChunkStatus(FullChunkStatus.FULL); + ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder); + this.onFullChunkLoadChange(true, changedFullStatus); + this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); + } + + if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); + this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); + ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder); + this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); + } + + if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); + this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); + ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder); + this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); + } + } else { + if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { + this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); + ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); + } + + if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { + this.changeEntityChunkStatus(FullChunkStatus.FULL); + ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.FULL); + } + + if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) { + this.onFullChunkLoadChange(false, changedFullStatus); + this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); + ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder); + this.updateCurrentState(FullChunkStatus.INACCESSIBLE); + } + } + } + } finally { + this.processingFullStatus = false; + } + } + + // note: must hold scheduling lock + // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) + boolean upgradeGenTarget(final ChunkStatus toStatus) { + if (toStatus == null) { + throw new NullPointerException("toStatus cannot be null"); + } + if (this.requestedGenStatus == null && this.generationTask == null) { + return false; + } + if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { + this.requestedGenStatus = toStatus; + } + return true; + } + + public void setGenerationTarget(final ChunkStatus toStatus) { + this.requestedGenStatus = toStatus; + } + + public boolean hasGenerationTask() { + return this.generationTask != null; + } + + public ChunkStatus getCurrentGenStatus() { + return this.currentGenStatus; + } + + public ChunkStatus getRequestedGenStatus() { + return this.requestedGenStatus; + } + + private final Reference2ObjectOpenHashMap>> statusWaiters = new Reference2ObjectOpenHashMap<>(); + + void addStatusConsumer(final ChunkStatus status, final Consumer consumer) { + this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { + return new ArrayList<>(4); + }).add(consumer); + } + + private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { + // need to tell future statuses to complete if cancelled + do { + this.completeStatusConsumers0(status, chunk); + } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus())); + } + + private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { + final List> consumers; + consumers = this.statusWaiters.remove(status); + + if (consumers == null) { + return; + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid + this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + }, PrioritisedExecutor.Priority.HIGHEST); + } + + private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); + + void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { + this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { + return new ArrayList<>(4); + }).add(consumer); + } + + private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { + final List> consumers; + consumers = this.fullStatusWaiters.remove(status); + + if (consumers == null) { + return; + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid + this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); + } catch (final Throwable thr) { + LOGGER.error("Failed to process chunk status callback", thr); + } + } + }, PrioritisedExecutor.Priority.HIGHEST); + } + + // note: must hold scheduling lock + private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, + final List scheduleList, final List changedLoadStatus) { + if (!this.neighboursBlockingGenTask.isEmpty()) { + throw new IllegalStateException("Cannot have neighbours blocking this gen task"); + } + if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { + this.completeStatusConsumers(newStatus, newChunk); + } + // done now, clear state (must be done before scheduling new tasks) + this.generationTask = null; + this.generationTaskStatus = null; + if (newChunk == null) { + // task was cancelled + // should be careful as this could be called while holding the schedule lock and/or inside the + // ticket level update + // while a task may be cancelled, it is possible for it to be later re-scheduled + // however, because generationTask is only set to null on _completion_, the scheduler leaves + // the rescheduling logic to us here + final ChunkStatus requestedGenStatus = this.requestedGenStatus; + this.requestedGenStatus = null; + if (requestedGenStatus != null) { + // it looks like it has been requested, so we must reschedule + if (!this.neighboursWaitingForUs.isEmpty()) { + for (final Iterator> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry entry = iterator.next(); + + final NewChunkHolder chunkHolder = entry.getKey(); + final ChunkStatus toStatus = entry.getValue(); + + if (!requestedGenStatus.isOrAfter(toStatus)) { + // if we were cancelled, we are responsible for removing the waiter + if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Corrupt state"); + } + if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { + chunkHolder.checkUnload(); + } + iterator.remove(); + continue; + } + } + } + + // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null + this.scheduler.schedule( + this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList + ); + + // return, can't do anything further + return; + } + + if (!this.neighboursWaitingForUs.isEmpty()) { + for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { + if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Corrupt state"); + } + if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { + chunkHolder.checkUnload(); + } + } + this.neighboursWaitingForUs.clear(); + } + // reset priority, we have nothing left to generate to + this.setPriority(null); + this.checkUnload(); + return; + } + + this.currentChunk = newChunk; + this.currentGenStatus = newStatus; + this.lastChunkCompletion = new ChunkCompletion(newChunk, newStatus); + + final ChunkStatus requestedGenStatus = this.requestedGenStatus; + + List needsScheduling = null; + boolean recalculatePriority = false; + for (final Iterator> iterator + = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry entry = iterator.next(); + final NewChunkHolder neighbour = entry.getKey(); + final ChunkStatus requiredStatus = entry.getValue(); + + if (!newStatus.isOrAfter(requiredStatus)) { + if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { + // if we're cancelled, still need to clear this map + if (!neighbour.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Neighbour is not waiting for us?"); + } + if (neighbour.neighboursBlockingGenTask.isEmpty()) { + neighbour.checkUnload(); + } + + iterator.remove(); + } + continue; + } + + // doesn't matter what isCancelled is here, we need to schedule if we can + + recalculatePriority = true; + if (!neighbour.neighboursBlockingGenTask.remove(this)) { + throw new IllegalStateException("Neighbour is not waiting for us?"); + } + + if (neighbour.neighboursBlockingGenTask.isEmpty()) { + if (neighbour.requestedGenStatus != null) { + if (needsScheduling == null) { + needsScheduling = new ArrayList<>(); + } + needsScheduling.add(neighbour); + } else { + neighbour.checkUnload(); + } + } + + // remove last; access to entry will throw if removed + iterator.remove(); + } + + if (newStatus == ChunkStatus.FULL) { + this.lockPriority(); + // try to push pending to FULL + if (this.updatePendingStatus()) { + changedLoadStatus.add(this); + } + } + + if (recalculatePriority) { + this.recalculateNeighbourRequestedPriority(); + } + + if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { + this.scheduleNeighbours(needsScheduling, scheduleList); + + // we need to schedule more tasks now + this.scheduler.schedule( + this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList + ); + } else { + // we're done now + if (requestedGenStatus != null) { + this.requestedGenStatus = null; + } + // reached final stage, so stop scheduling now + this.setPriority(null); + this.checkUnload(); + + this.scheduleNeighbours(needsScheduling, scheduleList); + } + } + + private void scheduleNeighbours(final List needsScheduling, final List scheduleList) { + if (needsScheduling != null) { + for (int i = 0, len = needsScheduling.size(); i < len; ++i) { + final NewChunkHolder neighbour = needsScheduling.get(i); + + this.scheduler.schedule( + neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList + ); + } + } + } + + public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, + final List neighbours) { + if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { + throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); + } + if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { + throw new IllegalStateException("Cannot schedule generation task when not requested"); + } + this.generationTask = generationTask; + this.generationTaskStatus = taskStatus; + + for (int i = 0, len = neighbours.size(); i < len; ++i) { + neighbours.get(i).addNeighbourUsingChunk(); + } + + this.checkUnload(); + + generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { + if (generationTask != this.generationTask) { + throw new IllegalStateException( + "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" + ); + } + if (thr != null) { + if (this.genTaskException != null) { + LOGGER.warn("Ignoring exception for " + this.toString(), thr); + return; + } + // don't set generation task to null, so that scheduling will not attempt to create another task and it + // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed + // task to complete + this.genTaskException = thr; + this.failedGenStatus = taskStatus; + this.genTaskFailedThread = Thread.currentThread(); + + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), + "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) + ), thr); + return; + } + + final boolean scheduleTasks; + List tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); + if (tasks == null) { + scheduleTasks = true; + tasks = new ArrayList<>(); + } else { + scheduleTasks = false; + // we are currently updating ticket levels, so we already hold the schedule lock + // this means we have to leave the ticket level update to handle the scheduling + } + final List changedLoadStatus = new ArrayList<>(); + // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); + try { + for (int i = 0, len = neighbours.size(); i < len; ++i) { + neighbours.get(i).removeNeighbourUsingChunk(); + } + this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); + + if (scheduleTasks) { + // can't hold the lock while scheduling, so we have to build the tasks and then schedule after + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + } + }); + } + + public PoiChunk getPoiChunk() { + return this.poiChunk; + } + + public ChunkEntitySlices getEntityChunk() { + return this.entityChunk; + } + + public long lastAutoSave; + + public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} + + public SaveStat save(final boolean shutdown) { + TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); + + ChunkAccess chunk = this.getCurrentChunk(); + PoiChunk poi = this.getPoiChunk(); + ChunkEntitySlices entities = this.getEntityChunk(); + boolean executedUnloadTask = false; + + if (shutdown) { + // make sure that the async unloads complete + if (this.unloadState != null) { + // must have errored during unload + chunk = this.unloadState.chunk(); + poi = this.unloadState.poiChunk(); + entities = this.unloadState.entityChunk(); + } + final UnloadTask chunkUnloadTask = this.chunkDataUnload; + final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); + if (chunkDataUnloadTask != null) { + final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); + if (unloadTask != null) { + executedUnloadTask = unloadTask.execute(); + } + } + } + + final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk); + + // can only synchronously save worldgen chunks during shutdown + boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); + boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty()); + boolean canSaveEntities = entities != null; + + if (canSaveChunk) { + canSaveChunk = this.saveChunk(chunk, false); + } + if (canSavePOI) { + canSavePOI = this.savePOI(poi, false); + } + if (canSaveEntities) { + // on shutdown, we need to force transient entity chunks to save + canSaveEntities = this.saveEntities(entities, shutdown); + if (shutdown) { + this.lastEntityUnload = null; + } + } + + return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; + } + + static final class AsyncChunkSerializeTask implements Runnable { + + private final ServerLevel world; + private final ChunkAccess chunk; + private final AsyncChunkSaveData asyncSaveData; + private final NewChunkHolder toComplete; + + public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData, + final NewChunkHolder toComplete) { + this.world = world; + this.chunk = chunk; + this.asyncSaveData = asyncSaveData; + this.toComplete = toComplete; + } + + @Override + public void run() { + final CompoundTag toSerialize; + try { + toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData); + } catch (final Throwable throwable) { + LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable); + final ChunkPos pos = this.chunk.getPos(); + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { + final CompoundTag synchronousSave; + try { + synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); + } catch (final Throwable throwable2) { + LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2); + AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); + return; + } + + AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave); + LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously"); + + }, PrioritisedExecutor.Priority.HIGHEST); + return; + } + this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize); + } + + @Override + public String toString() { + return "AsyncChunkSerializeTask{" + + "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" + + "}"; + } + } + + private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { + if (!chunk.isUnsaved()) { + if (unloading) { + this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); + } + return false; + } + boolean completing = false; + boolean failedAsyncPrepare = false; + try { + if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) { + try { + final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk); + + final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); + + this.chunkDataUnload.task().setTask(task); + + chunk.setUnsaved(false); + + task.queue(); + + return true; + } catch (final Throwable thr) { + LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr); + failedAsyncPrepare = true; + // fall through to synchronous save + } + } + + final CompoundTag save = ChunkSerializer.write(this.world, chunk); + + if (unloading) { + completing = true; + this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save); + if (failedAsyncPrepare) { + LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously"); + } + } else { + RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); + } + chunk.setUnsaved(false); + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); + if (unloading && !completing) { + this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); + } + } + + return true; + } + + private boolean lastEntitySaveNull; + private CompoundTag lastEntityUnload; + private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { + try { + CompoundTag mergeFrom = null; + if (entities.isTransient()) { + if (!unloading) { + // if we're a transient chunk, we cannot save until unloading because otherwise a double save will + // result in double adding the entities + return false; + } + try { + mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); + } catch (final Exception ex) { + LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); + } + } + + final CompoundTag save = entities.save(); + if (mergeFrom != null) { + if (save == null) { + // don't override the data on disk with nothing + return false; + } else { + ChunkEntitySlices.copyEntities(mergeFrom, save); + } + } + if (save == null && this.lastEntitySaveNull) { + return false; + } + + RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); + this.lastEntitySaveNull = save == null; + if (unloading) { + this.lastEntityUnload = save; + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); + } + + return true; + } + + private boolean lastPoiSaveNull; + private boolean savePOI(final PoiChunk poi, final boolean unloading) { + try { + final CompoundTag save = poi.save(); + poi.setDirty(false); + if (save == null && this.lastPoiSaveNull) { + if (unloading) { + this.poiDataUnload.completable().complete(null); + } + return false; + } + + RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); + this.lastPoiSaveNull = save == null; + if (unloading) { + this.poiDataUnload.completable().complete(save); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); + } + + return true; + } + + @Override + public String toString() { + final ChunkCompletion lastCompletion = this.lastChunkCompletion; + final ChunkEntitySlices entityChunk = this.entityChunk; + final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus; + final FullChunkStatus currentFullStatus = this.currentFullChunkStatus; + return "NewChunkHolder{" + + "world=" + WorldUtil.getWorldName(this.world) + + ", chunkX=" + this.chunkX + + ", chunkZ=" + this.chunkZ + + ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + + ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + + ", currentGenStatus=" + this.currentGenStatus + + ", requestedGenStatus=" + this.requestedGenStatus + + ", generationTask=" + this.generationTask + + ", generationTaskStatus=" + this.generationTaskStatus + + ", priority=" + this.priority + + ", priorityLocked=" + this.priorityLocked + + ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + + ", effective_priority=" + this.getEffectivePriority(null) + + ", oldTicketLevel=" + this.oldTicketLevel + + ", currentTicketLevel=" + this.currentTicketLevel + + ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + + ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + + ", currentChunkStatus=" + currentFullStatus + + ", pendingChunkStatus=" + pendingFullStatus + + ", is_unload_safe=" + this.isSafeToUnload() + + ", killed=" + this.unloaded + + '}'; + } + + private static JsonElement serializeStacktraceElement(final StackTraceElement element) { + return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString()); + } + + private static JsonObject serializeCompletable(final Completable completable) { + final JsonObject ret = new JsonObject(); + + if (completable == null) { + return ret; + } + + ret.addProperty("valid", Boolean.TRUE); + + final boolean isCompleted = completable.isCompleted(); + ret.addProperty("completed", Boolean.valueOf(isCompleted)); + + if (isCompleted) { + final Throwable throwable = completable.getThrowable(); + if (throwable != null) { + final JsonArray throwableJson = new JsonArray(); + ret.add("throwable", throwableJson); + + for (final StackTraceElement element : throwable.getStackTrace()) { + throwableJson.add(serializeStacktraceElement(element)); + } + } else { + final Object result = completable.getResult(); + ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName())); + } + } + + return ret; + } + + // (probably) holds ticket and scheduling lock + public JsonObject getDebugJson() { + final JsonObject ret = new JsonObject(); + + final ChunkCompletion lastCompletion = this.lastChunkCompletion; + final ChunkEntitySlices slices = this.entityChunk; + final PoiChunk poiChunk = this.poiChunk; + + ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); + ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); + ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); + ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); + ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); + ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); + ret.addProperty("priority", Objects.toString(this.priority)); + ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); + ret.addProperty("generation_task", Objects.toString(this.generationTask)); + ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); + ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); + ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); + ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); + + final JsonObject neighbourWaitState = new JsonObject(); + ret.add("neighbour_state", neighbourWaitState); + + final JsonArray blockingGenNeighbours = new JsonArray(); + neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); + for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { + final JsonObject neighbour = new JsonObject(); + blockingGenNeighbours.add(neighbour); + + neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); + neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); + } + + final JsonArray neighboursWaitingForUs = new JsonArray(); + neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); + for (final Reference2ObjectMap.Entry entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { + final NewChunkHolder holder = entry.getKey(); + final ChunkStatus status = entry.getValue(); + + final JsonObject neighbour = new JsonObject(); + neighboursWaitingForUs.add(neighbour); + + + neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); + neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); + neighbour.addProperty("waiting_for", Objects.toString(status)); + } + + ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus)); + ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus)); + ret.addProperty("generation_task", Objects.toString(this.generationTask)); + ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); + ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); + ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); + + final UnloadTask entityDataUnload = this.entityDataUnload; + final UnloadTask poiDataUnload = this.poiDataUnload; + final UnloadTask chunkDataUnload = this.chunkDataUnload; + + ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); + ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); + ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); + + final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); + if (unloadTask == null) { + ret.addProperty("unload_task_priority", "null"); + ret.addProperty("unload_task_priority_raw", "null"); + } else { + ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); + ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); + } + + ret.addProperty("killed", Boolean.valueOf(this.unloaded)); + + return ret; + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java new file mode 100644 index 0000000..261e094 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java @@ -0,0 +1,215 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.lang.invoke.VarHandle; + +public abstract class PriorityHolder { + + protected volatile int priority; + protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); + + protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; + protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; + + protected final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); + } + + protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); + } + + protected final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); + } + + protected final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); + } + + protected PriorityHolder(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.setPriorityPlain(priority.priority); + } + + // used only for debug json + public boolean isScheduled() { + return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; + } + + // returns false if cancelled + public boolean markExecuting() { + return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; + } + + public boolean isMarkedExecuted() { + return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; + } + + public void cancel() { + if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { + // cancelled already + return; + } + this.cancelScheduled(); + } + + public void schedule() { + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_SCHEDULED) != 0) { + throw new IllegalStateException("schedule() called twice"); + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); + + int failures = 0; + for (;;) { + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { + return; + } + + if ((priority & PRIORITY_SCHEDULED) != 0) { + throw new IllegalStateException("schedule() called twice"); + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final PrioritisedExecutor.Priority getPriority() { + final int ret = this.getPriorityVolatile(); + if ((ret & PRIORITY_EXECUTED) != 0) { + return PrioritisedExecutor.Priority.COMPLETING; + } + if ((ret & PRIORITY_SCHEDULED) != 0) { + return this.getScheduledPriority(); + } + return PrioritisedExecutor.Priority.getPriority(ret); + } + + public final void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.lowerPriorityScheduled(priority); + return; + } + + if (!priority.isLowerPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.setPriorityScheduled(priority); + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public final void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + return; + } + + if ((curr & PRIORITY_SCHEDULED) != 0) { + this.raisePriorityScheduled(priority); + return; + } + + if (!priority.isHigherPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + protected abstract void cancelScheduled(); + + protected abstract PrioritisedExecutor.Priority getScheduledPriority(); + + protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); + + protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); + + protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); + + protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java new file mode 100644 index 0000000..a36ca18 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java @@ -0,0 +1,1426 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; +import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.Short2ByteMap; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.LockSupport; + +public abstract class ThreadedTicketLevelPropagator { + + // sections are 64 in length + public static final int SECTION_SHIFT = 6; + public static final int SECTION_SIZE = 1 << SECTION_SHIFT; + private static final int LEVEL_BITS = SECTION_SHIFT; + private static final int LEVEL_COUNT = 1 << LEVEL_BITS; + private static final int MIN_SOURCE_LEVEL = 1; + // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate + // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation + private static final int MAX_SOURCE_LEVEL = 62; + + private final UpdateQueue updateQueue; + private final ConcurrentLong2ReferenceChainedHashTable
    sections; + + public ThreadedTicketLevelPropagator() { + this.updateQueue = new UpdateQueue(); + this.sections = new ConcurrentLong2ReferenceChainedHashTable<>(); + } + + // must hold ticket lock for: + // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) + public void setSource(final int posX, final int posZ, final int to) { + if (to < 1 || to > MAX_SOURCE_LEVEL) { + throw new IllegalArgumentException("Source: " + to); + } + + final int sectionX = posX >> SECTION_SHIFT; + final int sectionZ = posZ >> SECTION_SHIFT; + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + Section section = this.sections.get(coordinate); + if (section == null) { + if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { + throw new IllegalStateException("Race condition while creating new section"); + } + } + + final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short sLocalIdx = (short)localIdx; + + final short sourceAndLevel = section.levels[localIdx]; + final int currentSource = (sourceAndLevel >>> 8) & 0xFF; + + if (currentSource == to) { + // nothing to do + // make sure to kill the current update, if any + section.queuedSources.replace(sLocalIdx, (byte)to); + return; + } + + if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { + this.queueSectionUpdate(section); + } + } + + // must hold ticket lock for: + // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) + public void removeSource(final int posX, final int posZ) { + final int sectionX = posX >> SECTION_SHIFT; + final int sectionZ = posZ >> SECTION_SHIFT; + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = this.sections.get(coordinate); + + if (section == null) { + return; + } + + final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short sLocalIdx = (short)localIdx; + + final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; + + if (currentSource == 0) { + // we use replace here so that we do not possibly multi-queue a section for an update + section.queuedSources.replace(sLocalIdx, (byte)0); + return; + } + + if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { + this.queueSectionUpdate(section); + } + } + + private void queueSectionUpdate(final Section section) { + this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); + } + + public boolean hasPendingUpdates() { + return !this.updateQueue.isEmpty(); + } + + // holds ticket lock for every chunk section represented by any position in the key set + // updates is modifiable and passed to processSchedulingUpdates after this call + protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); + + // holds ticket lock for every chunk section represented by any position in the key set + // holds scheduling lock in max access radius for every position held by the ticket lock + // updates is cleared after this call + protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, + final List changedFullStatus); + + // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ + public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + if (!this.hasPendingUpdates()) { + return false; + } + + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = this.sections.get(coordinate); + + if (section == null || section.queuedSources.isEmpty()) { + // no section or no updates + return false; + } + + final Propagator propagator = Propagator.acquirePropagator(); + final boolean ret = this.performUpdate(section, null, propagator, + null, schedulingLock, scheduledTasks, changedFullStatus + ); + Propagator.returnPropagator(propagator); + return ret; + } + + private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, + final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + final int sectionX = section.sectionX; + final int sectionZ = section.sectionZ; + + final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; + final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; + final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); + final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); + + // set up encode offset first as we need to queue level changes _before_ + propagator.setupEncodeOffset(sectionX, sectionZ); + + final int coordinateOffset = propagator.coordinateOffset; + + final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); + final boolean ret; + try { + // first, check if this update was stolen + if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) { + // occurs when a stolen update deletes this section + // it is possible that another update is scheduled, but that one will have the correct section + if (node != null) { + this.updateQueue.remove(node); + } + return false; + } + + final int oldSourceSize = section.sources.size(); + + // process pending sources + for (final Iterator iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { + final Short2ByteMap.Entry entry = iterator.next(); + final int pos = (int)entry.getShortKey(); + final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); + final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); + final int newSource = (int)entry.getByteValue(); + + final short currentEncoded = section.levels[pos]; + final int currLevel = currentEncoded & 0xFF; + final int prevSource = (currentEncoded >>> 8) & 0xFF; + + if (prevSource == newSource) { + // nothing changed + continue; + } + + if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { + // just update the source, don't need to propagate change + section.levels[pos] = (short)(currLevel | (newSource << 8)); + // level is unchanged, don't add to changed positions + } else { + // set current level and current source to new source + section.levels[pos] = (short)(newSource | (newSource << 8)); + // must add to updated positions in case this is final + propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource); + if (newSource != 0) { + // queue increase with new source level + propagator.appendToIncreaseQueue( + ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | + ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | + (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) + ); + } + // queue decrease with previous level + if (newSource < currLevel) { + propagator.appendToDecreaseQueue( + ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | + ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | + (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) + ); + } + } + + if (newSource == 0) { + // prevSource != newSource, so we are removing this source + section.sources.remove((short)pos); + } else if (prevSource == 0) { + // prevSource != newSource, so we are adding this source + section.sources.add((short)pos); + } + } + + section.queuedSources.clear(); + + final int newSourceSize = section.sources.size(); + + if (oldSourceSize == 0 && newSourceSize != 0) { + // need to make sure the sections in 1 radius are initialised + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dx | dz) == 0) { + continue; + } + final int offX = dx + sectionX; + final int offZ = dz + sectionZ; + final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); + final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> { + return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap)); + }); + + // increase ref count + ++neighbour.oneRadNeighboursWithSources; + if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { + throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); + } + } + } + } + + if (propagator.hasUpdates()) { + propagator.setupCaches(this, sectionX, sectionZ, 1); + propagator.performDecrease(); + // don't need try-finally, as any exception will cause the propagator to not be returned + propagator.destroyCaches(); + } + + if (newSourceSize == 0) { + final boolean decrementRef = oldSourceSize != 0; + // check for section de-init + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final int offX = dx + sectionX; + final int offZ = dz + sectionZ; + final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); + final Section neighbour = this.sections.get(coordinate); + + if (neighbour == null) { + if (oldSourceSize == 0 && (dx | dz) != 0) { + // since we don't have sources, this section is allowed to be null + continue; + } + throw new IllegalStateException("??"); + } + + if (decrementRef && (dx | dz) != 0) { + // decrease ref count, but only for neighbours + --neighbour.oneRadNeighboursWithSources; + } + + // we need to check the current section for de-init as well + if (neighbour.oneRadNeighboursWithSources == 0) { + if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { + // need to de-init + this.sections.remove(coordinate); + } // else: neighbour is queued for an update, and it will de-init itself + } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { + throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); + } + } + } + } + + + ret = !propagator.updatedPositions.isEmpty(); + + if (ret) { + this.processLevelUpdates(propagator.updatedPositions); + + if (!propagator.updatedPositions.isEmpty()) { + // now we can actually update the ticket levels in the chunk holders + final int maxScheduleRadius = 2 * ChunkTaskScheduler.getMaxAccessRadius(); + + // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time + final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( + rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, + rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius + ); + try { + this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); + } finally { + schedulingLock.unlock(schedulingNode); + } + } + + propagator.updatedPositions.clear(); + } + } finally { + if (ticketLock != null) { + ticketLock.unlock(ticketNode); + } + } + + // finished + if (node != null) { + this.updateQueue.remove(node); + } + + return ret; + } + + public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, + final List scheduledTasks, final List changedFullStatus) { + if (this.updateQueue.isEmpty()) { + return false; + } + + final long maxOrder = this.updateQueue.getLastOrder(); + + boolean updated = false; + Propagator propagator = null; + + for (;;) { + final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextToUpdate(maxOrder); + if (toUpdate == null) { + this.updateQueue.awaitFirst(maxOrder); + + if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { + if (propagator != null) { + Propagator.returnPropagator(propagator); + } + return updated; + } + + continue; + } + + if (propagator == null) { + propagator = Propagator.acquirePropagator(); + } + + updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); + } + } + + // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer + // for the last update node being handled + private static final class UpdateQueue { + + private volatile UpdateQueueNode head; + private volatile UpdateQueueNode tail; + private volatile UpdateQueueNode lastUpdating; + + private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); + private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); + private static final VarHandle LAST_UPDATING = ConcurrentUtil.getVarHandle(UpdateQueue.class, "lastUpdating", UpdateQueueNode.class); + + /* head */ + + private final void setHeadPlain(final UpdateQueueNode newHead) { + HEAD_HANDLE.set(this, newHead); + } + + private final void setHeadOpaque(final UpdateQueueNode newHead) { + HEAD_HANDLE.setOpaque(this, newHead); + } + + private final UpdateQueueNode getHeadPlain() { + return (UpdateQueueNode)HEAD_HANDLE.get(this); + } + + private final UpdateQueueNode getHeadOpaque() { + return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); + } + + private final UpdateQueueNode getHeadAcquire() { + return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); + } + + /* tail */ + + private final void setTailPlain(final UpdateQueueNode newTail) { + TAIL_HANDLE.set(this, newTail); + } + + private final void setTailOpaque(final UpdateQueueNode newTail) { + TAIL_HANDLE.setOpaque(this, newTail); + } + + private final UpdateQueueNode getTailPlain() { + return (UpdateQueueNode)TAIL_HANDLE.get(this); + } + + private final UpdateQueueNode getTailOpaque() { + return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); + } + + /* lastUpdating */ + + private final UpdateQueueNode getLastUpdatingVolatile() { + return (UpdateQueueNode)LAST_UPDATING.getVolatile(this); + } + + private final UpdateQueueNode compareAndExchangeLastUpdatingVolatile(final UpdateQueueNode expect, final UpdateQueueNode update) { + return (UpdateQueueNode)LAST_UPDATING.compareAndExchange(this, expect, update); + } + + public UpdateQueue() { + final UpdateQueueNode dummy = new UpdateQueueNode(null, null); + dummy.order = -1L; + dummy.preventAdds(); + + this.setHeadPlain(dummy); + this.setTailPlain(dummy); + } + + public boolean isEmpty() { + return this.peek() == null; + } + + public boolean hasRemainingUpdates(final long maxUpdate) { + final UpdateQueueNode node = this.peek(); + return node != null && node.order <= maxUpdate; + } + + public long getLastOrder() { + for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { + final UpdateQueueNode next = curr.getNextVolatile(); + if (next == null) { + // try to update stale tail + if (this.getTailOpaque() == tail && curr != tail) { + this.setTailOpaque(curr); + } + return curr.order; + } + curr = next; + } + } + + public UpdateQueueNode acquireNextToUpdate(final long maxOrder) { + int failures = 0; + for (UpdateQueueNode prev = this.getLastUpdatingVolatile();;) { + UpdateQueueNode next = prev == null ? this.peek() : prev.next; + + if (next == null || next.order > maxOrder) { + return null; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (prev == (prev = this.compareAndExchangeLastUpdatingVolatile(prev, next))) { + return next; + } + + ++failures; + } + } + + public void awaitFirst(final long maxOrder) { + final UpdateQueueNode earliest = this.peek(); + if (earliest == null || earliest.order > maxOrder) { + return; + } + + final Thread currThread = Thread.currentThread(); + // we do not use add-blocking because we use the nullability of the section to block + // remove() does not begin to poll from the wait queue until the section is null'd, + // and so provided we check the nullability before parking there is no ordering of these operations + // such that remove() finishes polling from the wait queue while section is not null + earliest.add(currThread); + + // wait until completed + while (earliest.getSectionVolatile() != null) { + LockSupport.park(); + } + } + + public UpdateQueueNode peek() { + for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { + final UpdateQueueNode next = curr.getNextVolatile(); + final Section element = curr.getSectionVolatile(); /* Likely in sync */ + + if (element != null) { + if (this.getHeadOpaque() == head && curr != head) { + this.setHeadOpaque(curr); + } + return curr; + } + + if (next == null) { + if (this.getHeadOpaque() == head && curr != head) { + this.setHeadOpaque(curr); + } + return null; + } + curr = next; + } + } + + public void remove(final UpdateQueueNode node) { + // mark as removed + node.setSectionVolatile(null); + + // use peek to advance head + this.peek(); + + // unpark any waiters / block the wait queue + Thread unpark; + while ((unpark = node.poll()) != null) { + LockSupport.unpark(unpark); + } + } + + public void append(final UpdateQueueNode node) { + int failures = 0; + + for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final UpdateQueueNode next = curr.getNextVolatile(); + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (next == null) { + node.order = curr.order + 1L; + final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); + + if (compared == null) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* CAS to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(node); + } + return; + } + + ++failures; + curr = compared; + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + // each node also represents a set of waiters, represented by the MTQ + // if the queue is add-blocked, then the update is complete + private static final class UpdateQueueNode extends MultiThreadedQueue { + private long order; + private Section section; + private volatile UpdateQueueNode next; + + private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); + private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); + + public UpdateQueueNode(final Section section, final UpdateQueueNode next) { + SECTION_HANDLE.set(this, section); + NEXT_HANDLE.set(this, next); + } + + /* section */ + + private final Section getSectionPlain() { + return (Section)SECTION_HANDLE.get(this); + } + + private final Section getSectionVolatile() { + return (Section)SECTION_HANDLE.getVolatile(this); + } + + private final void setSectionPlain(final Section update) { + SECTION_HANDLE.set(this, update); + } + + private final void setSectionOpaque(final Section update) { + SECTION_HANDLE.setOpaque(this, update); + } + + private final void setSectionVolatile(final Section update) { + SECTION_HANDLE.setVolatile(this, update); + } + + private final Section getAndSetSectionVolatile(final Section update) { + return (Section)SECTION_HANDLE.getAndSet(this, update); + } + + private final Section compareExchangeSectionVolatile(final Section expect, final Section update) { + return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); + } + + /* next */ + + private final UpdateQueueNode getNextPlain() { + return (UpdateQueueNode)NEXT_HANDLE.get(this); + } + + private final UpdateQueueNode getNextOpaque() { + return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); + } + + private final UpdateQueueNode getNextAcquire() { + return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); + } + + private final UpdateQueueNode getNextVolatile() { + return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); + } + + private final void setNextPlain(final UpdateQueueNode next) { + NEXT_HANDLE.set(this, next); + } + + private final void setNextVolatile(final UpdateQueueNode next) { + NEXT_HANDLE.setVolatile(this, next); + } + + private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { + return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); + } + } + } + + private static final class Section { + + // upper 8 bits: sources, lower 8 bits: level + // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle + // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. + private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; + // set of local positions that represent sources + private final ShortOpenHashSet sources = new ShortOpenHashSet(); + // map of local index to new source level + // the source level _cannot_ be updated in the backing storage immediately since the update + private static final byte NO_QUEUED_UPDATE = (byte)-1; + private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); + { + this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); + } + private int oneRadNeighboursWithSources = 0; + + public final int sectionX; + public final int sectionZ; + + public Section(final int sectionX, final int sectionZ) { + this.sectionX = sectionX; + this.sectionZ = sectionZ; + } + + public boolean isZero() { + for (final short val : this.levels) { + if (val != 0) { + return false; + } + } + return true; + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(); + + for (int x = 0; x < SECTION_SIZE; ++x) { + ret.append("levels x=").append(x).append("\n"); + for (int z = 0; z < SECTION_SIZE; ++z) { + final short v = this.levels[x | (z << SECTION_SHIFT)]; + ret.append(v & 0xFF).append("."); + } + ret.append("\n"); + ret.append("sources x=").append(x).append("\n"); + for (int z = 0; z < SECTION_SIZE; ++z) { + final short v = this.levels[x | (z << SECTION_SHIFT)]; + ret.append((v >>> 8) & 0xFF).append("."); + } + ret.append("\n\n"); + } + + return ret.toString(); + } + } + + + private static final class Propagator { + + private static final ArrayDeque CACHED_PROPAGATORS = new ArrayDeque<>(); + private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; + + private static Propagator acquirePropagator() { + synchronized (CACHED_PROPAGATORS) { + final Propagator ret = CACHED_PROPAGATORS.pollFirst(); + if (ret != null) { + return ret; + } + } + return new Propagator(); + } + + private static void returnPropagator(final Propagator propagator) { + synchronized (CACHED_PROPAGATORS) { + if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { + CACHED_PROPAGATORS.add(propagator); + } + } + } + + private static final int SECTION_RADIUS = 2; + private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; + // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) + private static final int COORDINATE_BITS = 9; + private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; + static { + if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { + throw new IllegalStateException("Adjust COORDINATE_BITS"); + } + } + // index = x + (z * SECTION_CACHE_WIDTH) + // (this requires x >= 0 and z >= 0) + private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; + + private int encodeOffsetX; + private int encodeOffsetZ; + + private int coordinateOffset; + + private int encodeSectionOffsetX; + private int encodeSectionOffsetZ; + + private int sectionIndexOffset; + + public final boolean hasUpdates() { + return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; + } + + private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { + final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); + // must have that encoded >= 0 + // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] + // we want a range of [0, maxCoordinate*2] + // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset + this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); + this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); + + // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) + // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); + + // need encoded values to be >= 0 + // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset + this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; + this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; + + // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) + this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); + } + + // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad + // must call setupEncodeOffset + private final void setupCaches(final ThreadedTicketLevelPropagator propagator, + final int centerSectionX, final int centerSectionZ, + final int rad) { + for (int dz = -rad; dz <= rad; ++dz) { + for (int dx = -rad; dx <= rad; ++dx) { + final int sectionX = centerSectionX + dx; + final int sectionZ = centerSectionZ + dz; + final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final Section section = propagator.sections.get(coordinate); + + if (section == null) { + throw new IllegalStateException("Section at " + coordinate + " should not be null"); + } + + this.setSectionInCache(sectionX, sectionZ, section); + } + } + } + + private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { + this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; + } + + private final Section getSection(final int sectionX, final int sectionZ) { + return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; + } + + private final int getLevel(final int posX, final int posZ) { + final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; + if (section != null) { + return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; + } + + return 0; + } + + private final void setLevel(final int posX, final int posZ, final int to) { + final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; + if (section != null) { + final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + final short level = section.levels[index]; + section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); + this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to); + } + } + + private final void destroyCaches() { + Arrays.fill(this.sections, null); + } + + // contains: + // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) + // next LEVEL_BITS (6) bits: propagated level [0, 63] + // propagation directions bitset (16 bits): + private static final long ALL_DIRECTIONS_BITSET = ( + // z = -1 + (1L << ((1 - 1) | ((1 - 1) << 2))) | + (1L << ((1 + 0) | ((1 - 1) << 2))) | + (1L << ((1 + 1) | ((1 - 1) << 2))) | + + // z = 0 + (1L << ((1 - 1) | ((1 + 0) << 2))) | + //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) + (1L << ((1 + 1) | ((1 + 0) << 2))) | + + // z = 1 + (1L << ((1 - 1) | ((1 + 1) << 2))) | + (1L << ((1 + 0) | ((1 + 1) << 2))) | + (1L << ((1 + 1) | ((1 + 1) << 2))) + ); + + private void ex(int bitset) { + for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { + final int set = Integer.numberOfTrailingZeros(bitset); + final int tailingBit = (-bitset) & bitset; + // XOR to remove the trailing bit + bitset ^= tailingBit; + + // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits + // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the + // index of the set bit is the encoded value + // the encoded coordinate has 3 valid states: + // 0b00 (0) -> -1 + // 0b01 (1) -> 0 + // 0b10 (2) -> 1 + // the decode operation then is val - 1, and the encode operation is val + 1 + final int xOff = (set & 3) - 1; + final int zOff = ((set >>> 2) & 3) - 1; + System.out.println("Encoded: (" + xOff + "," + zOff + ")"); + } + } + + private void ch(long bs, int shift) { + int bitset = (int)(bs >>> shift); + for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { + final int set = Integer.numberOfTrailingZeros(bitset); + final int tailingBit = (-bitset) & bitset; + // XOR to remove the trailing bit + bitset ^= tailingBit; + + // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits + // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the + // index of the set bit is the encoded value + // the encoded coordinate has 3 valid states: + // 0b00 (0) -> -1 + // 0b01 (1) -> 0 + // 0b10 (2) -> 1 + // the decode operation then is val - 1, and the encode operation is val + 1 + final int xOff = (set & 3) - 1; + final int zOff = ((set >>> 2) & 3) - 1; + if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { + throw new IllegalStateException(); + } + } + } + + // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading + // updates for sources + private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0; + + private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; + private int increaseQueueInitialLength; + private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; + private int decreaseQueueInitialLength; + + private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); + + private final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + private final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + private final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + return; + } else { + queue[idx] = value; + return; + } + } + + private final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + return; + } else { + queue[idx] = value; + return; + } + } + + private final void performIncrease() { + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.sectionIndexOffset; + + final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; + final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; + final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); + // note: the above code requires coordinate bits * 2 < 32 + // bitset is 16 bits + int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLevel(posX, posZ) != propagatedLevel) { + // not at the level we expect, so something changed. + continue; + } + } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { + // these are used to restore sources after a propagation decrease + this.setLevel(posX, posZ, propagatedLevel); + } + + // this bitset represents the values that we have not propagated to + // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases + // significantly reducing the total number of ops + // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need + // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead + // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) + // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 + // index = x | (z << 3) + + // to start, we eliminate everything 1 radius from the current position as the previous propagator + // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius + // but the rest not propagated are already handled + long currentPropagation = ~( + // z = -1 + (1L << ((2 - 1) | ((2 - 1) << 3))) | + (1L << ((2 + 0) | ((2 - 1) << 3))) | + (1L << ((2 + 1) | ((2 - 1) << 3))) | + + // z = 0 + (1L << ((2 - 1) | ((2 + 0) << 3))) | + (1L << ((2 + 0) | ((2 + 0) << 3))) | + (1L << ((2 + 1) | ((2 + 0) << 3))) | + + // z = 1 + (1L << ((2 - 1) | ((2 + 1) << 3))) | + (1L << ((2 + 0) | ((2 + 1) << 3))) | + (1L << ((2 + 1) | ((2 + 1) << 3))) + ); + + final int toPropagate = propagatedLevel - 1; + + // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting + // the bits, the cpu loop predictor should perfectly predict the loop. + for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { + final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); + final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; + propagateDirectionBitset ^= tailingBit; + + // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset + // it has been split to save some cycles via parallelism + final int pDecodeX = (set & 3); + final int pDecodeZ = ((set >>> 2) & 3); + + // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX + final int offX = (posX - 1) + pDecodeX; + final int offZ = (posZ - 1) + pDecodeZ; + + final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; + final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + + // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset + // bitset idx = x | (z << 3) + + // read three bits, so we need 7L + // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 + // nstartidx1 = x rel -1 for z rel -1 + // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) + // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) + // = pDecodeX | (pDecodeZ << 3) = start + final int start = pDecodeX | (pDecodeZ << 3); + final long bitsetLine1 = currentPropagation & (7L << (start)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) + final long bitsetLine2 = currentPropagation & (7L << (start + 8)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) + final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); + + // remove ("take") lines from bitset + currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); + + // now try to propagate + final Section section = this.sections[sectionIndex]; + + // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag + final short currentStoredLevel = section.levels[localIndex]; + final int currentLevel = currentStoredLevel & 0xFF; + + if (currentLevel >= toPropagate) { + continue; // already at the level we want + } + + // update level + section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); + updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate); + + // queue next + if (toPropagate > 1) { + // now combine into one bitset to pass to child + // the child bitset is 4x4, so we just shift each line by 4 + // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value + final long childPropagation = + ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 + ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 + ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 + + // don't queue update if toPropagate cannot propagate anything to neighbours + // (for increase, propagating 0 to neighbours is useless) + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); + continue; + } + continue; + } + } + } + + private final void performDecrease() { + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.sectionIndexOffset; + + final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; + final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; + final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); + // note: the above code requires coordinate bits * 2 < 32 + // bitset is 16 bits + int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); + + // this bitset represents the values that we have not propagated to + // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases + // significantly reducing the total number of ops + // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need + // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead + // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) + // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 + // index = x | (z << 3) + + // to start, we eliminate everything 1 radius from the current position as the previous propagator + // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius + // but the rest not propagated are already handled + long currentPropagation = ~( + // z = -1 + (1L << ((2 - 1) | ((2 - 1) << 3))) | + (1L << ((2 + 0) | ((2 - 1) << 3))) | + (1L << ((2 + 1) | ((2 - 1) << 3))) | + + // z = 0 + (1L << ((2 - 1) | ((2 + 0) << 3))) | + (1L << ((2 + 0) | ((2 + 0) << 3))) | + (1L << ((2 + 1) | ((2 + 0) << 3))) | + + // z = 1 + (1L << ((2 - 1) | ((2 + 1) << 3))) | + (1L << ((2 + 0) | ((2 + 1) << 3))) | + (1L << ((2 + 1) | ((2 + 1) << 3))) + ); + + final int toPropagate = propagatedLevel - 1; + + // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting + // the bits, the cpu loop predictor should perfectly predict the loop. + for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { + final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); + final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; + propagateDirectionBitset ^= tailingBit; + + + // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset + // it has been split to save some cycles via parallelism + final int pDecodeX = (set & 3); + final int pDecodeZ = ((set >>> 2) & 3); + + // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX + final int offX = (posX - 1) + pDecodeX; + final int offZ = (posZ - 1) + pDecodeZ; + + final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; + final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); + + // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset + // bitset idx = x | (z << 3) + + // read three bits, so we need 7L + // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 + // nstartidx1 = x rel -1 for z rel -1 + // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) + // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) + // = pDecodeX | (pDecodeZ << 3) = start + final int start = pDecodeX | (pDecodeZ << 3); + final long bitsetLine1 = currentPropagation & (7L << (start)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) + final long bitsetLine2 = currentPropagation & (7L << (start + 8)); + + // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) + final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); + + // now try to propagate + final Section section = this.sections[sectionIndex]; + + // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag + final short currentStoredLevel = section.levels[localIndex]; + final int currentLevel = currentStoredLevel & 0xFF; + final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; + + if (currentLevel == 0) { + continue; // already at the level we want + } + + if (currentLevel > toPropagate) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); + continue; + } + + // remove ("take") lines from bitset + // can't do this during decrease, TODO WHY? + //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); + + // update level + section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); + updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0); + + if (sourceLevel != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); + } + + // queue next + // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 + // now combine into one bitset to pass to child + // the child bitset is 4x4, so we just shift each line by 4 + // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value + final long childPropagation = + ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 + ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 + ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 + + // don't queue update if toPropagate cannot propagate anything to neighbours + // (for increase, propagating 0 to neighbours is useless) + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | + ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | + (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; + continue; + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performIncrease(); + } + } + + /* + private static final java.util.Random random = new java.util.Random(4L); + private static final List> walkers = + new java.util.ArrayList<>(); + static final int PLAYERS = 0; + static final int RAD_BLOCKS = 10000; + static final int RAD = RAD_BLOCKS >> 4; + static final int RAD_BIG_BLOCKS = 100_000; + static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; + static final int VD = 4; + static final int BIG_PLAYERS = 50; + static final double WALK_CHANCE = 0.10; + static final double TP_CHANCE = 0.01; + static final int TP_BACK_PLAYERS = 200; + static final double TP_BACK_CHANCE = 0.25; + static final double TP_STEAL_CHANCE = 0.25; + private static final List> tpBack = + new java.util.ArrayList<>(); + + public static void main(final String[] args) { + final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); + final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); + final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); + final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); + final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { + if (newLevel == 0) { + refMap.remove(coordinate); + } else { + refMap.put(coordinate, newLevel); + } + }); + final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { + @Override + protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { + for (final long key : updates.keySet()) { + final byte val = updates.get(key); + if (val == 0) { + levelMap.remove(key); + } else { + levelMap.put(key, val); + } + } + } + + @Override + protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List scheduledTasks, List changedFullStatus) {} + }; + + for (;;) { + if (walkers.isEmpty() && tpBack.isEmpty()) { + for (int i = 0; i < PLAYERS; ++i) { + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { + @Override + protected void addCallback(Void parameter, int chunkX, int chunkZ) { + int src = 45 - 31 + 1; + ref.setSource(chunkX, chunkZ, src); + propagator.setSource(chunkX, chunkZ, src); + } + + @Override + protected void removeCallback(Void parameter, int chunkX, int chunkZ) { + ref.removeSource(chunkX, chunkZ); + propagator.removeSource(chunkX, chunkZ); + } + }; + + map.add(posX, posZ, VD); + + walkers.add(map); + } + for (int i = 0; i < TP_BACK_PLAYERS; ++i) { + int rad = RAD_BIG; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { + @Override + protected void addCallback(Void parameter, int chunkX, int chunkZ) { + int src = 45 - 31 + 1; + ref.setSource(chunkX, chunkZ, src); + propagator.setSource(chunkX, chunkZ, src); + } + + @Override + protected void removeCallback(Void parameter, int chunkX, int chunkZ) { + ref.removeSource(chunkX, chunkZ); + propagator.removeSource(chunkX, chunkZ); + } + }; + + map.add(posX, posZ, random.nextInt(1, 63)); + + tpBack.add(map); + } + } else { + for (int i = 0; i < PLAYERS; ++i) { + if (random.nextDouble() > WALK_CHANCE) { + continue; + } + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); + + int updateX = random.nextInt(-1, 2); + int updateZ = random.nextInt(-1, 2); + + map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); + } + + for (int i = 0; i < PLAYERS; ++i) { + if (random.nextDouble() > TP_CHANCE) { + continue; + } + + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); + + map.update(posX, posZ, VD); + } + + for (int i = 0; i < TP_BACK_PLAYERS; ++i) { + if (random.nextDouble() > TP_BACK_CHANCE) { + continue; + } + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = tpBack.get(i); + + map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); + + if (random.nextDouble() > TP_STEAL_CHANCE) { + propagator.performUpdate( + map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null + ); + propagator.performUpdate( + (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null + ); + } + } + } + + ref.propagateUpdates(); + propagator.performUpdates(ticketLock, schedulingLock, null, null); + + if (!refMap.equals(levelMap)) { + throw new IllegalStateException("Error!"); + } + } + } + */ +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java new file mode 100644 index 0000000..e0b26cc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java @@ -0,0 +1,668 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.PriorityQueue; + +public class RadiusAwarePrioritisedExecutor { + + private static final Comparator DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { + return Long.compare(t1.id, t2.id); + }; + + private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + private static final int NO_TASKS_QUEUED = -1; + private int selectedQueue = NO_TASKS_QUEUED; + private boolean canQueueTasks = true; + + public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { + for (int i = 0; i < this.queues.length; ++i) { + this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i); + } + } + + private boolean canQueueTasks() { + return this.canQueueTasks; + } + + private List treeFinished() { + this.canQueueTasks = true; + for (int priority = 0; priority < this.queues.length; ++priority) { + final DependencyTree queue = this.queues[priority]; + if (queue.hasWaitingTasks()) { + final List ret = queue.tryPushTasks(); + + if (ret == null || ret.isEmpty()) { + // this happens when the tasks in the wait queue were purged + // in this case, the queue was actually empty, we just had to purge it + // if we set the selected queue without scheduling any tasks, the queue will never be unselected + // as that requires a scheduled task completing... + continue; + } + + this.selectedQueue = priority; + return ret; + } + } + + this.selectedQueue = NO_TASKS_QUEUED; + + return null; + } + + private List queue(final Task task, final PrioritisedExecutor.Priority priority) { + final int priorityId = priority.priority; + final DependencyTree queue = this.queues[priorityId]; + + final DependencyNode node = new DependencyNode(task, queue); + + if (task.dependencyNode != null) { + throw new IllegalStateException(); + } + task.dependencyNode = node; + + queue.pushNode(node); + + if (this.selectedQueue == NO_TASKS_QUEUED) { + this.canQueueTasks = true; + this.selectedQueue = priorityId; + return queue.tryPushTasks(); + } + + if (!this.canQueueTasks) { + return null; + } + + if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) { + // prevent the lower priority tree from queueing more tasks + this.canQueueTasks = false; + return null; + } + + // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up + return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run, final PrioritisedExecutor.Priority priority) { + if (radius < 0) { + throw new IllegalArgumentException("Radius must be > 0: " + radius); + } + return new Task(this, chunkX, chunkZ, radius, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run) { + return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, + final Runnable run) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { + return new Task(this, 0, 0, -1, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { + return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); + + ret.queue(); + + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { + final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); + + ret.queue(); + + return ret; + } + + // all accesses must be synchronised by the radius aware object + private static final class DependencyTree { + + private final RadiusAwarePrioritisedExecutor scheduler; + private final PrioritisedExecutor executor; + private final int maxToSchedule; + private final int treeIndex; + + private int currentlyExecuting; + private long idGenerator; + + private final PriorityQueue awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); + + private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); + private boolean isInfiniteRadiusScheduled; + + private final Long2ReferenceOpenHashMap nodeByPosition = new Long2ReferenceOpenHashMap<>(); + + public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, + final int maxToSchedule, final int treeIndex) { + this.scheduler = scheduler; + this.executor = executor; + this.maxToSchedule = maxToSchedule; + this.treeIndex = treeIndex; + } + + public boolean hasWaitingTasks() { + return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); + } + + private long nextId() { + return this.idGenerator++; + } + + private boolean isExecutingAnyTasks() { + return this.currentlyExecuting != 0; + } + + private void pushNode(final DependencyNode node) { + if (!node.task.isFiniteRadius()) { + this.infiniteRadius.add(node); + return; + } + + // set up dependency for node + final Task task = node.task; + + final int centerX = task.chunkX; + final int centerZ = task.chunkZ; + final int radius = task.radius; + + final int minX = centerX - radius; + final int maxX = centerX + radius; + + final int minZ = centerZ - radius; + final int maxZ = centerZ + radius; + + ReferenceOpenHashSet parents = null; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); + if (dependency != null) { + if (parents == null) { + parents = new ReferenceOpenHashSet<>(); + } + if (parents.add(dependency)) { + // added a dependency, so we need to add as a child to the dependency + if (dependency.children == null) { + dependency.children = new ArrayList<>(); + } + dependency.children.add(node); + } + } + } + } + + if (parents == null) { + // no dependencies, add straight to awaiting + this.awaiting.add(node); + } else { + node.parents = parents.size(); + // we will be added to awaiting once we have no parents + } + } + + // called only when a node is returned after being executed + private List returnNode(final DependencyNode node) { + final Task task = node.task; + + // now that the task is completed, we can push its children to the awaiting queue + this.pushChildren(node); + + if (task.isFiniteRadius()) { + // remove from dependency map + this.removeNodeFromMap(node); + } else { + // mark as no longer executing infinite radius + if (!this.isInfiniteRadiusScheduled) { + throw new IllegalStateException(); + } + this.isInfiniteRadiusScheduled = false; + } + + // decrement executing count, we are done executing this task + --this.currentlyExecuting; + + if (this.currentlyExecuting == 0) { + return this.scheduler.treeFinished(); + } + + return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; + } + + private List tryPushTasks() { + // tasks are not queued, but only created here - we do hold the lock for the map + List ret = null; + PrioritisedExecutor.PrioritisedTask pushedTask; + while ((pushedTask = this.tryPushTask()) != null) { + if (ret == null) { + ret = new ArrayList<>(); + } + ret.add(pushedTask); + } + + return ret; + } + + private void removeNodeFromMap(final DependencyNode node) { + final Task task = node.task; + + final int centerX = task.chunkX; + final int centerZ = task.chunkZ; + final int radius = task.radius; + + final int minX = centerX - radius; + final int maxX = centerX + radius; + + final int minZ = centerZ - radius; + final int maxZ = centerZ + radius; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); + } + } + } + + private void pushChildren(final DependencyNode node) { + // add all the children that we can into awaiting + final List children = node.children; + if (children != null) { + for (int i = 0, len = children.size(); i < len; ++i) { + final DependencyNode child = children.get(i); + int newParents = --child.parents; + if (newParents == 0) { + // no more dependents, we can push to awaiting + // even if the child is purged, we need to push it so that its children will be pushed + this.awaiting.add(child); + } else if (newParents < 0) { + throw new IllegalStateException(); + } + } + } + } + + private DependencyNode pollAwaiting() { + final DependencyNode ret = this.awaiting.poll(); + if (ret == null) { + return ret; + } + + if (ret.parents != 0) { + throw new IllegalStateException(); + } + + if (ret.purged) { + // need to manually remove from state here + this.pushChildren(ret); + this.removeNodeFromMap(ret); + } // else: delay children push until the task has finished + + return ret; + } + + private DependencyNode pollInfinite() { + return this.infiniteRadius.poll(); + } + + public PrioritisedExecutor.PrioritisedTask tryPushTask() { + if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { + return null; + } + + DependencyNode firstInfinite; + while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { + this.pollInfinite(); + } + + DependencyNode firstAwaiting; + while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { + this.pollAwaiting(); + } + + if (firstInfinite == null && firstAwaiting == null) { + return null; + } + + // firstAwaiting compared to firstInfinite + final int compare; + + if (firstAwaiting == null) { + // we choose first infinite, or infinite < awaiting + compare = 1; + } else if (firstInfinite == null) { + // we choose first awaiting, or awaiting < infinite + compare = -1; + } else { + compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); + } + + if (compare >= 0) { + if (this.currentlyExecuting != 0) { + // don't queue infinite task while other tasks are executing in parallel + return null; + } + ++this.currentlyExecuting; + this.pollInfinite(); + this.isInfiniteRadiusScheduled = true; + return firstInfinite.task.pushTask(this.executor); + } else { + ++this.currentlyExecuting; + this.pollAwaiting(); + return firstAwaiting.task.pushTask(this.executor); + } + } + } + + private static final class DependencyNode { + + private final Task task; + private final DependencyTree tree; + + // dependency tree fields + // (must hold lock on the scheduler to use) + // null is the same as empty, we just use it so that we don't allocate the set unless we need to + private List children; + // 0 indicates that this task is considered "awaiting" + private int parents; + // false -> scheduled and not cancelled + // true -> scheduled but cancelled + private boolean purged; + private final long id; + + public DependencyNode(final Task task, final DependencyTree tree) { + this.task = task; + this.id = tree.nextId(); + this.tree = tree; + } + } + + private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { + + // task specific fields + private final RadiusAwarePrioritisedExecutor scheduler; + private final int chunkX; + private final int chunkZ; + private final int radius; + private Runnable run; + private PrioritisedExecutor.Priority priority; + + private DependencyNode dependencyNode; + private PrioritisedExecutor.PrioritisedTask queuedTask; + + private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, + final Runnable run, final PrioritisedExecutor.Priority priority) { + this.scheduler = scheduler; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.radius = radius; + this.run = run; + this.priority = priority; + } + + private boolean isFiniteRadius() { + return this.radius >= 0; + } + + private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { + return this.queuedTask = executor.createTask(this, this.priority); + } + + private void executeTask() { + final Runnable run = this.run; + this.run = null; + run.run(); + } + + private static void scheduleTasks(final List toSchedule) { + if (toSchedule != null) { + for (int i = 0, len = toSchedule.size(); i < len; ++i) { + toSchedule.get(i).queue(); + } + } + } + + private void returnNode() { + final List toSchedule; + synchronized (this.scheduler) { + final DependencyNode node = this.dependencyNode; + this.dependencyNode = null; + toSchedule = node.tree.returnNode(node); + } + + scheduleTasks(toSchedule); + } + + @Override + public void run() { + final Runnable run = this.run; + this.run = null; + try { + run.run(); + } finally { + this.returnNode(); + } + } + + @Override + public boolean queue() { + final List toSchedule; + synchronized (this.scheduler) { + if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + toSchedule = this.scheduler.queue(this, this.priority); + } + + scheduleTasks(toSchedule); + return true; + } + + @Override + public boolean cancel() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; + } + + return true; + } + } + + if (task.cancel()) { + // must manually return the node + this.run = null; + this.returnNode(); + return true; + } + return false; + } + + @Override + public boolean execute() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + if (this.dependencyNode != null) { + this.dependencyNode.purged = true; + this.dependencyNode = null; + } + // fall through to execution logic + } + } + + if (task != null) { + // will run the return node logic automatically + return task.execute(); + } else { + // don't run node removal/insertion logic, we aren't actually removed from the dependency tree + this.executeTask(); + return true; + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + final PrioritisedExecutor.PrioritisedTask task; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + return this.priority; + } + } + + return task.getPriority(); + } + + @Override + public boolean setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (this.priority == priority) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.setPriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + + @Override + public boolean raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (this.priority.isHigherOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.raisePriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + + @Override + public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + final PrioritisedExecutor.PrioritisedTask task; + List toSchedule = null; + synchronized (this.scheduler) { + if ((task = this.queuedTask) == null) { + if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (this.priority.isLowerOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.dependencyNode != null) { + // need to re-insert node + this.dependencyNode.purged = true; + this.dependencyNode = null; + toSchedule = this.scheduler.queue(this, priority); + } + } + } + + if (task != null) { + return task.lowerPriority(priority); + } + + scheduleTasks(toSchedule); + + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java new file mode 100644 index 0000000..d36b3ec --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java @@ -0,0 +1,137 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; + +public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class); + + private final NewChunkHolder chunkHolder; + private final ChunkAccess fromChunk; + private final PrioritisedExecutor.PrioritisedTask convertToFullTask; + + public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.fromChunk = fromChunk; + this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.FULL; + } + + @Override + public void run() { + // See Vanilla protoChunkToFullChunk for what this function should be doing + final LevelChunk chunk; + try { + // moved from the load from nbt stage into here + final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); + if (poiChunk == null) { + LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); + } else { + poiChunk.load(); + ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk); + } + + if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { + chunk = wrappedFull.getWrapped(); + } else { + final ServerLevel world = this.world; + final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; + chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { + ChunkMap.postLoadProtoChunk(world, protoChunk.getEntities()); + }); + } + + final NewChunkHolder chunkHolder = this.chunkHolder; + + chunk.setFullStatus(chunkHolder::getChunkStatus); + chunk.runPostLoad(); + // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) + // This brings entity addition back in line with older versions of the game + // Since we load the NBT in the empty status, this will never block for I/O + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); + + // we don't need the entitiesInLevel, not sure why it's there + chunk.setLoaded(true); + chunk.registerAllBlockEntitiesAfterLevelLoad(); + chunk.registerTickContainerInLevel(this.world); + } catch (final Throwable throwable) { + this.complete(null, throwable); + return; + } + this.complete(chunk, null); + } + + protected volatile boolean scheduled; + protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { + throw new IllegalStateException("Cannot double call schedule()"); + } + this.convertToFullTask.queue(); + } + + @Override + public void cancel() { + if (this.convertToFullTask.cancel()) { + this.complete(null, null); + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.convertToFullTask.getPriority(); + } + + @Override + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.lowerPriority(priority); + } + + @Override + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.setPriority(priority); + } + + @Override + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.convertToFullTask.raisePriority(priority); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java new file mode 100644 index 0000000..ec17c0b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java @@ -0,0 +1,181 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.function.BooleanSupplier; + +public final class ChunkLightTask extends ChunkProgressionTask { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final ChunkAccess fromChunk; + + private final LightTaskPriorityHolder priorityHolder; + + public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ); + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.priorityHolder = new LightTaskPriorityHolder(priority, this); + this.fromChunk = chunk; + } + + @Override + public boolean isScheduled() { + return this.priorityHolder.isScheduled(); + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.LIGHT; + } + + @Override + public void schedule() { + this.priorityHolder.schedule(); + } + + @Override + public void cancel() { + this.priorityHolder.cancel(); + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.priorityHolder.getPriority(); + } + + @Override + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + this.priorityHolder.raisePriority(priority); + } + + @Override + public void setPriority(final PrioritisedExecutor.Priority priority) { + this.priorityHolder.setPriority(priority); + } + + @Override + public void raisePriority(final PrioritisedExecutor.Priority priority) { + this.priorityHolder.raisePriority(priority); + } + + private static final class LightTaskPriorityHolder extends PriorityHolder { + + private final ChunkLightTask task; + + private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { + super(priority); + this.task = task; + } + + @Override + protected void cancelScheduled() { + final ChunkLightTask task = this.task; + task.complete(null, null); + } + + @Override + protected PrioritisedExecutor.Priority getScheduledPriority() { + final ChunkLightTask task = this.task; + return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ); + } + + @Override + protected void scheduleTask(final PrioritisedExecutor.Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); + lightQueue.setPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.setPriority(task.chunkX, task.chunkZ, priority); + } + + @Override + protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { + final ChunkLightTask task = this.task; + final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); + final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); + lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); + } + } + + private static final class LightTask implements BooleanSupplier { + + private final StarLightInterface lightEngine; + private final ChunkLightTask task; + + public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { + this.lightEngine = lightEngine; + this.task = task; + } + + @Override + public boolean getAsBoolean() { + final ChunkLightTask task = this.task; + // executed on light thread + if (!task.priorityHolder.markExecuting()) { + // cancelled + return false; + } + + try { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); + + if (task.fromChunk.isLightCorrect() && task.fromChunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); + this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); + } else { + task.fromChunk.setLightCorrect(false); + this.lightEngine.lightChunk(task.fromChunk, emptySections); + task.fromChunk.setLightCorrect(true); + } + // we need to advance status + if (task.fromChunk instanceof ProtoChunk chunk && chunk.getStatus() == ChunkStatus.LIGHT.getParent()) { + chunk.setStatus(ChunkStatus.LIGHT); + } + } catch (final Throwable thr) { + LOGGER.fatal( + "Failed to light chunk " + task.fromChunk.getPos().toString() + + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr + ); + + task.complete(null, thr); + + return true; + } + + task.complete(task.fromChunk, null); + return true; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java new file mode 100644 index 0000000..098a699 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java @@ -0,0 +1,487 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters; +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public final class ChunkLoadTask extends ChunkProgressionTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class); + + private final NewChunkHolder chunkHolder; + private final ChunkDataLoadTask loadTask; + + private volatile boolean cancelled; + private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; + private GenericDataLoadTask.TaskResult loadResult; + private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data + + public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ); + this.chunkHolder = chunkHolder; + this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); + this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { + ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement + ChunkLoadTask.this.tryCompleteLoad(); + }); + } + + private void tryCompleteLoad() { + final int count = this.taskCountToComplete.decrementAndGet(); + if (count == 0) { + final GenericDataLoadTask.TaskResult result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement + ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); + } else if (count < 0) { + throw new IllegalStateException("Called tryCompleteLoad() too many times"); + } + } + + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.EMPTY; + } + + private boolean scheduled; + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; + + final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { + ChunkLoadTask.this.tryCompleteLoad(); + }; + + // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because + // they must schedule a task to off main or to on main to complete + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + if (this.scheduled) { + throw new IllegalStateException("schedule() called twice"); + } + this.scheduled = true; + if (this.cancelled) { + return; + } + if (!this.chunkHolder.isEntityChunkNBTLoaded()) { + entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); + } else { + entityLoadTask = null; + this.tryCompleteLoad(); + } + + if (!this.chunkHolder.isPoiChunkLoaded()) { + poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); + } else { + poiLoadTask = null; + this.tryCompleteLoad(); + } + + this.entityLoadTask = entityLoadTask; + this.poiLoadTask = poiLoadTask; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + if (entityLoadTask != null) { + entityLoadTask.schedule(); + } + + if (poiLoadTask != null) { + poiLoadTask.schedule(); + } + + this.loadTask.schedule(false); + } + + @Override + public void cancel() { + // must be before load task access, so we can synchronise with the writes to the fields + final boolean scheduled; + final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); + try { + // must read field here, as it may be written later conucrrently - + // we need to know if we scheduled _before_ cancellation + scheduled = this.scheduled; + this.cancelled = true; + } finally { + this.scheduler.schedulingLockArea.unlock(schedulingLock); + } + + /* + Note: The entityLoadTask/poiLoadTask do not complete when cancelled, + so we need to manually try to complete in those cases + It is also important to note that we set the cancelled field first, just in case + the chunk load task attempts to complete with a non-null value + */ + + if (scheduled) { + // since we scheduled, we need to cancel the tasks + if (this.entityLoadTask != null) { + if (this.entityLoadTask.cancel()) { + this.tryCompleteLoad(); + } + } + if (this.poiLoadTask != null) { + if (this.poiLoadTask.cancel()) { + this.tryCompleteLoad(); + } + } + } else { + // since nothing was scheduled, we need to decrement the task count here ourselves + + // for entity load task + this.tryCompleteLoad(); + + // for poi load task + this.tryCompleteLoad(); + } + this.loadTask.cancel(); + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.loadTask.getPriority(); + } + + @Override + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.lowerPriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.lowerPriority(priority); + } + + this.loadTask.lowerPriority(priority); + } + + @Override + public void setPriority(final PrioritisedExecutor.Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.setPriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.setPriority(priority); + } + + this.loadTask.setPriority(priority); + } + + @Override + public void raisePriority(final PrioritisedExecutor.Priority priority) { + final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); + if (entityLoad != null) { + entityLoad.raisePriority(priority); + } + + final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); + + if (poiLoad != null) { + poiLoad.raisePriority(priority); + } + + this.loadTask.raisePriority(priority); + } + + protected static abstract class CallbackDataLoadTask extends GenericDataLoadTask { + + private TaskResult result; + private final MultiThreadedQueue>> waiters = new MultiThreadedQueue<>(); + + protected volatile boolean completed; + protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); + + protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final RegionFileIOThread.RegionFileType type, + final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ, type, priority); + } + + public void addCallback(final Consumer> consumer) { + if (!this.waiters.add(consumer)) { + try { + consumer.accept(this.result); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(consumer), + "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()), + "CallbackDataLoadTask impl", this.getClass().getName() + ), throwable); + } + } + } + + @Override + protected void onComplete(final TaskResult result) { + if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { + throw new IllegalStateException("Already completed"); + } + this.result = result; + Consumer> consumer; + while ((consumer = this.waiters.pollOrBlockAdds()) != null) { + try { + consumer.accept(result); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(consumer), + "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()), + "CallbackDataLoadTask impl", this.getClass().getName() + ), throwable); + return; + } + } + } + } + + private static final class ChunkDataLoadTask extends CallbackDataLoadTask { + private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return true; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); + } + + @Override + protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + return new TaskResult<>(null, throwable); + } + if (data == null) { + return new TaskResult<>(this.getEmptyChunk(), null); + } + + if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { + return this.deserialize(data); + } + // need to deserialize on main thread + return null; + } + + private ProtoChunk getEmptyChunk() { + return new ProtoChunk( + new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, + this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null + ); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); + return new TaskResult<>(null, null); + } + + if (data == null) { + return new TaskResult<>(null, null); + } + + try { + // run converters + final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data); + + return new TaskResult<>(converted, null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); + return new TaskResult<>(null, null); + } + } + + private TaskResult deserialize(final CompoundTag data) { + try { + final ChunkAccess deserialized = ChunkSerializer.read( + this.world, this.world.getPoiManager(), new ChunkPos(this.chunkX, this.chunkZ), data + ); + return new TaskResult<>(deserialized, null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); + return new TaskResult<>(this.getEmptyChunk(), null); + } + } + + @Override + protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { + // data != null && throwable == null + if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { + throw new UnsupportedOperationException(); + } + return this.deserialize(data); + } + } + + public static final class PoiDataLoadTask extends CallbackDataLoadTask { + + public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return false; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + + if (data == null || data.isEmpty()) { + // nothing to do + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + + try { + // run converters + final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world); + + // now we need to parse it + return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); + return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); + } + } + + @Override + protected TaskResult runOnMain(final PoiChunk data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + } + + public static final class EntityDataLoadTask extends CallbackDataLoadTask { + + public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); + } + + @Override + protected boolean hasOffMain() { + return true; + } + + @Override + protected boolean hasOnMain() { + return false; + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + return this.scheduler.loadExecutor.createTask(run, priority); + } + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + + @Override + protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); + return new TaskResult<>(null, null); + } + + if (data == null || data.isEmpty()) { + // nothing to do + return new TaskResult<>(null, null); + } + + try { + return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null); + } catch (final Throwable thr2) { + LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); + return new TaskResult<>(null, thr2); + } + } + + @Override + protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java new file mode 100644 index 0000000..70e900b --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java @@ -0,0 +1,101 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.function.BiConsumer; + +public abstract class ChunkProgressionTask { + + private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); + private ChunkAccess completedChunk; + private Throwable completedThrowable; + + protected final ChunkTaskScheduler scheduler; + protected final ServerLevel world; + protected final int chunkX; + protected final int chunkZ; + + protected volatile boolean completed; + protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); + + protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + // Used only for debug json + public abstract boolean isScheduled(); + + // Note: It is the responsibility of the task to set the chunk's status once it has completed + public abstract ChunkStatus getTargetStatus(); + + /* Only executed once */ + /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ + public abstract void schedule(); + + /* May be called multiple times */ + public abstract void cancel(); + + public abstract PrioritisedExecutor.Priority getPriority(); + + /* Schedule lock is always held for the priority update calls */ + + public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); + + public abstract void setPriority(final PrioritisedExecutor.Priority priority); + + public abstract void raisePriority(final PrioritisedExecutor.Priority priority); + + public final void onComplete(final BiConsumer onComplete) { + if (!this.waiters.add(onComplete)) { + try { + onComplete.accept(this.completedChunk, this.completedThrowable); + } catch (final Throwable throwable) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), + "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) + ), throwable); + } + } + } + + protected final void complete(final ChunkAccess chunk, final Throwable throwable) { + try { + this.complete0(chunk, throwable); + } catch (final Throwable thr2) { + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) + ), thr2); + } + } + + private void complete0(final ChunkAccess chunk, final Throwable throwable) { + if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { + throw new IllegalStateException("Already completed"); + } + this.completedChunk = chunk; + this.completedThrowable = throwable; + + BiConsumer consumer; + while ((consumer = this.waiters.pollOrBlockAdds()) != null) { + consumer.accept(chunk, throwable); + } + } + + @Override + public String toString() { + return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) + + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + + ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java new file mode 100644 index 0000000..454f616 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java @@ -0,0 +1,219 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.WorldGenContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class); + + private final ChunkAccess fromChunk; + private final ChunkStatus fromStatus; + private final ChunkStatus toStatus; + private final List neighbours; + + private final PrioritisedExecutor.PrioritisedTask generateTask; + + public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final ChunkAccess chunk, final List neighbours, + final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ); + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.fromChunk = chunk; + this.fromStatus = chunk.getStatus(); + this.toStatus = toStatus; + this.neighbours = neighbours; + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) { + this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); + } else { + final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius(); + if (writeRadius < 0) { + this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority); + } else { + this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority); + } + } + } + + @Override + public ChunkStatus getTargetStatus() { + return this.toStatus; + } + + private boolean isEmptyTask() { + // must use fromStatus here to avoid any race condition with run() overwriting the status + final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); + return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()); + } + + @Override + public void run() { + final ChunkAccess chunk = this.fromChunk; + + final ServerChunkCache serverChunkCache = this.world.getChunkSource(); + final ChunkMap chunkMap = serverChunkCache.chunkMap; + + final CompletableFuture completeFuture; + + final boolean generation; + boolean completing = false; + + // note: should optimise the case where the chunk does not need to execute the status, because + // schedule() calls this synchronously if it will run through that path + + final WorldGenContext ctx = new WorldGenContext( + this.world, + chunkMap.generator, + chunkMap.worldGenContext.structureManager(), + serverChunkCache.getLightEngine() + ); + try { + generation = !chunk.getStatus().isOrAfter(this.toStatus); + if (generation) { + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) { + if (chunk instanceof ProtoChunk) { + ((ProtoChunk)chunk).setStatus(this.toStatus); + } + completing = true; + this.complete(chunk, null); + return; + } + completeFuture = this.toStatus.generate(ctx, Runnable::run, null, this.neighbours) + .whenComplete((final ChunkAccess either, final Throwable throwable) -> { + if (either instanceof ProtoChunk proto) { + proto.setStatus(ChunkUpgradeGenericStatusTask.this.toStatus); + } + } + ); + } else { + if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) { + completing = true; + this.complete(chunk, null); + return; + } + completeFuture = this.toStatus.load(ctx, null, chunk); + } + } catch (final Throwable throwable) { + if (!completing) { + this.complete(null, throwable); + return; + } + + this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), + "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), + "Generation task", this + ), throwable); + + LOGGER.error( + "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + + "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world), + throwable + ); + + return; + } + + if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) { + LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); + } + + final ChunkAccess newChunk; + + try { + newChunk = completeFuture.join(); + } catch (final Throwable throwable) { + this.complete(null, throwable); + return; + } + + if (newChunk == null) { + this.complete(null, + new IllegalStateException( + "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + + ", generation: " + generation + " should not be null! Future: " + completeFuture + ).fillInStackTrace() + ); + return; + } + + this.complete(newChunk, null); + } + + private volatile boolean scheduled; + private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); + + @Override + public boolean isScheduled() { + return this.scheduled; + } + + @Override + public void schedule() { + if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { + throw new IllegalStateException("Cannot double call schedule()"); + } + if (this.isEmptyTask()) { + if (this.generateTask.cancel()) { + this.run(); + } + } else { + this.generateTask.queue(); + } + } + + @Override + public void cancel() { + if (this.generateTask.cancel()) { + this.complete(null, null); + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.generateTask.getPriority(); + } + + @Override + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.lowerPriority(priority); + } + + @Override + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.setPriority(priority); + } + + @Override + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.generateTask.raisePriority(priority); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java new file mode 100644 index 0000000..7a65d35 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java @@ -0,0 +1,673 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; + +import ca.spottedleaf.concurrentutil.completable.Completable; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; + +public abstract class GenericDataLoadTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class); + + protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); + + // reference count is the upper 32 bits + protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); + + protected static final long STAGE_MASK = 0xFFFFFFFFL; + protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; + protected static final long STAGE_NOT_STARTED = 0L; + protected static final long STAGE_LOADING = 1L; + protected static final long STAGE_PROCESSING = 2L; + protected static final long STAGE_COMPLETED = 3L; + + // for loading data off disk + protected final LoadDataFromDiskTask loadDataFromDiskTask; + // processing off-main + protected final PrioritisedExecutor.PrioritisedTask processOffMain; + // processing on-main + protected final PrioritisedExecutor.PrioritisedTask processOnMain; + + protected final ChunkTaskScheduler scheduler; + protected final ServerLevel world; + protected final int chunkX; + protected final int chunkZ; + protected final RegionFileIOThread.RegionFileType type; + + public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final RegionFileIOThread.RegionFileType type, + final PrioritisedExecutor.Priority priority) { + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.type = type; + + final ProcessOnMainTask mainTask; + if (this.hasOnMain()) { + mainTask = new ProcessOnMainTask(); + this.processOnMain = this.createOnMain(mainTask, priority); + } else { + mainTask = null; + this.processOnMain = null; + } + + final ProcessOffMainTask offMainTask; + if (this.hasOffMain()) { + offMainTask = new ProcessOffMainTask(mainTask); + this.processOffMain = this.createOffMain(offMainTask, priority); + } else { + offMainTask = null; + this.processOffMain = null; + } + + if (this.processOffMain == null && this.processOnMain == null) { + throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); + } + + this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); + } + + public static final record TaskResult(L left, R right) {} + + protected abstract boolean hasOffMain(); + + protected abstract boolean hasOnMain(); + + protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); + + protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); + + protected abstract TaskResult runOffMain(final CompoundTag data, final Throwable throwable); + + protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); + + protected abstract void onComplete(final TaskResult result); + + protected abstract TaskResult completeOnMainOffMain(final OnMain data, final Throwable throwable); + + @Override + public String toString() { + return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) + + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + + ", type: " + this.type.toString() + "}"; + } + + public PrioritisedExecutor.Priority getPriority() { + if (this.processOnMain != null) { + return this.processOnMain.getPriority(); + } else { + return this.processOffMain.getPriority(); + } + } + + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + // can't lower I/O tasks, we don't know what they affect + if (this.processOffMain != null) { + this.processOffMain.lowerPriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.lowerPriority(priority); + } + } + + public void setPriority(final PrioritisedExecutor.Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { + this.processOffMain.setPriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.setPriority(priority); + } + } + + public void raisePriority(final PrioritisedExecutor.Priority priority) { + // can't lower I/O tasks, we don't know what they affect + this.loadDataFromDiskTask.raisePriority(priority); + if (this.processOffMain != null) { + this.processOffMain.raisePriority(priority); + } + if (this.processOnMain != null) { + this.processOnMain.raisePriority(priority); + } + } + + // returns whether scheduleNow() needs to be called + public boolean schedule(final boolean delay) { + if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || + !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { + // try and increment reference count + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { + // cancelled or completed, nothing to do here + return false; + } + + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { + // successful + return false; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + if (!delay) { + this.scheduleNow(); + return false; + } + return true; + } + + public void scheduleNow() { + this.loadDataFromDiskTask.schedule(); // will schedule the rest + } + + // assumes the current stage cannot be completed + // returns false if cancelled, returns true if can proceed + private boolean advanceStage(final long expect, final long to) { + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) != expect) { + // must be cancelled + return false; + } + + final long newVal = (curr & ~STAGE_MASK) | to; + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + return true; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public boolean cancel() { + int failures = 0; + for (long curr = this.stageAndReferenceCount.get();;) { + if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { + return false; + } + + if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { + // no other references, so we can cancel + final long newVal = STAGE_CANCELLED; + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + this.loadDataFromDiskTask.cancel(); + if (this.processOffMain != null) { + this.processOffMain.cancel(); + } + if (this.processOnMain != null) { + this.processOnMain.cancel(); + } + this.onComplete(null); + return true; + } + } else { + if ((curr & ~STAGE_MASK) == (0L << 32)) { + throw new IllegalStateException("Reference count cannot be zero here"); + } + // just decrease the reference count + final long newVal = curr - (1L << 32); + if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { + return false; + } + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + private final class DataLoadCallback implements BiConsumer { + + private final ProcessOffMainTask offMainTask; + private final ProcessOnMainTask onMainTask; + + public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { + this.offMainTask = offMainTask; + this.onMainTask = onMainTask; + } + + @Override + public void accept(final CompoundTag compoundTag, final Throwable throwable) { + if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { + // don't try to schedule further + return; + } + + try { + if (compoundTag == CANCELLED_DATA) { + // cancelled, except this isn't possible + LOGGER.error("Data callback says cancelled, but stage does not?"); + return; + } + + // get off of the regionfile callback ASAP, no clue what locks are held right now... + if (GenericDataLoadTask.this.processOffMain != null) { + this.offMainTask.data = compoundTag; + this.offMainTask.throwable = throwable; + GenericDataLoadTask.this.processOffMain.queue(); + return; + } else { + // no off-main task, so go straight to main + this.onMainTask.data = (OnMain)compoundTag; + this.onMainTask.throwable = throwable; + GenericDataLoadTask.this.processOnMain.queue(); + } + } catch (final Throwable thr2) { + LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); + GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( + GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( + "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) + ), thr2 + ); + } + } + } + + private final class ProcessOffMainTask implements Runnable { + + private CompoundTag data; + private Throwable throwable; + private final ProcessOnMainTask schedule; + + public ProcessOffMainTask(final ProcessOnMainTask schedule) { + this.schedule = schedule; + } + + @Override + public void run() { + if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { + // cancelled + return; + } + final TaskResult newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); + + if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { + // don't try to schedule further + return; + } + + if (this.schedule != null) { + final TaskResult syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); + + if (syncComplete != null) { + if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { + GenericDataLoadTask.this.onComplete(syncComplete); + } // else: cancelled + return; + } + + this.schedule.data = newData.left; + this.schedule.throwable = newData.right; + + GenericDataLoadTask.this.processOnMain.queue(); + } else { + GenericDataLoadTask.this.onComplete((TaskResult)newData); + } + } + } + + private final class ProcessOnMainTask implements Runnable { + + private OnMain data; + private Throwable throwable; + + @Override + public void run() { + if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { + // cancelled + return; + } + final TaskResult result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); + + GenericDataLoadTask.this.onComplete(result); + } + } + + protected static final class LoadDataFromDiskTask { + + private volatile int priority; + private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); + + private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; + private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; + private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; + + private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; + + private final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); + } + + private final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); + } + + private final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); + } + + private final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); + } + + private final ServerLevel world; + private final int chunkX; + private final int chunkZ; + + private final RegionFileIOThread.RegionFileType type; + private Cancellable dataLoadTask; + private Cancellable dataUnloadCancellable; + private DelayedPrioritisedTask dataUnloadTask; + + private final BiConsumer onComplete; + private final AtomicBoolean scheduled = new AtomicBoolean(); + + // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does + // hold a priority lock. + public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, + final RegionFileIOThread.RegionFileType type, + final BiConsumer onComplete, + final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.type = type; + this.onComplete = onComplete; + this.setPriorityPlain(priority.priority); + } + + private void complete(final CompoundTag data, final Throwable throwable) { + try { + this.onComplete.accept(data, throwable); + } catch (final Throwable thr2) { + ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( + "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), + "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) + ), thr2); + } + } + + private boolean markExecuting() { + return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; + } + + private boolean isMarkedExecuted() { + return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; + } + + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.lowerPriority(priority); + } + // no return - we need to propagate priority + } + + if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.setPriority(priority); + } + // no return - we need to propagate priority + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + return; + } + + if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { + RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); + return; + } + + if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.raisePriority(priority); + } + // no return - we need to propagate priority + } + + if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void cancel() { + if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { + // cancelled or executed already + return; + } + + // OK if we miss the field read, the task cannot complete if the cancelled bit is set and + // the write to dataLoadTask will check for the cancelled bit + if (this.dataUnloadCancellable != null) { + this.dataUnloadCancellable.cancel(); + } + + if (this.dataLoadTask != null) { + this.dataLoadTask.cancel(); + } + + this.complete(CANCELLED_DATA, null); + } + + public void schedule() { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("schedule() called twice"); + } + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { + // because cancelScheduled() cannot actually stop this task from executing in every case, we need + // to mark complete here to ensure we do not double complete + if (LoadDataFromDiskTask.this.markExecuting()) { + LoadDataFromDiskTask.this.complete(data, thr); + } // else: cancelled + }; + + final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); + boolean scheduledUnload = false; + + final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); + if (holder != null) { + final BiConsumer unloadConsumer = (final CompoundTag data, final Throwable thr) -> { + if (data != null) { + consumer.accept(data, null); + } else { + // need to schedule task + LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); + } + }; + Cancellable unloadCancellable = null; + CompoundTag syncComplete = null; + final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists + final Completable unloadCompletable = unloadTask == null ? null : unloadTask.completable(); + if (unloadCompletable != null) { + unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); + if (unloadCancellable == null) { + syncComplete = unloadCompletable.getResult(); + } + } + + if (syncComplete != null) { + consumer.accept(syncComplete, null); + return; + } + + if (unloadCancellable != null) { + scheduledUnload = true; + this.dataUnloadCancellable = unloadCancellable; + this.dataUnloadTask = unloadTask.task(); + } + } + + this.schedule(scheduledUnload, consumer, initialPriority); + } + + private void schedule(final boolean scheduledUnload, final BiConsumer consumer, final PrioritisedExecutor.Priority initialPriority) { + int priority = this.getPriorityVolatile(); + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled + return; + } + + if (!scheduledUnload) { + this.dataLoadTask = RegionFileIOThread.loadDataAsync( + this.world, this.chunkX, this.chunkZ, this.type, consumer, + initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority + ); + } + + int failures = 0; + for (;;) { + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { + return; + } + + if ((priority & PRIORITY_EXECUTED) != 0) { + // cancelled or executed + if (this.dataUnloadCancellable != null) { + this.dataUnloadCancellable.cancel(); + } + + if (this.dataLoadTask != null) { + this.dataLoadTask.cancel(); + } + return; + } + + if (scheduledUnload) { + if (this.dataUnloadTask != null) { + this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + } else { + RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java new file mode 100644 index 0000000..21c9562 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java @@ -0,0 +1,7 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.server; + +public interface ChunkSystemMinecraftServer { + + public void moonrise$setChunkSystemCrash(final Throwable throwable); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java new file mode 100644 index 0000000..129a35f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.storage; + +import net.minecraft.world.level.chunk.storage.RegionFileStorage; + +public interface ChunkSystemChunkStorage { + + public RegionFileStorage moonrise$getRegionStorage(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java new file mode 100644 index 0000000..786e6ad --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.ticket; + +public interface ChunkSystemTicket { + + public long moonrise$getRemoveDelay(); + + public void moonrise$setRemoveDelay(final long removeDelay); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java new file mode 100644 index 0000000..2add7fd --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.ticks; + +public interface ChunkSystemLevelChunkTicks { + + public boolean moonrise$isDirty(final long tick); + + public void moonrise$clearDirty(); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java new file mode 100644 index 0000000..04ef7a9 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java @@ -0,0 +1,13 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.util; + +import net.minecraft.util.SortedArraySet; + +public interface ChunkSystemSortedArraySet { + + public SortedArraySet moonrise$copy(); + + public T moonrise$replace(final T object); + + public T moonrise$removeAndGet(final T object); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java new file mode 100644 index 0000000..51c9ed3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java @@ -0,0 +1,320 @@ +package ca.spottedleaf.moonrise.patches.chunk_system.util; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import java.util.Arrays; +import java.util.Objects; + +public class ParallelSearchRadiusIteration { + + // expected that this list returns for a given radius, the set of chunks ordered + // by manhattan distance + private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][]; + static { + for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { + // a BFS around -x, -z, +x, +z will give increasing manhatten distance + SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); + } + } + + public static long[] getSearchIteration(final int radius) { + return SEARCH_RADIUS_ITERATION_LIST[radius]; + } + + private static class CustomLongArray extends LongArrayList { + + public CustomLongArray() { + super(); + } + + public CustomLongArray(final int expected) { + super(expected); + } + + public boolean addAll(final CustomLongArray list) { + this.addElements(this.size, list.a, 0, list.size); + return list.size != 0; + } + + public void addUnchecked(final long value) { + this.a[this.size++] = value; + } + + public void forceSize(final int to) { + this.size = to; + } + + @Override + public int hashCode() { + long h = 1L; + + Objects.checkFromToIndex(0, this.size, this.a.length); + + for (int i = 0; i < this.size; ++i) { + h = HashCommon.mix(h + this.a[i]); + } + + return (int)h; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof CustomLongArray other)) { + return false; + } + + return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size); + } + } + + private static int getDistanceSize(final int radius, final int max) { + if (radius == 0) { + return 1; + } + final int diff = radius - max; + if (diff <= 0) { + return 4*radius; + } + return 4*(max - Math.max(0, diff - 1)); + } + + private static int getQ1DistanceSize(final int radius, final int max) { + if (radius == 0) { + return 1; + } + final int diff = radius - max; + if (diff <= 0) { + return radius+1; + } + return max - diff + 1; + } + + private static final class BasicFIFOLQueue { + + private final long[] values; + private int head, tail; + + public BasicFIFOLQueue(final int cap) { + if (cap <= 1) { + throw new IllegalArgumentException(); + } + this.values = new long[cap]; + } + + public boolean isEmpty() { + return this.head == this.tail; + } + + public long removeFirst() { + final long ret = this.values[this.head]; + + if (this.head == this.tail) { + throw new IllegalStateException(); + } + + ++this.head; + if (this.head == this.values.length) { + this.head = 0; + } + + return ret; + } + + public void addLast(final long value) { + this.values[this.tail++] = value; + + if (this.tail == this.head) { + throw new IllegalStateException(); + } + + if (this.tail == this.values.length) { + this.tail = 0; + } + } + } + + private static CustomLongArray[] makeQ1BFS(final int radius) { + final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1]; + final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1); + final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1)); + + seen.add(CoordinateUtils.getChunkKey(0, 0)); + queue.addLast(CoordinateUtils.getChunkKey(0, 0)); + while (!queue.isEmpty()) { + final long chunk = queue.removeFirst(); + final int chunkX = CoordinateUtils.getChunkX(chunk); + final int chunkZ = CoordinateUtils.getChunkZ(chunk); + + final int index = Math.abs(chunkX) + Math.abs(chunkZ); + final CustomLongArray list = ret[index]; + if (list != null) { + list.addUnchecked(chunk); + } else { + (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk); + } + + for (int i = 0; i < 4; ++i) { + // 0 -> -1, 0 + // 1 -> 0, -1 + // 2 -> 1, 0 + // 3 -> 0, 1 + + final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0) + // note: -n = (~n) + 1 + // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1) + + final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1 + final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0 + final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1 + + final int neighbourX = chunkX + dx; + final int neighbourZ = chunkZ + dz; + final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { + // don't enqueue out of range + continue; + } + + if (!seen.add(neighbour)) { + continue; + } + + queue.addLast(neighbour); + } + } + + return ret; + } + + // doesn't appear worth optimising this function now, even though it's 70% of the call + private static CustomLongArray spread(final CustomLongArray input, final int size) { + final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input); + final CustomLongArray added = new CustomLongArray(size); + + while (!notAdded.isEmpty()) { + if (added.isEmpty()) { + added.addUnchecked(notAdded.removeLastLong()); + continue; + } + + long maxChunk = -1L; + int maxDist = 0; + + // select the chunk from the not yet added set that has the largest minimum distance from + // the current set of added chunks + + for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) { + final long chunkKey = iterator.nextLong(); + final int chunkX = CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); + + int minDist = Integer.MAX_VALUE; + + final int len = added.size(); + final long[] addedArr = added.elements(); + Objects.checkFromToIndex(0, len, addedArr.length); + for (int i = 0; i < len; ++i) { + final long addedKey = addedArr[i]; + final int addedX = CoordinateUtils.getChunkX(addedKey); + final int addedZ = CoordinateUtils.getChunkZ(addedKey); + + // here we use square distance because chunk generation uses neighbours in a square radius + final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ)); + + minDist = Math.min(dist, minDist); + } + + if (minDist > maxDist) { + maxDist = minDist; + maxChunk = chunkKey; + } + } + + // move the selected chunk from the not added set to the added set + + if (!notAdded.remove(maxChunk)) { + throw new IllegalStateException(); + } + + added.addUnchecked(maxChunk); + } + + return added; + } + + private static void expandQuadrants(final CustomLongArray input, final int size) { + final int len = input.size(); + final long[] array = input.elements(); + + int writeIndex = size - 1; + for (int i = len - 1; i >= 0; --i) { + final long key = array[i]; + final int chunkX = CoordinateUtils.getChunkX(key); + final int chunkZ = CoordinateUtils.getChunkZ(key); + + if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) { + throw new IllegalStateException(); + } + + // Q4 + if (chunkZ != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ); + } + // Q3 + if (chunkX != 0 && chunkZ != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ); + } + // Q2 + if (chunkX != 0) { + array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ); + } + + array[writeIndex--] = key; + } + + input.forceSize(size); + + if (writeIndex != -1) { + throw new IllegalStateException(); + } + } + + private static long[] generateBFSOrder(final int radius) { + // by using only the first quadrant, we can reduce the total element size by 4 when spreading + final CustomLongArray[] byDistance = makeQ1BFS(radius); + + // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating + // this also means we are minimising locality + // but, we need to maintain sorted order by manhatten distance + + // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other + for (int i = 0, len = byDistance.length; i < len; ++i) { + final CustomLongArray points = byDistance[i]; + final int expectedSize = getDistanceSize(i, radius); + + final CustomLongArray spread = spread(points, expectedSize); + // add in Q2, Q3, Q4 + expandQuadrants(spread, expectedSize); + + byDistance[i] = spread; + } + + // now, rebuild the list so that it still maintains manhatten distance order + final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1)); + + for (final CustomLongArray dist : byDistance) { + ret.addAll(dist); + } + + return ret.elements(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java similarity index 73% rename from src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java rename to src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java index d4e9f99..ea6b6ed 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionEntityGetter.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java @@ -1,11 +1,11 @@ -package ca.spottedleaf.moonrise.patches.collisions.world; +package ca.spottedleaf.moonrise.patches.chunk_system.world; import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.AABB; import java.util.List; import java.util.function.Predicate; -public interface CollisionEntityGetter { +public interface ChunkSystemEntityGetter { public List moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate predicate); diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java index 7a53d82..8d2a2d5 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java @@ -1,11 +1,11 @@ package ca.spottedleaf.moonrise.patches.collisions; +import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter; import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState; -import ca.spottedleaf.moonrise.patches.collisions.entity.CollisionEntity; +import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape; -import ca.spottedleaf.moonrise.patches.collisions.world.CollisionEntityGetter; import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel; import ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevelChunkSection; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; @@ -1807,10 +1807,10 @@ public final class CollisionUtil { // specifically with boat collisions. aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); final List entities; - if (entity != null && ((CollisionEntity)entity).moonrise$isHardColliding()) { + if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) { entities = world.getEntities(entity, aabb, predicate); } else { - entities = ((CollisionEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); + entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); } for (int i = 0, len = entities.size(); i < len; ++i) { diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java deleted file mode 100644 index 95425ca..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/entity/CollisionEntity.java +++ /dev/null @@ -1,16 +0,0 @@ -package ca.spottedleaf.moonrise.patches.collisions.entity; - -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.monster.Shulker; -import net.minecraft.world.entity.vehicle.AbstractMinecart; -import net.minecraft.world.entity.vehicle.Boat; - -public interface CollisionEntity { - - public boolean moonrise$isHardColliding(); - - // for mods to override - public default boolean moonrise$isHardCollidingUncached() { - return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith(); - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java deleted file mode 100644 index 19db5d8..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/slices/EntityLookup.java +++ /dev/null @@ -1,545 +0,0 @@ -package ca.spottedleaf.moonrise.patches.collisions.slices; - -import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -import ca.spottedleaf.moonrise.common.util.WorldUtil; -import com.mojang.logging.LogUtils; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import net.minecraft.core.BlockPos; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; -import org.slf4j.Logger; - -import java.util.List; -import java.util.function.Predicate; - -public final class EntityLookup { - - private static final Logger LOGGER = LogUtils.getLogger(); - - protected static final int REGION_SHIFT = 5; - protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; - protected static final int REGION_SIZE = 1 << REGION_SHIFT; - - public final Level world; - protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(128, 0.5f); - - public EntityLookup(final Level world) { - this.world = world; - } - - public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); - } - } - } - } - } - - public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - chunk.getEntities(except, box, into, predicate); - } - } - } - } - } - - public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - chunk.getHardCollidingEntities(except, box, into, predicate); - } - } - } - } - } - - public void getEntities(final EntityType type, final AABB box, final List into, - final Predicate predicate) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - chunk.getEntities(type, box, (List)into, (Predicate)predicate); - } - } - } - } - } - - public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, - final Predicate predicate) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - chunk.getEntities(clazz, except, box, into, predicate); - } - } - } - } - } - - //////// Limited //////// - - public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate, - final int maxCount) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) { - return; - } - } - } - } - } - } - - public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate, - final int maxCount) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - if (chunk.getEntities(except, box, into, predicate, maxCount)) { - return; - } - } - } - } - } - } - - public void getEntities(final EntityType type, final AABB box, final List into, - final Predicate predicate, final int maxCount) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) { - return; - } - } - } - } - } - } - - public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, - final Predicate predicate, final int maxCount) { - final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; - final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; - final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; - final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; - - final int minRegionX = minChunkX >> REGION_SHIFT; - final int minRegionZ = minChunkZ >> REGION_SHIFT; - final int maxRegionX = maxChunkX >> REGION_SHIFT; - final int maxRegionZ = maxChunkZ >> REGION_SHIFT; - - for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { - final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; - final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; - - for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { - final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); - - if (region == null) { - continue; - } - - final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; - final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; - - for (int currZ = minZ; currZ <= maxZ; ++currZ) { - for (int currX = minX; currX <= maxX; ++currX) { - final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); - if (chunk == null || !chunk.sectionVisibility.isAccessible()) { - continue; - } - - if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) { - return; - } - } - } - } - } - } - - public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { - final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - if (region == null) { - return null; - } - - return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); - } - - public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { - final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - final int localIdx = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); - - ChunkEntitySlices ret; - if (region != null && (ret = region.get(localIdx)) != null) { - return ret; - } - - ret = new ChunkEntitySlices(this.world, chunkX, chunkZ); - this.addChunk(chunkX, chunkZ, ret); - return ret; - } - - public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { - return this.regions.get(CoordinateUtils.getChunkKey(regionX, regionZ)); - } - - private void removeChunk(final int chunkX, final int chunkZ) { - final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); - - final ChunkSlicesRegion region = this.regions.get(key); - final int remaining = region.remove(relIndex); - - if (remaining == 0) { - this.regions.remove(key); - } - } - - private void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { - final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); - - ChunkSlicesRegion region = this.regions.get(key); - if (region != null) { - region.add(relIndex, slices); - } else { - region = new ChunkSlicesRegion(); - region.add(relIndex, slices); - this.regions.put(key, region); - } - } - - public void addEntity(final Entity entity) { - final BlockPos pos = entity.blockPosition(); - final int sectionX = pos.getX() >> 4; - final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); - final int sectionZ = pos.getZ() >> 4; - - if (entity.isRemoved()) { - LOGGER.warn("Refusing to add removed entity: " + entity); - return; - } - - final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); - if (!slices.addEntity(entity, sectionY)) { - LOGGER.warn("Entity " + entity + " added to world, but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); - } - - return; - } - - public void moveEntity(final Entity entity, final long oldSection) { - final int minSection = WorldUtil.getMinSection(this.world); - final int maxSection = WorldUtil.getMaxSection(this.world); - - final int oldSectionX = CoordinateUtils.getChunkSectionX(oldSection); - final int oldSectionY = Mth.clamp(CoordinateUtils.getChunkSectionY(oldSection), minSection, maxSection); - final int oldSectionZ = CoordinateUtils.getChunkSectionZ(oldSection); - - final BlockPos newPos = entity.blockPosition(); - final int newSectionX = newPos.getX() >> 4; - final int newSectionY = Mth.clamp(newPos.getY() >> 4, minSection, maxSection); - final int newSectionZ = newPos.getZ() >> 4; - - final ChunkEntitySlices old = this.getChunk(oldSectionX, oldSectionZ); - final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); - - if (!old.removeEntity(entity, oldSectionY)) { - LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + oldSectionX + "," + oldSectionY + "," + oldSectionZ + ") since it was not contained in the section"); - } - - if (!slices.addEntity(entity, newSectionY)) { - LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); - } - } - - public void removeEntity(final Entity entity) { - final BlockPos newPos = entity.blockPosition(); - final int sectionX = newPos.getX() >> 4; - final int sectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); - final int sectionZ = newPos.getZ() >> 4; - - final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); - // all entities should be in a chunk - if (slices == null) { - LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); - } else { - if (!slices.removeEntity(entity, sectionY)) { - LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); - } - } - } - - public static final class ChunkSlicesRegion { - - protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; - protected int sliceCount; - - public ChunkEntitySlices get(final int index) { - return this.slices[index]; - } - - public int remove(final int index) { - final ChunkEntitySlices slices = this.slices[index]; - if (slices == null) { - throw new IllegalStateException(); - } - - this.slices[index] = null; - - return --this.sliceCount; - } - - public void add(final int index, final ChunkEntitySlices slices) { - final ChunkEntitySlices curr = this.slices[index]; - if (curr != null) { - throw new IllegalStateException(); - } - - this.slices[index] = slices; - - ++this.sliceCount; - } - } -} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java index 6b395f6..e851e81 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java @@ -1,13 +1,7 @@ package ca.spottedleaf.moonrise.patches.collisions.world; -import ca.spottedleaf.moonrise.patches.collisions.slices.EntityLookup; - public interface CollisionLevel { - public EntityLookup moonrise$getCollisionLookup(); - - // avoid name conflicts by appending mod name - public int moonrise$getMinSection(); public int moonrise$getMaxSection(); diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java index 2a50c47..382c9e4 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java @@ -1021,7 +1021,7 @@ public abstract class StarLightEngine { this.setNibbles(chunk, nibbles); for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { - lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ)); } // now do callback diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java index c1e26c0..2ce53ab 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java @@ -1,15 +1,21 @@ package ca.spottedleaf.moonrise.patches.starlight.light; +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; import ca.spottedleaf.moonrise.common.util.CoordinateUtils; import ca.spottedleaf.moonrise.common.util.WorldUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; -import ca.spottedleaf.moonrise.patches.starlight.world.StarlightWorld; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.shorts.ShortCollection; import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.FullChunkStatus; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.TicketType; import net.minecraft.world.level.ChunkPos; @@ -22,15 +28,20 @@ import net.minecraft.world.level.lighting.LayerLightEventListener; import net.minecraft.world.level.lighting.LevelLightEngine; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.IntConsumer; public final class StarLightInterface { - public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); + public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo); + public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT); + // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input + public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL; /** * Can be {@code null}, indicating the light is all empty. @@ -38,14 +49,14 @@ public final class StarLightInterface { public final Level world; public final LightChunkGetter lightAccess; - protected final ArrayDeque cachedSkyPropagators; - protected final ArrayDeque cachedBlockPropagators; + private final ArrayDeque cachedSkyPropagators; + private final ArrayDeque cachedBlockPropagators; - protected final LightQueue lightQueue = new LightQueue(this); + private final LightQueue lightQueue; - protected final LayerLightEventListener skyReader; - protected final LayerLightEventListener blockReader; - protected final boolean isClientSide; + private final LayerLightEventListener skyReader; + private final LayerLightEventListener blockReader; + private final boolean isClientSide; public final int minSection; public final int maxSection; @@ -74,6 +85,13 @@ public final class StarLightInterface { this.minLightSection = WorldUtil.getMinLightSection(this.world); this.maxLightSection = WorldUtil.getMaxLightSection(this.world); } + + if (this.world instanceof ServerLevel) { + this.lightQueue = new ServerLightQueue(this); + } else { + this.lightQueue = new ClientLightQueue(this); + } + this.lightEngine = lightEngine; this.hasBlockLight = hasBlockLight; this.hasSkyLight = hasSkyLight; @@ -184,6 +202,20 @@ public final class StarLightInterface { }; } + public ClientLightQueue getClientLightQueue() { + if (this.lightQueue instanceof ClientLightQueue clientLightQueue) { + return clientLightQueue; + } + return null; + } + + public ServerLightQueue getServerLightQueue() { + if (this.lightQueue instanceof ServerLightQueue serverLightQueue) { + return serverLightQueue; + } + return null; + } + public boolean hasSkyLight() { return this.hasSkyLight; } @@ -313,7 +345,7 @@ public final class StarLightInterface { // empty world return null; } - return ((StarlightWorld)this.world).starlight$getAnyChunkImmediately(chunkX, chunkZ); + return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); } public boolean hasUpdates() { @@ -490,179 +522,408 @@ public final class StarLightInterface { } } - public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { - final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); - - try { - if (skyEngine != null) { - skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); - } - } finally { - this.releaseSkyLightEngine(skyEngine); - } - } - - public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { - final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); - try { - if (blockEngine != null) { - blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); - } - } finally { - this.releaseBlockLightEngine(blockEngine); - } - } - - public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { - this.lightQueue.queueChunkLighting(pos, run); - } - - public void removeChunkTasks(final ChunkPos pos) { - this.lightQueue.removeChunk(pos); - } - public void propagateChanges() { - if (this.lightQueue.isEmpty()) { - return; + final LightQueue lightQueue = this.lightQueue; + if (lightQueue instanceof ClientLightQueue clientLightQueue) { + clientLightQueue.drainTasks(); + } // else: invalid usage, although we won't throw because mods... + } + + public static abstract class LightQueue { + + protected final StarLightInterface lightInterface; + + public LightQueue(final StarLightInterface lightInterface) { + this.lightInterface = lightInterface; } - LightQueue.ChunkTasks task; - while ((task = this.lightQueue.removeFirstTask()) != null) { - if (task.lightTasks != null) { - for (final Runnable run : task.lightTasks) { + public abstract boolean isEmpty(); + + public abstract ChunkTasks queueBlockChange(final BlockPos pos); + + public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue); + + public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections); + + public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections); + + public static abstract class ChunkTasks implements Runnable { + + public final long chunkCoordinate; + + protected final StarLightInterface lightEngine; + protected final LightQueue queue; + protected final MultiThreadedQueue onComplete = new MultiThreadedQueue<>(); + protected final Set changedPositions = new HashSet<>(); + protected Boolean[] changedSectionSet; + protected ShortOpenHashSet queuedEdgeChecksSky; + protected ShortOpenHashSet queuedEdgeChecksBlock; + protected List lightTasks; + + public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { + this.chunkCoordinate = chunkCoordinate; + this.lightEngine = lightEngine; + this.queue = queue; + } + + @Override + public abstract void run(); + + public void queueOrRunTask(final Runnable run) { + if (!this.onComplete.add(run)) { run.run(); } } - final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); - final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); - - try { - final long coordinate = task.chunkCoordinate; - final int chunkX = CoordinateUtils.getChunkX(coordinate); - final int chunkZ = CoordinateUtils.getChunkZ(coordinate); - - final Set positions = task.changedPositions; - final Boolean[] sectionChanges = task.changedSectionSet; - - if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { - skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); - } - if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { - blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); - } - - if (skyEngine != null && task.queuedEdgeChecksSky != null) { - skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); - } - if (blockEngine != null && task.queuedEdgeChecksBlock != null) { - blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); - } - } finally { - this.releaseSkyLightEngine(skyEngine); - this.releaseBlockLightEngine(blockEngine); + protected void addChangedPosition(final BlockPos pos) { + this.changedPositions.add(pos.immutable()); } - task.onComplete.complete(null); + protected void setChangedSection(final int y, final Boolean newEmptyValue) { + if (this.changedSectionSet == null) { + this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1]; + } + this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue; + } + + protected void addLightTask(final BooleanSupplier lightTask) { + if (this.lightTasks == null) { + this.lightTasks = new ArrayList<>(); + } + this.lightTasks.add(lightTask); + } + + protected void addEdgeChecksSky(final ShortCollection values) { + if (this.queuedEdgeChecksSky == null) { + this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size())); + } + this.queuedEdgeChecksSky.addAll(values); + } + + protected void addEdgeChecksBlock(final ShortCollection values) { + if (this.queuedEdgeChecksBlock == null) { + this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size())); + } + this.queuedEdgeChecksBlock.addAll(values); + } + + protected final void runTasks() { + boolean litChunk = false; + if (this.lightTasks != null) { + for (final BooleanSupplier run : this.lightTasks) { + if (run.getAsBoolean()) { + litChunk = true; + break; + } + } + } + + if (!litChunk) { + final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); + try { + final long coordinate = this.chunkCoordinate; + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + final Set positions = this.changedPositions; + final Boolean[] sectionChanges = this.changedSectionSet; + + if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); + } + + if (skyEngine != null && this.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); + } + if (blockEngine != null && this.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); + } + } finally { + this.lightEngine.releaseSkyLightEngine(skyEngine); + this.lightEngine.releaseBlockLightEngine(blockEngine); + } + } + + Runnable run; + while ((run = this.onComplete.pollOrBlockAdds()) != null) { + run.run(); + } + } } } - public static final class LightQueue { + public static final class ClientLightQueue extends LightQueue { - protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); - protected final StarLightInterface manager; + private final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); - public LightQueue(final StarLightInterface manager) { - this.manager = manager; + public ClientLightQueue(final StarLightInterface lightInterface) { + super(lightInterface); } + @Override public synchronized boolean isEmpty() { return this.chunkTasks.isEmpty(); } - public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) { - final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); - tasks.changedPositions.add(pos.immutable()); + // must hold synchronized lock on this object + private ClientChunkTasks getOrCreate(final long key) { + return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> { + return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this); + }); + } + + @Override + public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + tasks.addChangedPosition(pos); return tasks; } - public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { - final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + @Override + public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); - if (tasks.changedSectionSet == null) { - tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; - } - tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); + tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); return tasks; } - public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { - final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); - if (tasks.lightTasks == null) { - tasks.lightTasks = new ArrayList<>(); - } - tasks.lightTasks.add(lightTask); + @Override + public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); + + tasks.addEdgeChecksSky(sections); return tasks; } - public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { - final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + @Override + public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); - ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; - if (queuedEdges == null) { - queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); - } - queuedEdges.addAll(sections); + tasks.addEdgeChecksBlock(sections); return tasks; } - public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { - final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); - - ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; - if (queuedEdges == null) { - queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); - } - queuedEdges.addAll(sections); - - return tasks; - } - - public void removeChunk(final ChunkPos pos) { - final ChunkTasks tasks; - synchronized (this) { - tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); - } - if (tasks != null) { - tasks.onComplete.complete(null); - } - } - - public synchronized ChunkTasks removeFirstTask() { + public synchronized ClientChunkTasks removeFirstTask() { if (this.chunkTasks.isEmpty()) { return null; } return this.chunkTasks.removeFirst(); } - public static final class ChunkTasks { + public void drainTasks() { + ClientChunkTasks task; + while ((task = this.removeFirstTask()) != null) { + task.runTasks(); + } + } - public final Set changedPositions = new ObjectOpenHashSet<>(); - public Boolean[] changedSectionSet; - public ShortOpenHashSet queuedEdgeChecksSky; - public ShortOpenHashSet queuedEdgeChecksBlock; - public List lightTasks; + public static final class ClientChunkTasks extends ChunkTasks { - public boolean isTicketAdded = false; - public final CompletableFuture onComplete = new CompletableFuture<>(); + public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) { + super(chunkCoordinate, lightEngine, queue); + } - public final long chunkCoordinate; + @Override + public void run() { + this.runTasks(); + } + } + } - public ChunkTasks(final long chunkCoordinate) { - this.chunkCoordinate = chunkCoordinate; + public static final class ServerLightQueue extends LightQueue { + + private final ConcurrentLong2ReferenceChainedHashTable chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); + + public ServerLightQueue(final StarLightInterface lightInterface) { + super(lightInterface); + } + + public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.lowerPriority(priority); + } + } + + public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.setPriority(priority); + } + } + + public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + task.raisePriority(priority); + } + } + + public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { + final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (task != null) { + return task.getPriority(); + } + + return PrioritisedExecutor.Priority.COMPLETING; + } + + @Override + public boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + @Override + public ServerChunkTasks queueBlockChange(final BlockPos pos) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + valueInMap.addChangedPosition(pos); + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority + ); + } + + valueInMap.addLightTask(lightTask); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.addEdgeChecksSky(sections); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + @Override + public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { + if (valueInMap == null) { + valueInMap = new ServerChunkTasks( + keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this + ); + } + + valueInMap.addEdgeChecksBlock(sections); + + return valueInMap; + }); + + ret.schedule(); + + return ret; + } + + public static final class ServerChunkTasks extends ChunkTasks { + + private final AtomicBoolean ticketAdded = new AtomicBoolean(); + private final PrioritisedExecutor.PrioritisedTask task; + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, + final ServerLightQueue queue) { + this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); + } + + public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, + final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) { + super(chunkCoordinate, lightEngine, queue); + this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( + CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), + ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority + ); + } + + public boolean markTicketAdded() { + return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true); + } + + public void schedule() { + this.task.queue(); + } + + public boolean cancel() { + return this.task.cancel(); + } + + public PrioritisedExecutor.Priority getPriority() { + return this.task.getPriority(); + } + + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + this.task.lowerPriority(priority); + } + + public void setPriority(final PrioritisedExecutor.Priority priority) { + this.task.setPriority(priority); + } + + public void raisePriority(final PrioritisedExecutor.Priority priority) { + this.task.raisePriority(priority); + } + + @Override + public void run() { + ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this); + + this.runTasks(); } } } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java deleted file mode 100644 index 548af5d..0000000 --- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/world/StarlightWorld.java +++ /dev/null @@ -1,14 +0,0 @@ -package ca.spottedleaf.moonrise.patches.starlight.world; - -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.LevelChunk; - -public interface StarlightWorld { - - // rets full chunk without blocking - public LevelChunk starlight$getChunkAtImmediately(final int chunkX, final int chunkZ); - - // rets chunk at any stage, if it exists, immediately - public ChunkAccess starlight$getAnyChunkImmediately(final int chunkX, final int chunkZ); - -} diff --git a/src/main/resources/moonrise.accesswidener b/src/main/resources/moonrise.accesswidener index d8c3983..10364af 100644 --- a/src/main/resources/moonrise.accesswidener +++ b/src/main/resources/moonrise.accesswidener @@ -28,7 +28,7 @@ accessible class net/minecraft/world/level/chunk/PaletteResize # ChunkMap accessible field net/minecraft/server/level/ChunkMap level Lnet/minecraft/server/level/ServerLevel; accessible field net/minecraft/server/level/ChunkMap mainThreadExecutor Lnet/minecraft/util/thread/BlockableEventLoop; - +accessible method net/minecraft/server/level/ChunkMap postLoadProtoChunk (Lnet/minecraft/server/level/ServerLevel;Ljava/util/List;)V accessible method net/minecraft/server/level/ChunkMap getUpdatingChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; accessible method net/minecraft/server/level/ChunkMap getVisibleChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; accessible method net/minecraft/server/level/ChunkMap getChunkQueueLevel (J)Ljava/util/function/IntSupplier; @@ -41,6 +41,9 @@ mutable field net/minecraft/server/level/ChunkMap worldgenMailbox Lnet/minecraft mutable field net/minecraft/server/level/ChunkMap mainThreadMailbox Lnet/minecraft/util/thread/ProcessorHandle; accessible method net/minecraft/server/level/ChunkMap setServerViewDistance (I)V accessible method net/minecraft/server/level/ChunkMap upgradeChunkTag (Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag; +accessible field net/minecraft/server/level/ChunkMap generator Lnet/minecraft/world/level/chunk/ChunkGenerator; +accessible field net/minecraft/server/level/ChunkMap worldGenContext Lnet/minecraft/world/level/chunk/status/WorldGenContext; +accessible field net/minecraft/server/level/ChunkMap tickingGenerated Ljava/util/concurrent/atomic/AtomicInteger; # ChunkHolder @@ -52,12 +55,23 @@ mutable field net/minecraft/world/level/lighting/LevelLightEngine skyEngine Lnet # ThreadedLevelLightEngine accessible class net/minecraft/server/level/ThreadedLevelLightEngine$TaskType - +mutable field net/minecraft/server/level/ThreadedLevelLightEngine sorterMailbox Lnet/minecraft/util/thread/ProcessorHandle; +mutable field net/minecraft/server/level/ThreadedLevelLightEngine taskMailbox Lnet/minecraft/util/thread/ProcessorMailbox; # SectionStorage accessible field net/minecraft/world/level/chunk/storage/SectionStorage levelHeightAccessor Lnet/minecraft/world/level/LevelHeightAccessor; +mutable field net/minecraft/world/level/chunk/storage/SectionStorage simpleRegionStorage Lnet/minecraft/world/level/chunk/storage/SimpleRegionStorage; accessible method net/minecraft/world/level/chunk/storage/SectionStorage get (J)Ljava/util/Optional; accessible method net/minecraft/world/level/chunk/storage/SectionStorage getOrLoad (J)Ljava/util/Optional; +accessible method net/minecraft/world/level/chunk/storage/SectionStorage tryRead (Lnet/minecraft/world/level/ChunkPos;)Ljava/util/concurrent/CompletableFuture; +accessible method net/minecraft/world/level/chunk/storage/SectionStorage setDirty (J)V + +# SimpleRegionStorage +accessible field net/minecraft/world/level/chunk/storage/SimpleRegionStorage worker Lnet/minecraft/world/level/chunk/storage/IOWorker; + + +# IOWorker +accessible field net/minecraft/world/level/chunk/storage/IOWorker storage Lnet/minecraft/world/level/chunk/storage/RegionFileStorage; # PoiSection @@ -151,10 +165,16 @@ accessible class net/minecraft/world/level/chunk/status/ChunkStatus$GenerationTa accessible class net/minecraft/world/level/chunk/status/ChunkStatus$LoadingTask +# ChunkStatusTasks +accessible method net/minecraft/world/level/chunk/status/ChunkStatusTasks loadPassThrough (Lnet/minecraft/world/level/chunk/status/WorldGenContext;Lnet/minecraft/world/level/chunk/status/ChunkStatus;Lnet/minecraft/world/level/chunk/status/ToFullChunk;Lnet/minecraft/world/level/chunk/ChunkAccess;)Ljava/util/concurrent/CompletableFuture; + + # RegionFileStorage accessible method net/minecraft/world/level/chunk/storage/RegionFileStorage getRegionFile (Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/world/level/chunk/storage/RegionFile; extendable class net/minecraft/world/level/chunk/storage/RegionFileStorage accessible method net/minecraft/world/level/chunk/storage/RegionFileStorage (Lnet/minecraft/world/level/chunk/storage/RegionStorageInfo;Ljava/nio/file/Path;Z)V +accessible method net/minecraft/world/level/chunk/storage/RegionFileStorage write (Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/nbt/CompoundTag;)V + # ChunkMap.DistanceManager accessible class net/minecraft/server/level/ChunkMap$DistanceManager @@ -205,5 +225,32 @@ accessible class net/minecraft/server/level/ServerChunkCache$MainThreadExecutor mutable field net/minecraft/server/level/ServerLevel entityManager Lnet/minecraft/world/level/entity/PersistentEntitySectionManager; +# ServerLevel$EntityCallbacks +accessible class net/minecraft/server/level/ServerLevel$EntityCallbacks +accessible method net/minecraft/server/level/ServerLevel$EntityCallbacks (Lnet/minecraft/server/level/ServerLevel;)V + + # EntityStorage -accessible method net/minecraft/world/level/chunk/storage/EntityStorage readChunkPos (Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/world/level/ChunkPos; \ No newline at end of file +accessible method net/minecraft/world/level/chunk/storage/EntityStorage readChunkPos (Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/world/level/ChunkPos; +accessible method net/minecraft/world/level/chunk/storage/EntityStorage writeChunkPos (Lnet/minecraft/nbt/CompoundTag;Lnet/minecraft/world/level/ChunkPos;)V + +# Ticket +accessible method net/minecraft/server/level/Ticket (Lnet/minecraft/server/level/TicketType;ILjava/lang/Object;)V + + +# ChunkStorage +mutable field net/minecraft/world/level/chunk/storage/ChunkStorage worker Lnet/minecraft/world/level/chunk/storage/IOWorker; + + +# StructureCheck +mutable field net/minecraft/world/level/levelgen/structure/StructureCheck loadedChunks Lit/unimi/dsi/fastutil/longs/Long2ObjectMap; +mutable field net/minecraft/world/level/levelgen/structure/StructureCheck featureChecks Ljava/util/Map; + + +# ClientLevel +mutable field net/minecraft/client/multiplayer/ClientLevel entityStorage Lnet/minecraft/world/level/entity/TransientEntitySectionManager; + + +# ClientLevel$EntityCallbacks +accessible class net/minecraft/client/multiplayer/ClientLevel$EntityCallbacks +accessible method net/minecraft/client/multiplayer/ClientLevel$EntityCallbacks (Lnet/minecraft/client/multiplayer/ClientLevel;)V \ No newline at end of file diff --git a/src/main/resources/moonrise.mixins.json b/src/main/resources/moonrise.mixins.json index b05fa84..2fa10e2 100644 --- a/src/main/resources/moonrise.mixins.json +++ b/src/main/resources/moonrise.mixins.json @@ -14,6 +14,36 @@ "blockstate_propertyaccess.StateHolderMixin", "chunk_getblock.ChunkAccessMixin", "chunk_getblock.LevelChunkMixin", + "chunk_system.ChunkGeneratorMixin", + "chunk_system.ChunkHolderMixin", + "chunk_system.ChunkMap$DistanceManagerMixin", + "chunk_system.ChunkMapMixin", + "chunk_system.ChunkSerializerMixin", + "chunk_system.ChunkStatusMixin", + "chunk_system.ChunkStorageMixin", + "chunk_system.DistanceManagerMixin", + "chunk_system.EntityGetterMixin", + "chunk_system.EntityMixin", + "chunk_system.EntityTickListMixin", + "chunk_system.LevelChunkMixin", + "chunk_system.LevelChunkTicksMixin", + "chunk_system.LevelMixin", + "chunk_system.LevelReaderMixin", + "chunk_system.MinecraftServerMixin", + "chunk_system.NoiseBasedChunkGeneratorMixin", + "chunk_system.PlayerListMixin", + "chunk_system.PoiManagerMixin", + "chunk_system.PoiSectionMixin", + "chunk_system.RegionFileMixin", + "chunk_system.RegionFileStorageMixin", + "chunk_system.SectionStorageMixin", + "chunk_system.ServerChunkCache$MainThreadExecutorMixin", + "chunk_system.ServerChunkCacheMixin", + "chunk_system.ServerLevelMixin", + "chunk_system.ServerPlayerMixin", + "chunk_system.SortedArraySetMixin", + "chunk_system.StructureCheckMixin", + "chunk_system.TicketMixin", "collisions.ArmorStandMixin", "collisions.ArrayVoxelShapeMixin", "collisions.BitSetDiscreteVoxelShapeMixin", @@ -25,18 +55,14 @@ "collisions.DiscreteVoxelShapeMixin", "collisions.EntityGetterMixin", "collisions.EntityMixin", + "collisions.ExplosionMixin", "collisions.LevelChunkSectionMixin", "collisions.LevelMixin", "collisions.LivingEntityMixin", - "collisions.PersistentEntitySectionManagerCallbackMixin", - "collisions.PersistentEntitySectionManagerMixin", "collisions.ServerEntityMixin", "collisions.ShapesMixin", "collisions.SliceShapeMixin", - "collisions.TransientEntitySectionManagerCallbackMixin", - "collisions.TransientEntitySectionManagerMixin", "collisions.VoxelShapeMixin", - "collisions.ExplosionMixin", "farm_block.FarmBlockMixin", "fast_palette.CrudeIncrementalIntIdentityHashBiMapMixin", "fast_palette.HashMapPaletteMixin", @@ -60,21 +86,19 @@ "starlight.lightengine.LevelLightEngineMixin", "starlight.lightengine.ThreadedLevelLightEngineMixin", "starlight.world.ChunkSerializerMixin", - "starlight.world.LevelMixin", - "starlight.world.ServerWorldMixin", "starlight.world.WorldGenRegionMixin", "util_thread_counts.UtilMixin", "util_threading_detector.ThreadingDetectorMixin", "util_time_source.UtilMixin" ], "client": [ + "chunk_system.ClientLevelMixin", + "chunk_system.OptionsMixin", "collisions.LiquidBlockRendererMixin", "collisions.ParticleMixin", - "profiler.MinecraftMixin", "serverlist.ClientConnectionMixin", "serverlist.ServerSelectionListMixin", - "starlight.multiplayer.ClientPacketListenerMixin", - "starlight.world.ClientLevelMixin" + "starlight.multiplayer.ClientPacketListenerMixin" ], "injectors": { "defaultRequire": 1