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

Reimplement vanilla and eigencraft redstone cache

This commit is contained in:
Samsuik
2025-02-16 23:29:18 +00:00
parent 047821fb7d
commit 4fa7c85bce
22 changed files with 608 additions and 25 deletions

View File

@@ -14,7 +14,6 @@ import me.samsuik.sakura.physics.PhysicsVersion;
import net.minecraft.Util;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;

View File

@@ -165,7 +165,7 @@ public final class LocalConfigManager implements LocalStorageHandler {
public synchronized LocalValueConfig config(BlockPos position) {
long gameTime = this.level.getGameTime();
long ticks = this.expirationTick - gameTime;
long ticks = gameTime - this.expirationTick;
if (ticks >= CONFIG_CACHE_EXPIRATION / 3) {
this.chunkConfigCache.values().removeIf(pair -> pair.value().isExpired(gameTime));
this.expirationTick = gameTime;

View File

@@ -12,7 +12,6 @@ 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
@@ -26,7 +25,7 @@ public final class BlockChangeTracker {
this.level = level;
}
public long listenForChangesOnce(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions, Runnable callback) {
public long listenForChangesOnce(BlockChangeFilter filter, Set<BlockPos> positions, Runnable callback) {
LongConsumer singleUseCallback = (identifier) -> {
callback.run();
this.stopListening(identifier);
@@ -34,7 +33,7 @@ public final class BlockChangeTracker {
return this.listenForChanges(filter, positions, singleUseCallback);
}
public long listenForChanges(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions, LongConsumer callback) {
public long listenForChanges(BlockChangeFilter filter, Set<BlockPos> positions, LongConsumer callback) {
long identifier = this.identifier++;
Listener listener = new Listener(
filter, positions, identifier, callback
@@ -48,8 +47,10 @@ public final class BlockChangeTracker {
public void stopListening(long identifier) {
Listener listener = this.identifiersInUse.remove(identifier);
for (ChunkPos chunkPos : getChunkPositions(listener.positions())) {
this.removeListenerFronChunk(chunkPos, listener);
if (listener != null) {
for (ChunkPos chunkPos : getChunkPositions(listener.positions())) {
this.removeListenerFronChunk(chunkPos, listener);
}
}
}
@@ -88,14 +89,18 @@ public final class BlockChangeTracker {
return chunkPositions;
}
public record Listener(BiPredicate<BlockState, BlockState> filter, Set<BlockPos> positions,
public interface BlockChangeFilter {
boolean test(Level level, BlockPos pos, BlockState newBlock, 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(BlockPos pos, BlockState newBlock, BlockState oldBlock) {
return this.filter.test(newBlock, oldBlock) && this.positions.contains(pos);
public boolean test(Level level, BlockPos pos, BlockState newBlock, BlockState oldBlock) {
return this.filter.test(level, pos, newBlock, oldBlock) && this.positions.contains(pos);
}
}
}

View File

@@ -0,0 +1,185 @@
package me.samsuik.sakura.redstone;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.*;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.redstone.ExperimentalRedstoneUtils;
import net.minecraft.world.level.redstone.NeighborUpdater;
import net.minecraft.world.level.redstone.Orientation;
import org.jspecify.annotations.NullMarked;
import java.util.BitSet;
import java.util.List;
@NullMarked
public final class RedstoneNetwork {
private final List<RedstoneWireUpdate> wireUpdates;
private final List<BlockPos> updates;
private final Object2ObjectMap<BlockPos, RedstoneOriginalPower> originalWirePower;
private final LongArrayList listeners = new LongArrayList();
private final BitSet redundantUpdates = new BitSet();
private final TickExpiry expiry;
public RedstoneNetwork(List<RedstoneWireUpdate> wireUpdates, List<BlockPos> updates, Object2ObjectMap<BlockPos, RedstoneOriginalPower> originalWirePower, TickExpiry expiry) {
this.wireUpdates = new ObjectArrayList<>(wireUpdates);
this.updates = new ObjectArrayList<>(updates);
this.originalWirePower = new Object2ObjectOpenHashMap<>(originalWirePower);
this.expiry = expiry;
}
private static boolean hasRedstoneComponentChanged(Level level, BlockPos pos, BlockState newBlock, BlockState oldBlock) {
return newBlock.isRedstoneConductor(level, pos) != oldBlock.isRedstoneConductor(level, pos)
|| newBlock.isSignalSource() != oldBlock.isSignalSource();
}
private static boolean hasBlockChanged(Level level, BlockPos pos, BlockState newBlock, BlockState oldBlock) {
return newBlock.getBlock() != oldBlock.getBlock();
}
public TickExpiry getExpiry() {
return this.expiry;
}
private boolean isRegistered() {
return !this.listeners.isEmpty();
}
public boolean hasWire(BlockPos pos) {
return this.originalWirePower.containsKey(pos);
}
public void invalidate(Level level) {
for (long identifier : this.listeners) {
level.blockChangeTracker.stopListening(identifier);
}
this.listeners.clear();
}
private void markNeighboringWiresForShapeUpdates(BlockPos pos, Object2ObjectMap<BlockPos, RedstoneWireUpdate> wires) {
for (Direction direction : NeighborUpdater.UPDATE_ORDER) {
BlockPos neighborPos = pos.relative(direction);
RedstoneWireUpdate wireUpdate = wires.get(neighborPos);
//noinspection ConstantValue
if (wireUpdate != null) {
wireUpdate.updateShapes();
}
}
}
public boolean prepareAndRegister(Level level) {
Object2ObjectLinkedOpenHashMap<BlockPos, RedstoneWireUpdate> processedWires = new Object2ObjectLinkedOpenHashMap<>();
for (RedstoneWireUpdate wireUpdate : this.wireUpdates.reversed()) {
BlockPos wirePos = wireUpdate.getPosition();
//noinspection ConstantValue
if (processedWires.putAndMoveToFirst(wirePos, wireUpdate) == null) {
// It's possible for the block below the redstone to break while the network is updating
BlockState state = level.getBlockState(wirePos);
if (state.is(Blocks.PISTON_HEAD)) {
return false;
}
} else if (this.originalWirePower.get(wirePos).firstPower() != wireUpdate.getPower()) {
// Filter out wires updates that are not the first and last update
// This significantly reduces the amount of updates when unpowering
wireUpdate.skipWireUpdate();
}
}
for (int i = 0; i < this.updates.size(); ++i) {
BlockPos updatePos = this.updates.get(i);
BlockState state = level.getBlockState(updatePos);
// Filter out redundant neighbor updates
if (state.isAir() || state.liquid() || !state.isSpecialBlock() || processedWires.containsKey(updatePos)) {
this.redundantUpdates.set(i);
}
// Look for blocks that actually need shape updates
Block block = state.getBlock();
if (state.is(Blocks.OBSERVER) || state.liquid() || block instanceof FallingBlock || block instanceof LiquidBlockContainer) {
this.markNeighboringWiresForShapeUpdates(updatePos, processedWires);
}
}
this.addBlockListeners(level);
return true;
}
private void addBlockListeners(Level level) {
ObjectOpenHashSet<BlockPos> positions = new ObjectOpenHashSet<>(this.updates);
positions.addAll(this.originalWirePower.keySet());
// Register block change listeners
this.listeners.add(level.blockChangeTracker.listenForChangesOnce(
RedstoneNetwork::hasRedstoneComponentChanged, positions, () -> this.invalidate(level)
));
this.listeners.add(level.blockChangeTracker.listenForChangesOnce(
RedstoneNetwork::hasBlockChanged, positions, this.redundantUpdates::clear
));
}
private boolean verifyWiresInNetwork(Level level) {
for (Object2ObjectMap.Entry<BlockPos, RedstoneOriginalPower> wireEntry : this.originalWirePower.object2ObjectEntrySet()) {
BlockState state = level.getBlockState(wireEntry.getKey());
if (!state.is(Blocks.REDSTONE_WIRE)) {
this.invalidate(level);
return false;
}
if (state.getValue(RedStoneWireBlock.POWER) != wireEntry.getValue().originalPower()) {
return false;
}
}
return true;
}
private void performUpdates(Level level, Orientation orientation, RedStoneWireBlock wireBlock, int updateFrom, int updateTo) {
for (int updateIndex = updateFrom; updateIndex < updateTo; ++updateIndex) {
if (this.redundantUpdates.get(updateIndex)) {
continue;
}
BlockPos updatePos = this.updates.get(updateIndex);
level.getBlockState(updatePos).handleNeighborChanged(level, updatePos, wireBlock, orientation, false);
}
}
public boolean applyFromCache(Level level) {
this.expiry.refresh(level.getGameTime());
if (!this.isRegistered() || !this.verifyWiresInNetwork(level)) {
return false;
}
Orientation orientation = ExperimentalRedstoneUtils.initialOrientation(level, null, null);
RedStoneWireBlock wireBlock = (RedStoneWireBlock) Blocks.REDSTONE_WIRE;
int updateFrom = 0;
for (RedstoneWireUpdate wireUpdate : this.wireUpdates) {
if (wireUpdate.canSkipWireUpdate()) {
updateFrom = wireUpdate.getUpdateIndex();
continue;
}
int updateTo = wireUpdate.getUpdateIndex();
this.performUpdates(level, orientation, wireBlock, updateFrom, updateTo);
updateFrom = updateTo;
BlockPos wirePos = wireUpdate.getPosition();
BlockState state = level.getBlockState(wirePos);
BlockState newState = state.setValue(RedStoneWireBlock.POWER, wireUpdate.getPower());
if (level.setBlock(wirePos, newState, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE)) {
if (wireUpdate.needsShapeUpdate()) {
wireBlock.turbo.updateNeighborShapes(level, wirePos, newState);
}
}
}
this.performUpdates(level, orientation, wireBlock, updateFrom, this.updates.size());
return true;
}
}

View File

@@ -0,0 +1,22 @@
package me.samsuik.sakura.redstone;
import io.papermc.paper.configuration.WorldConfiguration;
import me.samsuik.sakura.configuration.local.LocalValueConfig;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.redstone.Orientation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public record RedstoneNetworkSource(WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation,
BlockPos position, @Nullable Orientation orientation,
int updateDepth, int newPower, int oldPower) {
public static RedstoneNetworkSource createNetworkSource(Level level, BlockPos pos, @Nullable Orientation orientation, int newPower, int oldPower) {
LocalValueConfig localConfig = level.localConfig().config(pos);
WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation = localConfig.redstoneImplementation;
int updateDepth = level.neighborUpdater.getUpdateDepth();
return new RedstoneNetworkSource(redstoneImplementation, pos, orientation, updateDepth, newPower, oldPower);
}
}

View File

@@ -0,0 +1,5 @@
package me.samsuik.sakura.redstone;
public record RedstoneOriginalPower(int originalPower, int firstPower) {
}

View File

@@ -0,0 +1,113 @@
package me.samsuik.sakura.redstone;
import it.unimi.dsi.fastutil.objects.*;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.redstone.NeighborUpdater;
import net.minecraft.world.level.redstone.Orientation;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
import java.util.Map;
@NullMarked
public final class RedstoneWireCache {
private final Map<RedstoneNetworkSource, RedstoneNetwork> networkCache = new Object2ObjectOpenHashMap<>();
private @Nullable RedstoneNetworkSource networkSource;
private final List<RedstoneWireUpdate> wireUpdates = new ObjectArrayList<>();
private final List<BlockPos> updates = new ObjectArrayList<>();
private final Object2ObjectMap<BlockPos, RedstoneOriginalPower> originalWirePower = new Object2ObjectOpenHashMap<>();
private final Level level;
private @Nullable RedstoneNetwork updatingNetwork;
public RedstoneWireCache(Level level) {
this.level = level;
}
public boolean isWireUpdating(BlockPos pos) {
return this.updatingNetwork != null && this.updatingNetwork.hasWire(pos);
}
private boolean isTrackingWireUpdates() {
return this.networkSource != null;
}
public boolean tryApplyFromCache(BlockPos pos, @Nullable Orientation orientation, int newPower, int oldPower) {
if (!this.isTrackingWireUpdates()) {
if (this.updatingNetwork != null) {
return true;
}
RedstoneNetworkSource networkSource = RedstoneNetworkSource.createNetworkSource(this.level, pos, orientation, newPower, oldPower);
RedstoneNetwork network = this.networkCache.get(networkSource);
if (network != null) {
try {
this.updatingNetwork = network;
return network.applyFromCache(this.level);
} finally {
this.updatingNetwork = null;
}
}
this.networkSource = networkSource;
}
return false;
}
public void trackWirePower(BlockPos pos, int newPower, int oldPower) {
if (this.isTrackingWireUpdates()) {
this.originalWirePower.putIfAbsent(pos, new RedstoneOriginalPower(oldPower, newPower));
this.wireUpdates.add(new RedstoneWireUpdate(pos, newPower, this.updates.size()));
}
}
public void trackNeighbor(BlockPos pos) {
if (this.isTrackingWireUpdates()) {
this.updates.add(pos);
}
}
public void trackNeighborsAt(BlockPos pos) {
if (this.isTrackingWireUpdates()) {
for (Direction neighbor : NeighborUpdater.UPDATE_ORDER) {
this.updates.add(pos.relative(neighbor));
}
}
}
public void expire(long tick) {
this.networkCache.values().removeIf(network -> {
if (network.getExpiry().isExpired(tick)) {
network.invalidate(this.level);
return true;
}
return false;
});
}
public void stopTracking() {
if (!this.isTrackingWireUpdates()) {
return;
}
if (!this.wireUpdates.isEmpty()) {
// Cache expires if it has not been used in 600 ticks
TickExpiry expiration = new TickExpiry(this.level.getGameTime(), 600);
RedstoneNetwork redstoneNetwork = new RedstoneNetwork(
this.wireUpdates, this.updates, this.originalWirePower, expiration
);
if (redstoneNetwork.prepareAndRegister(this.level)) {
this.networkCache.put(this.networkSource, redstoneNetwork);
}
}
this.wireUpdates.clear();
this.updates.clear();
this.originalWirePower.clear();
this.networkSource = null;
}
}

View File

@@ -0,0 +1,47 @@
package me.samsuik.sakura.redstone;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class RedstoneWireUpdate {
private final BlockPos position;
private final int power;
private final int updateIndex;
private boolean updateShape;
private boolean skipWire;
public RedstoneWireUpdate(BlockPos position, int power, int updateIndex) {
this.position = position;
this.power = power;
this.updateIndex = updateIndex;
}
public BlockPos getPosition() {
return this.position;
}
public int getPower() {
return this.power;
}
public int getUpdateIndex() {
return this.updateIndex;
}
public boolean needsShapeUpdate() {
return this.updateShape;
}
public void updateShapes() {
this.updateShape = true;
}
public boolean canSkipWireUpdate() {
return this.skipWire;
}
public void skipWireUpdate() {
this.skipWire = true;
}
}