diff --git a/src/main/java/carpet/script/command/CommandArgument.java b/src/main/java/carpet/script/command/CommandArgument.java new file mode 100644 index 00000000..d0cd30cc --- /dev/null +++ b/src/main/java/carpet/script/command/CommandArgument.java @@ -0,0 +1,251 @@ +package carpet.script.command; + +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import com.google.common.collect.Lists; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.command.argument.BlockPosArgumentType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import net.minecraft.util.math.BlockPos; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public abstract class CommandArgument +{ + private static final List baseTypes = Lists.newArrayList( + new WordArgument(), new PosArgument(), new FloatArgument() + ); + + public static final Map builtIns = baseTypes.stream().collect(Collectors.toMap(CommandArgument::getTypeSuffix, a -> a)); + + private String suffix; + private final Collection examples; + + protected CommandArgument( + String suffix, + Collection examples) + { + this.suffix = suffix; + this.examples = examples; + } + + public abstract ArgumentType getArgument(); + + public abstract Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException; + + + + public String getTypeSuffix() + { + return suffix; + } + + public Collection getExamples() + { + return examples; + } + + + public static CommandArgument buildFromConfig(String suffix, Map config) + { + if (!config.containsKey("type")) + throw new InternalExpressionException("Custom types should at least specify the base type"); + String baseType = config.get("type").getString(); + if (!builtIns.containsKey(baseType)) + throw new InternalExpressionException("Unknown base type: "+baseType); + CommandArgument variant = builtIns.get(baseType).builder().get(); + variant.configure(config); + variant.suffix = suffix; + return variant; + }; + + protected abstract void configure(Map config); + + + protected CompletableFuture suggest( + CommandContext context, + SuggestionsBuilder suggestionsBuilder + ) + { + String prefix = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); + suggestFor(prefix).forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + } + + protected List suggestFor(String prefix) + { + return getOptions().stream().filter(s -> optionMatchesPrefix(prefix, s)).collect(Collectors.toList()); + } + + protected Collection getOptions() + { + return Collections.emptyList(); + } + + protected boolean optionMatchesPrefix(String prefix, String option) + { + for(int i = 0; !option.startsWith(prefix, i); ++i) + { + i = option.indexOf('_', i); + if (i < 0) return false; + } + return true; + } + + protected abstract Supplier builder(); + + public static class WordArgument extends CommandArgument + { + Set validOptions = Collections.emptySet(); + boolean caseSensitive = false; + private WordArgument() + { + super("string", StringArgumentType.StringType.SINGLE_WORD.getExamples()); + } + + @Override + public ArgumentType getArgument() + { + return StringArgumentType.word(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + String choseValue = StringArgumentType.getString(context, param); + if (!caseSensitive) choseValue = choseValue.toLowerCase(Locale.ROOT); + if (!validOptions.isEmpty() && !validOptions.contains(choseValue)) + { + throw new SimpleCommandExceptionType(new LiteralText("")).create(); + } + return StringValue.of(choseValue); + } + + @Override + protected void configure(Map config) + { + caseSensitive = config.getOrDefault("case_sensitive", Value.FALSE).getBoolean(); + if (config.containsKey("options")) + { + Value optionsValue = config.get("options"); + if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list"); + validOptions = ((ListValue) optionsValue).getItems().stream() + .map(v -> caseSensitive?v.getString():v.getString().toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + } + } + + @Override + protected Collection getOptions() { return validOptions; } + + @Override + protected Supplier builder() { return WordArgument::new; } + } + + public static class PosArgument extends CommandArgument + { + private boolean mustBeLoaded = false; + + private PosArgument() + { + super("pos", BlockPosArgumentType.blockPos().getExamples()); + } + + @Override + public ArgumentType getArgument() + { + return BlockPosArgumentType.blockPos(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + BlockPos pos = mustBeLoaded + ? BlockPosArgumentType.getLoadedBlockPos(context, param) + : BlockPosArgumentType.getBlockPos(context, param); + return ValueConversions.fromPos(pos); + } + + @Override + protected void configure(Map config) + { + mustBeLoaded = config.getOrDefault("loaded", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier builder() + { + return PosArgument::new; + } + } + + public static class FloatArgument extends CommandArgument + { + private Double min = null; + private Double max = null; + private FloatArgument() + { + super("float", DoubleArgumentType.doubleArg().getExamples()); + } + + @Override + public ArgumentType getArgument() + { + if (min != null) + { + if (max != null) + { + return DoubleArgumentType.doubleArg(min, max); + } + return DoubleArgumentType.doubleArg(min); + } + return DoubleArgumentType.doubleArg(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + return new NumericValue(DoubleArgumentType.getDouble(context, param)); + } + + @Override + protected void configure(Map config) + { + if (config.containsKey("min")) + { + min = NumericValue.asNumber(config.get("min"), "min").getDouble(); + } + if (config.containsKey("max")) + { + max = NumericValue.asNumber(config.get("max"), "max").getDouble(); + } + if (max != null && min == null) throw new InternalExpressionException("Double types cannot be only upper-bounded"); + } + + @Override + protected Supplier builder() + { + return FloatArgument::new; + } + } +}