diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitPlatform.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitPlatform.java index 9a6729b0a..21bd8791a 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitPlatform.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitPlatform.java @@ -31,34 +31,11 @@ public class BukkitPlatform implements Platform { Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command); } - @SuppressWarnings("unchecked") - @Override - public Object snbtToJava(String nbt) { - try { - Object tag = FastNMS.INSTANCE.method$TagParser$parseCompoundFully("{\"root\":" + nbt + "}"); - Map map = (Map) MRegistryOps.NBT.convertTo(MRegistryOps.JAVA, tag); - return map.get("root"); - } catch (CommandSyntaxException e) { - throw new LocalizedResourceConfigException("warning.config.type.snbt.invalid_syntax", e, nbt); - } - } - @Override public Tag jsonToSparrowNBT(JsonElement json) { return MRegistryOps.JSON.convertTo(MRegistryOps.SPARROW_NBT, json); } - @Override - public Tag snbtToSparrowNBT(String nbt) { - try { - Object tag = FastNMS.INSTANCE.method$TagParser$parseCompoundFully("{\"root\":" + nbt + "}"); - CompoundTag map = (CompoundTag) MRegistryOps.NBT.convertTo(MRegistryOps.SPARROW_NBT, tag); - return map.get("root"); - } catch (CommandSyntaxException e) { - throw new LocalizedResourceConfigException("warning.config.type.snbt.invalid_syntax", e, nbt); - } - } - @Override public Tag javaToSparrowNBT(Object object) { return MRegistryOps.JAVA.convertTo(MRegistryOps.SPARROW_NBT, object); diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml index 9e9be8c07..b4b992ed4 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -105,6 +105,32 @@ warning.config.type.vec3d: "Issue found in file - Failed to load warning.config.type.map: "Issue found in file - Failed to load '': Cannot cast '' to Map type for option ''." warning.config.type.aabb: "Issue found in file - Failed to load '': Cannot cast '' to AABB type for option ''." warning.config.type.snbt.invalid_syntax: "Issue found in file - Failed to load '': Invalid snbt syntax ''." +warning.config.type.snbt.invalid_syntax.parse_error: " at position : " +warning.config.type.snbt.invalid_syntax.here: "<--[HERE]" +warning.config.type.snbt.parser.expected_string_uuid: "Expected a string representing a valid UUID" +warning.config.type.snbt.parser.expected_number_or_boolean: "Expected a number or a boolean" +warning.config.type.snbt.parser.trailing: "Unexpected trailing data" +warning.config.type.snbt.parser.expected.compound: "Expected compound tag" +warning.config.type.snbt.parser.number_parse_failure: "Failed to parse number: " +warning.config.type.snbt.parser.expected_hex_escape: "Expected a character literal of length " +warning.config.type.snbt.parser.invalid_codepoint: "Invalid Unicode character value: " +warning.config.type.snbt.parser.no_such_operation: "No such operation: " +warning.config.type.snbt.parser.expected_integer_type: "Expected an integer number" +warning.config.type.snbt.parser.expected_float_type: "Expected a floating point number" +warning.config.type.snbt.parser.expected_non_negative_number: "Expected a non-negative number" +warning.config.type.snbt.parser.invalid_character_name: "Invalid Unicode character name" +warning.config.type.snbt.parser.invalid_array_element_type: "Invalid array element type" +warning.config.type.snbt.parser.invalid_unquoted_start: "Unquoted strings can't start with digits 0-9, + or -" +warning.config.type.snbt.parser.expected_unquoted_string: "Expected a valid unquoted string" +warning.config.type.snbt.parser.invalid_string_contents: "Invalid string contents" +warning.config.type.snbt.parser.expected_binary_numeral: "Expected a binary number" +warning.config.type.snbt.parser.underscore_not_allowed: "Underscore is not allowed in binary numerals" +warning.config.type.snbt.parser.expected_decimal_numeral: "Expected a decimal number" +warning.config.type.snbt.parser.expected_hex_numeral: "Expected a hexadecimal number" +warning.config.type.snbt.parser.empty_key: "Key cannot be empty" +warning.config.type.snbt.parser.leading_zero_not_allowed: "Decimal numbers can't start with 0" +warning.config.type.snbt.parser.infinity_not_allowed: "Non-finite numbers are not allowed" +warning.config.type.snbt.parser.incorrect: "Expected literal " warning.config.number.missing_type: "Issue found in file - The config '' is missing the required 'type' argument for number argument." warning.config.number.invalid_type: "Issue found in file - The config '' is using an invalid number argument type ''." warning.config.number.missing_argument: "Issue found in file - The config '' is missing the argument for 'number'." diff --git a/common-files/src/main/resources/translations/zh_cn.yml b/common-files/src/main/resources/translations/zh_cn.yml index 54850c5f5..71fbf6f5e 100644 --- a/common-files/src/main/resources/translations/zh_cn.yml +++ b/common-files/src/main/resources/translations/zh_cn.yml @@ -102,6 +102,32 @@ warning.config.type.vector3f: "在文件 发现问题 - 无法 warning.config.type.vec3d: "在文件 发现问题 - 无法加载 '': 无法将 '' 转换为双精度浮点数三维向量类型 (选项 '')" warning.config.type.map: "在文件 发现问题 - 无法加载 '': 无法将 '' 转换为映射类型 (选项 '')" warning.config.type.snbt.invalid_syntax: "在文件 发现问题 - 无法加载 '': 无效的 SNBT 语法 ''" +warning.config.type.snbt.invalid_syntax.parse_error: ",位于第个字符:" +warning.config.type.snbt.invalid_syntax.here: "<--[此处]" +warning.config.type.snbt.parser.expected_string_uuid: "应为表示有效UUID的字符串" +warning.config.type.snbt.parser.expected_number_or_boolean: "应为数字或布尔型" +warning.config.type.snbt.parser.trailing: "多余的尾随数据" +warning.config.type.snbt.parser.expected.compound: "应为复合标签" +warning.config.type.snbt.parser.number_parse_failure: "解析数字失败:" +warning.config.type.snbt.parser.expected_hex_escape: "字符字面量长度应为" +warning.config.type.snbt.parser.invalid_codepoint: "无效的Unicode字符码位:" +warning.config.type.snbt.parser.no_such_operation: "不存在的操作: " +warning.config.type.snbt.parser.expected_integer_type: "应为整数" +warning.config.type.snbt.parser.expected_float_type: "应为浮点数" +warning.config.type.snbt.parser.expected_non_negative_number: "应为非负数" +warning.config.type.snbt.parser.invalid_character_name: "无效的Unicode字符名称" +warning.config.type.snbt.parser.invalid_array_element_type: "无效的数组元素类型" +warning.config.type.snbt.parser.invalid_unquoted_start: "无引号字符串不能以数字0-9、+或-开头" +warning.config.type.snbt.parser.expected_unquoted_string: "应为有效的无引号字符串" +warning.config.type.snbt.parser.invalid_string_contents: "无效的字符串内容" +warning.config.type.snbt.parser.expected_binary_numeral: "应为二进制数" +warning.config.type.snbt.parser.underscore_not_allowed: "数字的开头和结尾不允许使用下划线字符" +warning.config.type.snbt.parser.expected_decimal_numeral: "应为十进制数" +warning.config.type.snbt.parser.expected_hex_numeral: "应为十六进制数" +warning.config.type.snbt.parser.empty_key: "键不能为空" +warning.config.type.snbt.parser.leading_zero_not_allowed: "十进制数不能以0开头" +warning.config.type.snbt.parser.infinity_not_allowed: "不允许使用非有限数的数值" +warning.config.type.snbt.parser.incorrect: "应为字面量" warning.config.number.missing_type: "在文件 发现问题 - 配置项 '' 缺少数字类型所需的 'type' 参数" warning.config.number.invalid_type: "在文件 发现问题 - 配置项 '' 使用了无效的数字类型 ''" warning.config.number.missing_argument: "在文件 发现问题 - 配置项 '' 缺少数字参数" diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/modifier/ComponentsModifier.java b/core/src/main/java/net/momirealms/craftengine/core/item/modifier/ComponentsModifier.java index 143e4572f..e5f551690 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/modifier/ComponentsModifier.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/modifier/ComponentsModifier.java @@ -1,12 +1,15 @@ package net.momirealms.craftengine.core.item.modifier; import com.google.gson.JsonElement; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.momirealms.craftengine.core.item.*; import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; import net.momirealms.craftengine.core.util.GsonHelper; import net.momirealms.craftengine.core.util.Key; import net.momirealms.craftengine.core.util.Pair; import net.momirealms.craftengine.core.util.ResourceConfigUtils; +import net.momirealms.craftengine.core.util.snbt.TagParser; import net.momirealms.sparrow.nbt.CompoundTag; import net.momirealms.sparrow.nbt.Tag; @@ -41,7 +44,12 @@ public class ComponentsModifier implements ItemDataModifier { if (string.startsWith("(json) ")) { return CraftEngine.instance().platform().jsonToSparrowNBT(GsonHelper.get().fromJson(string.substring("(json) ".length()), JsonElement.class)); } else if (string.startsWith("(snbt) ")) { - return CraftEngine.instance().platform().snbtToSparrowNBT(string.substring("(snbt) ".length())); + String snbt = string.substring("(snbt) ".length()); + try { + return TagParser.parseTagFully(snbt); + } catch (CommandSyntaxException e) { + throw new LocalizedResourceConfigException("warning.config.type.snbt.invalid_syntax", e.getMessage()); + } } } return CraftEngine.instance().platform().javaToSparrowNBT(value); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/Platform.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/Platform.java index 6a8ec531b..1195d6904 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/Platform.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/Platform.java @@ -10,12 +10,8 @@ public interface Platform { void dispatchCommand(String command); - Object snbtToJava(String nbt); - Tag jsonToSparrowNBT(JsonElement json); - Tag snbtToSparrowNBT(String nbt); - Tag javaToSparrowNBT(Object object); World getWorld(String name); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/ArgumentString.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/ArgumentString.java index f3846da86..15f821b76 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/ArgumentString.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/ArgumentString.java @@ -1,8 +1,9 @@ package net.momirealms.craftengine.core.plugin.config.template; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.momirealms.craftengine.core.plugin.config.template.argument.TemplateArgument; import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; -import net.momirealms.craftengine.core.util.SNBTReader; +import net.momirealms.craftengine.core.util.snbt.TagParser; import java.util.ArrayList; import java.util.List; @@ -67,8 +68,14 @@ public interface ArgumentString { } else { this.placeholder = placeholderContent.substring(0, separatorIndex); String defaultValueString = placeholderContent.substring(separatorIndex + 2); + Object parsed; try { - this.defaultValue = ((TemplateManagerImpl) TemplateManager.INSTANCE).preprocessUnknownValue(new SNBTReader(defaultValueString).deserializeAsJava()); + parsed = TagParser.parseObjectFully(defaultValueString); + } catch (CommandSyntaxException e) { + throw new LocalizedResourceConfigException("warning.config.type.snbt.invalid_syntax", e.getMessage()); + } + try { + this.defaultValue = ((TemplateManagerImpl) TemplateManager.INSTANCE).preprocessUnknownValue(parsed); } catch (LocalizedResourceConfigException e) { e.appendTailArgument(this.placeholder); throw e; diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/MiscUtils.java b/core/src/main/java/net/momirealms/craftengine/core/util/MiscUtils.java index 523bbc364..6f327817a 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/MiscUtils.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/MiscUtils.java @@ -415,4 +415,8 @@ public class MiscUtils { } return false; } + + public static int growByHalf(int value, int minValue) { + return (int) Math.max(Math.min((long) value + (value >> 1), 2147483639L), minValue); + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/SNBTReader.java b/core/src/main/java/net/momirealms/craftengine/core/util/SNBTReader.java deleted file mode 100644 index 0be81388d..000000000 --- a/core/src/main/java/net/momirealms/craftengine/core/util/SNBTReader.java +++ /dev/null @@ -1,289 +0,0 @@ -package net.momirealms.craftengine.core.util; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -public final class SNBTReader extends DefaultStringReader { - private static final char COMPOUND_START = '{'; - private static final char COMPOUND_END = '}'; - private static final char LIST_START = '['; - private static final char LIST_END = ']'; - private static final char STRING_DELIMITER = '"'; - private static final char SINGLE_QUOTES = '\''; - private static final char DOUBLE_QUOTES = '"'; - private static final char KEY_VALUE_SEPARATOR = ':'; - private static final char ELEMENT_SEPARATOR = ','; - - private static final char ARRAY_DELIMITER = ';'; - private static final char BYTE_ARRAY = 'b'; - private static final char INT_ARRAY = 'i'; - private static final char LONG_ARRAY = 'l'; - - public SNBTReader(String content) { - super(content); - } - - public Object deserializeAsJava() { - Object result = this.parseValue(); - this.skipWhitespace(); - if (getCursor() != getTotalLength()) - throw new IllegalArgumentException("Extra content at end: " + substring(getCursor(), getTotalLength())); - return result; - } - - // 开始解析, 步进字符. - private Object parseValue() { - skipWhitespace(); - return switch (peek()) { - case COMPOUND_START -> parseCompound(); - case LIST_START -> parseList(); - case DOUBLE_QUOTES -> { - skip(); - yield readStringUntil(DOUBLE_QUOTES); - } - case SINGLE_QUOTES -> { - skip(); - yield readStringUntil(SINGLE_QUOTES); - } - default -> parsePrimitive(); - }; - } - - // 解析包小肠 {} - private Map parseCompound() { - skip(); // 跳过 '{' - skipWhitespace(); - - Map compoundMap = new LinkedHashMap<>(); - - if (canRead() && peek() != COMPOUND_END) { - do { - String key = parseKey(); - if (!canRead() || peek() != KEY_VALUE_SEPARATOR) { - throw new IllegalArgumentException("Expected ':' at position " + getCursor()); - } - skip(); // 跳过 ':' - Object value = parseValue(); - compoundMap.put(key, value); - skipWhitespace(); - } while (canRead() && peek() == ELEMENT_SEPARATOR && ++super.cursor > 0 /* 跳过 ',' */); - } - - if (!canRead() || peek() != COMPOUND_END) { - throw new IllegalArgumentException("Expected '}' at position " + getCursor()); - } - skip(); // 跳过 '}' - return compoundMap; - } - - // 解析列表值 [1, 2, 3] - private Object parseList() { - skip(); // 跳过 '[' - skipWhitespace(); - - // 检查接下来的2个非空格字符, 确认是否要走数组解析. - if (canRead()) { - setMarker(cursor); // 记录指针, 尝试解析数组. - char typeChar = Character.toLowerCase(peek()); - if (typeChar == BYTE_ARRAY || typeChar == INT_ARRAY || typeChar == LONG_ARRAY) { - skip(); - skipWhitespace(); - if (canRead() && peek() == ARRAY_DELIMITER) { // 下一个必须是 ';' - skip(); - switch (typeChar) { // 解析并返回数组喵 - case BYTE_ARRAY -> { - return parseArray(list -> { - byte[] bytes = new byte[list.size()]; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = list.get(i).byteValue(); - } - return bytes; - }); - } - case INT_ARRAY -> { - return parseArray(list -> { - int[] ints = new int[list.size()]; - for (int i = 0; i < ints.length; i++) { - ints[i] = list.get(i).intValue(); - } - return ints; - }); - } - case LONG_ARRAY -> { - return parseArray(list -> { - long[] longs = new long[list.size()]; - for (int i = 0; i < longs.length; i++) { - longs[i] = list.get(i).longValue(); - } - return longs; - }); - } - } - } - } - restore(); // 复原指针. - } - - List elementList = new ArrayList<>(); - - if (canRead() && peek() != LIST_END) { - do { - elementList.add(parseValue()); - skipWhitespace(); - } while (canRead() && peek() == ELEMENT_SEPARATOR && ++super.cursor > 0 /* 跳过 ',' */); - } - - if (!canRead() || peek() != LIST_END) { - throw new IllegalArgumentException("Expected ']' at position " + getCursor()); - } - skip(); // 跳过 ']' - return elementList; - } - - // 解析数组 [I; 11, 41, 54] - // ArrayType -> B, I, L. - private Object parseArray(Function, Object> convertor) { - skipWhitespace(); - // 用来暂存解析出的数字 - List elements = new ArrayList<>(); - if (canRead() && peek() != LIST_END) { - do { - Object element = parseValue(); - - // 1.21.6的SNBT原版是支持 {key:[B;1,2b,0xFF]} 这种奇葩写法的, 越界部分会被自动舍弃, 如0xff的byte值为-1. - // 如果需要和原版对齐, 那么只需要判断是否是数字就行了. - // if (!(element instanceof Number number)) - // throw new IllegalArgumentException("Error element type at pos " + getCursor()); - if (!(element instanceof Number number)) - throw new IllegalArgumentException("Error parsing number at pos " + getCursor()); - - elements.add(number); // 校验通过后加入 - skipWhitespace(); - } while (canRead() && peek() == ELEMENT_SEPARATOR && ++cursor > 0 /* 跳过 ',' */); - } - - if (!canRead() || peek() != LIST_END) - throw new IllegalArgumentException("Expected ']' at position " + getCursor()); - skip(); // 跳过 ']' - return convertor.apply(elements); - } - - // 解析Key值 - private String parseKey() { - skipWhitespace(); - if (!canRead()) { - throw new IllegalArgumentException("Unterminated key at " + getCursor()); - } - - // 如果有双引号就委托给string解析处理. - char peek = peek(); - if (peek == STRING_DELIMITER) { - skip(); - return readStringUntil(STRING_DELIMITER); - } else if (peek == SINGLE_QUOTES) { - skip(); - return readStringUntil(SINGLE_QUOTES); - } - - int start = getCursor(); - while (canRead()) { - char c = peek(); - if (c == ' ') break; // 忽略 key 后面的空格, { a :1} 应当解析成 {a:1} - if (Character.isJavaIdentifierPart(c)) skip(); else break; - } - - String key = substring(start, getCursor()); - skipWhitespace(); // 跳过 key 后面的空格. - return key; - } - - // 解析原生值 - private Object parsePrimitive() { - // 先解析获取值的长度 - int tokenStart = getCursor(); - int lastWhitespace = 0; // 记录值末尾的空格数量,{a:炒鸡 大保健} 和 {a: 炒鸡 大保健 } 都应解析成 "炒鸡 大保健". - boolean contentHasWhitespace = false; // 记录值中有没有空格. - while (canRead()) { - char c = peek(); - if (c == ',' || c == ']' || c == '}') break; - skip(); - if (c == ' ') { - lastWhitespace++; // 遇到空格先增加值, 代表值尾部空格数量. - continue; - } - if (lastWhitespace > 0) { - lastWhitespace = 0; // 遇到正常字符时清空记录的尾部空格数. - contentHasWhitespace = true; - } - } - int tokenLength = getCursor() - tokenStart - lastWhitespace; // 计算值长度需要再减去尾部空格. - if (tokenLength == 0) return null; // 如果值长度为0则返回null. - if (contentHasWhitespace) return substring(tokenStart, tokenStart + tokenLength); // 如果值的中间有空格, 一定是字符串, 可直接返回. - - // 布尔值检查 - if (tokenLength == 4) { - if (matchesAt(tokenStart, "true")) return Boolean.TRUE; - if (matchesAt(tokenStart, "null")) return null; // 支持 {key:null}. - } else if (tokenLength == 5) { - if (matchesAt(tokenStart, "false")) return Boolean.FALSE; - } - if (tokenLength > 1) { - // 至少有1个字符,给了后缀的可能性 - char lastChar = charAt(tokenStart + tokenLength - 1); - try { - switch (lastChar) { - case 'b', 'B' -> { - return Byte.parseByte(substring(tokenStart, tokenStart + tokenLength - 1)); - } - case 's', 'S' -> { - return Short.parseShort(substring(tokenStart, tokenStart + tokenLength - 1)); - } - case 'l', 'L' -> { - return Long.parseLong(substring(tokenStart, tokenStart + tokenLength - 1)); - } - case 'f', 'F' -> { - return Float.parseFloat(substring(tokenStart, tokenStart + tokenLength)); - } - case 'd', 'D' -> { - return Double.parseDouble(substring(tokenStart, tokenStart + tokenLength)); - } - default -> { - String fullString = substring(tokenStart, tokenStart + tokenLength); - try { - double d = Double.parseDouble(fullString); - if (d % 1 != 0 || fullString.contains(".") || fullString.contains("e")) { - return d; - } else { - return (int) d; - } - } catch (NumberFormatException e) { - return fullString; - } - } - } - } catch (NumberFormatException e) { - return substring(tokenStart, tokenStart + tokenLength); - } - } else { - char onlyChar = charAt(tokenStart); - if (isNumber(onlyChar)) { - return onlyChar - '0'; - } else { - return String.valueOf(onlyChar); - } - } - } - - // 工具函数: 快速检查布尔值字符串匹配, 忽略大小写. - private boolean matchesAt(int start, String target) { - for (int i = 0; i < target.length(); i++) { - char c1 = charAt(start + i); - char c2 = target.charAt(i); - if (c1 != c2 && c1 != (c2 ^ 32)) return false; // 忽略大小写比较 - } - return true; - } -} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtGrammar.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtGrammar.java new file mode 100644 index 000000000..0ff4982f2 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtGrammar.java @@ -0,0 +1,1015 @@ +package net.momirealms.craftengine.core.util.snbt; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JavaOps; +import it.unimi.dsi.fastutil.bytes.ByteArrayList; +import it.unimi.dsi.fastutil.bytes.ByteList; +import it.unimi.dsi.fastutil.chars.CharList; +import net.momirealms.craftengine.core.util.VersionHelper; +import net.momirealms.craftengine.core.util.snbt.parse.*; +import net.momirealms.craftengine.core.util.snbt.parse.Dictionary; +import net.momirealms.craftengine.core.util.snbt.parse.grammar.*; +import net.momirealms.sparrow.nbt.codec.LegacyJavaOps; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +public class SnbtGrammar { + private static final DynamicCommandExceptionType ERROR_NUMBER_PARSE_FAILURE = new LocalizedDynamicCommandExceptionType( + message -> new LocalizedMessage("warning.config.type.snbt.parser.number_parse_failure", String.valueOf(message)) + ); + static final DynamicCommandExceptionType ERROR_EXPECTED_HEX_ESCAPE = new LocalizedDynamicCommandExceptionType( + length -> new LocalizedMessage("warning.config.type.snbt.parser.expected_hex_escape", String.valueOf(length)) + ); + private static final DynamicCommandExceptionType ERROR_INVALID_CODEPOINT = new LocalizedDynamicCommandExceptionType( + codepoint -> new LocalizedMessage("warning.config.type.snbt.parser.invalid_codepoint", String.valueOf(codepoint)) + ); + private static final DynamicCommandExceptionType ERROR_NO_SUCH_OPERATION = new LocalizedDynamicCommandExceptionType( + operation -> new LocalizedMessage("warning.config.type.snbt.parser.no_such_operation", String.valueOf(operation)) + ); + static final DelayedException ERROR_EXPECTED_INTEGER_TYPE = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_integer_type")) + ); + private static final DelayedException ERROR_EXPECTED_FLOAT_TYPE = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_float_type")) + ); + static final DelayedException ERROR_EXPECTED_NON_NEGATIVE_NUMBER = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_non_negative_number")) + ); + private static final DelayedException ERROR_INVALID_CHARACTER_NAME = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.invalid_character_name")) + ); + static final DelayedException ERROR_INVALID_ARRAY_ELEMENT_TYPE = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.invalid_array_element_type")) + ); + private static final DelayedException ERROR_INVALID_UNQUOTED_START = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.invalid_unquoted_start")) + ); + private static final DelayedException ERROR_EXPECTED_UNQUOTED_STRING = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_unquoted_string")) + ); + private static final DelayedException ERROR_INVALID_STRING_CONTENTS = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.invalid_string_contents")) + ); + private static final DelayedException ERROR_EXPECTED_BINARY_NUMERAL = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_binary_numeral")) + ); + private static final DelayedException ERROR_UNDERSCORE_NOT_ALLOWED = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.underscore_not_allowed")) + ); + private static final DelayedException ERROR_EXPECTED_DECIMAL_NUMERAL = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_decimal_numeral")) + ); + private static final DelayedException ERROR_EXPECTED_HEX_NUMERAL = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_hex_numeral")) + ); + private static final DelayedException ERROR_EMPTY_KEY = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.empty_key")) + ); + private static final DelayedException ERROR_LEADING_ZERO_NOT_ALLOWED = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.leading_zero_not_allowed")) + ); + private static final DelayedException ERROR_INFINITY_NOT_ALLOWED = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.infinity_not_allowed")) + ); + private static final NumberRunParseRule BINARY_NUMERAL = new NumberRunParseRule(ERROR_EXPECTED_BINARY_NUMERAL, ERROR_UNDERSCORE_NOT_ALLOWED) { + @Override + protected boolean isAccepted(char c) { + return switch (c) { + case '0', '1', '_' -> true; + default -> false; + }; + } + }; + private static final NumberRunParseRule DECIMAL_NUMERAL = new NumberRunParseRule(ERROR_EXPECTED_DECIMAL_NUMERAL, ERROR_UNDERSCORE_NOT_ALLOWED) { + @Override + protected boolean isAccepted(char c) { + return switch (c) { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_' -> true; + default -> false; + }; + } + }; + private static final NumberRunParseRule HEX_NUMERAL = new NumberRunParseRule(ERROR_EXPECTED_HEX_NUMERAL, ERROR_UNDERSCORE_NOT_ALLOWED) { + @Override + protected boolean isAccepted(char c) { + return switch (c) { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', '_', 'a', 'b', 'c', 'd', 'e', 'f' -> true; + default -> false; + }; + } + }; + private static final GreedyPredicateParseRule PLAIN_STRING_CHUNK = new GreedyPredicateParseRule(1, ERROR_INVALID_STRING_CONTENTS) { + @Override + protected boolean isAccepted(char c) { + return switch (c) { + case '"', '\'', '\\' -> false; + default -> true; + }; + } + }; + private static final StringReaderTerms.TerminalCharacters NUMBER_LOOKEAHEAD = new StringReaderTerms.TerminalCharacters(CharList.of()) { + @Override + protected boolean isAccepted(char c) { + return canStartNumber(c); + } + }; + private static final Pattern UNICODE_NAME = Pattern.compile("[-a-zA-Z0-9 ]+"); + + static DelayedException createNumberParseError(NumberFormatException ex) { + return DelayedException.create(ERROR_NUMBER_PARSE_FAILURE, ex.getMessage()); + } + + private static boolean isAllowedToStartUnquotedString(char c) { + return !canStartNumber(c); + } + + static boolean canStartNumber(char c) { + return switch (c) { + case '+', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> true; + default -> false; + }; + } + + static boolean needsUnderscoreRemoval(String contents) { + return contents.indexOf(95) != -1; + } + + private static void cleanAndAppend(StringBuilder output, String contents) { + cleanAndAppend(output, contents, needsUnderscoreRemoval(contents)); + } + + static void cleanAndAppend(StringBuilder output, String contents, boolean needsUnderscoreRemoval) { + if (needsUnderscoreRemoval) { + for (char c : contents.toCharArray()) { + if (c != '_') { + output.append(c); + } + } + return; + } + output.append(contents); + } + + static short parseUnsignedShort(String string, int radix) { + int parse = Integer.parseInt(string, radix); + if (parse >> 16 == 0) { + return (short) parse; + } + throw new NumberFormatException("out of range: " + parse); + } + + @Nullable + private static T createFloat( + DynamicOps ops, + Sign sign, + @Nullable String whole, + @Nullable String fraction, + @Nullable Signed exponent, + @Nullable TypeSuffix typeSuffix, + ParseState state + ) { + StringBuilder result = new StringBuilder(); + sign.append(result); + if (whole != null) { + cleanAndAppend(result, whole); + } + + if (fraction != null) { + result.append('.'); + cleanAndAppend(result, fraction); + } + + if (exponent != null) { + result.append('e'); + exponent.sign().append(result); + cleanAndAppend(result, exponent.value); + } + + try { + String string = result.toString(); + + return switch (typeSuffix) { + case null -> convertDouble(ops, state, string); + case FLOAT -> convertFloat(ops, state, string); + case DOUBLE -> convertDouble(ops, state, string); + default -> { + state.errorCollector().store(state.mark(), ERROR_EXPECTED_FLOAT_TYPE); + yield null; + } + }; + } catch (NumberFormatException e) { + state.errorCollector().store(state.mark(), createNumberParseError(e)); + return null; + } + } + + @Nullable + private static T convertFloat(DynamicOps ops, ParseState state, String contents) { + float value = Float.parseFloat(contents); + if (!Float.isFinite(value)) { + state.errorCollector().store(state.mark(), ERROR_INFINITY_NOT_ALLOWED); + return null; + } + return ops.createFloat(value); + } + + @Nullable + private static T convertDouble(DynamicOps ops, ParseState state, String contents) { + double value = Double.parseDouble(contents); + if (!Double.isFinite(value)) { + state.errorCollector().store(state.mark(), ERROR_INFINITY_NOT_ALLOWED); + return null; + } + return ops.createDouble(value); + } + + private static String joinList(List list) { + return switch (list.size()) { + case 0 -> ""; + case 1 -> list.getFirst(); + default -> String.join("", list); + }; + } + + @SuppressWarnings("unchecked") + public static Grammar createParser(DynamicOps ops) { + T trueValue = ops.createBoolean(true); + T falseValue = ops.createBoolean(false); + T emptyMapValue = ops.emptyMap(); + T emptyList = ops.emptyList(); + T nullString = ops.createString(SnbtOperations.BUILTIN_NULL); + boolean isJavaType = SnbtOperations.BUILTIN_NULL.equals(nullString); // 确定是 Java 类型的 + + Dictionary rules = new Dictionary<>(); + + // 符号解析规则 + Atom sign = Atom.of("sign"); + rules.put( + sign, + Term.alternative( + Term.sequence(StringReaderTerms.character('+'), Term.marker(sign, Sign.PLUS)), + Term.sequence(StringReaderTerms.character('-'), Term.marker(sign, Sign.MINUS)) + ), + scope -> scope.getOrThrow(sign) + ); + + // 整数后缀解析规则 + Atom integerSuffix = Atom.of("integer_suffix"); + rules.put( + integerSuffix, + Term.alternative( + Term.sequence( + StringReaderTerms.characters('u', 'U'), + Term.alternative( + Term.sequence(StringReaderTerms.characters('b', 'B'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.BYTE))), + Term.sequence(StringReaderTerms.characters('s', 'S'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.SHORT))), + Term.sequence(StringReaderTerms.characters('i', 'I'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.INT))), + Term.sequence(StringReaderTerms.characters('l', 'L'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.LONG))) + ) + ), + Term.sequence( + StringReaderTerms.characters('s', 'S'), + Term.alternative( + Term.sequence(StringReaderTerms.characters('b', 'B'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.BYTE))), + Term.sequence(StringReaderTerms.characters('s', 'S'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.SHORT))), + Term.sequence(StringReaderTerms.characters('i', 'I'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.INT))), + Term.sequence(StringReaderTerms.characters('l', 'L'), Term.marker(integerSuffix, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.LONG))) + ) + ), + Term.sequence(StringReaderTerms.characters('b', 'B'), Term.marker(integerSuffix, new IntegerSuffix(null, TypeSuffix.BYTE))), + Term.sequence(StringReaderTerms.characters('s', 'S'), Term.marker(integerSuffix, new IntegerSuffix(null, TypeSuffix.SHORT))), + Term.sequence(StringReaderTerms.characters('i', 'I'), Term.marker(integerSuffix, new IntegerSuffix(null, TypeSuffix.INT))), + Term.sequence(StringReaderTerms.characters('l', 'L'), Term.marker(integerSuffix, new IntegerSuffix(null, TypeSuffix.LONG))) + ), + scope -> scope.getOrThrow(integerSuffix) + ); + + // 二进制解析规则 + Atom binaryNumeral = Atom.of("binary_numeral"); + rules.put(binaryNumeral, BINARY_NUMERAL); + + // 十进制解析规则 + Atom decimalNumeral = Atom.of("decimal_numeral"); + rules.put(decimalNumeral, DECIMAL_NUMERAL); + + // 十六进制解析规则 + Atom hexNumeral = Atom.of("hex_numeral"); + rules.put(hexNumeral, HEX_NUMERAL); + + // 整数常量解析规则 + Atom integerLiteral = Atom.of("integer_literal"); + NamedRule integerLiteralRule = rules.put( + integerLiteral, + Term.sequence( + Term.optional(rules.named(sign)), + Term.alternative( + Term.sequence( + StringReaderTerms.character('0'), + Term.cut(), + Term.alternative( + Term.sequence(StringReaderTerms.characters('x', 'X'), Term.cut(), rules.named(hexNumeral)), + Term.sequence(StringReaderTerms.characters('b', 'B'), rules.named(binaryNumeral)), + Term.sequence(rules.named(decimalNumeral), Term.cut(), Term.fail(ERROR_LEADING_ZERO_NOT_ALLOWED)), + Term.marker(decimalNumeral, "0") + ) + ), + rules.named(decimalNumeral) + ), + Term.optional(rules.named(integerSuffix)) + ), + scope -> { + IntegerSuffix suffix = scope.getOrDefault(integerSuffix, IntegerSuffix.EMPTY); + Sign signValue = scope.getOrDefault(sign, Sign.PLUS); + String decimalContents = scope.get(decimalNumeral); + if (decimalContents != null) { + return new IntegerLiteral(signValue, Base.DECIMAL, decimalContents, suffix); + } + String hexContents = scope.get(hexNumeral); + if (hexContents != null) { + return new IntegerLiteral(signValue, Base.HEX, hexContents, suffix); + } + String binaryContents = scope.getOrThrow(binaryNumeral); + return new IntegerLiteral(signValue, Base.BINARY, binaryContents, suffix); + } + ); + + // 浮点型后缀解析规则 + Atom floatTypeSuffix = Atom.of("float_type_suffix"); + rules.put( + floatTypeSuffix, + Term.alternative( + Term.sequence(StringReaderTerms.characters('f', 'F'), Term.marker(floatTypeSuffix, TypeSuffix.FLOAT)), + Term.sequence(StringReaderTerms.characters('d', 'D'), Term.marker(floatTypeSuffix, TypeSuffix.DOUBLE)) + ), + scope -> scope.getOrThrow(floatTypeSuffix) + ); + + // 浮点数指数部分解析规则 + Atom> floatExponentPart = Atom.of("float_exponent_part"); + rules.put( + floatExponentPart, + Term.sequence( + StringReaderTerms.characters('e', 'E'), + Term.optional(rules.named(sign)), + rules.named(decimalNumeral) + ), + scope -> new Signed<>(scope.getOrDefault(sign, Sign.PLUS), scope.getOrThrow(decimalNumeral)) + ); + + // 浮点数常量解析规则 + Atom floatWholePart = Atom.of("float_whole_part"); // 整数部分 + Atom floatFractionPart = Atom.of("float_fraction_part"); // 小数部分 + Atom floatLiteral = Atom.of("float_literal"); + rules.putComplex( + floatLiteral, + Term.sequence( + Term.optional(rules.named(sign)), + Term.alternative( + Term.sequence( + rules.namedWithAlias(decimalNumeral, floatWholePart), + StringReaderTerms.character('.'), + Term.cut(), + Term.optional(rules.namedWithAlias(decimalNumeral, floatFractionPart)), + Term.optional(rules.named(floatExponentPart)), + Term.optional(rules.named(floatTypeSuffix)) + ), + Term.sequence( + StringReaderTerms.character('.'), + Term.cut(), + rules.namedWithAlias(decimalNumeral, floatFractionPart), + Term.optional(rules.named(floatExponentPart)), + Term.optional(rules.named(floatTypeSuffix)) + ), + Term.sequence( + rules.namedWithAlias(decimalNumeral, floatWholePart), + rules.named(floatExponentPart), + Term.cut(), + Term.optional(rules.named(floatTypeSuffix)) + ), + Term.sequence( + rules.namedWithAlias(decimalNumeral, floatWholePart), + Term.optional(rules.named(floatExponentPart)), + rules.named(floatTypeSuffix) + ) + ) + ), + state -> { + Scope scope = state.scope(); + Sign wholeSign = scope.getOrDefault(sign, Sign.PLUS); + String whole = scope.get(floatWholePart); + String fraction = scope.get(floatFractionPart); + Signed exponent = scope.get(floatExponentPart); + TypeSuffix typeSuffix = scope.get(floatTypeSuffix); + return createFloat(ops, wholeSign, whole, fraction, exponent, typeSuffix, state); + } + ); + + // 二位十六进制字符串解析规则 + Atom stringHex2 = Atom.of("string_hex_2"); + rules.put(stringHex2, new SimpleHexLiteralParseRule(2)); + + // 四位十六进制字符串解析规则 + Atom stringHex4 = Atom.of("string_hex_4"); + rules.put(stringHex4, new SimpleHexLiteralParseRule(4)); + + // 八位十六进制字符串解析规则 + Atom stringHex8 = Atom.of("string_hex_8"); + rules.put(stringHex8, new SimpleHexLiteralParseRule(8)); + + // Unicode名称字符串解析规则 + Atom stringUnicodeName = Atom.of("string_unicode_name"); + rules.put(stringUnicodeName, new GreedyPatternParseRule(UNICODE_NAME, ERROR_INVALID_CHARACTER_NAME)); + + // 字符串转义序列解析规则 + Atom stringEscapeSequence = Atom.of("string_escape_sequence"); + rules.putComplex( + stringEscapeSequence, + Term.alternative( + Term.sequence(StringReaderTerms.character('b'), Term.marker(stringEscapeSequence, "\b")), + Term.sequence(StringReaderTerms.character('s'), Term.marker(stringEscapeSequence, " ")), + Term.sequence(StringReaderTerms.character('t'), Term.marker(stringEscapeSequence, "\t")), + Term.sequence(StringReaderTerms.character('n'), Term.marker(stringEscapeSequence, "\n")), + Term.sequence(StringReaderTerms.character('f'), Term.marker(stringEscapeSequence, "\f")), + Term.sequence(StringReaderTerms.character('r'), Term.marker(stringEscapeSequence, "\r")), + Term.sequence(StringReaderTerms.character('\\'), Term.marker(stringEscapeSequence, "\\")), + Term.sequence(StringReaderTerms.character('\''), Term.marker(stringEscapeSequence, "'")), + Term.sequence(StringReaderTerms.character('"'), Term.marker(stringEscapeSequence, "\"")), + Term.sequence(StringReaderTerms.character('x'), rules.named(stringHex2)), + Term.sequence(StringReaderTerms.character('u'), rules.named(stringHex4)), + Term.sequence(StringReaderTerms.character('U'), rules.named(stringHex8)), + Term.sequence( + StringReaderTerms.character('N'), + StringReaderTerms.character('{'), + rules.named(stringUnicodeName), + StringReaderTerms.character('}') + ) + ), + state -> { + Scope scope = state.scope(); + String plainEscape = scope.getAny(stringEscapeSequence); + if (plainEscape != null) { + return plainEscape; + } + String hexEscape = scope.getAny(stringHex2, stringHex4, stringHex8); + if (hexEscape != null) { + int codePoint = HexFormat.fromHexDigits(hexEscape); + if (!Character.isValidCodePoint(codePoint)) { + state.errorCollector().store(state.mark(), DelayedException.create(ERROR_INVALID_CODEPOINT, String.format(Locale.ROOT, "U+%08X", codePoint))); + return null; + } + return Character.toString(codePoint); + } + String character = scope.getOrThrow(stringUnicodeName); + + int codePoint; + try { + codePoint = Character.codePointOf(character); + } catch (IllegalArgumentException var12x) { + state.errorCollector().store(state.mark(), ERROR_INVALID_CHARACTER_NAME); + return null; + } + + return Character.toString(codePoint); + } + ); + + // 纯文本字符串解析规则 + Atom stringPlainContents = Atom.of("string_plain_contents"); + rules.put(stringPlainContents, PLAIN_STRING_CHUNK); + + // 字符串解析规则 + Atom> stringChunks = Atom.of("string_chunks"); // 字符串块 + Atom stringContents = Atom.of("string_contents"); // 字符串内容 + + // 单引号字符串块解析规则 + Atom singleQuotedStringChunk = Atom.of("single_quoted_string_chunk"); + NamedRule singleQuotedStringChunkRule = rules.put( + singleQuotedStringChunk, + Term.alternative( + rules.namedWithAlias(stringPlainContents, stringContents), + Term.sequence(StringReaderTerms.character('\\'), rules.namedWithAlias(stringEscapeSequence, stringContents)), + Term.sequence(StringReaderTerms.character('"'), Term.marker(stringContents, "\"")) + ), + scope -> scope.getOrThrow(stringContents) + ); + + // 单引号字符串内容解析规则 + Atom singleQuotedStringContents = Atom.of("single_quoted_string_contents"); + rules.put( + singleQuotedStringContents, + Term.repeated(singleQuotedStringChunkRule, stringChunks), + scope -> joinList(scope.getOrThrow(stringChunks)) + ); + + // 双引号字符串块解析规则 + Atom doubleQuotedStringChunk = Atom.of("double_quoted_string_chunk"); + NamedRule doubleQuotedStringChunkRule = rules.put( + doubleQuotedStringChunk, + Term.alternative( + rules.namedWithAlias(stringPlainContents, stringContents), + Term.sequence(StringReaderTerms.character('\\'), rules.namedWithAlias(stringEscapeSequence, stringContents)), + Term.sequence(StringReaderTerms.character('\''), Term.marker(stringContents, "'")) + ), + scope -> scope.getOrThrow(stringContents) + ); + + // 双引号字符串内容解析规则 + Atom doubleQuotedStringContents = Atom.of("double_quoted_string_contents"); + rules.put( + doubleQuotedStringContents, + Term.repeated(doubleQuotedStringChunkRule, stringChunks), + scope -> joinList(scope.getOrThrow(stringChunks)) + ); + + // 带引号的字符串字面量解析规则 + Atom quotedStringLiteral = Atom.of("quoted_string_literal"); + rules.put( + quotedStringLiteral, + Term.alternative( + Term.sequence( + StringReaderTerms.character('"'), + Term.cut(), + Term.optional(rules.namedWithAlias(doubleQuotedStringContents, stringContents)), + StringReaderTerms.character('"') + ), + Term.sequence( + StringReaderTerms.character('\''), + Term.optional(rules.namedWithAlias(singleQuotedStringContents, stringContents)), + StringReaderTerms.character('\'') + ) + ), + scope -> scope.getOrThrow(stringContents) + ); + + // 不带引号的字符串解析规则 + Atom unquotedString = Atom.of("unquoted_string"); + rules.put( + unquotedString, + new UnquotedStringParseRule(1, ERROR_EXPECTED_UNQUOTED_STRING) + ); + + // 列表解析规则 + Atom literal = Atom.of("literal"); // 字面量 + Atom> argumentList = Atom.of("arguments"); // 参数 + rules.put( + argumentList, + Term.repeatedWithTrailingSeparator( + rules.forward(literal), + argumentList, + StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR) + ), + scope -> scope.getOrThrow(argumentList) + ); + + // 不带引号的字符串或内置表达式解析规则 + Atom unquotedStringOrBuiltIn = Atom.of("unquoted_string_or_builtin"); + rules.putComplex( + unquotedStringOrBuiltIn, + Term.sequence( + rules.named(unquotedString), + Term.optional(Term.sequence( + StringReaderTerms.character('('), + rules.named(argumentList), + StringReaderTerms.character(')') + )) + ), + state -> { + Scope scope = state.scope(); + String contents = scope.getOrThrow(unquotedString); + if (!contents.isEmpty() && isAllowedToStartUnquotedString(contents.charAt(0))) { // 非空且是合法开头字符 + List arguments = scope.get(argumentList); + if (arguments != null) { // 带参数尝试解析内置表达式 + SnbtOperations.BuiltinKey key = new SnbtOperations.BuiltinKey(contents, arguments.size()); + SnbtOperations.BuiltinOperation operation = SnbtOperations.BUILTIN_OPERATIONS.get(key); + if (operation != null) { + return operation.run(ops, arguments, state); + } + state.errorCollector().store(state.mark(), DelayedException.create(ERROR_NO_SUCH_OPERATION, key.toString())); + return null; + } else if (contents.equalsIgnoreCase(SnbtOperations.BUILTIN_TRUE)) { // 解析不带引号的 true 为布尔值 + return trueValue; + } else if (contents.equalsIgnoreCase(SnbtOperations.BUILTIN_FALSE)) { // 解析不带引号的 false 为布尔值 + return falseValue; + } else if (contents.equalsIgnoreCase(SnbtOperations.BUILTIN_NULL)) { // 解析不带引号的 null 为空值,该功能并非标准 SNBT 语法 + return Objects.requireNonNullElseGet(ops.empty() /*一般来说这里都不会是null*/, () -> { + if (isJavaType) { + // 如果是 null 一般就是使用 Object 对象的 Java 类型,可以安全的强行转换 + return (T) CachedParseState.JAVA_NULL_VALUE_MARKER; + } + return nullString; // 意外情况保持 SNBT 默认行为 + }); + } + return ops.createString(contents); + } + state.errorCollector().store(state.mark(), SnbtOperations.BUILTIN_IDS, ERROR_INVALID_UNQUOTED_START); + return null; + } + ); + + // 映射键解析规则 + Atom mapKey = Atom.of("map_key"); + rules.put( + mapKey, + Term.alternative( + rules.named(quotedStringLiteral), + rules.named(unquotedString) + ), + scope -> scope.getAnyOrThrow(quotedStringLiteral, unquotedString) + ); + + // 映射条目解析规则 + Atom> mapEntry = Atom.of("map_entry"); + NamedRule> mapEntryRule = rules.putComplex( + mapEntry, + Term.sequence( + rules.named(mapKey), + StringReaderTerms.character(TagParser.NAME_VALUE_SEPARATOR), + rules.named(literal) + ), + state -> { + Scope scope = state.scope(); + String key = scope.getOrThrow(mapKey); + if (key.isEmpty()) { + state.errorCollector().store(state.mark(), ERROR_EMPTY_KEY); + return null; + } + T value = scope.getOrThrow(literal); + return Map.entry(key, value); + } + ); + + // 映射条目集合解析规则 + Atom>> mapEntries = Atom.of("map_entries"); + rules.put( + mapEntries, + Term.repeatedWithTrailingSeparator( + mapEntryRule, + mapEntries, + StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR) + ), + scope -> scope.getOrThrow(mapEntries) + ); + + // 映射字面量解析规则 + Atom mapLiteral = Atom.of("map_literal"); + rules.put( + mapLiteral, + Term.sequence( + StringReaderTerms.character('{'), + Scope.increaseDepth(), + rules.named(mapEntries), + Scope.decreaseDepth(), + StringReaderTerms.character('}') + ), + scope -> { + List> entries = scope.getOrThrow(mapEntries); + if (entries.isEmpty()) { + return emptyMapValue; + } + Builder builder = ImmutableMap.builderWithExpectedSize(entries.size()); + for (Entry e : entries) { + builder.put(ops.createString(e.getKey()), e.getValue()); + } + return ops.createMap(builder.buildKeepingLast()); + } + ); + + // 列表条目集合解析规则 + Atom> listEntries = Atom.of("list_entries"); + rules.put( + listEntries, + Term.repeatedWithTrailingSeparator( + rules.forward(literal), + listEntries, + StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR) + ), + scope -> scope.getOrThrow(listEntries) + ); + + // 数组前缀解析规则 + Atom arrayPrefix = Atom.of("array_prefix"); + rules.put( + arrayPrefix, + Term.alternative( + Term.sequence(StringReaderTerms.character('B'), Term.marker(arrayPrefix, ArrayPrefix.BYTE)), + Term.sequence(StringReaderTerms.character('L'), Term.marker(arrayPrefix, ArrayPrefix.LONG)), + Term.sequence(StringReaderTerms.character('I'), Term.marker(arrayPrefix, ArrayPrefix.INT)) + ), + scope -> scope.getOrThrow(arrayPrefix) + ); + + // 整数数组条目集合解析规则 + Atom> intArrayEntries = Atom.of("int_array_entries"); + rules.put( + intArrayEntries, + Term.repeatedWithTrailingSeparator( + integerLiteralRule, + intArrayEntries, + StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR) + ), + scope -> scope.getOrThrow(intArrayEntries) + ); + + // 列表字面量解析规则 + Atom listLiteral = Atom.of("list_literal"); + rules.putComplex( + listLiteral, + Term.sequence( + StringReaderTerms.character('['), + Scope.increaseDepth(), + Term.alternative( + Term.sequence( + rules.named(arrayPrefix), + StringReaderTerms.character(';'), + rules.named(intArrayEntries) + ), + rules.named(listEntries) + ), + Scope.decreaseDepth(), + StringReaderTerms.character(']') + ), + state -> { + Scope scope = state.scope(); + ArrayPrefix arrayType = scope.get(arrayPrefix); + if (arrayType != null) { + List entries = scope.getOrThrow(intArrayEntries); + return entries.isEmpty() ? arrayType.create(ops) : arrayType.create(ops, entries, state); + } + List entries = scope.getOrThrow(listEntries); + return entries.isEmpty() ? emptyList : ops.createList(entries.stream()); + } + ); + + // 基本规则解析规则 + NamedRule literalRule = rules.putComplex( + literal, + Term.alternative( + Term.sequence( + Term.positiveLookahead(NUMBER_LOOKEAHEAD), + Term.alternative( + rules.namedWithAlias(floatLiteral, literal), + rules.named(integerLiteral) + ) + ), + Term.sequence( + Term.positiveLookahead(StringReaderTerms.characters('"', '\'')), + Term.cut(), + rules.named(quotedStringLiteral) + ), + Term.sequence( + Term.positiveLookahead(StringReaderTerms.character('{')), + Term.cut(), + rules.namedWithAlias(mapLiteral, literal) + ), + Term.sequence( + Term.positiveLookahead(StringReaderTerms.character('[')), + Term.cut(), + rules.namedWithAlias(listLiteral, literal) + ), + rules.namedWithAlias(unquotedStringOrBuiltIn, literal) + ), + state -> { + Scope scope = state.scope(); + String quotedString = scope.get(quotedStringLiteral); + if (quotedString != null) { + return ops.createString(quotedString); + } + IntegerLiteral integer = scope.get(integerLiteral); + return integer != null ? integer.create(ops, state) : scope.getOrThrow(literal); + } + ); + + return new Grammar<>(rules, literalRule); + } + + enum ArrayPrefix { + BYTE(TypeSuffix.BYTE) { + private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new byte[0]); + + @Override + public T create(DynamicOps ops) { + return ops.createByteList(EMPTY_BUFFER); + } + + @Nullable + @Override + public T create(DynamicOps ops, List entries, ParseState state) { + ByteList result = new ByteArrayList(); + for (IntegerLiteral entry : entries) { + Number number = this.buildNumber(entry, state); + if (number == null) { + return null; + } + result.add(number.byteValue()); + } + return ops.createByteList(ByteBuffer.wrap(result.toByteArray())); + } + }, + INT(TypeSuffix.INT, TypeSuffix.BYTE, TypeSuffix.SHORT) { + @Override + public T create(DynamicOps ops) { + return ops.createIntList(IntStream.empty()); + } + + @Nullable + @Override + public T create(DynamicOps ops, List entries, ParseState state) { + IntStream.Builder result = IntStream.builder(); + for (IntegerLiteral entry : entries) { + Number parsedNumber = this.buildNumber(entry, state); + if (parsedNumber == null) { + return null; + } + result.add(parsedNumber.intValue()); + } + return ops.createIntList(result.build()); + } + }, + LONG(TypeSuffix.LONG, TypeSuffix.BYTE, TypeSuffix.SHORT, TypeSuffix.INT) { + @Override + public T create(DynamicOps ops) { + return ops.createLongList(LongStream.empty()); + } + + @Nullable + @Override + public T create(DynamicOps ops, List entries, ParseState state) { + LongStream.Builder result = LongStream.builder(); + for (IntegerLiteral entry : entries) { + Number parsedNumber = this.buildNumber(entry, state); + if (parsedNumber == null) { + return null; + } + result.add(parsedNumber.longValue()); + } + return ops.createLongList(result.build()); + } + }; + + private final TypeSuffix defaultType; + private final Set additionalTypes; + + ArrayPrefix(final TypeSuffix defaultType, final TypeSuffix... additionalTypes) { + this.additionalTypes = Set.of(additionalTypes); + this.defaultType = defaultType; + } + + public boolean isAllowed(TypeSuffix type) { + return type == this.defaultType || this.additionalTypes.contains(type); + } + public abstract T create(DynamicOps ops); + + @Nullable + public abstract T create(DynamicOps ops, List entries, ParseState state); + + @Nullable + protected Number buildNumber(IntegerLiteral entry, ParseState state) { + TypeSuffix actualType = this.computeType(entry.suffix); + if (actualType == null) { + state.errorCollector().store(state.mark(), ERROR_INVALID_ARRAY_ELEMENT_TYPE); + return null; + } + return (Number) entry.create(VersionHelper.isOrAbove1_20_5() ? JavaOps.INSTANCE : LegacyJavaOps.INSTANCE, actualType, state); + } + + @Nullable + private TypeSuffix computeType(IntegerSuffix value) { + TypeSuffix type = value.type(); + if (type == null) { + return this.defaultType; + } + return !this.isAllowed(type) ? null : type; + } + } + enum Base { + BINARY, + DECIMAL, + HEX + } + + record IntegerLiteral(Sign sign, Base base, String digits, IntegerSuffix suffix) { + private SignedPrefix signedOrDefault() { + return Objects.requireNonNullElseGet(this.suffix.signed, () -> switch (this.base) { + case BINARY, HEX -> SignedPrefix.UNSIGNED; + case DECIMAL -> SignedPrefix.SIGNED; + }); + } + + private String cleanupDigits(Sign sign) { + boolean needsUnderscoreRemoval = needsUnderscoreRemoval(this.digits); + if (sign != Sign.MINUS && !needsUnderscoreRemoval) { + return this.digits; + } + StringBuilder result = new StringBuilder(); + sign.append(result); + cleanAndAppend(result, this.digits, needsUnderscoreRemoval); + return result.toString(); + } + + @Nullable + public T create(DynamicOps ops, ParseState state) { + return this.create(ops, Objects.requireNonNullElse(this.suffix.type, TypeSuffix.INT), state); + } + + @Nullable + public T create(DynamicOps ops, TypeSuffix type, ParseState state) { + boolean isSigned = this.signedOrDefault() == SignedPrefix.SIGNED; + if (!isSigned && this.sign == Sign.MINUS) { + state.errorCollector().store(state.mark(), ERROR_EXPECTED_NON_NEGATIVE_NUMBER); + return null; + } + String fixedDigits = this.cleanupDigits(this.sign); + + int radix = switch (this.base) { + case BINARY -> 2; + case DECIMAL -> 10; + case HEX -> 16; + }; + + try { + if (isSigned) { + return switch (type) { + case BYTE -> ops.createByte(Byte.parseByte(fixedDigits, radix)); + case SHORT -> ops.createShort(Short.parseShort(fixedDigits, radix)); + case INT -> ops.createInt(Integer.parseInt(fixedDigits, radix)); + case LONG -> ops.createLong(Long.parseLong(fixedDigits, radix)); + default -> { + state.errorCollector().store(state.mark(), ERROR_EXPECTED_INTEGER_TYPE); + yield null; + } + }; + } + return switch (type) { + case BYTE -> ops.createByte(com.google.common.primitives.UnsignedBytes.parseUnsignedByte(fixedDigits, radix)); + case SHORT -> ops.createShort(parseUnsignedShort(fixedDigits, radix)); + case INT -> ops.createInt(Integer.parseUnsignedInt(fixedDigits, radix)); + case LONG -> ops.createLong(Long.parseUnsignedLong(fixedDigits, radix)); + default -> { + state.errorCollector().store(state.mark(), ERROR_EXPECTED_INTEGER_TYPE); + yield null; + } + }; + } catch (NumberFormatException var8) { + state.errorCollector().store(state.mark(), createNumberParseError(var8)); + return null; + } + } + } + + record IntegerSuffix(@Nullable SignedPrefix signed, @Nullable TypeSuffix type) { + public static final IntegerSuffix EMPTY = new IntegerSuffix(null, null); + } + + enum Sign { + PLUS, + MINUS; + + public void append(StringBuilder output) { + if (this == MINUS) { + output.append("-"); + } + } + } + + record Signed(Sign sign, T value) { + } + + enum SignedPrefix { + SIGNED, + UNSIGNED + } + + static class SimpleHexLiteralParseRule extends GreedyPredicateParseRule { + public SimpleHexLiteralParseRule(int size) { + super(size, size, DelayedException.create(ERROR_EXPECTED_HEX_ESCAPE, String.valueOf(size))); + } + + @Override + protected boolean isAccepted(char c) { + return switch (c) { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f' -> true; + default -> false; + }; + } + } + + enum TypeSuffix { + FLOAT, + DOUBLE, + BYTE, + SHORT, + INT, + LONG + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtOperations.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtOperations.java new file mode 100644 index 000000000..53a6fa7f0 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtOperations.java @@ -0,0 +1,92 @@ +package net.momirealms.craftengine.core.util.snbt; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.serialization.DynamicOps; +import net.momirealms.craftengine.core.util.snbt.parse.*; +import net.momirealms.sparrow.nbt.util.UUIDUtil; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class SnbtOperations { + static final DelayedException ERROR_EXPECTED_STRING_UUID = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_string_uuid")) + ); + static final DelayedException ERROR_EXPECTED_NUMBER_OR_BOOLEAN = DelayedException.create( + new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_number_or_boolean")) + ); + public static final String BUILTIN_TRUE = "true"; + public static final String BUILTIN_FALSE = "false"; + public static final String BUILTIN_NULL = "null"; + public static final Map BUILTIN_OPERATIONS = Map.of( + new BuiltinKey("bool", 1), new BuiltinOperation() { + @Override + public T run(DynamicOps ops, List arguments, ParseState state) { + Boolean result = convert(ops, arguments.getFirst()); + if (result == null) { + state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_NUMBER_OR_BOOLEAN); + return null; + } + return ops.createBoolean(result); + } + + @Nullable + private static Boolean convert(DynamicOps ops, T arg) { + Optional asBoolean = ops.getBooleanValue(arg).result(); + if (asBoolean.isPresent()) { + return asBoolean.get(); + } else { + Optional asNumber = ops.getNumberValue(arg).result(); + return asNumber.isPresent() ? asNumber.get().doubleValue() != 0.0 : null; + } + } + }, new BuiltinKey("uuid", 1), new BuiltinOperation() { + @Override + public T run(DynamicOps ops, List arguments, ParseState state) { + Optional arg = ops.getStringValue(arguments.getFirst()).result(); + if (arg.isEmpty()) { + state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_STRING_UUID); + return null; + } + UUID uuid; + try { + uuid = UUID.fromString(arg.get()); + } catch (IllegalArgumentException var7) { + state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_STRING_UUID); + return null; + } + + return ops.createIntList(IntStream.of(UUIDUtil.uuidToIntArray(uuid))); + } + } + ); + public static final SuggestionSupplier BUILTIN_IDS = new SuggestionSupplier<>() { + private final Set keys = Stream.concat( + Stream.of(BUILTIN_FALSE, BUILTIN_TRUE, BUILTIN_NULL), SnbtOperations.BUILTIN_OPERATIONS.keySet().stream().map(BuiltinKey::id) + ) + .collect(Collectors.toSet()); + + @Override + public Stream possibleValues(ParseState state) { + return this.keys.stream(); + } + }; + + public record BuiltinKey(String id, int argCount) { + @Override + public @NotNull String toString() { + return this.id + "/" + this.argCount; + } + } + + public interface BuiltinOperation { + @Nullable + T run(DynamicOps ops, List arguments, ParseState state); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/TagParser.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/TagParser.java new file mode 100644 index 000000000..f963c635a --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/TagParser.java @@ -0,0 +1,97 @@ +package net.momirealms.craftengine.core.util.snbt; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JavaOps; +import net.momirealms.craftengine.core.util.VersionHelper; +import net.momirealms.craftengine.core.util.snbt.parse.LocalizedMessage; +import net.momirealms.craftengine.core.util.snbt.parse.LocalizedSimpleCommandExceptionType; +import net.momirealms.craftengine.core.util.snbt.parse.grammar.Grammar; +import net.momirealms.sparrow.nbt.CompoundTag; +import net.momirealms.sparrow.nbt.Tag; +import net.momirealms.sparrow.nbt.codec.LegacyJavaOps; +import net.momirealms.sparrow.nbt.codec.LegacyNBTOps; +import net.momirealms.sparrow.nbt.codec.NBTOps; + +@SuppressWarnings("unused") +public class TagParser { + public static final SimpleCommandExceptionType ERROR_TRAILING_DATA = new LocalizedSimpleCommandExceptionType( + new LocalizedMessage("warning.config.type.snbt.parser.trailing") + ); + public static final SimpleCommandExceptionType ERROR_EXPECTED_COMPOUND = new LocalizedSimpleCommandExceptionType( + new LocalizedMessage("warning.config.type.snbt.parser.expected.compound") + ); + public static final char ELEMENT_SEPARATOR = ','; + public static final char NAME_VALUE_SEPARATOR = ':'; + public static final TagParser NBT_OPS_PARSER = create(VersionHelper.isOrAbove1_20_5() ? NBTOps.INSTANCE : LegacyNBTOps.INSTANCE); + public static final TagParser JAVA_OPS_PARSER = create(VersionHelper.isOrAbove1_20_5() ? JavaOps.INSTANCE : LegacyJavaOps.INSTANCE); + private final DynamicOps ops; + private final Grammar grammar; + + private TagParser(DynamicOps ops, Grammar grammar) { + this.ops = ops; + this.grammar = grammar; + } + + public DynamicOps ops() { + return this.ops; + } + + public static TagParser create(DynamicOps ops) { + return new TagParser<>(ops, SnbtGrammar.createParser(ops)); + } + + private static CompoundTag castToCompoundOrThrow(StringReader reader, Tag result) throws CommandSyntaxException { + if (result instanceof CompoundTag compoundTag) { + return compoundTag; + } + throw ERROR_EXPECTED_COMPOUND.createWithContext(reader); + } + public static CompoundTag parseCompoundFully(String input) throws CommandSyntaxException { + StringReader reader = new StringReader(input); + Tag result = NBT_OPS_PARSER.parseFully(reader); + return castToCompoundOrThrow(reader, result); + } + + public static Tag parseTagFully(String input) throws CommandSyntaxException { + StringReader reader = new StringReader(input); + return NBT_OPS_PARSER.parseFully(reader); + } + + public static Object parseObjectFully(String input) throws CommandSyntaxException { + StringReader reader = new StringReader(input); + return JAVA_OPS_PARSER.parseFully(reader); + } + + public T parseFully(String input) throws CommandSyntaxException { + return this.parseFully(new StringReader(input)); + } + + public T parseFully(StringReader reader) throws CommandSyntaxException { + T result = this.grammar.parse(reader); + reader.skipWhitespace(); + if (reader.canRead()) { + throw ERROR_TRAILING_DATA.createWithContext(reader); + } + return result; + } + + public T parseAsArgument(StringReader reader) throws CommandSyntaxException { + return this.grammar.parse(reader); + } + + public static CompoundTag parseCompoundAsArgument(StringReader reader) throws CommandSyntaxException { + Tag result = parseTagAsArgument(reader); + return castToCompoundOrThrow(reader, result); + } + + public static Tag parseTagAsArgument(StringReader reader) throws CommandSyntaxException { + return NBT_OPS_PARSER.parseAsArgument(reader); + } + + public static Object parseObjectAsArgument(StringReader reader) throws CommandSyntaxException { + return JAVA_OPS_PARSER.parseAsArgument(reader); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Atom.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Atom.java new file mode 100644 index 000000000..1601e947b --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Atom.java @@ -0,0 +1,15 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public record Atom(String name) { + @Override + public @NotNull String toString() { + return "<" + this.name + ">"; + } + + public static Atom of(String name) { + return new Atom<>(name); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/CachedParseState.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/CachedParseState.java new file mode 100644 index 000000000..b71d66c24 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/CachedParseState.java @@ -0,0 +1,235 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import net.momirealms.craftengine.core.util.MiscUtils; + +import javax.annotation.Nullable; + +@SuppressWarnings("unchecked") +public abstract class CachedParseState implements ParseState { + private PositionCache[] positionCache = new PositionCache[256]; + private final ErrorCollector errorCollector; + private final Scope scope = new Scope(); + private SimpleControl[] controlCache = new SimpleControl[16]; + private int nextControlToReturn; + private final Silent silent = new Silent(); + public static final Object JAVA_NULL_VALUE_MARKER = new Object() { + @Override + public String toString() { + return "null"; + } + }; + + protected CachedParseState(ErrorCollector errorCollector) { + this.errorCollector = errorCollector; + } + + @Override + public Scope scope() { + return this.scope; + } + + @Override + public ErrorCollector errorCollector() { + return this.errorCollector; + } + + @Nullable + @Override + public T parse(NamedRule rule) { + int markBeforeParse = this.mark(); + PositionCache positionCache = this.getCacheForPosition(markBeforeParse); + int entryIndex = positionCache.findKeyIndex(rule.name()); + if (entryIndex != -1) { + CacheEntry value = positionCache.getValue(entryIndex); + if (value != null) { + if (value == CachedParseState.CacheEntry.NEGATIVE) { + return null; + } + this.restore(value.markAfterParse); + return value.value; + } + } else { + entryIndex = positionCache.allocateNewEntry(rule.name()); + } + + T result = rule.value().parse(this); + CacheEntry entry; + if (result == null) { + entry = CacheEntry.negativeEntry(); + } else { + int markAfterParse = this.mark(); + entry = new CacheEntry<>(result, markAfterParse); + } + + positionCache.setValue(entryIndex, entry); + return result; + } + + private PositionCache getCacheForPosition(int index) { + int currentSize = this.positionCache.length; + if (index >= currentSize) { + int newSize = MiscUtils.growByHalf(currentSize, index + 1); + PositionCache[] newCache = new PositionCache[newSize]; + System.arraycopy(this.positionCache, 0, newCache, 0, currentSize); + this.positionCache = newCache; + } + + PositionCache result = this.positionCache[index]; + if (result == null) { + result = new PositionCache(); + this.positionCache[index] = result; + } + + return result; + } + + @Override + public Control acquireControl() { + int currentSize = this.controlCache.length; + if (this.nextControlToReturn >= currentSize) { + int newSize = MiscUtils.growByHalf(currentSize, this.nextControlToReturn + 1); + SimpleControl[] newControlCache = new SimpleControl[newSize]; + System.arraycopy(this.controlCache, 0, newControlCache, 0, currentSize); + this.controlCache = newControlCache; + } + + int controlIndex = this.nextControlToReturn++; + SimpleControl entry = this.controlCache[controlIndex]; + if (entry == null) { + entry = new SimpleControl(); + this.controlCache[controlIndex] = entry; + } else { + entry.reset(); + } + + return entry; + } + + @Override + public void releaseControl() { + this.nextControlToReturn--; + } + + @Override + public ParseState silent() { + return this.silent; + } + + record CacheEntry(@Nullable T value, int markAfterParse) { + public static final CacheEntry NEGATIVE = new CacheEntry<>(null, -1); + + public static CacheEntry negativeEntry() { + return (CacheEntry) NEGATIVE; + } + } + + static class PositionCache { + public static final int ENTRY_STRIDE = 2; + private static final int NOT_FOUND = -1; + private Object[] atomCache = new Object[16]; + private int nextKey; + + public int findKeyIndex(Atom key) { + for (int i = 0; i < this.nextKey; i += ENTRY_STRIDE) { + if (this.atomCache[i] == key) { + return i; + } + } + + return NOT_FOUND; + } + + public int allocateNewEntry(Atom key) { + int newKeyIndex = this.nextKey; + this.nextKey += ENTRY_STRIDE; + int newValueIndex = newKeyIndex + 1; + int currentSize = this.atomCache.length; + if (newValueIndex >= currentSize) { + int newSize = MiscUtils.growByHalf(currentSize, newValueIndex + 1); + Object[] newCache = new Object[newSize]; + System.arraycopy(this.atomCache, 0, newCache, 0, currentSize); + this.atomCache = newCache; + } + + this.atomCache[newKeyIndex] = key; + return newKeyIndex; + } + + @Nullable + public CacheEntry getValue(int keyIndex) { + return (CacheEntry) this.atomCache[keyIndex + 1]; + } + + public void setValue(int keyIndex, CacheEntry entry) { + this.atomCache[keyIndex + 1] = entry; + } + } + + class Silent implements ParseState { + private final ErrorCollector silentCollector = new ErrorCollector.Nop<>(); + + @Override + public ErrorCollector errorCollector() { + return this.silentCollector; + } + + @Override + public Scope scope() { + return CachedParseState.this.scope(); + } + + @Nullable + @Override + public T parse(NamedRule rule) { + return CachedParseState.this.parse(rule); + } + + @Override + public S input() { + return CachedParseState.this.input(); + } + + @Override + public int mark() { + return CachedParseState.this.mark(); + } + + @Override + public void restore(int mark) { + CachedParseState.this.restore(mark); + } + + @Override + public Control acquireControl() { + return CachedParseState.this.acquireControl(); + } + + @Override + public void releaseControl() { + CachedParseState.this.releaseControl(); + } + + @Override + public ParseState silent() { + return this; + } + } + + static class SimpleControl implements Control { + private boolean hasCut; + + @Override + public void cut() { + this.hasCut = true; + } + + @Override + public boolean hasCut() { + return this.hasCut; + } + + public void reset() { + this.hasCut = false; + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Control.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Control.java new file mode 100644 index 000000000..b17ce13ec --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Control.java @@ -0,0 +1,18 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +public interface Control { + Control UNBOUND = new Control() { + @Override + public void cut() { + } + + @Override + public boolean hasCut() { + return false; + } + }; + + void cut(); + + boolean hasCut(); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/DelayedException.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/DelayedException.java new file mode 100644 index 000000000..173b85840 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/DelayedException.java @@ -0,0 +1,19 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.momirealms.craftengine.core.util.snbt.parse.grammar.StringReaderTerms; + +@FunctionalInterface +public interface DelayedException { + T create(String contents, int position); + + static DelayedException create(SimpleCommandExceptionType type) { + return (contents, position) -> type.createWithContext(StringReaderTerms.createReader(contents, position)); + } + + static DelayedException create(DynamicCommandExceptionType type, String argument) { + return (contents, position) -> type.createWithContext(StringReaderTerms.createReader(contents, position), argument); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Dictionary.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Dictionary.java new file mode 100644 index 000000000..66ea4e088 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Dictionary.java @@ -0,0 +1,93 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import javax.annotation.Nullable; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +@SuppressWarnings("unchecked") +public class Dictionary { + private final Map, Entry> terms = new IdentityHashMap<>(); + + public NamedRule put(Atom name, Rule entry) { + Entry holder = (Entry)this.terms.computeIfAbsent(name, Entry::new); + if (holder.value != null) { + throw new IllegalArgumentException("Trying to override rule: " + name); + } + holder.value = entry; + return holder; + } + + public NamedRule putComplex(Atom name, Term term, Rule.RuleAction action) { + return this.put(name, Rule.fromTerm(term, action)); + } + + public NamedRule put(Atom name, Term term, Rule.SimpleRuleAction action) { + return this.put(name, Rule.fromTerm(term, action)); + } + + public void checkAllBound() { + List> unboundNames = this.terms.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .toList(); + if (!unboundNames.isEmpty()) { + throw new IllegalStateException("Unbound names: " + unboundNames); + } + } + + public NamedRule forward(Atom name) { + return this.getOrCreateEntry(name); + } + + private Entry getOrCreateEntry(Atom name) { + return (Entry)this.terms.computeIfAbsent(name, Entry::new); + } + + public Term named(Atom name) { + return new Reference<>(this.getOrCreateEntry(name), name); + } + + public Term namedWithAlias(Atom nameToParse, Atom nameToStore) { + return new Reference<>(this.getOrCreateEntry(nameToParse), nameToStore); + } + + static class Entry implements NamedRule, Supplier { + private final Atom name; + @Nullable + Rule value; + + private Entry(Atom name) { + this.name = name; + } + + @Override + public Atom name() { + return this.name; + } + + @Override + public Rule value() { + return Objects.requireNonNull(this.value, this); + } + + @Override + public String get() { + return "Unbound rule " + this.name; + } + } + + record Reference(Entry ruleToParse, Atom nameToStore) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + T result = state.parse(this.ruleToParse); + if (result == null) { + return false; + } + scope.put(this.nameToStore, result); + return true; + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorCollector.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorCollector.java new file mode 100644 index 000000000..1bb43a0ae --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorCollector.java @@ -0,0 +1,97 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import net.momirealms.craftengine.core.util.MiscUtils; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unchecked") +public interface ErrorCollector { + void store(int cursor, SuggestionSupplier suggestions, Object reason); + + default void store(int cursor, Object reason) { + this.store(cursor, SuggestionSupplier.empty(), reason); + } + + void finish(int finalCursor); + + class LongestOnly implements ErrorCollector { + private MutableErrorEntry[] entries = new MutableErrorEntry[16]; + private int nextErrorEntry; + private int lastCursor = -1; + + private void discardErrorsFromShorterParse(int cursor) { + if (cursor > this.lastCursor) { + this.lastCursor = cursor; + this.nextErrorEntry = 0; + } + } + + @Override + public void finish(int finalCursor) { + this.discardErrorsFromShorterParse(finalCursor); + } + + @Override + public void store(int cursor, SuggestionSupplier suggestions, Object reason) { + this.discardErrorsFromShorterParse(cursor); + if (cursor == this.lastCursor) { + this.addErrorEntry(suggestions, reason); + } + } + + private void addErrorEntry(SuggestionSupplier suggestions, Object reason) { + int currentSize = this.entries.length; + if (this.nextErrorEntry >= currentSize) { + int newSize = MiscUtils.growByHalf(currentSize, this.nextErrorEntry + 1); + MutableErrorEntry[] newEntries = new MutableErrorEntry[newSize]; + System.arraycopy(this.entries, 0, newEntries, 0, currentSize); + this.entries = newEntries; + } + + int entryIndex = this.nextErrorEntry++; + MutableErrorEntry entry = this.entries[entryIndex]; + if (entry == null) { + entry = new MutableErrorEntry<>(); + this.entries[entryIndex] = entry; + } + + entry.suggestions = suggestions; + entry.reason = reason; + } + + public List> entries() { + int errorCount = this.nextErrorEntry; + if (errorCount == 0) { + return List.of(); + } + List> result = new ArrayList<>(errorCount); + + for (int i = 0; i < errorCount; i++) { + MutableErrorEntry mutableErrorEntry = this.entries[i]; + result.add(new ErrorEntry<>(this.lastCursor, mutableErrorEntry.suggestions, mutableErrorEntry.reason)); + } + + return result; + } + + public int cursor() { + return this.lastCursor; + } + + static class MutableErrorEntry { + SuggestionSupplier suggestions = SuggestionSupplier.empty(); + Object reason = "empty"; + } + } + + class Nop implements ErrorCollector { + @Override + public void store(int cursor, SuggestionSupplier suggestions, Object reason) { + } + + @Override + public void finish(int finalCursor) { + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorEntry.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorEntry.java new file mode 100644 index 000000000..dff487beb --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorEntry.java @@ -0,0 +1,4 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +public record ErrorEntry(int cursor, SuggestionSupplier suggestions, Object reason) { +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedCommandSyntaxException.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedCommandSyntaxException.java new file mode 100644 index 000000000..21204ec40 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedCommandSyntaxException.java @@ -0,0 +1,84 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.plugin.locale.TranslationManager; +import net.momirealms.craftengine.core.util.AdventureHelper; + +import java.util.Optional; +import java.util.function.Supplier; + +public class LocalizedCommandSyntaxException extends CommandSyntaxException { + public static final int CONTEXT_AMOUNT = 50; + public static final String PARSE_ERROR_NODE = "warning.config.type.snbt.invalid_syntax.parse_error"; + public static final String HERE_NODE = "warning.config.type.snbt.invalid_syntax.here"; + private final Message message; + private final String input; + private final int cursor; + + public LocalizedCommandSyntaxException(CommandExceptionType type, Message message) { + super(type, message); + this.message = message; + this.input = null; + this.cursor = -1; + } + + public LocalizedCommandSyntaxException(CommandExceptionType type, Message message, String input, int cursor) { + super(type, message, input, cursor); + this.message = message; + this.input = input; + this.cursor = cursor; + } + + @Override + public String getMessage() { + String message = this.message.getString(); + final String context = getContext(); + if (context == null) { + return message; + } + return generateLocalizedMessage( + PARSE_ERROR_NODE, + () -> message + " at position " + this.cursor + ": " + context, + message, String.valueOf(this.cursor), context + ); + } + + @Override + public String getContext() { + if (this.input == null || this.cursor < 0) { + return null; + } + final StringBuilder builder = new StringBuilder(); + final int cursor = Math.min(this.input.length(), this.cursor); + + if (cursor > CONTEXT_AMOUNT) { + builder.append("..."); + } + + builder.append(this.input, Math.max(0, cursor - CONTEXT_AMOUNT), cursor); + builder.append(generateLocalizedMessage(HERE_NODE, () -> "<--[HERE]")); + + return builder.toString(); + } + + + private String generateLocalizedMessage(String node, Supplier fallback, String... arguments) { + try { + String rawMessage = Optional.ofNullable(TranslationManager.instance() + .miniMessageTranslation(node)).orElse(fallback.get()); + String cleanMessage = AdventureHelper.miniMessage() + .stripTags(rawMessage); + for (int i = 0; i < arguments.length; i++) { + cleanMessage = cleanMessage.replace( + "", + arguments[i] != null ? arguments[i] : "null" + ); + } + return cleanMessage; + } catch (Exception e) { + return fallback.get(); + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedDynamicCommandExceptionType.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedDynamicCommandExceptionType.java new file mode 100644 index 000000000..ee80a3bee --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedDynamicCommandExceptionType.java @@ -0,0 +1,28 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; + +import java.util.function.Function; + +public class LocalizedDynamicCommandExceptionType extends DynamicCommandExceptionType { + private final Function function; + + public LocalizedDynamicCommandExceptionType(Function function) { + super(function); + this.function = function; + } + + @Override + public CommandSyntaxException create(final Object arg) { + return new LocalizedCommandSyntaxException(this, function.apply(arg)); + } + + @Override + public CommandSyntaxException createWithContext(final ImmutableStringReader reader, final Object arg) { + return new LocalizedCommandSyntaxException(this, function.apply(arg), reader.getString(), reader.getCursor()); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedMessage.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedMessage.java new file mode 100644 index 000000000..c8da1cab7 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedMessage.java @@ -0,0 +1,53 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.mojang.brigadier.Message; +import net.momirealms.craftengine.core.plugin.locale.TranslationManager; +import net.momirealms.craftengine.core.util.AdventureHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Optional; + +public class LocalizedMessage implements Message { + private final String node; + private final String[] arguments; + + public LocalizedMessage( + @NotNull String node, + @Nullable String... arguments + ) { + this.node = node; + this.arguments = arguments != null + ? Arrays.copyOf(arguments, arguments.length) + : new String[0]; + } + + @Override + public String getString() { + return generateLocalizedMessage(); + } + + private String generateLocalizedMessage() { + try { + String rawMessage = Optional.ofNullable(TranslationManager.instance() + .miniMessageTranslation(this.node)).orElse(this.node); + String cleanMessage = AdventureHelper.miniMessage() + .stripTags(rawMessage); + for (int i = 0; i < arguments.length; i++) { + cleanMessage = cleanMessage.replace( + "", + arguments[i] != null ? arguments[i] : "null" + ); + } + return cleanMessage; + } catch (Exception e) { + return String.format( + "Failed to translate. Node: %s, Arguments: %s. Cause: %s", + node, + Arrays.toString(arguments), + e.getMessage() + ); + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedSimpleCommandExceptionType.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedSimpleCommandExceptionType.java new file mode 100644 index 000000000..b046fab0c --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/LocalizedSimpleCommandExceptionType.java @@ -0,0 +1,25 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; + +public class LocalizedSimpleCommandExceptionType extends SimpleCommandExceptionType { + private final Message message; + + public LocalizedSimpleCommandExceptionType(Message message) { + super(message); + this.message = message; + } + + @Override + public CommandSyntaxException create() { + return new LocalizedCommandSyntaxException(this, message); + } + + @Override + public CommandSyntaxException createWithContext(final ImmutableStringReader reader) { + return new LocalizedCommandSyntaxException(this, message, reader.getString(), reader.getCursor()); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/NamedRule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/NamedRule.java new file mode 100644 index 000000000..d4f634e1d --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/NamedRule.java @@ -0,0 +1,7 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +public interface NamedRule { + Atom name(); + + Rule value(); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ParseState.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ParseState.java new file mode 100644 index 000000000..2ab971364 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ParseState.java @@ -0,0 +1,37 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import javax.annotation.Nullable; +import java.util.Optional; + +public interface ParseState { + Scope scope(); + + ErrorCollector errorCollector(); + + default Optional parseTopRule(NamedRule rule) { + T result = this.parse(rule); + if (result != null) { + this.errorCollector().finish(this.mark()); + } + + if (!this.scope().hasOnlySingleFrame()) { + throw new IllegalStateException("Malformed scope: " + this.scope()); + } + return Optional.ofNullable(result); + } + + @Nullable + T parse(NamedRule rule); + + S input(); + + int mark(); + + void restore(int mark); + + Control acquireControl(); + + void releaseControl(); + + ParseState silent(); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Rule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Rule.java new file mode 100644 index 000000000..cfb7403c4 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Rule.java @@ -0,0 +1,51 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import javax.annotation.Nullable; + +public interface Rule { + @Nullable + T parse(ParseState state); + + static Rule fromTerm(Term child, RuleAction action) { + return new WrappedTerm<>(action, child); + } + + static Rule fromTerm(Term child, SimpleRuleAction action) { + return new WrappedTerm<>(action, child); + } + + @FunctionalInterface + interface RuleAction { + @Nullable + T run(ParseState state); + } + + @FunctionalInterface + interface SimpleRuleAction extends RuleAction { + T run(Scope ruleScope); + + @Override + default T run(ParseState state) { + return this.run(state.scope()); + } + } + + record WrappedTerm(RuleAction action, Term child) implements Rule { + @Nullable + @Override + public T parse(ParseState state) { + Scope scope = state.scope(); + scope.pushFrame(); + + try { + if (!this.child.parse(state, scope, Control.UNBOUND)) { + return null; + } + + return this.action.run(state); + } finally { + scope.popFrame(); + } + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Scope.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Scope.java new file mode 100644 index 000000000..d152ecc89 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Scope.java @@ -0,0 +1,315 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import com.google.common.annotations.VisibleForTesting; +import net.momirealms.craftengine.core.util.MiscUtils; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unchecked") +public final class Scope { + private static final int NOT_FOUND = -1; + private static final Object FRAME_START_MARKER = new Object() { + @Override + public String toString() { + return "frame"; + } + }; + private static final int ENTRY_STRIDE = 2; + private Object[] stack = new Object[128]; + private int topEntryKeyIndex = 0; + private int topMarkerKeyIndex = 0; + private int depth; + + public Scope() { + this.stack[0] = FRAME_START_MARKER; + this.stack[1] = null; + } + + private int valueIndex(Atom atom) { + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object key = this.stack[i]; + + assert key instanceof Atom; + + if (key == atom) { + return i + 1; + } + } + + return NOT_FOUND; + } + + public int valueIndexForAny(Atom... atoms) { + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object key = this.stack[i]; + + assert key instanceof Atom; + + for (Atom atom : atoms) { + if (atom == key) { + return i + 1; + } + } + } + + return NOT_FOUND; + } + + private void ensureCapacity(int additionalEntryCount) { + int currentSize = this.stack.length; + int currentLastValueIndex = this.topEntryKeyIndex + 1; + int newLastValueIndex = currentLastValueIndex + additionalEntryCount * 2; + if (newLastValueIndex >= currentSize) { + int newSize = MiscUtils.growByHalf(currentSize, newLastValueIndex + 1); + Object[] newStack = new Object[newSize]; + System.arraycopy(this.stack, 0, newStack, 0, currentSize); + this.stack = newStack; + } + + assert this.validateStructure(); + } + + private void setupNewFrame() { + this.topEntryKeyIndex += ENTRY_STRIDE; + this.stack[this.topEntryKeyIndex] = FRAME_START_MARKER; + this.stack[this.topEntryKeyIndex + 1] = this.topMarkerKeyIndex; + this.topMarkerKeyIndex = this.topEntryKeyIndex; + } + + public void pushFrame() { + this.ensureCapacity(1); + this.setupNewFrame(); + + assert this.validateStructure(); + } + + private int getPreviousMarkerIndex(int markerKeyIndex) { + return (Integer) this.stack[markerKeyIndex + 1]; + } + + public void popFrame() { + assert this.topMarkerKeyIndex != 0; + + this.topEntryKeyIndex = this.topMarkerKeyIndex - ENTRY_STRIDE; + this.topMarkerKeyIndex = this.getPreviousMarkerIndex(this.topMarkerKeyIndex); + + assert this.validateStructure(); + } + + public void splitFrame() { + int currentFrameMarkerIndex = this.topMarkerKeyIndex; + int nonMarkerEntriesInFrame = (this.topEntryKeyIndex - this.topMarkerKeyIndex) / ENTRY_STRIDE; + this.ensureCapacity(nonMarkerEntriesInFrame + 1); + this.setupNewFrame(); + int sourceCursor = currentFrameMarkerIndex + ENTRY_STRIDE; + int targetCursor = this.topEntryKeyIndex; + + for (int i = 0; i < nonMarkerEntriesInFrame; i++) { + targetCursor += ENTRY_STRIDE; + Object key = this.stack[sourceCursor]; + + assert key != null; + + this.stack[targetCursor] = key; + this.stack[targetCursor + 1] = null; + sourceCursor += ENTRY_STRIDE; + } + + this.topEntryKeyIndex = targetCursor; + + assert this.validateStructure(); + } + + public void clearFrameValues() { + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + assert this.stack[i] instanceof Atom; + + this.stack[i + 1] = null; + } + + assert this.validateStructure(); + } + + public void mergeFrame() { + int previousMarkerIndex = this.getPreviousMarkerIndex(this.topMarkerKeyIndex); + int previousFrameCursor = previousMarkerIndex; + int currentFrameCursor = this.topMarkerKeyIndex; + + while (currentFrameCursor < this.topEntryKeyIndex) { + previousFrameCursor += ENTRY_STRIDE; + currentFrameCursor += ENTRY_STRIDE; + Object newKey = this.stack[currentFrameCursor]; + + assert newKey instanceof Atom; + + Object newValue = this.stack[currentFrameCursor + 1]; + Object oldKey = this.stack[previousFrameCursor]; + if (oldKey != newKey) { + this.stack[previousFrameCursor] = newKey; + this.stack[previousFrameCursor + 1] = newValue; + } else if (newValue != null) { + this.stack[previousFrameCursor + 1] = newValue; + } + } + + this.topEntryKeyIndex = previousFrameCursor; + this.topMarkerKeyIndex = previousMarkerIndex; + + assert this.validateStructure(); + } + + public void put(Atom name, @Nullable T value) { + int valueIndex = this.valueIndex(name); + if (valueIndex != NOT_FOUND) { + this.stack[valueIndex] = value; + } else { + this.ensureCapacity(1); + this.topEntryKeyIndex += ENTRY_STRIDE; + this.stack[this.topEntryKeyIndex] = name; + this.stack[this.topEntryKeyIndex + 1] = value; + } + + assert this.validateStructure(); + } + + @Nullable + public T get(Atom name) { + int valueIndex = this.valueIndex(name); + return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : null); + } + + public T getOrThrow(Atom name) { + int valueIndex = this.valueIndex(name); + if (valueIndex == NOT_FOUND) { + throw new IllegalArgumentException("No value for atom " + name); + } + return (T) this.stack[valueIndex]; + } + + public T getOrDefault(Atom name, T fallback) { + int valueIndex = this.valueIndex(name); + return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : fallback); + } + + @Nullable + @SafeVarargs + public final T getAny(Atom... names) { + int valueIndex = this.valueIndexForAny(names); + return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : null); + } + + @SafeVarargs + public final T getAnyOrThrow(Atom... names) { + int valueIndex = this.valueIndexForAny(names); + if (valueIndex == NOT_FOUND) { + throw new IllegalArgumentException("No value for atoms " + Arrays.toString(names)); + } + return (T) this.stack[valueIndex]; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + boolean afterFrame = true; + + for (int i = 0; i <= this.topEntryKeyIndex; i += ENTRY_STRIDE) { + Object key = this.stack[i]; + Object value = this.stack[i + 1]; + if (key == FRAME_START_MARKER) { + result.append('|'); + afterFrame = true; + } else { + if (!afterFrame) { + result.append(','); + } + + afterFrame = false; + result.append(key).append(':').append(value); + } + } + + return result.toString(); + } + + @VisibleForTesting + public Map, ?> lastFrame() { + HashMap, Object> result = new HashMap<>(); + + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object key = this.stack[i]; + Object value = this.stack[i + 1]; + result.put((Atom) key, value); + } + + return result; + } + + public boolean hasOnlySingleFrame() { + for (int i = this.topEntryKeyIndex; i > 0; i--) { + if (this.stack[i] == FRAME_START_MARKER) { + return false; + } + } + + if (this.stack[0] != FRAME_START_MARKER) { + throw new IllegalStateException("Corrupted stack"); + } + return true; + } + + private boolean validateStructure() { + assert this.topMarkerKeyIndex >= 0; + + assert this.topEntryKeyIndex >= this.topMarkerKeyIndex; + + for (int i = 0; i <= this.topEntryKeyIndex; i += ENTRY_STRIDE) { + Object object = this.stack[i]; + if (object != FRAME_START_MARKER && !(object instanceof Atom)) { + return false; + } + } + + for (int ix = this.topMarkerKeyIndex; ix != 0; ix = this.getPreviousMarkerIndex(ix)) { + Object object = this.stack[ix]; + if (object != FRAME_START_MARKER) { + return false; + } + } + + return true; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Term increaseDepth() { + class IncreasingDepthTerm implements Term { + public static final IncreasingDepthTerm INSTANCE = new IncreasingDepthTerm(); + + @Override + public boolean parse(final ParseState state, final Scope scope, final Control control) { + if (++scope.depth > 512) { + state.errorCollector().store(state.mark(), new IllegalStateException("Too deep")); + return false; + } + return true; + } + } + return (Term) IncreasingDepthTerm.INSTANCE; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Term decreaseDepth() { + class DecreasingDepthTerm implements Term { + public static final DecreasingDepthTerm INSTANCE = new DecreasingDepthTerm(); + + @Override + public boolean parse(final ParseState state, final Scope scope, final Control control) { + scope.depth--; + return true; + } + } + return (Term) DecreasingDepthTerm.INSTANCE; + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/SuggestionSupplier.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/SuggestionSupplier.java new file mode 100644 index 000000000..30c32ac7c --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/SuggestionSupplier.java @@ -0,0 +1,11 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import java.util.stream.Stream; + +public interface SuggestionSupplier { + Stream possibleValues(ParseState state); + + static SuggestionSupplier empty() { + return state -> Stream.empty(); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Term.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Term.java new file mode 100644 index 000000000..383602b0e --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Term.java @@ -0,0 +1,235 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import java.util.ArrayList; +import java.util.List; + +public interface Term { + boolean parse(ParseState state, Scope scope, Control control); + + static Term marker(Atom name, T value) { + return new Marker<>(name, value); + } + + @SafeVarargs + static Term sequence(Term... terms) { + return new Sequence<>(terms); + } + + @SafeVarargs + static Term alternative(Term... terms) { + return new Alternative<>(terms); + } + + static Term optional(Term term) { + return new Maybe<>(term); + } + + static Term repeated(NamedRule element, Atom> listName) { + return repeated(element, listName, 0); + } + + static Term repeated(NamedRule element, Atom> listName, int minRepetitions) { + return new Repeated<>(element, listName, minRepetitions); + } + + static Term repeatedWithTrailingSeparator(NamedRule element, Atom> listName, Term separator) { + return repeatedWithTrailingSeparator(element, listName, separator, 0); + } + + static Term repeatedWithTrailingSeparator(NamedRule element, Atom> listName, Term seperator, int minRepetitions) { + return new RepeatedWithSeparator<>(element, listName, seperator, minRepetitions, true); + } + + static Term positiveLookahead(Term term) { + return new LookAhead<>(term, true); + } + + static Term cut() { + return new Term<>() { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + control.cut(); + return true; + } + + @Override + public String toString() { + return "↑"; + } + }; + } + + static Term empty() { + return new Term<>() { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + return true; + } + + @Override + public String toString() { + return "ε"; + } + }; + } + + static Term fail(final Object message) { + return new Term<>() { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + state.errorCollector().store(state.mark(), message); + return false; + } + + @Override + public String toString() { + return "fail"; + } + }; + } + + record Alternative(Term[] elements) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + Control controlForThis = state.acquireControl(); + + try { + int mark = state.mark(); + scope.splitFrame(); + + for (Term element : this.elements) { + if (element.parse(state, scope, controlForThis)) { + scope.mergeFrame(); + return true; + } + + scope.clearFrameValues(); + state.restore(mark); + if (controlForThis.hasCut()) { + break; + } + } + + scope.popFrame(); + return false; + } finally { + state.releaseControl(); + } + } + } + + record LookAhead(Term term, boolean positive) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + int mark = state.mark(); + boolean result = this.term.parse(state.silent(), scope, control); + state.restore(mark); + return this.positive == result; + } + } + + record Marker(Atom name, T value) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + scope.put(this.name, this.value); + return true; + } + } + + record Maybe(Term term) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + int mark = state.mark(); + if (!this.term.parse(state, scope, control)) { + state.restore(mark); + } + + return true; + } + } + + record Repeated(NamedRule element, Atom> listName, int minRepetitions) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + int mark = state.mark(); + List elements = new ArrayList<>(this.minRepetitions); + + while (true) { + int entryMark = state.mark(); + T parsedElement = state.parse(this.element); + if (parsedElement == null) { + state.restore(entryMark); + if (elements.size() < this.minRepetitions) { + state.restore(mark); + return false; + } + scope.put(this.listName, elements); + return true; + } + + elements.add(parsedElement); + } + } + } + + record RepeatedWithSeparator( + NamedRule element, Atom> listName, Term separator, int minRepetitions, boolean allowTrailingSeparator + ) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + int listMark = state.mark(); + List elements = new ArrayList<>(this.minRepetitions); + boolean first = true; + + while (true) { + int markBeforeSeparator = state.mark(); + if (!first && !this.separator.parse(state, scope, control)) { + state.restore(markBeforeSeparator); + break; + } + + int markAfterSeparator = state.mark(); + T parsedElement = state.parse(this.element); + if (parsedElement == null) { + if (first) { + state.restore(markAfterSeparator); + } else { + if (!this.allowTrailingSeparator) { + state.restore(listMark); + return false; + } + + state.restore(markAfterSeparator); + } + break; + } + + elements.add(parsedElement); + first = false; + } + + if (elements.size() < this.minRepetitions) { + state.restore(listMark); + return false; + } + scope.put(this.listName, elements); + return true; + } + } + + record Sequence(Term[] elements) implements Term { + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + int mark = state.mark(); + + for (Term element : this.elements) { + if (!element.parse(state, scope, control)) { + state.restore(mark); + return false; + } + } + + return true; + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/Grammar.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/Grammar.java new file mode 100644 index 000000000..e93117570 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/Grammar.java @@ -0,0 +1,51 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.util.snbt.parse.*; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public record Grammar(Dictionary rules, NamedRule top) { + public Grammar { + rules.checkAllBound(); + } + + public Optional parse(ParseState state) { + return state.parseTopRule(this.top); + } + + public T parse(StringReader reader) throws CommandSyntaxException { + ErrorCollector.LongestOnly errorCollector = new ErrorCollector.LongestOnly<>(); + StringReaderParserState stringReaderParserState = new StringReaderParserState(errorCollector, reader); + Optional optionalResult = this.parse(stringReaderParserState); + if (optionalResult.isPresent()) { + T result = optionalResult.get(); + if (CachedParseState.JAVA_NULL_VALUE_MARKER.equals(result)) { + result = null; + } + return result; + } + List> errorEntries = errorCollector.entries(); + List exceptions = errorEntries.stream().mapMulti((entry, output) -> { + if (entry.reason() instanceof DelayedException delayedException) { + output.accept(delayedException.create(reader.getString(), entry.cursor())); + } else if (entry.reason() instanceof Exception exception1) { + output.accept(exception1); + } + }).toList(); + + for (Exception exception : exceptions) { + if (exception instanceof CommandSyntaxException cse) { + throw cse; + } + } + + if (exceptions.size() == 1 && exceptions.getFirst() instanceof RuntimeException re) { + throw re; + } + throw new IllegalStateException("Failed to parse: " + errorEntries.stream().map(ErrorEntry::toString).collect(Collectors.joining(", "))); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPatternParseRule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPatternParseRule.java new file mode 100644 index 000000000..58bf9d797 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPatternParseRule.java @@ -0,0 +1,33 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.util.snbt.parse.DelayedException; +import net.momirealms.craftengine.core.util.snbt.parse.ParseState; +import net.momirealms.craftengine.core.util.snbt.parse.Rule; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class GreedyPatternParseRule implements Rule { + private final Pattern pattern; + private final DelayedException error; + + public GreedyPatternParseRule(Pattern pattern, DelayedException error) { + this.pattern = pattern; + this.error = error; + } + + @Override + public String parse(ParseState state) { + StringReader input = state.input(); + String fullString = input.getString(); + Matcher matcher = this.pattern.matcher(fullString).region(input.getCursor(), fullString.length()); + if (!matcher.lookingAt()) { + state.errorCollector().store(state.mark(), this.error); + return null; + } + input.setCursor(matcher.end()); + return matcher.group(0); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPredicateParseRule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPredicateParseRule.java new file mode 100644 index 000000000..3144dcc27 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPredicateParseRule.java @@ -0,0 +1,48 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.util.snbt.parse.DelayedException; +import net.momirealms.craftengine.core.util.snbt.parse.ParseState; +import net.momirealms.craftengine.core.util.snbt.parse.Rule; + +import javax.annotation.Nullable; + +public abstract class GreedyPredicateParseRule implements Rule { + private final int minSize; + private final int maxSize; + private final DelayedException error; + + public GreedyPredicateParseRule(int minSize, DelayedException error) { + this(minSize, Integer.MAX_VALUE, error); + } + + public GreedyPredicateParseRule(int minSize, int maxSize, DelayedException error) { + this.minSize = minSize; + this.maxSize = maxSize; + this.error = error; + } + + @Nullable + @Override + public String parse(ParseState state) { + StringReader input = state.input(); + String fullString = input.getString(); + int start = input.getCursor(); + int pos = start; + + while (pos < fullString.length() && this.isAccepted(fullString.charAt(pos)) && pos - start < this.maxSize) { + pos++; + } + + int length = pos - start; + if (length < this.minSize) { + state.errorCollector().store(state.mark(), this.error); + return null; + } + input.setCursor(pos); + return fullString.substring(start, pos); + } + + protected abstract boolean isAccepted(char c); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/NumberRunParseRule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/NumberRunParseRule.java new file mode 100644 index 000000000..dd4e6249b --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/NumberRunParseRule.java @@ -0,0 +1,46 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.util.snbt.parse.DelayedException; +import net.momirealms.craftengine.core.util.snbt.parse.ParseState; +import net.momirealms.craftengine.core.util.snbt.parse.Rule; + +import javax.annotation.Nullable; + +public abstract class NumberRunParseRule implements Rule { + private final DelayedException noValueError; + private final DelayedException underscoreNotAllowedError; + + public NumberRunParseRule(DelayedException noValueError, DelayedException underscoreNotAllowedError) { + this.noValueError = noValueError; + this.underscoreNotAllowedError = underscoreNotAllowedError; + } + + @Nullable + @Override + public String parse(ParseState state) { + StringReader input = state.input(); + input.skipWhitespace(); + String fullString = input.getString(); + int start = input.getCursor(); + int pos = start; + + while (pos < fullString.length() && this.isAccepted(fullString.charAt(pos))) { + pos++; + } + + int length = pos - start; + if (length == 0) { + state.errorCollector().store(state.mark(), this.noValueError); + return null; + } else if (fullString.charAt(start) != '_' && fullString.charAt(pos - 1) != '_') { + input.setCursor(pos); + return fullString.substring(start, pos); + } + state.errorCollector().store(state.mark(), this.underscoreNotAllowedError); + return null; + } + + protected abstract boolean isAccepted(char c); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderParserState.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderParserState.java new file mode 100644 index 000000000..d076fafd4 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderParserState.java @@ -0,0 +1,29 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import net.momirealms.craftengine.core.util.snbt.parse.CachedParseState; +import net.momirealms.craftengine.core.util.snbt.parse.ErrorCollector; + +public class StringReaderParserState extends CachedParseState { + private final StringReader input; + + public StringReaderParserState(ErrorCollector errorCollector, StringReader input) { + super(errorCollector); + this.input = input; + } + + @Override + public StringReader input() { + return this.input; + } + + @Override + public int mark() { + return this.input.getCursor(); + } + + @Override + public void restore(int mark) { + this.input.setCursor(mark); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderTerms.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderTerms.java new file mode 100644 index 000000000..14fe3f3aa --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderTerms.java @@ -0,0 +1,64 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import it.unimi.dsi.fastutil.chars.CharList; +import net.momirealms.craftengine.core.util.snbt.parse.*; + +import java.util.stream.Collectors; + +public interface StringReaderTerms { + DynamicCommandExceptionType LITERAL_INCORRECT = new LocalizedDynamicCommandExceptionType( + expected -> new LocalizedMessage("warning.config.type.snbt.parser.incorrect", String.valueOf(expected)) + ); + + static Term character(final char value) { + return new TerminalCharacters(CharList.of(value)) { + @Override + protected boolean isAccepted(char v) { + return value == v; + } + }; + } + + static Term characters(final char v1, final char v2) { + return new TerminalCharacters(CharList.of(v1, v2)) { + @Override + protected boolean isAccepted(char v) { + return v == v1 || v == v2; + } + }; + } + + static StringReader createReader(String contents, int cursor) { + StringReader reader = new StringReader(contents); + reader.setCursor(cursor); + return reader; + } + + abstract class TerminalCharacters implements Term { + private final DelayedException error; + private final SuggestionSupplier suggestions; + + public TerminalCharacters(CharList values) { + String joinedValues = values.intStream().mapToObj(Character::toString).collect(Collectors.joining("|")); + this.error = DelayedException.create(LITERAL_INCORRECT, joinedValues); + this.suggestions = s -> values.intStream().mapToObj(Character::toString); + } + + @Override + public boolean parse(ParseState state, Scope scope, Control control) { + state.input().skipWhitespace(); + int cursor = state.mark(); + if (state.input().canRead() && this.isAccepted(state.input().read())) { + return true; + } + state.errorCollector().store(cursor, this.suggestions, this.error); + return false; + } + + protected abstract boolean isAccepted(char value); + } + +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/UnquotedStringParseRule.java b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/UnquotedStringParseRule.java new file mode 100644 index 000000000..2d46092b7 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/UnquotedStringParseRule.java @@ -0,0 +1,32 @@ +package net.momirealms.craftengine.core.util.snbt.parse.grammar; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.momirealms.craftengine.core.util.snbt.parse.DelayedException; +import net.momirealms.craftengine.core.util.snbt.parse.ParseState; +import net.momirealms.craftengine.core.util.snbt.parse.Rule; + +import javax.annotation.Nullable; + +public class UnquotedStringParseRule implements Rule { + private final int minSize; + private final DelayedException error; + + public UnquotedStringParseRule(int minSize, DelayedException error) { + this.minSize = minSize; + this.error = error; + } + + @Nullable + @Override + public String parse(ParseState state) { + state.input().skipWhitespace(); + int cursor = state.mark(); + String value = state.input().readUnquotedString(); + if (value.length() < this.minSize) { + state.errorCollector().store(cursor, this.error); + return null; + } + return value; + } +}