From 3161eb5662126fc4473f1f7b2591fe20bdc035b8 Mon Sep 17 00:00:00 2001 From: mattcc Date: Thu, 5 Aug 2021 16:52:17 -0500 Subject: [PATCH] Better bulk block add using wildcards (#356) --- .../dev/fiki/forgehax/api/BlockHelper.java | 18 +++- .../forgehax/api/cmd/AbstractSettingMap.java | 11 +++ .../fiki/forgehax/api/cmd/ISettingMap.java | 3 +- .../forgehax/api/extension/GeneralEx.java | 45 ++++++++++ .../forgehax/main/commands/BlocksCommand.java | 13 +-- .../forgehax/main/mods/world/Markers.java | 57 +++++++++---- .../forgehax/main/mods/world/XrayMod.java | 62 ++++++++++++++ .../forgehax/api/extension/GeneralExTest.java | 82 +++++++++++++++++++ 8 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 src/test/java/dev/fiki/forgehax/api/extension/GeneralExTest.java diff --git a/src/main/java/dev/fiki/forgehax/api/BlockHelper.java b/src/main/java/dev/fiki/forgehax/api/BlockHelper.java index 83ff34645..9bc62513e 100644 --- a/src/main/java/dev/fiki/forgehax/api/BlockHelper.java +++ b/src/main/java/dev/fiki/forgehax/api/BlockHelper.java @@ -1,6 +1,7 @@ package dev.fiki.forgehax.api; import com.google.common.collect.Lists; +import dev.fiki.forgehax.api.extension.GeneralEx; import dev.fiki.forgehax.api.extension.LocalPlayerEx; import dev.fiki.forgehax.api.math.VectorUtil; import lombok.AllArgsConstructor; @@ -22,13 +23,15 @@ import net.minecraft.util.math.vector.Vector3d; import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static dev.fiki.forgehax.main.Common.getLocalPlayer; import static dev.fiki.forgehax.main.Common.getWorld; public class BlockHelper { - @Deprecated public static UniqueBlock newUniqueBlock(Block block, int metadata, BlockPos pos) { throw new UnsupportedOperationException(); @@ -43,6 +46,19 @@ public static UniqueBlock newUniqueBlock(BlockPos pos) { return new UniqueBlock(getWorld().getBlockState(pos).getBlock(), pos); } + public static Set getBlocksMatching(Iterable blocks, String match) { + final Pattern pattern = Pattern.compile(GeneralEx.globToRegex(match), Pattern.CASE_INSENSITIVE); + return StreamSupport.stream(blocks.spliterator(), false) + .filter(block -> block != Blocks.AIR) + .filter(block -> block.getRegistryName() != null) + .filter(block -> pattern.matcher(getBlockRegistryName(block)).matches()) + .collect(Collectors.toSet()); + } + + public static String getBlockRegistryName(Block block) { + return block.getRegistryName().toString(); + } + public static BlockTraceInfo newBlockTrace(BlockPos pos, Direction side) { return new BlockTraceInfo(pos, side); } diff --git a/src/main/java/dev/fiki/forgehax/api/cmd/AbstractSettingMap.java b/src/main/java/dev/fiki/forgehax/api/cmd/AbstractSettingMap.java index 9bd3588b1..6d8418d10 100644 --- a/src/main/java/dev/fiki/forgehax/api/cmd/AbstractSettingMap.java +++ b/src/main/java/dev/fiki/forgehax/api/cmd/AbstractSettingMap.java @@ -42,6 +42,17 @@ protected String printableValue(V o) { return String.valueOf(o); } + @Override + public boolean removeKeys(Collection collection) { + int beforeSize = size(); + collection.forEach(wrapping::remove); + if (beforeSize != size()) { + callUpdateListeners(); + return true; + } + return false; + } + @Override public int size() { return wrapping.size(); diff --git a/src/main/java/dev/fiki/forgehax/api/cmd/ISettingMap.java b/src/main/java/dev/fiki/forgehax/api/cmd/ISettingMap.java index 0a95353a1..c9fc40daa 100644 --- a/src/main/java/dev/fiki/forgehax/api/cmd/ISettingMap.java +++ b/src/main/java/dev/fiki/forgehax/api/cmd/ISettingMap.java @@ -2,8 +2,9 @@ import dev.fiki.forgehax.api.serialization.IJsonSerializable; +import java.util.Collection; import java.util.Map; public interface ISettingMap> extends IParentCommand, IJsonSerializable, Map { - + boolean removeKeys(Collection collection); } diff --git a/src/main/java/dev/fiki/forgehax/api/extension/GeneralEx.java b/src/main/java/dev/fiki/forgehax/api/extension/GeneralEx.java index 0987d97a2..1696ab7cb 100644 --- a/src/main/java/dev/fiki/forgehax/api/extension/GeneralEx.java +++ b/src/main/java/dev/fiki/forgehax/api/extension/GeneralEx.java @@ -76,4 +76,49 @@ public static float clamp(float value, float min, float max) { public static double scale(double x, double from_min, double from_max, double to_min, double to_max) { return to_min + (to_max - to_min) * ((x - from_min) / (from_max - from_min)); } + + public static String globToRegex(String glob) { + StringBuilder builder = new StringBuilder("^"); + + boolean literal = false; + for (int i = 0; i < glob.length(); i++) { + char at = glob.charAt(i); + switch (at) { + case '*': + case '?': + // this is an expression, end literal if started + if (literal) { + builder.append("\\E"); + literal = false; + } + // nasty + switch (at) { + case '*': + // do not allow repeated wildcards + if (i - 1 < 0 || glob.charAt(i - 1) != '*') { + // * = match any multiple characters + builder.append(".*"); + } + break; + case '?': + // ? = match any single character + builder.append('.'); + } + break; + default: + if (!literal) { + builder.append("\\Q"); + literal = true; + } + builder.append(at); + } + } + + // end literal if started + if (literal) { + builder.append("\\E"); + } + + return builder.append("$").toString(); + } } diff --git a/src/main/java/dev/fiki/forgehax/main/commands/BlocksCommand.java b/src/main/java/dev/fiki/forgehax/main/commands/BlocksCommand.java index de28b29be..dbf806beb 100644 --- a/src/main/java/dev/fiki/forgehax/main/commands/BlocksCommand.java +++ b/src/main/java/dev/fiki/forgehax/main/commands/BlocksCommand.java @@ -1,14 +1,11 @@ package dev.fiki.forgehax.main.commands; +import dev.fiki.forgehax.api.BlockHelper; import dev.fiki.forgehax.api.cmd.argument.Arguments; import dev.fiki.forgehax.api.mod.CommandMod; import dev.fiki.forgehax.api.modloader.RegisterMod; -import net.minecraft.block.Block; -import net.minecraft.util.ResourceLocation; -import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static dev.fiki.forgehax.main.Common.getBlockRegistry; @@ -17,7 +14,6 @@ */ @RegisterMod public class BlocksCommand extends CommandMod { - { newSimpleCommand() .name("blocks") @@ -28,11 +24,8 @@ public class BlocksCommand extends CommandMod { .executor(args -> { String find = args.getFirst().getStringValue(); - args.inform(StreamSupport.stream(getBlockRegistry().spliterator(), false) - .map(Block::getRegistryName) - .filter(Objects::nonNull) - .map(ResourceLocation::toString) - .filter(block -> block.toLowerCase().contains(find)) + args.inform(BlockHelper.getBlocksMatching(getBlockRegistry(), find).stream() + .map(BlockHelper::getBlockRegistryName) .limit(25) .collect(Collectors.joining(", "))); }) diff --git a/src/main/java/dev/fiki/forgehax/main/mods/world/Markers.java b/src/main/java/dev/fiki/forgehax/main/mods/world/Markers.java index 7b7a13294..d8aa36373 100644 --- a/src/main/java/dev/fiki/forgehax/main/mods/world/Markers.java +++ b/src/main/java/dev/fiki/forgehax/main/mods/world/Markers.java @@ -3,6 +3,7 @@ import com.google.common.collect.Maps; import com.mojang.blaze3d.matrix.MatrixStack; import com.mojang.blaze3d.systems.RenderSystem; +import dev.fiki.forgehax.api.BlockHelper; import dev.fiki.forgehax.api.asm.MapField; import dev.fiki.forgehax.api.cmd.argument.Arguments; import dev.fiki.forgehax.api.cmd.listener.Listeners; @@ -26,14 +27,12 @@ import dev.fiki.forgehax.main.Common; import lombok.RequiredArgsConstructor; import net.minecraft.block.Block; -import net.minecraft.block.Blocks; import net.minecraft.client.renderer.ViewFrustum; import net.minecraft.client.renderer.WorldRenderer; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.client.renderer.vertex.VertexBuffer; import net.minecraft.client.world.ClientWorld; -import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.vector.Vector3d; @@ -42,7 +41,6 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static dev.fiki.forgehax.main.Common.*; @@ -85,8 +83,10 @@ public class Markers extends ToggleMod implements Common { { blocks.newSimpleCommand() - .name("bulk-add") - .description("Add all matching a given string.") + .name("match-add") + .alias("madd") + .alias("bulk-add") + .description("Add all matching a given string. Use ? to match exactly 1 character, and * to match 0 or more") .argument(Arguments.newStringArgument() .label("search blocks") .maxArgumentsConsumed(1) @@ -98,25 +98,50 @@ public class Markers extends ToggleMod implements Common { .build()) .executor(args -> { final String searchString = args.getFirst().getValue(); - final String lower = searchString.toLowerCase(); final Color color = args.getSecond().getValue(); - - Set blocks = StreamSupport.stream(getBlockRegistry().spliterator(), false) - .filter(block -> Blocks.AIR != block) - .filter(block -> block.getRegistryName() != null) - .filter(block -> block.getRegistryName().toString().toLowerCase().contains(lower)) - .collect(Collectors.toSet()); + final Set blocks = BlockHelper.getBlocksMatching(getBlockRegistry(), searchString); if (blocks.isEmpty()) { - args.warn("Found no blocks matching name %s", searchString); + args.warn("Found no blocks matching %s.", searchString); + if (!searchString.contains(":")) { + args.warn("Did you mean \"minecraft:%s\"?", searchString); + } } else { args.inform("Adding blocks %s with color %s", blocks.stream() - .map(Block::getRegistryName) - .map(ResourceLocation::toString) + .map(BlockHelper::getBlockRegistryName) .collect(Collectors.joining(", ")), args.getSecond().getStringValue()); - blocks.forEach(block -> this.blocks.put(block, color)); + this.blocks.putAll(blocks.stream() + .collect(Collectors.toMap(block -> block, block -> color))); + } + }) + .build(); + + blocks.newSimpleCommand() + .name("match-remove") + .alias("mremove") + .alias("mdelete") + .alias("bulk-remove") + .description("Remove all matching a given string. Use ? to match exactly 1 character, and * to match 0 or more") + .argument(Arguments.newStringArgument() + .label("search blocks") + .maxArgumentsConsumed(1) + .build()) + .executor(args -> { + final String searchString = args.getFirst().getValue(); + final Set blocks = BlockHelper.getBlocksMatching(this.blocks.keySet(), searchString); + + if (blocks.isEmpty()) { + args.warn("Found no blocks matching %s", searchString); + if (!searchString.contains(":")) { + args.warn("Did you mean \"minecraft:%s\"?", searchString); + } + } else { + args.inform("Removing blocks %s", blocks.stream() + .map(BlockHelper::getBlockRegistryName) + .collect(Collectors.joining(", "))); + this.blocks.removeKeys(blocks); } }) .build(); diff --git a/src/main/java/dev/fiki/forgehax/main/mods/world/XrayMod.java b/src/main/java/dev/fiki/forgehax/main/mods/world/XrayMod.java index 8a2883172..1f4fb5543 100644 --- a/src/main/java/dev/fiki/forgehax/main/mods/world/XrayMod.java +++ b/src/main/java/dev/fiki/forgehax/main/mods/world/XrayMod.java @@ -1,6 +1,7 @@ package dev.fiki.forgehax.main.mods.world; import com.google.common.collect.Sets; +import dev.fiki.forgehax.api.BlockHelper; import dev.fiki.forgehax.api.cmd.argument.Arguments; import dev.fiki.forgehax.api.cmd.flag.EnumFlag; import dev.fiki.forgehax.api.cmd.listener.Listeners; @@ -13,9 +14,13 @@ import dev.fiki.forgehax.api.modloader.RegisterMod; import dev.fiki.forgehax.asm.events.render.CullCavesEvent; import dev.fiki.forgehax.asm.hooks.XrayHooks; +import dev.fiki.forgehax.main.Common; import net.minecraft.block.Block; import net.minecraft.block.Blocks; +import java.util.Set; +import java.util.stream.Collectors; + import static dev.fiki.forgehax.main.Common.isInWorld; import static dev.fiki.forgehax.main.Common.reloadChunkSmooth; @@ -60,6 +65,63 @@ public class XrayMod extends ToggleMod { })) .build(); + { + blocks.newSimpleCommand() + .name("match-add") + .alias("madd") + .alias("bulk-add") + .description("Add all matching a given string. Use ? to match exactly 1 character, and * to match 0 or more") + .argument(Arguments.newStringArgument() + .label("search blocks") + .maxArgumentsConsumed(1) + .build()) + .executor(args -> { + final String searchString = args.getFirst().getValue(); + final Set blocks = BlockHelper.getBlocksMatching(Common.getBlockRegistry(), searchString); + + if (blocks.isEmpty()) { + args.warn("Found no blocks matching %s.", searchString); + if (!searchString.contains(":")) { + args.warn("Did you mean \"minecraft:%s\"?", searchString); + } + } else { + args.inform("Adding blocks %s", blocks.stream() + .map(BlockHelper::getBlockRegistryName) + .collect(Collectors.joining(", "))); + this.blocks.addAll(blocks); + } + }) + .build(); + + blocks.newSimpleCommand() + .name("match-remove") + .alias("mremove") + .alias("mdelete") + .alias("bulk-remove") + .description("Remove all matching a given string. Use ? to match exactly 1 character, and * to match 0 or more") + .argument(Arguments.newStringArgument() + .label("search blocks") + .maxArgumentsConsumed(1) + .build()) + .executor(args -> { + final String searchString = args.getFirst().getValue(); + final Set blocks = BlockHelper.getBlocksMatching(this.blocks, searchString); + + if (blocks.isEmpty()) { + args.warn("Found no blocks matching %s", searchString); + if (!searchString.contains(":")) { + args.warn("Did you mean \"minecraft:%s\"?", searchString); + } + } else { + args.inform("Removing blocks %s", blocks.stream() + .map(BlockHelper::getBlockRegistryName) + .collect(Collectors.joining(", "))); + this.blocks.removeAll(blocks); + } + }) + .build(); + } + private void reloadWorldChunks() { if (isEnabled() && isInWorld()) { reloadChunkSmooth(); diff --git a/src/test/java/dev/fiki/forgehax/api/extension/GeneralExTest.java b/src/test/java/dev/fiki/forgehax/api/extension/GeneralExTest.java new file mode 100644 index 000000000..b869dfb07 --- /dev/null +++ b/src/test/java/dev/fiki/forgehax/api/extension/GeneralExTest.java @@ -0,0 +1,82 @@ +package dev.fiki.forgehax.api.extension; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class GeneralExTest { + + @Test + void globToRegex() { + // sanity checks + + assertThat(GeneralEx.globToRegex("*foobar")).isEqualTo("^.*\\Qfoobar\\E$"); + assertThat(GeneralEx.globToRegex("foobar*")).isEqualTo("^\\Qfoobar\\E.*$"); + assertThat(GeneralEx.globToRegex("*")).isEqualTo("^.*$"); + assertThat(GeneralEx.globToRegex("?")).isEqualTo("^.$"); + + // * + { + assertThat(GeneralEx.globToRegex("123*9")) + .isEqualTo("^\\Q123\\E.*\\Q9\\E$") + .satisfies(allOf( + new Condition<>("1239"::matches, "0 chars"), + new Condition<>("123x9"::matches, "single char"), + new Condition<>("123xxxxx9"::matches, "multiple chars"), + not(new Condition<>(""::matches, "empty string")), + not(new Condition<>("123"::matches, "missing 9")), + not(new Condition<>("9"::matches, "missing 123")), + not(new Condition<>("01239"::matches, "0 prefix")), + not(new Condition<>("12390"::matches, "0 suffix")) + )); + + assertThat(GeneralEx.globToRegex("foo_***_bar")) + .describedAs("multiple asterisks should be condensed down to one") + .isEqualTo("^\\Qfoo_\\E.*\\Q_bar\\E$"); + + assertThat(GeneralEx.globToRegex("*AAA*BBB*CCC*")) + .satisfies(allOf( + new Condition<>("AAABBBCCC"::matches, "fill 0 per wildcard"), + new Condition<>("xAAAxBBBxCCCx"::matches, "fill 1 per wildcard") + )); + } + + // ? + { + assertThat(GeneralEx.globToRegex("123?9")) + .isEqualTo("^\\Q123\\E.\\Q9\\E$") + .satisfies(allOf( + new Condition<>("123x9"::matches, "1 char"), + not(new Condition<>(""::matches, "empty string")), + not(new Condition<>("123x"::matches, "missing 9")), + not(new Condition<>("x9"::matches, "missing 123")), + not(new Condition<>("0123x9"::matches, "0 prefix")), + not(new Condition<>("123x90"::matches, "0 suffix")) + )); + + assertThat(GeneralEx.globToRegex("123???9")) + .isEqualTo("^\\Q123\\E...\\Q9\\E$") + .satisfies(allOf( + new Condition<>("123xxx9"::matches, "all chars"), + not(new Condition<>(""::matches, "empty string")), + not(new Condition<>("1239"::matches, "missing all chars")), + not(new Condition<>("123x9"::matches, "missing 2 chars")), + not(new Condition<>("123xx9"::matches, "missing 1 char")) + )); + } + + // strange combos + + assertThat(GeneralEx.globToRegex("123*??*9")) + .describedAs("both wildcard types") + .isEqualTo("^\\Q123\\E.*...*\\Q9\\E$") + .satisfies(allOf( + new Condition<>("123xxxyyxxx9"::matches, "all chars"), + new Condition<>("123yy9"::matches, "only single wildcards"), + not(new Condition<>(""::matches, "empty string")), + not(new Condition<>("1239"::matches, "missing all chars")), + not(new Condition<>("123y9"::matches, "missing 1 char")) + )); + } +}