diff --git a/src/main/java/net/earthcomputer/clientcommands/command/FindBlockCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/FindBlockCommand.java index 94ad5560..1ba0ccdb 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/FindBlockCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/FindBlockCommand.java @@ -2,6 +2,7 @@ import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.earthcomputer.clientcommands.command.arguments.ClientBlockPredicateArgument; import net.earthcomputer.clientcommands.task.RenderDistanceScanTask; @@ -26,18 +27,22 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; public class FindBlockCommand { + public static final Flag FLAG_KEEP_SEARCHING = Flag.ofFlag("keep-searching").build(); + public static void register(CommandDispatcher dispatcher, CommandBuildContext context) { - dispatcher.register(literal("cfindblock") + var cfindblock = dispatcher.register(literal("cfindblock") .then(argument("block", withString(blockPredicate(context))) .executes(ctx -> { var blockWithString = getWithString(ctx, "block", ClientBlockPredicateArgument.ParseResult.class); - return findBlock(Component.translatable("commands.cfindblock.starting", blockWithString.string()), getBlockPredicate(blockWithString.value())); + return findBlock(ctx, Component.translatable("commands.cfindblock.starting", blockWithString.string()), getBlockPredicate(blockWithString.value())); }))); + FLAG_KEEP_SEARCHING.addToCommand(dispatcher, cfindblock, ctx -> true); } - public static int findBlock(Component startingMessage, ClientBlockPredicate block) throws CommandSyntaxException { + public static int findBlock(CommandContext ctx, Component startingMessage, ClientBlockPredicate block) throws CommandSyntaxException { + boolean keepSearching = getFlag(ctx, FLAG_KEEP_SEARCHING); sendFeedback(startingMessage); - TaskManager.addTask("cfindblock", new FindBlockTask(block)); + TaskManager.addTask("cfindblock", new FindBlockTask(block, keepSearching)); return Command.SINGLE_SUCCESS; } @@ -47,8 +52,8 @@ private static final class FindBlockTask extends RenderDistanceScanTask { @Nullable private BlockPos closestBlock; - FindBlockTask(ClientBlockPredicate predicate) { - super(false); + FindBlockTask(ClientBlockPredicate predicate, boolean keepSearching) { + super(keepSearching); this.predicate = predicate; } @@ -59,6 +64,7 @@ protected void scanBlock(Entity cameraEntity, BlockPos pos) throws CommandSyntax Vec3 cameraPos = cameraEntity.getEyePosition(0); if ((closestBlock == null || pos.distToCenterSqr(cameraPos) < closestBlock.distToCenterSqr(cameraPos)) && predicate.test(level.registryAccess(), level, pos)) { closestBlock = pos.immutable(); + keepSearching = false; } } @@ -75,6 +81,7 @@ protected boolean canScanChunkSection(Entity cameraEntity, SectionPos pos) { @Override public void onCompleted() { + super.onCompleted(); if (closestBlock == null) { sendError(Component.translatable("commands.cfindblock.notFound")); } else { diff --git a/src/main/java/net/earthcomputer/clientcommands/command/SignSearchCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/SignSearchCommand.java index 37ea1e9c..af08775e 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/SignSearchCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/SignSearchCommand.java @@ -17,7 +17,6 @@ import net.minecraft.world.level.block.entity.SignText; import net.minecraft.world.level.block.state.BlockState; -import java.util.function.Predicate; import java.util.regex.Pattern; import static com.mojang.brigadier.arguments.StringArgumentType.*; @@ -25,15 +24,15 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; public class SignSearchCommand { - public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal("csignsearch") + var csignsearch = dispatcher.register(literal("csignsearch") .then(literal("text") .then(argument("query", greedyString()) - .executes(ctx -> FindBlockCommand.findBlock(Component.translatable("commands.csignsearch.starting"), predicate(getString(ctx, "query")))))) + .executes(ctx -> FindBlockCommand.findBlock(ctx, Component.translatable("commands.csignsearch.starting"), predicate(getString(ctx, "query")))))) .then(literal("regex") .then(argument("query", greedyRegex()) - .executes(ctx -> FindBlockCommand.findBlock(Component.translatable("commands.csignsearch.starting"), predicate(getRegex(ctx, "query"))))))); + .executes(ctx -> FindBlockCommand.findBlock(ctx, Component.translatable("commands.csignsearch.starting"), predicate(getRegex(ctx, "query"))))))); + FindBlockCommand.FLAG_KEEP_SEARCHING.addToCommand(dispatcher, csignsearch, ctx -> true); } private static ClientBlockPredicateArgument.ClientBlockPredicate predicate(String query) { diff --git a/src/main/java/net/earthcomputer/clientcommands/event/ClientLevelEvents.java b/src/main/java/net/earthcomputer/clientcommands/event/ClientLevelEvents.java index aed8cc23..cd667552 100644 --- a/src/main/java/net/earthcomputer/clientcommands/event/ClientLevelEvents.java +++ b/src/main/java/net/earthcomputer/clientcommands/event/ClientLevelEvents.java @@ -3,6 +3,10 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; public final class ClientLevelEvents { public static final Event LOAD_LEVEL = EventFactory.createArrayBacked(LoadLevel.class, listeners -> level -> { @@ -17,6 +21,24 @@ public final class ClientLevelEvents { } }); + public static final Event CHUNK_UPDATE = EventFactory.createArrayBacked(ChunkUpdate.class, listeners -> (level, pos, oldState, newState) -> { + for (ChunkUpdate listener : listeners) { + listener.onBlockStateUpdate(level, pos, oldState, newState); + } + }); + + public static final Event LOAD_CHUNK = EventFactory.createArrayBacked(LoadChunk.class, listeners -> (level, pos) -> { + for (LoadChunk listener : listeners) { + listener.onLoadChunk(level, pos); + } + }); + + public static final Event UNLOAD_CHUNK = EventFactory.createArrayBacked(UnloadChunk.class, listeners -> (level, pos) -> { + for (UnloadChunk listener : listeners) { + listener.onUnloadChunk(level, pos); + } + }); + @FunctionalInterface public interface LoadLevel { void onLoadLevel(ClientLevel level); @@ -26,4 +48,19 @@ public interface LoadLevel { public interface UnloadLevel { void onUnloadLevel(boolean isDisconnect); } + + @FunctionalInterface + public interface ChunkUpdate { + void onBlockStateUpdate(ClientLevel level, BlockPos pos, BlockState oldState, BlockState newState); + } + + @FunctionalInterface + public interface LoadChunk { + void onLoadChunk(ClientLevel level, ChunkPos chunkPos); + } + + @FunctionalInterface + public interface UnloadChunk { + void onUnloadChunk(ClientLevel level, ChunkPos chunkPos); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/ClientLevelMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/ClientLevelMixin.java new file mode 100644 index 00000000..6811fb71 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/ClientLevelMixin.java @@ -0,0 +1,23 @@ +package net.earthcomputer.clientcommands.mixin.commands.findblock; + +import net.earthcomputer.clientcommands.event.ClientLevelEvents; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunk; +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(ClientLevel.class) +public class ClientLevelMixin { + @Inject(method = "unload", at = @At("HEAD")) + private void onChunkUnload(LevelChunk chunk, CallbackInfo ci) { + ClientLevelEvents.UNLOAD_CHUNK.invoker().onUnloadChunk((ClientLevel) (Object) this, chunk.getPos()); + } + + @Inject(method = "onChunkLoaded", at = @At("HEAD")) + private void onChunkLoad(ChunkPos chunkPos, CallbackInfo ci) { + ClientLevelEvents.LOAD_CHUNK.invoker().onLoadChunk((ClientLevel) (Object) this, chunkPos); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/LevelChunkMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/LevelChunkMixin.java new file mode 100644 index 00000000..f674f99d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/findblock/LevelChunkMixin.java @@ -0,0 +1,30 @@ +package net.earthcomputer.clientcommands.mixin.commands.findblock; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.earthcomputer.clientcommands.event.ClientLevelEvents; +import net.minecraft.client.multiplayer.ClientLevel; +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 net.minecraft.world.level.chunk.LevelChunkSection; +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; + +@Mixin(LevelChunk.class) +public class LevelChunkMixin { + + @Shadow @Final private Level level; + + @WrapOperation(method = "setBlockState", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunkSection;setBlockState(IIILnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/block/state/BlockState;")) + private BlockState onSetBlockState(LevelChunkSection instance, int x, int y, int z, BlockState state, Operation original, BlockPos pos, BlockState redundant, boolean isMoving) { + BlockState oldState = original.call(instance, x, y, z, state); + if (level.isClientSide) { + ClientLevelEvents.CHUNK_UPDATE.invoker().onBlockStateUpdate((ClientLevel) level, pos, oldState, state); + } + return oldState; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/task/RenderDistanceScanTask.java b/src/main/java/net/earthcomputer/clientcommands/task/RenderDistanceScanTask.java index 50bf18a7..090acf81 100644 --- a/src/main/java/net/earthcomputer/clientcommands/task/RenderDistanceScanTask.java +++ b/src/main/java/net/earthcomputer/clientcommands/task/RenderDistanceScanTask.java @@ -1,7 +1,9 @@ package net.earthcomputer.clientcommands.task; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.event.ClientLevelEvents; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; @@ -16,16 +18,50 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; -import java.util.Iterator; +import java.lang.ref.WeakReference; +import java.util.Set; import java.util.function.Predicate; public abstract class RenderDistanceScanTask extends SimpleTask { private static final long MAX_SCAN_TIME = 30_000_000L; // 30ms + private static final Set MUTEX_KEYS = Set.of(RenderDistanceScanTask.class); + + static { + ClientLevelEvents.CHUNK_UPDATE.register((level, pos, oldState, newState) -> { + WeakReference currentScanTask = RenderDistanceScanTask.currentScanTask; + if (currentScanTask != null) { + RenderDistanceScanTask scanTask = currentScanTask.get(); + if (scanTask != null) { + scanTask.onBlockStateUpdate(level, pos, oldState, newState); + } + } + }); + ClientLevelEvents.UNLOAD_CHUNK.register((level, pos) -> { + WeakReference currentScanTask = RenderDistanceScanTask.currentScanTask; + if (currentScanTask != null) { + RenderDistanceScanTask scanTask = currentScanTask.get(); + if (scanTask != null) { + scanTask.onUnloadChunk(level, pos); + } + } + }); + ClientLevelEvents.LOAD_CHUNK.register((level, pos) -> { + WeakReference currentScanTask = RenderDistanceScanTask.currentScanTask; + if (currentScanTask != null) { + RenderDistanceScanTask scanTask = currentScanTask.get(); + if (scanTask != null) { + scanTask.onLoadChunk(level, pos); + } + } + }); + } - private final boolean keepSearching; - - private Iterator squarePosIterator; + @Nullable + private static WeakReference currentScanTask = null; + protected boolean keepSearching; + private LongLinkedOpenHashSet remainingChunks; protected RenderDistanceScanTask(boolean keepSearching) { this.keepSearching = keepSearching; @@ -33,12 +69,19 @@ protected RenderDistanceScanTask(boolean keepSearching) { @Override public void initialize() { - squarePosIterator = createIterator(); + remainingChunks = new LongLinkedOpenHashSet(); + Entity cameraEntity = Minecraft.getInstance().cameraEntity; + if (cameraEntity == null) { + _break(); + return; + } + BlockPos.spiralAround(new BlockPos(Mth.floor(cameraEntity.getX()) >> 4, 0, Mth.floor(cameraEntity.getZ()) >> 4), Minecraft.getInstance().options.renderDistance().get(), Direction.EAST, Direction.SOUTH).iterator().forEachRemaining(pos -> remainingChunks.add(ChunkPos.asLong(pos.getX(), pos.getZ()))); + currentScanTask = new WeakReference<>(this); } @Override public boolean condition() { - return squarePosIterator != null; + return hasChunksRemaining() || keepSearching; } @Override @@ -50,6 +93,10 @@ protected final void onTick() { } } + protected boolean hasChunksRemaining() { + return !remainingChunks.isEmpty(); + } + protected void doTick() throws CommandSyntaxException { Entity cameraEntity = Minecraft.getInstance().cameraEntity; if (cameraEntity == null) { @@ -61,9 +108,8 @@ protected void doTick() throws CommandSyntaxException { assert level != null; long startTime = System.nanoTime(); - while (squarePosIterator.hasNext()) { - BlockPos chunkPosAsBlockPos = squarePosIterator.next(); - ChunkPos chunkPos = new ChunkPos(chunkPosAsBlockPos.getX(), chunkPosAsBlockPos.getZ()); + while (hasChunksRemaining()) { + ChunkPos chunkPos = new ChunkPos(remainingChunks.removeFirst()); if (canScanChunk(cameraEntity, chunkPos)) { int minSection = level.getMinSection(); @@ -81,18 +127,38 @@ protected void doTick() throws CommandSyntaxException { return; } } + } + @Override + public void onCompleted() { + currentScanTask = null; + } + + @Override + public Set getMutexKeys() { + return MUTEX_KEYS; + } + + protected void onBlockStateUpdate(ClientLevel level, BlockPos pos, BlockState oldState, BlockState newState) { if (keepSearching) { - if (canKeepSearchingNow()) { - squarePosIterator = createIterator(); + try { + scanBlock(Minecraft.getInstance().cameraEntity, pos); + } catch (CommandSyntaxException e) { + ClientCommandHelper.sendError(ComponentUtils.fromMessage(e.getRawMessage())); } - } else { - squarePosIterator = null; } } - protected boolean canKeepSearchingNow() { - return true; + protected void onLoadChunk(ClientLevel level, ChunkPos pos) { + if (keepSearching) { + remainingChunks.add(pos.toLong()); + } + } + + protected void onUnloadChunk(ClientLevel level, ChunkPos pos) { + if (keepSearching) { + remainingChunks.add(pos.toLong()); + } } protected boolean canScanChunk(Entity cameraEntity, ChunkPos pos) { @@ -142,13 +208,4 @@ private void scanChunkSection(Entity cameraEntity, SectionPos sectionPos) throws } protected abstract void scanBlock(Entity cameraEntity, BlockPos pos) throws CommandSyntaxException; - - private Iterator createIterator() { - Entity cameraEntity = Minecraft.getInstance().cameraEntity; - if (cameraEntity == null) { - _break(); - return null; - } - return BlockPos.spiralAround(new BlockPos(Mth.floor(cameraEntity.getX()) >> 4, 0, Mth.floor(cameraEntity.getZ()) >> 4), Minecraft.getInstance().options.renderDistance().get(), Direction.EAST, Direction.SOUTH).iterator(); - } } diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 3bf3d890..df7f8eb8 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -8,6 +8,7 @@ "commands.enchant.EnchantmentHelperMixin", "commands.enchant.EnchantmentScreenMixin", "commands.enchant.ProtectionEnchantmentMixin", + "commands.findblock.LevelChunkMixin", "commands.fish.FishingHookMixin", "commands.fish.FishingRodItemMixin", "commands.fish.ItemEntityMixin", @@ -67,6 +68,7 @@ "c2c.ClientPacketListenerMixin", "commands.alias.ClientSuggestionProviderMixin", "commands.enchant.MultiPlayerGameModeMixin", + "commands.findblock.ClientLevelMixin", "commands.generic.CommandSuggestionsMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin",