diff --git a/src/main/java/xyz/nucleoid/creator_tools/CreatorTools.java b/src/main/java/xyz/nucleoid/creator_tools/CreatorTools.java index 374e9a3..fe9301e 100644 --- a/src/main/java/xyz/nucleoid/creator_tools/CreatorTools.java +++ b/src/main/java/xyz/nucleoid/creator_tools/CreatorTools.java @@ -24,6 +24,8 @@ public final class CreatorTools implements ModInitializer { public void onInitialize() { CreatorToolsItems.register(); + MapMetadataCommand.registerArgumentTypes(); + CommandRegistrationCallback.EVENT.register((dispatcher, dedicated, environment) -> { MapManageCommand.register(dispatcher); MapMetadataCommand.register(dispatcher); diff --git a/src/main/java/xyz/nucleoid/creator_tools/command/MapMetadataCommand.java b/src/main/java/xyz/nucleoid/creator_tools/command/MapMetadataCommand.java index 2be4e5f..4b7b29c 100644 --- a/src/main/java/xyz/nucleoid/creator_tools/command/MapMetadataCommand.java +++ b/src/main/java/xyz/nucleoid/creator_tools/command/MapMetadataCommand.java @@ -2,13 +2,18 @@ import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.datafixers.util.Either; import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.minecraft.command.CommandSource; import net.minecraft.command.argument.BlockPosArgumentType; import net.minecraft.command.argument.EntityArgumentType; @@ -16,6 +21,7 @@ import net.minecraft.command.argument.NbtCompoundArgumentType; import net.minecraft.command.argument.NbtElementArgumentType; import net.minecraft.command.argument.NbtPathArgumentType; +import net.minecraft.command.argument.serialize.ConstantArgumentSerializer; import net.minecraft.entity.EntityType; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.nbt.NbtCompound; @@ -30,11 +36,13 @@ import net.minecraft.util.math.BlockPos; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import xyz.nucleoid.creator_tools.CreatorTools; import xyz.nucleoid.creator_tools.workspace.MapWorkspace; import xyz.nucleoid.creator_tools.workspace.MapWorkspaceManager; import xyz.nucleoid.creator_tools.workspace.WorkspaceRegion; import xyz.nucleoid.map_templates.BlockBounds; +import java.util.Arrays; import java.util.Collection; import java.util.stream.Stream; @@ -64,13 +72,92 @@ public final class MapMetadataCommand { arg -> Text.stringifiedTranslatable("commands.data.modify.expected_object", arg) ); + public static final DynamicCommandExceptionType INVALID_REGION_SELECTOR = new DynamicCommandExceptionType( + arg -> Text.stringifiedTranslatable("text.nucleoid_creator_tools.map.region.reserved_name", arg) + ); + + private enum SpecialRegionSelector { + All("all"); + + private final static char PREFIX = '@'; + + public final String name; + SpecialRegionSelector(String name) { + this.name = name; + } + + public static Stream suggestions() { + return Arrays.stream(values()).map(selector -> PREFIX + selector.name); + } + } + + private record RegionSelector(Either inner) { + static RegionSelector special(SpecialRegionSelector selector) { + return new RegionSelector(Either.right(selector)); + } + + static RegionSelector named(String name) { + return new RegionSelector(Either.left(name)); + } + + public boolean matches(WorkspaceRegion region) { + // This is to make sure we don't miss this if we add more special selectors: + // noinspection ConstantValue + return this.inner.map( + name -> region.marker().equals(name), + special -> switch (special) { + case All -> true; + } + ); + } + } + + private static final class RegionSelectorArgumentType implements ArgumentType { + @Override + public RegionSelector parse(StringReader reader) throws CommandSyntaxException { + if (reader.peek() == SpecialRegionSelector.PREFIX) { + reader.skip(); // Skip prefix + + // Find whichever special selector matches + String selectorStr = reader.readString(); + SpecialRegionSelector selector = Arrays.stream(SpecialRegionSelector.values()) + .filter(s -> s.name.equals(selectorStr)) + .findAny() + .orElseThrow(() -> INVALID_REGION_SELECTOR.create(SpecialRegionSelector.PREFIX + selectorStr)); + return RegionSelector.special(selector); + } else { + return RegionSelector.named(reader.readString()); + } + } + + public static RegionSelector get(final CommandContext context, final String name) { + return context.getArgument(name, RegionSelector.class); + } + } + + private static RequiredArgumentBuilder localRegionMarkerArg(String name) { + return argument(name, new RegionSelectorArgumentType()).suggests(localRegionSuggestions()); + } + + private static RequiredArgumentBuilder regionMarkerArg(String name) { + return argument(name, new RegionSelectorArgumentType()).suggests(regionSuggestions(true)); + } + + public static void registerArgumentTypes() { + ArgumentTypeRegistry.registerArgumentType( + new Identifier(CreatorTools.ID, "region_selector"), + MapMetadataCommand.RegionSelectorArgumentType.class, + ConstantArgumentSerializer.of(MapMetadataCommand.RegionSelectorArgumentType::new) + ); + } + // @formatter:off public static void register(CommandDispatcher dispatcher) { dispatcher.register( literal("map").requires(Permissions.require("nucleoid_creator_extras.map", 2)) .then(literal("region") .then(literal("add") - .then(argument("marker", StringArgumentType.word()) + .then(argument("marker", StringArgumentType.word()).suggests(regionSuggestions(false)) .then(argument("min", BlockPosArgumentType.blockPos()) .then(argument("max", BlockPosArgumentType.blockPos()) .executes(MapMetadataCommand::addRegion) @@ -79,13 +166,13 @@ public static void register(CommandDispatcher dispatcher) { ))))) .then(literal("rename") .then(literal("all") - .then(argument("old", StringArgumentType.word()).suggests(regionSuggestions()) - .then(argument("new", StringArgumentType.word()) + .then(regionMarkerArg("old") + .then(argument("new", StringArgumentType.word()).suggests(regionSuggestions(false)) .executes(context -> renameRegions(context, (region, oldMarker, pos) -> region.marker().equals(oldMarker))) ))) .then(literal("here") - .then(argument("old", StringArgumentType.word()).suggests(localRegionSuggestions()) - .then(argument("new", StringArgumentType.word()) + .then(localRegionMarkerArg("old") + .then(argument("new", StringArgumentType.word()).suggests(regionSuggestions(false)) .executes( context -> renameRegions(context, (region, oldMarker, pos) -> region.marker().equals(oldMarker) && region.bounds().contains(pos)) @@ -93,11 +180,11 @@ public static void register(CommandDispatcher dispatcher) { ))) ) .then(literal("bounds") - .then(argument("marker", StringArgumentType.word()).suggests(regionSuggestions()) + .then(regionMarkerArg("marker") .executes(MapMetadataCommand::getRegionBounds)) ) .then(literal("data") - .then(argument("marker", StringArgumentType.word()).suggests(localRegionSuggestions()) + .then(localRegionMarkerArg("marker") .then(literal("get").executes(executeInRegions("", MapMetadataCommand::executeRegionDataGet))) .then(literal("merge") .then(argument("nbt", NbtCompoundArgumentType.nbtCompound()) @@ -114,7 +201,7 @@ public static void register(CommandDispatcher dispatcher) { )) .then(literal("remove") .then(literal("here") - .then(argument("marker", StringArgumentType.word()).suggests(localRegionSuggestions()) + .then(localRegionMarkerArg("marker") .executes(executeInRegions("Removed %d regions.", MapMetadataCommand::executeRemoveNamedRegionsHere)) )) .then(literal("at") @@ -123,7 +210,7 @@ public static void register(CommandDispatcher dispatcher) { )) ) .then(literal("commit") - .then(argument("marker", StringArgumentType.word()) + .then(argument("marker", StringArgumentType.word()).suggests(regionSuggestions(false)) .executes(MapMetadataCommand::commitRegion) .then(argument("data", NbtCompoundArgumentType.nbtCompound()) .executes(context -> commitRegion(context, NbtCompoundArgumentType.getNbtCompound(context, "data"))) @@ -234,10 +321,10 @@ private static int getRegionBounds(CommandContext context) var source = context.getSource(); var map = getWorkspaceForSource(source); - var marker = StringArgumentType.getString(context, "marker"); + var regionSelector = RegionSelectorArgumentType.get(context, "marker"); var regions = map.getRegions().stream() - .filter(region -> region.marker().equals(marker)) + .filter(regionSelector::matches) .toList(); source.sendFeedback(() -> Text.translatable("text.nucleoid_creator_tools.map.region.bounds.get.header", regions.size()).formatted(Formatting.BOLD), false); @@ -549,11 +636,12 @@ private static SuggestionProvider entityTypeSuggestions() { return (ctx, builder) -> CommandSource.suggestIdentifiers(Registries.ENTITY_TYPE.getIds(), builder); } - private static SuggestionProvider regionSuggestions() { + private static SuggestionProvider regionSuggestions(boolean includeAll) { return (context, builder) -> { var map = getWorkspaceForSource(context.getSource()); + var regions = map.getRegions().stream().map(WorkspaceRegion::marker); return CommandSource.suggestMatching( - map.getRegions().stream().map(WorkspaceRegion::marker), + includeAll ? Stream.concat(SpecialRegionSelector.suggestions(), regions) : regions, builder ); }; @@ -563,9 +651,9 @@ private static SuggestionProvider localRegionSuggestions() return (context, builder) -> { var map = getWorkspaceForSource(context.getSource()); var sourcePos = context.getSource().getPlayerOrThrow().getBlockPos(); + var localRegions = map.getRegions().stream().filter(region -> region.bounds().contains(sourcePos)).map(WorkspaceRegion::marker); return CommandSource.suggestMatching( - map.getRegions().stream().filter(region -> region.bounds().contains(sourcePos)) - .map(WorkspaceRegion::marker), + Stream.concat(SpecialRegionSelector.suggestions(), localRegions), builder ); }; @@ -586,11 +674,12 @@ private static Command executeInRegions(String message, Reg var source = context.getSource(); var pos = source.getPlayerOrThrow().getBlockPos(); - var marker = StringArgumentType.getString(context, "marker"); + var regionSelector = RegionSelectorArgumentType.get(context, "marker"); var map = getWorkspaceForSource(context.getSource()); var regions = map.getRegions().stream() - .filter(region -> region.bounds().contains(pos) && region.marker().equals(marker)) + .filter(region -> region.bounds().contains(pos)) + .filter(regionSelector::matches) .toList(); int count = 0; diff --git a/src/main/resources/data/nucleoid_creator_tools/lang/en_us.json b/src/main/resources/data/nucleoid_creator_tools/lang/en_us.json index 8736cdc..4d64d64 100644 --- a/src/main/resources/data/nucleoid_creator_tools/lang/en_us.json +++ b/src/main/resources/data/nucleoid_creator_tools/lang/en_us.json @@ -47,6 +47,7 @@ "text.nucleoid_creator_tools.map.open.map_already_exists": "Map with id '%s' already exists!", "text.nucleoid_creator_tools.map.open.success": "Opened workspace '%s'! Use %s to join this map", "text.nucleoid_creator_tools.map.origin.set": "Updated origin for workspace", + "text.nucleoid_creator_tools.map.region.reserved_name": "Invalid region selector '%s'.", "text.nucleoid_creator_tools.map.region.add.success": "Added region '%s'.", "text.nucleoid_creator_tools.map.region.add.success.excited": "Added region '%s'!", "text.nucleoid_creator_tools.map.region.bounds.get": "%s to %s",