diff --git a/src/main/java/xyz/nifeather/morph/commands/MorphCommand.java b/src/main/java/xyz/nifeather/morph/commands/MorphCommand.java index df454364..0be8b198 100644 --- a/src/main/java/xyz/nifeather/morph/commands/MorphCommand.java +++ b/src/main/java/xyz/nifeather/morph/commands/MorphCommand.java @@ -13,6 +13,7 @@ import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import xiamomc.pluginbase.Annotations.Initializer; import xiamomc.pluginbase.Annotations.Resolved; import xiamomc.pluginbase.Messages.FormattableMessage; import xyz.nifeather.morph.MorphManager; @@ -25,6 +26,7 @@ import xyz.nifeather.morph.misc.DisguiseMeta; import xyz.nifeather.morph.misc.gui.DisguiseSelectScreenWrapper; +import java.util.List; import java.util.concurrent.CompletableFuture; @SuppressWarnings("UnstableApiUsage") @@ -42,6 +44,8 @@ public class MorphCommand extends MorphPluginObject implements IConvertibleBriga @Resolved private MorphManager morphs; + private final ValueMapArgumentType propertyArgument = new ValueMapArgumentType(); + @Override public boolean register(Commands dispatcher) { @@ -53,12 +57,8 @@ public boolean register(Commands dispatcher) .suggests(this::suggestID) .executes(this::execWithID) //.then( - // Commands.argument("properties", new ValueMapArgumentType()) + // Commands.argument("properties", propertyArgument) // .executes(this::execExperimental) - // .then( - // Commands.argument("extra", IntegerArgumentType.integer()) - // .executes(this::execExperimentala) - // ) //) ) .build()); @@ -66,19 +66,14 @@ public boolean register(Commands dispatcher) return true; } - private int execExperimental(CommandContext context) + @Initializer + private void load() { - var input = ValueMapArgumentType.get("properties", context); - - input.forEach((k, v) -> - { - context.getSource().getSender().sendMessage("Key '%s', Value '%s'".formatted(k, v)); - }); - - return 1; + this.propertyArgument.setProperty("morph:frog_variant", List.of("cold", "warm")); + this.propertyArgument.setProperty("morph:cat_variant", List.of("tabby", "black")); } - private int execExperimentala(CommandContext context) + private int execExperimental(CommandContext context) { var input = ValueMapArgumentType.get("properties", context); @@ -87,15 +82,6 @@ private int execExperimentala(CommandContext context) context.getSource().getSender().sendMessage("Key '%s', Value '%s'".formatted(k, v)); }); - try - { - context.getSource().getSender().sendPlainMessage("Extra is " + IntegerArgumentType.getInteger(context, "extra")); - } - catch (Throwable t) - { - context.getSource().getSender().sendPlainMessage("No extra: " + t.getMessage()); - } - return 1; } diff --git a/src/main/java/xyz/nifeather/morph/commands/brigadier/arguments/ValueMapArgumentType.java b/src/main/java/xyz/nifeather/morph/commands/brigadier/arguments/ValueMapArgumentType.java index 3d939d76..ed29da10 100644 --- a/src/main/java/xyz/nifeather/morph/commands/brigadier/arguments/ValueMapArgumentType.java +++ b/src/main/java/xyz/nifeather/morph/commands/brigadier/arguments/ValueMapArgumentType.java @@ -6,6 +6,8 @@ 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 io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.argument.CustomArgumentType; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; @@ -13,19 +15,69 @@ import net.minecraft.network.chat.Component; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import org.slf4j.Logger; import xyz.nifeather.morph.FeatherMorphMain; import java.util.Collection; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; @SuppressWarnings("UnstableApiUsage") public class ValueMapArgumentType implements CustomArgumentType, String> { public static final Collection EXAMPLES = ObjectArrayList.of("[foo=bar]", "[foo=bar, aabb=\"ccdd\"]"); + private static final Map> EMPTY_MAP = new HashMap<>(); + private static final Logger log = FeatherMorphMain.getInstance().getSLF4JLogger(); private static final Map defaultMap = new Object2ObjectOpenHashMap<>(); + private final Map> properties = new ConcurrentHashMap<>(); + + @Unmodifiable + public Map> properties() + { + return new Object2ObjectOpenHashMap<>(this.properties); + } + + public void setProperty(String key, @Nullable List values) + { + if (values == null) + { + this.unsetProperty(key); + return; + } + + this.properties.put(key, values); + } + + public void unsetProperty(String key) + { + this.properties.remove(key); + } + + public ValueMapArgumentType(Map> properties) + { + this.properties.putAll(properties); + } + + public ValueMapArgumentType() + { + this(EMPTY_MAP); + } + + /** + * + * @param properties name <-> values Map + */ + public static ValueMapArgumentType withArguments(Map> properties) + { + return new ValueMapArgumentType(properties); + } + public static Map get(String name, CommandContext context) { return context.getArgument(name, defaultMap.getClass()); @@ -41,7 +93,12 @@ public static Map get(String name, CommandContext CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) + { + // Starting at /morph xx [abcd=ef] + // ^ HERE + // + // What we want the cursor to stop at + // /morph xx [ab=cd, cd=ef] + // ^ ^ ^ ^ + // THESE PLACE + + var masterReader = new StringReader(builder.getInput()); + masterReader.setCursor(builder.getStart()); + + return CompletableFuture.supplyAsync(() -> this.doSuggest(masterReader, builder)); + } + + private Suggestions doSuggest(StringReader masterReader, SuggestionsBuilder defaultBuilder) + { + if (masterReader.peek() == '[') + masterReader.skip(); + + // 循环到最后一个KVP + KeyValuePair keyValuePair = new KeyValuePair(null, null, masterReader.getCursor(), masterReader.getCursor()); + + try + { + var parseResults = parseAll(masterReader, ',', ']'); + + keyValuePair = parseResults.isEmpty() ? keyValuePair : parseResults.getLast(); + } + catch (Throwable t) + { + //log.error("Failed to list suggestions!" + t.getMessage()); + //t.printStackTrace(); + + return defaultBuilder.createOffset(masterReader.getCursor()).suggest("???", ERR_SUGGEST_FAIL).build(); + } + + // 读取一下输入的最后一个字符 + var peekReader = new StringReader(masterReader); + peekReader.setCursor(masterReader.canRead() ? masterReader.getCursor() : peekReader.getTotalLength() - 1); + + //log.info("Peek is " + peekReader.peek() + " At " + masterReader.getCursor()); + + // 遇到结尾了直接退出 + if (peekReader.peek() == ']') + return defaultBuilder.build(); + + // 最终Builder,该Builder的期望是在上面提到的一些地方停下 + SuggestionsBuilder finalBuilder; + + String keyInput = keyValuePair.key == null ? "" : keyValuePair.key; + var hasMatchKey = this.properties.keySet().stream().anyMatch(k -> k.equals(keyInput)); + + // Suggest Key + if (!keyValuePair.metEqual()) + { + finalBuilder = defaultBuilder.createOffset(keyValuePair.keyCursor); + + if (!hasMatchKey) + { + boolean isEmptyInput = keyValuePair.key == null; + + this.properties.keySet().forEach(k -> + { + if (isEmptyInput) finalBuilder.suggest(k); + else if (k.contains(keyInput)) finalBuilder.suggest(k); + }); + } + } + else // Suggest Value + { + String valueInput = keyValuePair.value == null ? "" : keyValuePair.value; + boolean isEmptyInput = keyValuePair.value == null; + + finalBuilder = defaultBuilder.createOffset(keyValuePair.valueCursor); + + var availableValues = this.properties.getOrDefault(keyValuePair.key, List.of()); + availableValues.forEach(v -> + { + if (isEmptyInput) finalBuilder.suggest(v); + else if (v.contains(valueInput)) finalBuilder.suggest(v); + }); + } + + return finalBuilder.build(); + } @Override public Map parse(StringReader reader) throws CommandSyntaxException @@ -67,97 +211,157 @@ public Map parse(StringReader reader) throws CommandSyntaxExcept if (reader.peek() != '[') throw new SimpleCommandExceptionType(ERR_NO_BRACKET).createWithContext(reader); + // 跳过方括号 reader.skip(); Map map = new Object2ObjectOpenHashMap<>(); - boolean closeBracketMet = false; - while (reader.canRead()) - { - if (reader.peek() == ']') - { - closeBracketMet = true; - break; - } + var values = this.parseAll(reader, ',', ']'); - int beginCursor = reader.getCursor(); - var parseResult = parseOnce(reader, ',', ']'); + // Check last + var peekReader = new StringReader(reader); + peekReader.setCursor(reader.canRead() ? reader.getCursor() : reader.getTotalLength() - 1); + + // 读完了但是没有遇到闭合括号! + if (peekReader.peek() != ']') + throw new SimpleCommandExceptionType(Component.translatable("parsing.expected", "]")).createWithContext(reader); - if (parseResult.key == null) + for (KeyValuePair pair : values) + { + if (pair.key == null) { - reader.setCursor(beginCursor); + reader.setCursor(pair.keyCursor); throw new SimpleCommandExceptionType(ERR_EMPTY_KEY).createWithContext(reader); } - if (parseResult.value != null) + if (pair.value == null) { - if (map.containsKey(parseResult.key)) - { - var index = reader.getString().substring(beginCursor).indexOf(parseResult.key); - reader.setCursor(beginCursor + index); - - throw forDuplicateKey(parseResult.key).createWithContext(reader); - } - - map.put(parseResult.key, parseResult.value); + reader.setCursor(pair.keyCursor); + throw errorForKeyNoValue(pair.key).createWithContext(reader); } - else - { - var index = reader.getString().substring(beginCursor).indexOf(parseResult.key); - reader.setCursor(beginCursor + index); - throw forKeyNoValue(parseResult.key).createWithContext(reader); + if (map.containsKey(pair.key)) + { + reader.setCursor(pair.keyCursor); + throw errorForDuplicateKey(pair.key).createWithContext(reader); } - } - if (closeBracketMet) - reader.skip(); - else - throw new SimpleCommandExceptionType(Component.translatable("parsing.expected", "]")).createWithContext(reader); + map.put(pair.key, pair.value); + } return map; } - public record KeyValuePair(@Nullable String key, @Nullable String value) + public record KeyValuePair(@Nullable String key, @Nullable String value, int keyCursor, int valueCursor) { + /** + * 输入中是否存在等于号 + */ + public boolean metEqual() + { + return valueCursor != keyCursor; + } } /** - * @return A string, NULL if the input equals 'terminator' + * + * @apiNote 总是会停在 terminator 和 endOfString 上 + * @param reader + * @param terminator + * @param endOfString + * @return + */ + public List parseAll(StringReader reader, char terminator, char endOfString) + { + List list = new ObjectArrayList<>(); + + while (reader.canRead()) + { + KeyValuePair pair = null; + + try + { + pair = this.parseOnce(reader, terminator, endOfString); + } + catch (Throwable t) + { + //log.error("Error parsing arguments: " + t.getMessage()); + break; + } + + //log.info("[parseAll] Adding " + pair); + + list.add(pair); + + // 如果我们没有读到末尾 + if (reader.canRead()) + { + char peek = reader.peek(); + + if (peek == terminator) + { + reader.skip(); + + if (!reader.canRead()) // 如果Terminator后面没有东西,那么结束读取 + { + //log.info("[parseAll] EOF after terminator! Adding new NULL"); + list.add(new KeyValuePair(null, null, reader.getCursor(), reader.getCursor())); + break; + } + } + + if (peek == endOfString) + break; + } + + if (pair.key == null) + break; + } + + return list; + } + + /** + * @return A string, NULL if the input equals 'terminator' or 'endOfString' * @throws CommandSyntaxException */ + // Possible end at: + // [a=b] ||| [a=b,c=d] ||| [a=b + // ^ ^ ^ @NotNull - public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfString) throws CommandSyntaxException + public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfString) { //log.info("Starting read... Peek is '%s'".formatted(reader.peek())); StringBuilder keyStringBuilder = new StringBuilder(); @Nullable StringBuilder valueStringBuilder = null; + int keyCursor = reader.getCursor(); + int valueCursor = reader.getCursor(); boolean isKey = true; String key = null; String value = null; - while (true) + while (reader.canRead()) { - if (!reader.canRead()) - break; - char next = reader.peek(); + //log.info("[parseOnce] Next is " + next); + // 如果遇到了闭合括号,break; if (next == endOfString) break; - // Next变Current - reader.skip(); - - //log.info("Current: '%s'".formatted(next)); - // 遇到了结束符 if (next == terminator) break; + char current = next; + + reader.skip(); + + //log.info("Current: '%s'".formatted(next)); + //region 识别Key var builder = isKey ? keyStringBuilder : valueStringBuilder; @@ -166,6 +370,8 @@ public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfSt if (next == '=' && isKey) { isKey = false; + valueCursor = reader.canRead() ? reader.getCursor() + 1 : reader.getCursor(); + continue; } @@ -175,9 +381,20 @@ public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfSt //endregion 识别Key // 遇到引号了 - if (StringReader.isQuotedStringStart(next)) + if (StringReader.isQuotedStringStart(current)) { - var str = reader.readStringUntil(next); + var quoteReader = new StringReader(reader); + String str; + + try + { + str = quoteReader.readStringUntil(current); + reader.setCursor(quoteReader.getCursor()); + } + catch (Throwable ignored) + { + str = reader.readUnquotedString(); + } //log.info("APPENDING QUOTE STRING [%s]".formatted(str)); builder.append(str); @@ -185,12 +402,12 @@ public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfSt } // 是空格 - if (Character.isWhitespace(next)) + if (Character.isWhitespace(current)) continue; // 其他情况 //log.info("APPENDING [%s]".formatted(current)); - builder.append(next); + builder.append(current); } if (!keyStringBuilder.isEmpty()) @@ -200,7 +417,7 @@ public KeyValuePair parseOnce(StringReader reader, char terminator, char endOfSt value = valueStringBuilder.toString(); //log.info("DONE! result is [%s]".formatted(builder.toString())); - return new KeyValuePair(key, value); + return new KeyValuePair(key, value, keyCursor, valueCursor); } @Override