9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2026-01-04 15:31:43 +00:00

rework block state change tracker

This commit is contained in:
Samsuik
2025-11-14 15:28:19 +00:00
parent c84ccb873d
commit bb796a0f1e
17 changed files with 351 additions and 209 deletions

View File

@@ -17,7 +17,7 @@ index c6efa1eefd5d47ada2caddda8c9e542571397a15..4dfd1c9bdd43d748449ea941bdb59aa5
this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 0832a9edc81d6fbf87801dd6ab97c39c3ebbbea3..7cbb9af035092cf2764913c565a3a9398f09e747 100644
index 0832a9edc81d6fbf87801dd6ab97c39c3ebbbea3..6cefbd63f3390ef373c4de91313d54200b91022e 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -830,6 +830,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@@ -25,82 +25,44 @@ index 0832a9edc81d6fbf87801dd6ab97c39c3ebbbea3..7cbb9af035092cf2764913c565a3a939
}
// Paper end - optimise random ticking
+ // Sakura start - track block changes and tick scheduler
+ public final me.samsuik.sakura.listener.LevelTickScheduler levelTickScheduler = new me.samsuik.sakura.listener.LevelTickScheduler(this);
+ public final me.samsuik.sakura.listener.BlockChangeTracker blockChangeTracker = new me.samsuik.sakura.listener.BlockChangeTracker(this);
+ public final me.samsuik.sakura.scheduler.LevelTickScheduler levelTickScheduler = new me.samsuik.sakura.scheduler.LevelTickScheduler(this);
+ public final me.samsuik.sakura.block_change.BlockStateChangeTracker blockStateChangeTracker = new me.samsuik.sakura.block_change.BlockStateChangeTracker(this);
+ // Sakura end - track block changes and tick scheduler
protected Level(
WritableLevelData levelData,
diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
index de59760d27280555a334bda4f436164568cffbd6..47c11ffb876019167895b3b4d1a2e455285253b2 100644
index de59760d27280555a334bda4f436164568cffbd6..b3d7a39894a5c86898289f463c343a5547e04508 100644
--- a/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/net/minecraft/world/level/chunk/LevelChunk.java
@@ -136,6 +136,16 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
@@ -136,6 +136,13 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
return this.getBlockStateFinal(x, y, z);
}
// Paper end - get block chunk optimisation
+ // Sakura start - track block changes and tick scheduler
+ public final void updateBlockChangeListeners(final it.unimi.dsi.fastutil.ints.Int2ObjectMap<List<me.samsuik.sakura.listener.BlockChangeTracker.Listener>> chunkSectionListeners) {
+ chunkSectionListeners.forEach((pos, listeners) -> {
+ final int sectionIndex = this.getSectionIndexFromSectionY(pos);
+ if (sectionIndex >= 0 && sectionIndex < this.sections.length) {
+ this.sections[sectionIndex].setBlockChangeListeners(listeners);
+ }
+ });
+ private final me.samsuik.sakura.block_change.ChunkBlockStateWatcher blockStateWatcher = new me.samsuik.sakura.block_change.ChunkBlockStateWatcher();
+
+ public final me.samsuik.sakura.block_change.ChunkBlockStateWatcher getBlockStateWatcher() {
+ return this.blockStateWatcher;
+ }
+ // Sakura end - track block changes and tick scheduler
public LevelChunk(Level level, ChunkPos pos) {
this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null);
@@ -173,6 +183,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
@@ -173,6 +180,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
this.debug = !empty && this.level.isDebug();
this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE;
// Paper end - get block chunk optimisation
+ this.updateBlockChangeListeners(level.blockChangeTracker.getListenersForChunk(pos, this.minSection, this.maxSection)); // Sakura - track block changes and tick scheduler
+ this.blockStateWatcher.load(this); // Sakura - track block changes and tick scheduler
}
public LevelChunk(ServerLevel level, ProtoChunk chunk, @Nullable LevelChunk.PostLoadProcessor postLoad) {
@@ -423,6 +434,12 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
@@ -423,6 +431,8 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
if (!section.getBlockState(i, i1, i2).is(block)) {
return null;
} else {
+ // Sakura start - track block changes and tick scheduler
+ if (state.getBlock() != blockState.getBlock()) {
+ section.blockChange(this.level, pos, state, blockState);
+ }
+ // Sakura end - track block changes and tick scheduler
+ this.blockStateWatcher.blockChange(this.level, pos, state, blockState); // Sakura - track block changes and tick scheduler
+
if (!this.level.isClientSide() && (flags & 512) == 0 && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
state.onPlace(this.level, pos, blockState, flag1);
}
diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java
index d354fad5e9c9e4a16a52dbd8c7eb1f177a75f681..955f97ea45c7b980f0f2c4321d27050bec1eabe5 100644
--- a/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -43,6 +43,26 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
return this.tickingBlocks;
}
// Paper end - block counting
+ // Sakura start - track block changes and tick scheduler
+ private java.util.List<me.samsuik.sakura.listener.BlockChangeTracker.Listener> blockChangeListeners = java.util.List.of();
+
+ public void setBlockChangeListeners(final java.util.List<me.samsuik.sakura.listener.BlockChangeTracker.Listener> listeners) {
+ this.blockChangeListeners = listeners;
+ }
+
+ public void blockChange(
+ final net.minecraft.world.level.Level level,
+ final net.minecraft.core.BlockPos pos,
+ final BlockState newBlock,
+ final BlockState oldBlock
+ ) {
+ for (final me.samsuik.sakura.listener.BlockChangeTracker.Listener listener : this.blockChangeListeners) {
+ if (listener.test(level, pos, newBlock, oldBlock)) {
+ listener.call();
+ }
+ }
+ }
+ // Sakura end - track block changes and tick scheduler
private LevelChunkSection(LevelChunkSection section) {
this.nonEmptyBlockCount = section.nonEmptyBlockCount;

View File

@@ -280,12 +280,12 @@ index ef61333619d772a3acba6e2f6997655016b399f9..06e2f16032d615bf0083376ce3496941
@Nullable
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index c51231f40c31b153ac463e31656584e6c70d5a78..e7ca94521052d45bf42f614b9600f607dd9941fb 100644
index 02d3abf1ae8e822457febb611a4156398edf1685..294f9148a54c5e0bd00e2ebe3943f216f582ff00 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -834,6 +834,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public final me.samsuik.sakura.listener.LevelTickScheduler levelTickScheduler = new me.samsuik.sakura.listener.LevelTickScheduler(this);
public final me.samsuik.sakura.listener.BlockChangeTracker blockChangeTracker = new me.samsuik.sakura.listener.BlockChangeTracker(this);
public final me.samsuik.sakura.scheduler.LevelTickScheduler levelTickScheduler = new me.samsuik.sakura.scheduler.LevelTickScheduler(this);
public final me.samsuik.sakura.block_change.BlockStateChangeTracker blockStateChangeTracker = new me.samsuik.sakura.block_change.BlockStateChangeTracker(this);
// Sakura end - track block changes and tick scheduler
+ public final me.samsuik.sakura.entity.merge.EntityMergeHandler mergeHandler = new me.samsuik.sakura.entity.merge.EntityMergeHandler(); // Sakura - merge cannon entities

View File

@@ -17,11 +17,11 @@ index a42a0672d56e32baaa594c9c1b9db0e7487e7d59..60fc3ff0b7ed04a2f71d14658783d150
// Paper start
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index e7ca94521052d45bf42f614b9600f607dd9941fb..3abf59c6f672e71509bc41e9b30964cf9c2c7e6c 100644
index 294f9148a54c5e0bd00e2ebe3943f216f582ff00..d501e9cca821c20f6ce2366538d95b794c81ef00 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -835,6 +835,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public final me.samsuik.sakura.listener.BlockChangeTracker blockChangeTracker = new me.samsuik.sakura.listener.BlockChangeTracker(this);
public final me.samsuik.sakura.block_change.BlockStateChangeTracker blockStateChangeTracker = new me.samsuik.sakura.block_change.BlockStateChangeTracker(this);
// Sakura end - track block changes and tick scheduler
public final me.samsuik.sakura.entity.merge.EntityMergeHandler mergeHandler = new me.samsuik.sakura.entity.merge.EntityMergeHandler(); // Sakura - merge cannon entities
+ public final me.samsuik.sakura.explosion.density.BlockDensityCache densityCache = new me.samsuik.sakura.explosion.density.BlockDensityCache(this); // Sakura - optimise explosion density cache

View File

@@ -42,7 +42,7 @@ index 8e6f097b4d17aaaf8eccc16e11ce2bd01ad63322..4baa578a2d277676647ca60487a104f8
boolean isEmpty();
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index d86e523ba9a3f99337dcb11a56f0eaa04da95e0a..e6d6f98627884bd4ecb1266320bc745b34dea344 100644
index c0a1f3bbdeb5288dd4c49e00abeb45685103f744..70c192db173f91ca742a29737cf77d7efcafb9ad 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -1436,7 +1436,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@@ -287,10 +287,10 @@ index 28e3b73507b988f7234cbf29c4024c88180d0aef..a0d247aa883553708c4b921582324255
+ // Sakura end - optimise hopper ticking
}
diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
index 47c11ffb876019167895b3b4d1a2e455285253b2..0e3b46adf54a6d68ad5b809c92b5cc13a7cdbf57 100644
index b3d7a39894a5c86898289f463c343a5547e04508..7fb612a6e1f55c9428f6deddfcd1967888f8c55a 100644
--- a/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/net/minecraft/world/level/chunk/LevelChunk.java
@@ -1019,6 +1019,13 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
@@ -1012,6 +1012,13 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
return BlockEntityType.getKey(this.blockEntity.getType()).toString();
}
@@ -304,7 +304,7 @@ index 47c11ffb876019167895b3b4d1a2e455285253b2..0e3b46adf54a6d68ad5b809c92b5cc13
@Override
public String toString() {
return "Level ticker for " + this.getType() + "@" + this.getPos();
@@ -1067,6 +1074,13 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
@@ -1060,6 +1067,13 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot
return this.ticker.getType();
}

View File

@@ -42,7 +42,7 @@ index 5d95d8747c4816d27bab6455b3bdfb7b50ae265f..079e887cac1a903af23c1a9ace47b9e3
for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
final int blockY = currY | (currChunkY << 4);
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 45a50997c34562facb20c311d2d88a5fa4f4f670..025502d3b5e8d61fb048fe2859eeb613faa8b68b 100644
index 5de63d612b1c2b292e2cf3579ecea71a18fa8b96..b4612fb1d884b8bf43c9a64e36cb58e7c6dc60f3 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -622,6 +622,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@@ -79,13 +79,13 @@ index 45a50997c34562facb20c311d2d88a5fa4f4f670..025502d3b5e8d61fb048fe2859eeb613
for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
final int blockY = currY | (currChunkY << 4);
diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java
index 955f97ea45c7b980f0f2c4321d27050bec1eabe5..db0b34e0e6b132715dd81a185a3401d80ec40708 100644
index d354fad5e9c9e4a16a52dbd8c7eb1f177a75f681..0f6c7e63b9bd507f9e0f99986a0f9dd879b1baee 100644
--- a/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -63,6 +63,13 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
}
@@ -43,6 +43,13 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
return this.tickingBlocks;
}
// Sakura end - track block changes and tick scheduler
// Paper end - block counting
+ // Sakura start - optimise block counting for cannon entities
+ private short movingPistonBlocks;
+
@@ -96,7 +96,7 @@ index 955f97ea45c7b980f0f2c4321d27050bec1eabe5..db0b34e0e6b132715dd81a185a3401d8
private LevelChunkSection(LevelChunkSection section) {
this.nonEmptyBlockCount = section.nonEmptyBlockCount;
@@ -133,6 +140,18 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
@@ -113,6 +120,18 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
}
}
@@ -115,7 +115,7 @@ index 955f97ea45c7b980f0f2c4321d27050bec1eabe5..db0b34e0e6b132715dd81a185a3401d8
final boolean oldTicking = oldState.isRandomlyTicking();
final boolean newTicking = newState.isRandomlyTicking();
if (oldTicking != newTicking) {
@@ -208,6 +227,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
@@ -188,6 +207,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
this.tickingBlockCount = (short)0;
this.tickingFluidCount = (short)0;
this.specialCollidingBlocks = (short)0;
@@ -123,7 +123,7 @@ index 955f97ea45c7b980f0f2c4321d27050bec1eabe5..db0b34e0e6b132715dd81a185a3401d8
this.tickingBlocks.clear();
if (this.maybeHas((final BlockState state) -> !state.isAir())) {
@@ -236,6 +256,12 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
@@ -216,6 +236,12 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
continue;
}

View File

@@ -0,0 +1,39 @@
package me.samsuik.sakura.block_change;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jspecify.annotations.NullMarked;
@NullMarked
public interface BlockFilter {
BlockFilter ANY = (level, pos, newBlock, oldBlock) -> true;
BlockFilter BLOCK_TYPE = (level, pos, newBlock, oldBlock) -> !oldBlock.is(newBlock.getBlock());
BlockFilter MOVING_BLOCK = (level, pos, newBlock, oldBlock) -> newBlock.is(Blocks.MOVING_PISTON);
BlockFilter COLLISION = (level, pos, newBlock, oldBlock) -> {
if (oldBlock.is(newBlock.getBlock())) {
return false;
}
final VoxelShape oldShape = oldBlock.getCollisionShape(level, pos);
final VoxelShape newShape = newBlock.getCollisionShape(level, pos);
return !Shapes.equal(oldShape, newShape);
};
BlockFilter REDSTONE_COMPONENT = (level, pos, oldBlock, newBlock) -> {
if (oldBlock.is(newBlock.getBlock())) {
return false;
}
return newBlock.isRedstoneConductor(level, pos) != oldBlock.isRedstoneConductor(level, pos)
|| newBlock.isSignalSource() != oldBlock.isSignalSource();
};
boolean test(final Level level, final BlockPos pos, final BlockState newBlock, final BlockState oldBlock);
}

View File

@@ -0,0 +1,102 @@
package me.samsuik.sakura.block_change;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
import me.samsuik.sakura.block_change.callback.BlockChangeCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.LevelChunk;
import org.jspecify.annotations.NullMarked;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.LongConsumer;
@SuppressWarnings("ConstantValue")
@NullMarked
public final class BlockStateChangeTracker {
private final Long2ObjectMap<ListeningToPositions> identifiersInUse = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<LongOpenHashSet> chunkAssociatedIdentifiers = new Long2ObjectOpenHashMap<>();
private final Level level;
private long identifier = Long.MIN_VALUE;
public BlockStateChangeTracker(final Level level) {
this.level = level;
}
public long firstChange(final Set<BlockPos> positions, final BlockFilter filter, final Runnable callback) {
final LongConsumer removeAfterUse = identifier -> {
callback.run();
this.stopListening(identifier);
};
return this.listenForChanges(positions, filter, BlockChangeCallback.identifier(removeAfterUse));
}
public long allChanges(final Set<BlockPos> positions, final BlockFilter filter, final LongConsumer callback) {
return this.listenForChanges(positions, filter, BlockChangeCallback.identifier(callback));
}
public long listenForChanges(final Set<BlockPos> positions, final BlockFilter filter, final BlockChangeCallback callback) {
final ObjectImmutableList<BlockPos> immutablePositions = new ObjectImmutableList<>(positions);
final long identifier = this.identifier++;
final ListeningToPositions listener = new ListeningToPositions(immutablePositions, identifier, filter, callback);
for (final BlockPos pos : immutablePositions) {
final long chunkKey = ChunkPos.asLong(pos);
final LongOpenHashSet chunkIdentifiers = this.chunkAssociatedIdentifiers.computeIfAbsent(chunkKey, k -> new LongOpenHashSet());
if (!chunkIdentifiers.add(identifier)) {
continue;
}
final LevelChunk chunk = this.level.getChunkIfLoaded(pos);
if (chunk != null) {
chunk.getBlockStateWatcher().startListening(listener);
}
}
this.identifiersInUse.put(identifier, listener);
return identifier;
}
public void stopListening(final long identifier) {
final ListeningToPositions listener = this.identifiersInUse.remove(identifier);
if (listener == null) {
return;
}
for (final BlockPos pos : listener.positions()) {
final long chunkKey = ChunkPos.asLong(pos);
final LongOpenHashSet chunkIdentifiers = this.chunkAssociatedIdentifiers.get(chunkKey);
if (chunkIdentifiers == null || !chunkIdentifiers.remove(identifier)) {
continue;
}
final LevelChunk chunk = this.level.getChunkIfLoaded(pos);
if (chunk != null) {
chunk.getBlockStateWatcher().stopListening(listener);
}
}
}
public List<ListeningToPositions> getListenersAtChunk(final LevelChunk chunk) {
final LongOpenHashSet identifiers = this.chunkAssociatedIdentifiers.get(chunk.coordinateKey);
if (identifiers == null) {
return Collections.emptyList();
}
final List<ListeningToPositions> listeners = new ArrayList<>();
for (final long identifier : identifiers) {
listeners.add(this.identifiersInUse.get(identifier));
}
return listeners;
}
}

View File

@@ -0,0 +1,52 @@
package me.samsuik.sakura.block_change;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
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.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@NullMarked
public final class ChunkBlockStateWatcher {
private final Map<BlockPos, List<ListeningToPositions>> positionListeners = new Object2ObjectOpenHashMap<>();
public void blockChange(
final Level level,
final BlockPos pos,
final BlockState newBlock,
final BlockState oldBlock
) {
final List<ListeningToPositions> changes = this.positionListeners.get(pos);
if (changes != null) {
for (final ListeningToPositions change : changes) {
change.onChange(level, pos, newBlock, oldBlock);
}
}
}
public void load(final LevelChunk chunk) {
for (final ListeningToPositions listener : chunk.level.blockStateChangeTracker.getListenersAtChunk(chunk)) {
this.startListening(listener);
}
}
public void startListening(final ListeningToPositions listener) {
for (final BlockPos pos : listener.positions()) {
this.positionListeners.computeIfAbsent(pos, p -> new CopyOnWriteArrayList<>()).add(listener);
}
}
public void stopListening(final ListeningToPositions listener) {
for (final BlockPos pos : listener.positions()) {
final List<ListeningToPositions> listeners = this.positionListeners.get(pos);
if (listeners != null) {
listeners.remove(listener);
}
}
}
}

View File

@@ -0,0 +1,22 @@
package me.samsuik.sakura.block_change;
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
import me.samsuik.sakura.block_change.callback.BlockChangeCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.NullMarked;
@NullMarked
public record ListeningToPositions(
ObjectImmutableList<BlockPos> positions,
long identifier,
BlockFilter filter,
BlockChangeCallback callback
) {
public void onChange(final Level level, final BlockPos pos, final BlockState newBlock, final BlockState oldBlock) {
if (this.filter.test(level, pos, newBlock, oldBlock)) {
this.callback.call(pos, newBlock, oldBlock, this.identifier);
}
}
}

View File

@@ -0,0 +1,16 @@
package me.samsuik.sakura.block_change.callback;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.NullMarked;
import java.util.function.LongConsumer;
@NullMarked
public interface BlockChangeCallback {
static BlockChangeCallback identifier(final LongConsumer identifierConsumer) {
return (pos, newBlock, oldBlock, identifier) -> identifierConsumer.accept(identifier);
}
void call(final BlockPos pos, final BlockState newBlock, final BlockState oldBlock, final long identifier);
}

View File

@@ -0,0 +1,35 @@
package me.samsuik.sakura.block_change.callback;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.NullMarked;
import java.util.function.LongConsumer;
@NullMarked
public final class CallbackAfterChanges implements BlockChangeCallback {
private final int count;
private int changes = 0;
private final LongConsumer callback;
public CallbackAfterChanges(final int count, final LongConsumer callback) {
this.count = count;
this.callback = callback;
}
public void reset() {
this.changes = 0;
}
@Override
public void call(
final BlockPos pos,
final BlockState newBlock,
final BlockState oldBlock,
final long identifier
) {
if (++this.changes >= this.count) {
this.callback.accept(identifier);
}
}
}

View File

@@ -0,0 +1,37 @@
package me.samsuik.sakura.block_change.callback;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.NullMarked;
import java.util.HashMap;
import java.util.Map;
@NullMarked
public final class TrackModifiedStates implements BlockChangeCallback {
private final Map<BlockPos, BlockState> expectedStates = new HashMap<>();
private int count = 0;
public TrackModifiedStates(final Map<BlockPos, BlockState> states) {
this.expectedStates.putAll(states);
}
public boolean isModified() {
return this.count != 0;
}
@Override
public void call(
final BlockPos pos,
final BlockState newBlock,
final BlockState oldBlock,
final long identifier
) {
final BlockState expectedState = this.expectedStates.get(pos);
if (expectedState == newBlock) {
this.count--;
} else if (expectedState == oldBlock) {
this.count++;
}
}
}

View File

@@ -1,130 +0,0 @@
package me.samsuik.sakura.listener;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
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.LongConsumer;
@NullMarked
public final class BlockChangeTracker {
private final Long2ObjectMap<List<Listener>> chunkSectionListeners = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<Listener> identifiersInUse = new Long2ObjectOpenHashMap<>();
private final Level level;
private long identifier = Long.MIN_VALUE;
public BlockChangeTracker(final Level level) {
this.level = level;
}
public long listenForChangesOnce(final BlockChangeFilter filter, final Set<BlockPos> positions, final Runnable callback) {
final LongConsumer singleUseCallback = identifier -> {
callback.run();
this.stopListening(identifier);
};
return this.listenForChanges(filter, positions, singleUseCallback);
}
public long listenForChanges(final BlockChangeFilter filter, final Set<BlockPos> positions, final LongConsumer callback) {
final long identifier = this.identifier++;
final Listener listener = new Listener(filter, positions, identifier, callback);
for (final long sectionPos : getSectionPositions(positions)) {
this.addListenerToSection(sectionPos, listener);
}
this.identifiersInUse.put(identifier, listener);
return identifier;
}
public void stopListening(final long identifier) {
final Listener listener = this.identifiersInUse.remove(identifier);
//noinspection ConstantValue
if (listener != null) {
for (final long sectionPos : getSectionPositions(listener.positions())) {
this.removeListenerFromSection(sectionPos, listener);
}
}
}
private static LongOpenHashSet getSectionPositions(final Set<BlockPos> positions) {
final LongOpenHashSet sections = new LongOpenHashSet();
for (final BlockPos pos : positions) {
sections.add(SectionPos.asLong(pos));
}
return sections;
}
private void removeListenerFromSection(final long sectionPos, final Listener listener) {
final List<Listener> listeners = this.chunkSectionListeners.computeIfPresent(sectionPos, (k, present) -> {
present.remove(listener);
return present.isEmpty() ? null : present;
});
this.updateListeners(sectionPos, Objects.requireNonNullElse(listeners, Collections.emptyList()));
}
private void addListenerToSection(final long sectionPos, final Listener listener) {
final List<Listener> listeners = this.chunkSectionListeners.computeIfAbsent(sectionPos, k -> new ArrayList<>());
listeners.add(listener);
this.updateListeners(sectionPos, listeners);
}
private void updateListeners(final long sectionPos, final List<Listener> listeners) {
final int chunkX = SectionPos.x(sectionPos);
final int chunkZ = SectionPos.x(sectionPos);
final LevelChunk chunk = ((ServerLevel) this.level).chunkSource.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
if (chunk != null) {
final Int2ObjectMap<List<Listener>> sectionListeners = Int2ObjectMaps.singleton(
SectionPos.y(sectionPos),
List.copyOf(listeners)
);
chunk.updateBlockChangeListeners(sectionListeners);
}
}
public Int2ObjectMap<List<Listener>> getListenersForChunk(final ChunkPos chunkPos, final int minSection, final int maxSection) {
final Int2ObjectOpenHashMap<List<Listener>> sectionListeners = new Int2ObjectOpenHashMap<>();
for (int sectionY = minSection; sectionY <= maxSection; ++sectionY) {
final long sectionPos = SectionPos.asLong(chunkPos.x, sectionY, chunkPos.z);
final List<Listener> listeners = this.chunkSectionListeners.getOrDefault(sectionPos, Collections.emptyList());
sectionListeners.put(sectionY, List.copyOf(listeners));
}
return sectionListeners;
}
public interface BlockChangeFilter {
BlockChangeFilter ANY = (l, p, n, o) -> true;
BlockChangeFilter REDSTONE_COMPONENT = (level, pos, oldBlock, newBlock) -> {
return newBlock.isRedstoneConductor(level, pos) != oldBlock.isRedstoneConductor(level, pos)
|| newBlock.isSignalSource() != oldBlock.isSignalSource();
};
boolean test(final Level level, final BlockPos pos, final BlockState newBlock, final BlockState oldBlock);
}
public record Listener(BlockChangeFilter filter, Set<BlockPos> positions, long identifier, LongConsumer callback) {
public void call() {
this.callback.accept(this.identifier);
}
public boolean test(final Level level, final BlockPos pos, final BlockState newBlock, final BlockState oldBlock) {
return this.filter.test(level, pos, newBlock, oldBlock)
&& this.positions.contains(pos);
}
}
}

View File

@@ -2,7 +2,8 @@ package me.samsuik.sakura.redstone.cache;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.*;
import me.samsuik.sakura.listener.BlockChangeTracker;
import me.samsuik.sakura.block_change.BlockFilter;
import me.samsuik.sakura.block_change.BlockStateChangeTracker;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -57,9 +58,9 @@ public final class RedstoneNetwork {
return this.originalWirePower.containsKey(pos);
}
public void invalidate(final Level level) {
public void invalidate(final BlockStateChangeTracker tracker) {
for (final long identifier : this.listeners) {
level.blockChangeTracker.stopListening(identifier);
tracker.stopListening(identifier);
}
this.listeners.clear();
}
@@ -134,17 +135,22 @@ public final class RedstoneNetwork {
}
private void addBlockListeners(final Level level) {
ObjectOpenHashSet<BlockPos> positions = new ObjectOpenHashSet<>(this.neighborUpdates);
final ObjectOpenHashSet<BlockPos> positions = new ObjectOpenHashSet<>(this.neighborUpdates);
positions.addAll(this.originalWirePower.keySet());
positions.remove(null);
// Register block change listeners
this.listeners.add(level.blockChangeTracker.listenForChangesOnce(
BlockChangeTracker.BlockChangeFilter.REDSTONE_COMPONENT, positions, () -> this.invalidate(level)
final BlockStateChangeTracker tracker = level.blockStateChangeTracker;
this.listeners.add(tracker.firstChange(
positions,
BlockFilter.REDSTONE_COMPONENT,
() -> this.invalidate(tracker)
));
this.listeners.add(level.blockChangeTracker.listenForChangesOnce(
BlockChangeTracker.BlockChangeFilter.ANY, positions, this::allowRedundantNeighborUpdates
this.listeners.add(tracker.firstChange(
positions,
BlockFilter.BLOCK_TYPE,
this::allowRedundantNeighborUpdates
));
}
@@ -152,7 +158,7 @@ public final class RedstoneNetwork {
for (final Object2ObjectMap.Entry<BlockPos, RedstoneOriginalPower> wireEntry : this.originalWirePower.object2ObjectEntrySet()) {
final BlockState state = level.getBlockState(wireEntry.getKey());
if (!state.is(Blocks.REDSTONE_WIRE)) {
this.invalidate(level);
this.invalidate(level.blockStateChangeTracker);
return false;
}

View File

@@ -118,7 +118,7 @@ public final class RedstoneWireCache {
public void expire(final long tick) {
this.networkCache.values().removeIf(network -> {
if (network.getExpiry().isExpired(tick)) {
network.invalidate(this.level);
network.invalidate(this.level.blockStateChangeTracker);
return true;
}

View File

@@ -1,4 +1,4 @@
package me.samsuik.sakura.listener;
package me.samsuik.sakura.scheduler;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;