9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-28 19:29:07 +00:00

Track block changes and level tick scheduler

This commit is contained in:
Samsuik
2025-02-09 17:55:44 +00:00
parent 3f1d6eb297
commit 2a182f3b5f
31 changed files with 300 additions and 87 deletions

View File

@@ -43,14 +43,12 @@ public final class EntityMergeHandler {
}
/**
* Called every tick and provided the current server tick to remove any unneeded merge history.
* Called every 200 ticks and the tick is used remove any unneeded merge history.
*
* @param tick server tick
*/
public void expire(int tick) {
if (tick % 200 == 0) {
this.trackedHistory.expire(tick);
}
public void expire(long tick) {
this.trackedHistory.expire(tick);
}
/**

View File

@@ -39,7 +39,7 @@ public final class TrackedMergeHistory {
return this.historyMap.get(originPosition);
}
public void expire(int tick) {
public void expire(long tick) {
this.historyMap.values().removeIf(p -> p.expiry().isExpired(tick));
}

View File

@@ -0,0 +1,101 @@
package me.samsuik.sakura.listener;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import org.jspecify.annotations.NullMarked;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.LongConsumer;
@NullMarked
public final class BlockChangeTracker {
private final Long2ObjectMap<List<Listener>> chunkListeners = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<Listener> identifiersInUse = new Long2ObjectOpenHashMap<>();
private final Level level;
private long identifier = Long.MIN_VALUE;
public BlockChangeTracker(Level level) {
this.level = level;
}
public long listenForChangesOnce(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions, Runnable callback) {
LongConsumer singleUseCallback = (identifier) -> {
callback.run();
this.stopListening(identifier);
};
return this.listenForChanges(filter, positions, singleUseCallback);
}
public long listenForChanges(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions, LongConsumer callback) {
long identifier = this.identifier++;
Listener listener = new Listener(
filter, positions, identifier, callback
);
for (ChunkPos chunkPos : getChunkPositions(positions)) {
this.addListenerToChunk(chunkPos, listener);
}
this.identifiersInUse.put(identifier, listener);
return identifier;
}
public void stopListening(long identifier) {
Listener listener = this.identifiersInUse.remove(identifier);
for (ChunkPos chunkPos : getChunkPositions(listener.positions())) {
this.removeListenerFronChunk(chunkPos, listener);
}
}
private void removeListenerFronChunk(ChunkPos chunkPos, Listener listener) {
long chunkKey = chunkPos.toLong();
List<Listener> listeners = this.chunkListeners.computeIfPresent(chunkKey, (k, present) -> {
present.remove(listener);
return present.isEmpty() ? null : present;
});
this.updateListeners(chunkPos, Objects.requireNonNullElse(listeners, Collections.emptyList()));
}
private void addListenerToChunk(ChunkPos chunkPos, Listener listener) {
long chunkKey = chunkPos.toLong();
List<Listener> listeners = this.chunkListeners.computeIfAbsent(chunkKey, i -> new ArrayList<>());
listeners.add(listener);
this.updateListeners(chunkPos, listeners);
}
private void updateListeners(ChunkPos chunkPos, List<Listener> listeners) {
LevelChunk chunk = ((ServerLevel) this.level).chunkSource.getChunkAtIfLoadedImmediately(chunkPos.x, chunkPos.z);
if (chunk != null) {
chunk.updateBlockChangeListeners(List.copyOf(listeners));
}
}
public List<Listener> getListenersForChunk(ChunkPos chunkPos) {
return this.chunkListeners.getOrDefault(chunkPos.toLong(), Collections.emptyList());
}
private static Set<ChunkPos> getChunkPositions(Set<BlockPos> positions) {
Set<ChunkPos> chunkPositions = new ObjectOpenHashSet<>();
for (BlockPos pos : positions) {
chunkPositions.add(new ChunkPos(pos));
}
return chunkPositions;
}
public record Listener(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions,
long identifier, LongConsumer callback) {
public void call() {
this.callback.accept(this.identifier);
}
public boolean test(BlockPos pos, BlockState newBlock, BlockState oldBlock) {
return this.filter.test(newBlock, oldBlock) && this.positions.contains(pos);
}
}
}

View File

@@ -0,0 +1,47 @@
package me.samsuik.sakura.listener;
import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.NullMarked;
import java.util.ArrayList;
import java.util.List;
@NullMarked
public final class LevelTickScheduler {
private final Int2ObjectMap<List<TickTask>> tickTasks = new Int2ObjectLinkedOpenHashMap<>();
private final Object2IntMap<TickTask> taskIntervals = new Object2IntOpenHashMap<>();
public void registerNewTask(Runnable runnable, int interval) {
this.registerNewTask(tick -> runnable.run(), interval);
}
public void registerNewTask(TickTask task, int interval) {
int safeInterval = Math.max(interval + 1, 1);
this.tickTasks.computeIfAbsent(safeInterval, i -> new ArrayList<>())
.add(task);
this.taskIntervals.put(task, Math.max(safeInterval + 1, 1));
}
private void runTasks(List<TickTask> tasks, long gameTime) {
for (TickTask tickTask : tasks) {
tickTask.run(gameTime);
}
}
public void levelTick(Level level) {
long gameTime = level.getGameTime();
for (int interval : this.tickTasks.keySet()) {
if (gameTime % interval == 0) {
this.runTasks(this.tickTasks.get(interval), gameTime);
}
}
}
public interface TickTask {
void run(long tick);
}
}