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 fce2f7ea7..63419843e 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -90,6 +90,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 da06eeff2..160e44580 100644 --- a/common-files/src/main/resources/translations/zh_cn.yml +++ b/common-files/src/main/resources/translations/zh_cn.yml @@ -88,6 +88,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..113f33324 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.parseCompoundFully(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..ad96f56e9 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtGrammar.java @@ -0,0 +1,865 @@ +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.snbt.parse.*; +import net.momirealms.craftengine.core.util.snbt.parse.Dictionary; +import net.momirealms.craftengine.core.util.snbt.parse.grammar.*; + +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( + number -> new LocalizedMessage("warning.config.type.snbt.parser.number_parse_failure", String.valueOf(number)) + ); + 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("Expected a hexadecimal number")) + ); + 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 numberFormatException) { + return DelayedException.create(ERROR_NUMBER_PARSE_FAILURE, numberFormatException.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 text) { + return text.indexOf(95) != -1; + } + + private static void cleanAndAppend(StringBuilder stringBuilder, String text) { + cleanAndAppend(stringBuilder, text, needsUnderscoreRemoval(text)); + } + + static void cleanAndAppend(StringBuilder stringBuilder, String text, boolean removeUnderscores) { + if (removeUnderscores) { + for (char c : text.toCharArray()) { + if (c != '_') { + stringBuilder.append(c); + } + } + } + stringBuilder.append(text); + } + + static short parseUnsignedShort(String text, int radix) { + int i = Integer.parseInt(text, radix); + if (i >> 16 == 0) { + return (short)i; + } + throw new NumberFormatException("out of range: " + i); + } + + @Nullable + private static T createFloat( + DynamicOps ops, + Sign sign, + @Nullable String wholePart, + @Nullable String fractionPart, + @Nullable Signed exponentPart, + @Nullable TypeSuffix suffix, + ParseState parseState + ) { + StringBuilder stringBuilder = new StringBuilder(); + sign.append(stringBuilder); + if (wholePart != null) { + cleanAndAppend(stringBuilder, wholePart); + } + + if (fractionPart != null) { + stringBuilder.append('.'); + cleanAndAppend(stringBuilder, fractionPart); + } + + if (exponentPart != null) { + stringBuilder.append('e'); + exponentPart.sign().append(stringBuilder); + cleanAndAppend(stringBuilder, exponentPart.value); + } + + try { + String string = stringBuilder.toString(); + + return switch (suffix) { + case null -> convertDouble(ops, parseState, string); + case FLOAT -> convertFloat(ops, parseState, string); + case DOUBLE -> convertDouble(ops, parseState, string); + default -> { + parseState.errorCollector().store(parseState.mark(), ERROR_EXPECTED_FLOAT_TYPE); + yield null; + } + }; + } catch (NumberFormatException var11) { + parseState.errorCollector().store(parseState.mark(), createNumberParseError(var11)); + return null; + } + } + + @Nullable + private static T convertFloat(DynamicOps ops, ParseState parseState, String value) { + float f = Float.parseFloat(value); + if (!Float.isFinite(f)) { + parseState.errorCollector().store(parseState.mark(), ERROR_INFINITY_NOT_ALLOWED); + return null; + } + return ops.createFloat(f); + } + + @Nullable + private static T convertDouble(DynamicOps ops, ParseState parseState, String value) { + double d = Double.parseDouble(value); + if (!Double.isFinite(d)) { + parseState.errorCollector().store(parseState.mark(), ERROR_INFINITY_NOT_ALLOWED); + return null; + } + return ops.createDouble(d); + } + + 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 object = ops.createBoolean(true); + T object1 = ops.createBoolean(false); + T object2 = ops.emptyMap(); + T object3 = ops.emptyList(); + Dictionary dictionary = new Dictionary<>(); + Atom atom = Atom.of("sign"); + dictionary.put( + atom, + Term.alternative( + Term.sequence(StringReaderTerms.character('+'), Term.marker(atom, Sign.PLUS)), + Term.sequence(StringReaderTerms.character('-'), Term.marker(atom, Sign.MINUS)) + ), + scope -> scope.getOrThrow(atom) + ); + Atom atom1 = Atom.of("integer_suffix"); + dictionary.put( + atom1, + Term.alternative( + Term.sequence( + StringReaderTerms.characters('u', 'U'), + Term.alternative( + Term.sequence( + StringReaderTerms.characters('b', 'B'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.BYTE)) + ), + Term.sequence( + StringReaderTerms.characters('s', 'S'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.SHORT)) + ), + Term.sequence( + StringReaderTerms.characters('i', 'I'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.INT)) + ), + Term.sequence( + StringReaderTerms.characters('l', 'L'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.UNSIGNED, TypeSuffix.LONG)) + ) + ) + ), + Term.sequence( + StringReaderTerms.characters('s', 'S'), + Term.alternative( + Term.sequence( + StringReaderTerms.characters('b', 'B'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.BYTE)) + ), + Term.sequence( + StringReaderTerms.characters('s', 'S'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.SHORT)) + ), + Term.sequence( + StringReaderTerms.characters('i', 'I'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.INT)) + ), + Term.sequence( + StringReaderTerms.characters('l', 'L'), + Term.marker(atom1, new IntegerSuffix(SignedPrefix.SIGNED, TypeSuffix.LONG)) + ) + ) + ), + Term.sequence(StringReaderTerms.characters('b', 'B'), Term.marker(atom1, new IntegerSuffix(null, TypeSuffix.BYTE))), + Term.sequence(StringReaderTerms.characters('s', 'S'), Term.marker(atom1, new IntegerSuffix(null, TypeSuffix.SHORT))), + Term.sequence(StringReaderTerms.characters('i', 'I'), Term.marker(atom1, new IntegerSuffix(null, TypeSuffix.INT))), + Term.sequence(StringReaderTerms.characters('l', 'L'), Term.marker(atom1, new IntegerSuffix(null, TypeSuffix.LONG))) + ), + scope -> scope.getOrThrow(atom1) + ); + Atom atom2 = Atom.of("binary_numeral"); + dictionary.put(atom2, BINARY_NUMERAL); + Atom atom3 = Atom.of("decimal_numeral"); + dictionary.put(atom3, DECIMAL_NUMERAL); + Atom atom4 = Atom.of("hex_numeral"); + dictionary.put(atom4, HEX_NUMERAL); + Atom atom5 = Atom.of("integer_literal"); + NamedRule namedRule = dictionary.put( + atom5, + Term.sequence( + Term.optional(dictionary.named(atom)), + Term.alternative( + Term.sequence( + StringReaderTerms.character('0'), + Term.cut(), + Term.alternative( + Term.sequence(StringReaderTerms.characters('x', 'X'), Term.cut(), dictionary.named(atom4)), + Term.sequence(StringReaderTerms.characters('b', 'B'), dictionary.named(atom2)), + Term.sequence(dictionary.named(atom3), Term.cut(), Term.fail(ERROR_LEADING_ZERO_NOT_ALLOWED)), + Term.marker(atom3, "0") + ) + ), + dictionary.named(atom3) + ), + Term.optional(dictionary.named(atom1)) + ), + scope -> { + IntegerSuffix integerSuffix = scope.getOrDefault(atom1, IntegerSuffix.EMPTY); + Sign sign = scope.getOrDefault(atom, Sign.PLUS); + String string = scope.get(atom3); + if (string != null) { + return new IntegerLiteral(sign, Base.DECIMAL, string, integerSuffix); + } + String string1 = scope.get(atom4); + if (string1 != null) { + return new IntegerLiteral(sign, Base.HEX, string1, integerSuffix); + } + String string2 = scope.getOrThrow(atom2); + return new IntegerLiteral(sign, Base.BINARY, string2, integerSuffix); + } + ); + Atom atom6 = Atom.of("float_type_suffix"); + dictionary.put( + atom6, + Term.alternative( + Term.sequence(StringReaderTerms.characters('f', 'F'), Term.marker(atom6, TypeSuffix.FLOAT)), + Term.sequence(StringReaderTerms.characters('d', 'D'), Term.marker(atom6, TypeSuffix.DOUBLE)) + ), + scope -> scope.getOrThrow(atom6) + ); + Atom> atom7 = Atom.of("float_exponent_part"); + dictionary.put( + atom7, + Term.sequence(StringReaderTerms.characters('e', 'E'), Term.optional(dictionary.named(atom)), dictionary.named(atom3)), + scope -> new Signed<>(scope.getOrDefault(atom, Sign.PLUS), scope.getOrThrow(atom3)) + ); + Atom atom8 = Atom.of("float_whole_part"); + Atom atom9 = Atom.of("float_fraction_part"); + Atom atom10 = Atom.of("float_literal"); + dictionary.putComplex( + atom10, + Term.sequence( + Term.optional(dictionary.named(atom)), + Term.alternative( + Term.sequence( + dictionary.namedWithAlias(atom3, atom8), + StringReaderTerms.character('.'), + Term.cut(), + Term.optional(dictionary.namedWithAlias(atom3, atom9)), + Term.optional(dictionary.named(atom7)), + Term.optional(dictionary.named(atom6)) + ), + Term.sequence( + StringReaderTerms.character('.'), + Term.cut(), + dictionary.namedWithAlias(atom3, atom9), + Term.optional(dictionary.named(atom7)), + Term.optional(dictionary.named(atom6)) + ), + Term.sequence(dictionary.namedWithAlias(atom3, atom8), dictionary.named(atom7), Term.cut(), Term.optional(dictionary.named(atom6))), + Term.sequence(dictionary.namedWithAlias(atom3, atom8), Term.optional(dictionary.named(atom7)), dictionary.named(atom6)) + ) + ), + parseState -> { + Scope scope = parseState.scope(); + Sign sign = scope.getOrDefault(atom, Sign.PLUS); + String string = scope.get(atom8); + String string1 = scope.get(atom9); + Signed signed = scope.get(atom7); + TypeSuffix typeSuffix = scope.get(atom6); + return createFloat(ops, sign, string, string1, signed, typeSuffix, parseState); + } + ); + Atom atom11 = Atom.of("string_hex_2"); + dictionary.put(atom11, new SimpleHexLiteralParseRule(2)); + Atom atom12 = Atom.of("string_hex_4"); + dictionary.put(atom12, new SimpleHexLiteralParseRule(4)); + Atom atom13 = Atom.of("string_hex_8"); + dictionary.put(atom13, new SimpleHexLiteralParseRule(8)); + Atom atom14 = Atom.of("string_unicode_name"); + dictionary.put(atom14, new GreedyPatternParseRule(UNICODE_NAME, ERROR_INVALID_CHARACTER_NAME)); + Atom atom15 = Atom.of("string_escape_sequence"); + dictionary.putComplex( + atom15, + Term.alternative( + Term.sequence(StringReaderTerms.character('b'), Term.marker(atom15, "\b")), + Term.sequence(StringReaderTerms.character('s'), Term.marker(atom15, " ")), + Term.sequence(StringReaderTerms.character('t'), Term.marker(atom15, "\t")), + Term.sequence(StringReaderTerms.character('n'), Term.marker(atom15, "\n")), + Term.sequence(StringReaderTerms.character('f'), Term.marker(atom15, "\f")), + Term.sequence(StringReaderTerms.character('r'), Term.marker(atom15, "\r")), + Term.sequence(StringReaderTerms.character('\\'), Term.marker(atom15, "\\")), + Term.sequence(StringReaderTerms.character('\''), Term.marker(atom15, "'")), + Term.sequence(StringReaderTerms.character('"'), Term.marker(atom15, "\"")), + Term.sequence(StringReaderTerms.character('x'), dictionary.named(atom11)), + Term.sequence(StringReaderTerms.character('u'), dictionary.named(atom12)), + Term.sequence(StringReaderTerms.character('U'), dictionary.named(atom13)), + Term.sequence(StringReaderTerms.character('N'), StringReaderTerms.character('{'), dictionary.named(atom14), StringReaderTerms.character('}')) + ), + parseState -> { + Scope scope = parseState.scope(); + String string = scope.getAny(atom15); + if (string != null) { + return string; + } + String string1 = scope.getAny(atom11, atom12, atom13); + if (string1 != null) { + int i = HexFormat.fromHexDigits(string1); + if (!Character.isValidCodePoint(i)) { + parseState.errorCollector() + .store(parseState.mark(), DelayedException.create(ERROR_INVALID_CODEPOINT, String.format(Locale.ROOT, "U+%08X", i))); + return null; + } + return Character.toString(i); + } + String string2 = scope.getOrThrow(atom14); + + int i1; + try { + i1 = Character.codePointOf(string2); + } catch (IllegalArgumentException var12x) { + parseState.errorCollector().store(parseState.mark(), ERROR_INVALID_CHARACTER_NAME); + return null; + } + + return Character.toString(i1); + } + ); + Atom atom16 = Atom.of("string_plain_contents"); + dictionary.put(atom16, PLAIN_STRING_CHUNK); + Atom> atom17 = Atom.of("string_chunks"); + Atom atom18 = Atom.of("string_contents"); + Atom atom19 = Atom.of("single_quoted_string_chunk"); + NamedRule namedRule1 = dictionary.put( + atom19, + Term.alternative( + dictionary.namedWithAlias(atom16, atom18), + Term.sequence(StringReaderTerms.character('\\'), dictionary.namedWithAlias(atom15, atom18)), + Term.sequence(StringReaderTerms.character('"'), Term.marker(atom18, "\"")) + ), + scope -> scope.getOrThrow(atom18) + ); + Atom atom20 = Atom.of("single_quoted_string_contents"); + dictionary.put(atom20, Term.repeated(namedRule1, atom17), scope -> joinList(scope.getOrThrow(atom17))); + Atom atom21 = Atom.of("double_quoted_string_chunk"); + NamedRule namedRule2 = dictionary.put( + atom21, + Term.alternative( + dictionary.namedWithAlias(atom16, atom18), + Term.sequence(StringReaderTerms.character('\\'), dictionary.namedWithAlias(atom15, atom18)), + Term.sequence(StringReaderTerms.character('\''), Term.marker(atom18, "'")) + ), + scope -> scope.getOrThrow(atom18) + ); + Atom atom22 = Atom.of("double_quoted_string_contents"); + dictionary.put(atom22, Term.repeated(namedRule2, atom17), scope -> joinList(scope.getOrThrow(atom17))); + Atom atom23 = Atom.of("quoted_string_literal"); + dictionary.put( + atom23, + Term.alternative( + Term.sequence( + StringReaderTerms.character('"'), Term.cut(), Term.optional(dictionary.namedWithAlias(atom22, atom18)), StringReaderTerms.character('"') + ), + Term.sequence(StringReaderTerms.character('\''), Term.optional(dictionary.namedWithAlias(atom20, atom18)), StringReaderTerms.character('\'')) + ), + scope -> scope.getOrThrow(atom18) + ); + Atom atom24 = Atom.of("unquoted_string"); + dictionary.put(atom24, new UnquotedStringParseRule(1, ERROR_EXPECTED_UNQUOTED_STRING)); + Atom atom25 = Atom.of("literal"); + Atom> atom26 = Atom.of("arguments"); + dictionary.put( + atom26, Term.repeatedWithTrailingSeparator(dictionary.forward(atom25), atom26, StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR)), scope -> scope.getOrThrow(atom26) + ); + Atom atom27 = Atom.of("unquoted_string_or_builtin"); + dictionary.putComplex( + atom27, + Term.sequence( + dictionary.named(atom24), + Term.optional(Term.sequence(StringReaderTerms.character('('), dictionary.named(atom26), StringReaderTerms.character(')'))) + ), + parseState -> { + Scope scope = parseState.scope(); + String string = scope.getOrThrow(atom24); + if (!string.isEmpty() && isAllowedToStartUnquotedString(string.charAt(0))) { + List list = scope.get(atom26); + if (list != null) { + SnbtOperations.BuiltinKey builtinKey = new SnbtOperations.BuiltinKey(string, list.size()); + SnbtOperations.BuiltinOperation builtinOperation = SnbtOperations.BUILTIN_OPERATIONS.get(builtinKey); + if (builtinOperation != null) { + return builtinOperation.run(ops, list, parseState); + } + parseState.errorCollector().store(parseState.mark(), DelayedException.create(ERROR_NO_SUCH_OPERATION, builtinKey.toString())); + return null; + } else if (string.equalsIgnoreCase("true")) { + return object; + } else if (string.equalsIgnoreCase("false")) { + return object1; + } else if (string.equalsIgnoreCase("null")) { + return Objects.requireNonNullElseGet(ops.empty(), () -> { + T nullString = ops.createString("null"); + if ("null".equals(nullString)) { // 确定是 Java 类型的 + return (T) CachedParseState.JAVA_NULL_VALUE_MARKER; + } + return nullString; + }); + } + return ops.createString(string); + } + parseState.errorCollector().store(parseState.mark(), SnbtOperations.BUILTIN_IDS, ERROR_INVALID_UNQUOTED_START); + return null; + } + ); + Atom atom28 = Atom.of("map_key"); + dictionary.put(atom28, Term.alternative(dictionary.named(atom23), dictionary.named(atom24)), scope -> scope.getAnyOrThrow(atom23, atom24)); + Atom> atom29 = Atom.of("map_entry"); + NamedRule> namedRule3 = dictionary.putComplex( + atom29, Term.sequence(dictionary.named(atom28), StringReaderTerms.character(TagParser.NAME_VALUE_SEPARATOR), dictionary.named(atom25)), parseState -> { + Scope scope = parseState.scope(); + String string = scope.getOrThrow(atom28); + if (string.isEmpty()) { + parseState.errorCollector().store(parseState.mark(), ERROR_EMPTY_KEY); + return null; + } + T orThrow = scope.getOrThrow(atom25); + return Map.entry(string, orThrow); + } + ); + Atom>> atom30 = Atom.of("map_entries"); + dictionary.put(atom30, Term.repeatedWithTrailingSeparator(namedRule3, atom30, StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR)), scope -> scope.getOrThrow(atom30)); + Atom atom31 = Atom.of("map_literal"); + dictionary.put(atom31, Term.sequence(StringReaderTerms.character('{'), Scope.increaseDepth(), dictionary.named(atom30), Scope.decreaseDepth(), StringReaderTerms.character('}')), scope -> { // Paper - track depth + List> list = scope.getOrThrow(atom30); + if (list.isEmpty()) { + return object2; + } else { + Builder builder = ImmutableMap.builderWithExpectedSize(list.size()); + + for (Entry entry : list) { + builder.put(ops.createString(entry.getKey()), entry.getValue()); + } + + return ops.createMap(builder.buildKeepingLast()); + } + }); + Atom> atom32 = Atom.of("list_entries"); + dictionary.put( + atom32, Term.repeatedWithTrailingSeparator(dictionary.forward(atom25), atom32, StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR)), scope -> scope.getOrThrow(atom32) + ); + Atom atom33 = Atom.of("array_prefix"); + dictionary.put( + atom33, + Term.alternative( + Term.sequence(StringReaderTerms.character('B'), Term.marker(atom33, ArrayPrefix.BYTE)), + Term.sequence(StringReaderTerms.character('L'), Term.marker(atom33, ArrayPrefix.LONG)), + Term.sequence(StringReaderTerms.character('I'), Term.marker(atom33, ArrayPrefix.INT)) + ), + scope -> scope.getOrThrow(atom33) + ); + Atom> atom34 = Atom.of("int_array_entries"); + dictionary.put(atom34, Term.repeatedWithTrailingSeparator(namedRule, atom34, StringReaderTerms.character(TagParser.ELEMENT_SEPARATOR)), scope -> scope.getOrThrow(atom34)); + Atom atom35 = Atom.of("list_literal"); + dictionary.putComplex( + atom35, + Term.sequence( + StringReaderTerms.character('['), + Scope.increaseDepth(), + Term.alternative(Term.sequence(dictionary.named(atom33), StringReaderTerms.character(';'), dictionary.named(atom34)), dictionary.named(atom32)), + Scope.decreaseDepth(), + StringReaderTerms.character(']') + ), + parseState -> { + Scope scope = parseState.scope(); + ArrayPrefix arrayPrefix = scope.get(atom33); + if (arrayPrefix != null) { + List list = scope.getOrThrow(atom34); + return list.isEmpty() ? arrayPrefix.create(ops) : arrayPrefix.create(ops, list, parseState); + } + List list = scope.getOrThrow(atom32); + return list.isEmpty() ? object3 : ops.createList(list.stream()); + } + ); + NamedRule namedRule4 = dictionary.putComplex( + atom25, + Term.alternative( + Term.sequence(Term.positiveLookahead(NUMBER_LOOKEAHEAD), Term.alternative(dictionary.namedWithAlias(atom10, atom25), dictionary.named(atom5))), + Term.sequence(Term.positiveLookahead(StringReaderTerms.characters('"', '\'')), Term.cut(), dictionary.named(atom23)), + Term.sequence(Term.positiveLookahead(StringReaderTerms.character('{')), Term.cut(), dictionary.namedWithAlias(atom31, atom25)), + Term.sequence(Term.positiveLookahead(StringReaderTerms.character('[')), Term.cut(), dictionary.namedWithAlias(atom35, atom25)), + dictionary.namedWithAlias(atom27, atom25) + ), + parseState -> { + Scope scope = parseState.scope(); + String string = scope.get(atom23); + if (string != null) { + return ops.createString(string); + } + IntegerLiteral integerLiteral = scope.get(atom5); + return integerLiteral != null ? integerLiteral.create(ops, parseState) : scope.getOrThrow(atom25); + } + ); + return new Grammar<>(dictionary, namedRule4); + } + + 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 values, ParseState parseState) { + ByteList list = new ByteArrayList(); + + for (IntegerLiteral integerLiteral : values) { + Number number = this.buildNumber(integerLiteral, parseState); + if (number == null) { + return null; + } + + list.add(number.byteValue()); + } + + return ops.createByteList(ByteBuffer.wrap(list.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 values, ParseState parseState) { + IntStream.Builder builder = IntStream.builder(); + + for (IntegerLiteral integerLiteral : values) { + Number number = this.buildNumber(integerLiteral, parseState); + if (number == null) { + return null; + } + + builder.add(number.intValue()); + } + + return ops.createIntList(builder.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 values, ParseState parseState) { + LongStream.Builder builder = LongStream.builder(); + + for (IntegerLiteral integerLiteral : values) { + Number number = this.buildNumber(integerLiteral, parseState); + if (number == null) { + return null; + } + + builder.add(number.longValue()); + } + + return ops.createLongList(builder.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 suffix) { + return suffix == this.defaultType || this.additionalTypes.contains(suffix); + } + + public abstract T create(DynamicOps ops); + + @Nullable + public abstract T create(DynamicOps ops, List values, ParseState parseState); + + @Nullable + protected Number buildNumber(IntegerLiteral value, ParseState parseState) { + TypeSuffix typeSuffix = this.computeType(value.suffix); + if (typeSuffix == null) { + parseState.errorCollector().store(parseState.mark(), ERROR_INVALID_ARRAY_ELEMENT_TYPE); + return null; + } + return (Number)value.create(JavaOps.INSTANCE, typeSuffix, parseState); + } + + @Nullable + private TypeSuffix computeType(IntegerSuffix suffix) { + TypeSuffix typeSuffix = suffix.type(); + if (typeSuffix == null) { + return this.defaultType; + } + return !this.isAllowed(typeSuffix) ? null : typeSuffix; + } + } + + 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 flag = needsUnderscoreRemoval(this.digits); + if (sign != Sign.MINUS && !flag) { + return this.digits; + } + StringBuilder stringBuilder = new StringBuilder(); + sign.append(stringBuilder); + cleanAndAppend(stringBuilder, this.digits, flag); + return stringBuilder.toString(); + } + + @Nullable + public T create(DynamicOps ops, ParseState parseState) { + return this.create(ops, Objects.requireNonNullElse(this.suffix.type, TypeSuffix.INT), parseState); + } + + @Nullable + public T create(DynamicOps ops, TypeSuffix typeSuffix, ParseState parseState) { + boolean flag = this.signedOrDefault() == SignedPrefix.SIGNED; + if (!flag && this.sign == Sign.MINUS) { + parseState.errorCollector().store(parseState.mark(), ERROR_EXPECTED_NON_NEGATIVE_NUMBER); + return null; + } + String string = this.cleanupDigits(this.sign); + + int i = switch (this.base) { + case BINARY -> 2; + case DECIMAL -> 10; + case HEX -> 16; + }; + + try { + if (flag) { + return switch (typeSuffix) { + case BYTE -> ops.createByte(Byte.parseByte(string, i)); + case SHORT -> ops.createShort(Short.parseShort(string, i)); + case INT -> ops.createInt(Integer.parseInt(string, i)); + case LONG -> ops.createLong(Long.parseLong(string, i)); + default -> { + parseState.errorCollector().store(parseState.mark(), ERROR_EXPECTED_INTEGER_TYPE); + yield null; + } + }; + } + return switch (typeSuffix) { + case BYTE -> ops.createByte(com.google.common.primitives.UnsignedBytes.parseUnsignedByte(string, i)); + case SHORT -> ops.createShort(parseUnsignedShort(string, i)); + case INT -> ops.createInt(Integer.parseUnsignedInt(string, i)); + case LONG -> ops.createLong(Long.parseUnsignedLong(string, i)); + default -> { + parseState.errorCollector().store(parseState.mark(), ERROR_EXPECTED_INTEGER_TYPE); + yield null; + } + }; + } catch (NumberFormatException var8) { + parseState.errorCollector().store(parseState.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 stringBuilder) { + if (this == MINUS) { + stringBuilder.append("-"); + } + } + } + + record Signed(Sign sign, T value) { + } + + enum SignedPrefix { + SIGNED, + UNSIGNED + } + + static class SimpleHexLiteralParseRule extends GreedyPredicateParseRule { + public SimpleHexLiteralParseRule(int minSize) { + super(minSize, minSize, DelayedException.create(ERROR_EXPECTED_HEX_ESCAPE, String.valueOf(minSize))); + } + + @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..9c684725d --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/SnbtOperations.java @@ -0,0 +1,93 @@ +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 Map BUILTIN_OPERATIONS = Map.of( + new BuiltinKey("bool", 1), new BuiltinOperation() { + @Override + public T run(DynamicOps ops, List args, ParseState parseState) { + Boolean bool = convert(ops, args.getFirst()); + if (bool == null) { + parseState.errorCollector().store(parseState.mark(), SnbtOperations.ERROR_EXPECTED_NUMBER_OR_BOOLEAN); + return null; + } else { + return ops.createBoolean(bool); + } + } + + @Nullable + private static Boolean convert(DynamicOps ops, T value) { + Optional optional = ops.getBooleanValue(value).result(); + if (optional.isPresent()) { + return optional.get(); + } else { + Optional optional1 = ops.getNumberValue(value).result(); + return optional1.isPresent() ? optional1.get().doubleValue() != 0.0 : null; + } + } + }, new BuiltinKey("uuid", 1), new BuiltinOperation() { + @Override + public T run(DynamicOps ops, List args, ParseState parseState) { + Optional optional = ops.getStringValue(args.getFirst()).result(); + if (optional.isEmpty()) { + parseState.errorCollector().store(parseState.mark(), SnbtOperations.ERROR_EXPECTED_STRING_UUID); + return null; + } else { + UUID uuid; + try { + uuid = UUID.fromString(optional.get()); + } catch (IllegalArgumentException var7) { + parseState.errorCollector().store(parseState.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), SnbtOperations.BUILTIN_OPERATIONS.keySet().stream().map(BuiltinKey::id) + ) + .collect(Collectors.toSet()); + + @Override + public Stream possibleValues(ParseState parseState) { + 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 args, ParseState parseState); + } +} 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..6c7badbf9 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/TagParser.java @@ -0,0 +1,87 @@ +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; + +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 = ':'; + private static final TagParser NBT_OPS_PARSER = create(VersionHelper.isOrAbove1_20_5() ? NBTOps.INSTANCE : LegacyNBTOps.INSTANCE); + private 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 tag) throws CommandSyntaxException { + if (tag instanceof CompoundTag compoundTag) { + return compoundTag; + } + throw ERROR_EXPECTED_COMPOUND.createWithContext(reader); + } + + public static CompoundTag parseCompoundFully(String data) throws CommandSyntaxException { + StringReader stringReader = new StringReader(data); + return parseCompoundAsArgument(stringReader); + } + + public static Object parseObjectFully(String data) throws CommandSyntaxException { + StringReader stringReader = new StringReader(data); + return parseObjectAsArgument(stringReader); + } + + public T parseFully(String text) throws CommandSyntaxException { + return this.parseFully(new StringReader(text)); + } + + public T parseFully(StringReader reader) throws CommandSyntaxException { + T object = this.grammar.parse(reader); + reader.skipWhitespace(); + if (reader.canRead()) { + throw ERROR_TRAILING_DATA.createWithContext(reader); + } + return object; + } + + public T parseAsArgument(StringReader reader) throws CommandSyntaxException { + return this.grammar.parse(reader); + } + + public static CompoundTag parseCompoundAsArgument(StringReader reader) throws CommandSyntaxException { + Tag tag = NBT_OPS_PARSER.parseAsArgument(reader); + return castToCompoundOrThrow(reader, tag); + } + + 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..03c439c82 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/CachedParseState.java @@ -0,0 +1,253 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +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(); + private final IntList markedNull = new IntArrayList(); + 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 i = this.mark(); + PositionCache cacheForPosition = this.getCacheForPosition(i); + int i1 = cacheForPosition.findKeyIndex(rule.name()); + if (i1 != -1) { + CacheEntry value = cacheForPosition.getValue(i1); + if (value != null) { + if (value == CachedParseState.CacheEntry.NEGATIVE) { + return null; + } + this.restore(value.markAfterParse); + return value.value; + } + } else { + i1 = cacheForPosition.allocateNewEntry(rule.name()); + } + + T object = rule.value().parse(this); + CacheEntry cacheEntry; + if (object == null) { + cacheEntry = (CacheEntry) CacheEntry.NEGATIVE; + } else { + cacheEntry = new CacheEntry<>(object, this.mark()); + } + + cacheForPosition.setValue(i1, cacheEntry); + return object; + } + + private PositionCache getCacheForPosition(int position) { + int i = this.positionCache.length; + if (position >= i) { + int i1 = MiscUtils.growByHalf(i, position + 1); + PositionCache[] positionCaches = new PositionCache[i1]; + System.arraycopy(this.positionCache, 0, positionCaches, 0, i); + this.positionCache = positionCaches; + } + + PositionCache positionCache = this.positionCache[position]; + if (positionCache == null) { + positionCache = new PositionCache(); + this.positionCache[position] = positionCache; + } + + return positionCache; + } + + @Override + public Control acquireControl() { + int i = this.controlCache.length; + if (this.nextControlToReturn >= i) { + int i1 = MiscUtils.growByHalf(i, this.nextControlToReturn + 1); + SimpleControl[] simpleControls = new SimpleControl[i1]; + System.arraycopy(this.controlCache, 0, simpleControls, 0, i); + this.controlCache = simpleControls; + } + + int i1 = this.nextControlToReturn++; + SimpleControl simpleControl = this.controlCache[i1]; + if (simpleControl == null) { + simpleControl = new SimpleControl(); + this.controlCache[i1] = simpleControl; + } else { + simpleControl.reset(); + } + + return simpleControl; + } + + @Override + public void releaseControl() { + this.nextControlToReturn--; + } + + @Override + public ParseState silent() { + return this.silent; + } + + @Override + public void markNull(int mark) { + this.markedNull.add(mark); + } + + @Override + public boolean isNull(int mark) { + return this.markedNull.contains(mark); + } + + record CacheEntry(@Nullable T value, int markAfterParse) { + public static final CacheEntry NEGATIVE = new CacheEntry<>(null, -1); + } + + 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 atom) { + for (int i = 0; i < this.nextKey; i += ENTRY_STRIDE) { + if (this.atomCache[i] == atom) { + return i; + } + } + + return NOT_FOUND; + } + + public int allocateNewEntry(Atom entry) { + int i = this.nextKey; + this.nextKey += 2; + int i1 = i + 1; + int i2 = this.atomCache.length; + if (i1 >= i2) { + int i3 = MiscUtils.growByHalf(i2, i1 + 1); + Object[] objects = new Object[i3]; + System.arraycopy(this.atomCache, 0, objects, 0, i2); + this.atomCache = objects; + } + + this.atomCache[i] = entry; + return i; + } + + @Nullable + public CacheEntry getValue(int index) { + return (CacheEntry)this.atomCache[index + 1]; + } + + public void setValue(int index, CacheEntry value) { + this.atomCache[index + 1] = value; + } + } + + 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 cursor) { + CachedParseState.this.restore(cursor); + } + + @Override + public Control acquireControl() { + return CachedParseState.this.acquireControl(); + } + + @Override + public void releaseControl() { + CachedParseState.this.releaseControl(); + } + + @Override + public ParseState silent() { + return this; + } + + @Override + public void markNull(int mark) { + CachedParseState.this.markNull(mark); + } + + @Override + public boolean isNull(int mark) { + return CachedParseState.this.isNull(mark); + } + } + + 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..ee79b450c --- /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 message, int cursor); + + static DelayedException create(SimpleCommandExceptionType exception) { + return (message, cursor) -> exception.createWithContext(StringReaderTerms.createReader(message, cursor)); + } + + static DelayedException create(DynamicCommandExceptionType exception, String argument) { + return (message, cursor) -> exception.createWithContext(StringReaderTerms.createReader(message, cursor), 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..20d6c4bb2 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Dictionary.java @@ -0,0 +1,92 @@ +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 rule) { + Entry entry = (Entry)this.terms.computeIfAbsent(name, Entry::new); + if (entry.value != null) { + throw new IllegalArgumentException("Trying to override rule: " + name); + } else { + entry.value = rule; + return entry; + } + } + + public NamedRule putComplex(Atom name, Term term, Rule.RuleAction ruleAction) { + return this.put(name, Rule.fromTerm(term, ruleAction)); + } + + public NamedRule put(Atom name, Term term, Rule.SimpleRuleAction ruleAction) { + return this.put(name, Rule.fromTerm(term, ruleAction)); + } + + public void checkAllBound() { + List> list = this.terms.entrySet().stream().filter(entry -> entry.getValue() == null).map(Map.Entry::getKey).toList(); + if (!list.isEmpty()) { + throw new IllegalStateException("Unbound names: " + list); + } + } + + 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 name, Atom alias) { + return new Reference<>(this.getOrCreateEntry(name), alias); + } + + 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 parseState, Scope scope, Control control) { + T object = parseState.parse(this.ruleToParse); + if (object == null) { + return false; + } else { + scope.put(this.nameToStore, object); + 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..ef0aac7e3 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ErrorCollector.java @@ -0,0 +1,98 @@ +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 cursor); + + 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 cursor) { + this.discardErrorsFromShorterParse(cursor); + } + + @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 i = this.entries.length; + if (this.nextErrorEntry >= i) { + int i1 = MiscUtils.growByHalf(i, this.nextErrorEntry + 1); + MutableErrorEntry[] mutableErrorEntrys = new MutableErrorEntry[i1]; + System.arraycopy(this.entries, 0, mutableErrorEntrys, 0, i); + this.entries = mutableErrorEntrys; + } + + int i1 = this.nextErrorEntry++; + MutableErrorEntry mutableErrorEntry = this.entries[i1]; + if (mutableErrorEntry == null) { + mutableErrorEntry = new MutableErrorEntry<>(); + this.entries[i1] = mutableErrorEntry; + } + + mutableErrorEntry.suggestions = suggestions; + mutableErrorEntry.reason = reason; + } + + public List> entries() { + int i = this.nextErrorEntry; + if (i == 0) { + return List.of(); + } else { + List> list = new ArrayList<>(i); + + for (int i1 = 0; i1 < i; i1++) { + MutableErrorEntry mutableErrorEntry = this.entries[i1]; + list.add(new ErrorEntry<>(this.lastCursor, mutableErrorEntry.suggestions, mutableErrorEntry.reason)); + } + + return list; + } + } + + 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 cursor) { + } + } +} 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..cb4912898 --- /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 = 10; + 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..5a631476b --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/ParseState.java @@ -0,0 +1,42 @@ +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 object = this.parse(rule); + if (object != null) { + this.errorCollector().finish(this.mark()); + } + + if (!this.scope().hasOnlySingleFrame()) { + throw new IllegalStateException("Malformed scope: " + this.scope()); + } else { + return Optional.ofNullable(object); + } + } + + @Nullable + T parse(NamedRule rule); + + S input(); + + int mark(); + + void restore(int cursor); + + Control acquireControl(); + + void releaseControl(); + + ParseState silent(); + + void markNull(int mark); + + boolean isNull(int mark); +} 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..6a5728ec4 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Rule.java @@ -0,0 +1,54 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import javax.annotation.Nullable; + +public interface Rule { + @Nullable + T parse(ParseState parseState); + + 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 parseState); + } + + @FunctionalInterface + interface SimpleRuleAction extends RuleAction { + T run(Scope scope); + + @Override + default T run(ParseState parseState) { + return this.run(parseState.scope()); + } + } + + record WrappedTerm(RuleAction action, Term child) implements Rule { + @Nullable + @Override + public T parse(ParseState parseState) { + Scope scope = parseState.scope(); + scope.pushFrame(); + + T var3; + try { + if (!this.child.parse(parseState, scope, Control.UNBOUND)) { + return null; + } + + var3 = this.action.run(parseState); + } finally { + scope.popFrame(); + } + + return var3; + } + } +} 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..81ad7ecfd --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Scope.java @@ -0,0 +1,316 @@ +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 name) { + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object object = this.stack[i]; + + assert object instanceof Atom; + + if (object == name) { + return i + 1; + } + } + + return NOT_FOUND; + } + + public int valueIndexForAny(Atom... names) { + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object object = this.stack[i]; + + assert object instanceof Atom; + + for (Atom atom : names) { + if (atom == object) { + return i + 1; + } + } + } + + return NOT_FOUND; + } + + private void ensureCapacity(int requiredCapacitty) { + int i = this.stack.length; + int i1 = this.topEntryKeyIndex + 1; + int i2 = i1 + requiredCapacitty * 2; + if (i2 >= i) { + int i3 = MiscUtils.growByHalf(i, i2 + 1); + Object[] objects = new Object[i3]; + System.arraycopy(this.stack, 0, objects, 0, i); + this.stack = objects; + } + + 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 markerIndex) { + return (Integer)this.stack[markerIndex + 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 i = this.topMarkerKeyIndex; + int i1 = (this.topEntryKeyIndex - this.topMarkerKeyIndex) / ENTRY_STRIDE; + this.ensureCapacity(i1 + 1); + this.setupNewFrame(); + int i2 = i + ENTRY_STRIDE; + int i3 = this.topEntryKeyIndex; + + for (int i4 = 0; i4 < i1; i4++) { + i3 += ENTRY_STRIDE; + Object object = this.stack[i2]; + + assert object != null; + + this.stack[i3] = object; + this.stack[i3 + 1] = null; + i2 += ENTRY_STRIDE; + } + + this.topEntryKeyIndex = i3; + + 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 i = previousMarkerIndex; + int i1 = this.topMarkerKeyIndex; + + while (i1 < this.topEntryKeyIndex) { + i += ENTRY_STRIDE; + i1 += ENTRY_STRIDE; + Object object = this.stack[i1]; + + assert object instanceof Atom; + + Object object1 = this.stack[i1 + 1]; + Object object2 = this.stack[i]; + if (object2 != object) { + this.stack[i] = object; + this.stack[i + 1] = object1; + } else if (object1 != null) { + this.stack[i + 1] = object1; + } + } + + this.topEntryKeyIndex = i; + this.topMarkerKeyIndex = previousMarkerIndex; + + assert this.validateStructure(); + } + + public void put(Atom atom, @Nullable T value) { + int i = this.valueIndex(atom); + if (i != NOT_FOUND) { + this.stack[i] = value; + } else { + this.ensureCapacity(1); + this.topEntryKeyIndex += ENTRY_STRIDE; + this.stack[this.topEntryKeyIndex] = atom; + this.stack[this.topEntryKeyIndex + 1] = value; + } + + assert this.validateStructure(); + } + + @Nullable + public T get(Atom atom) { + int i = this.valueIndex(atom); + return (T)(i != NOT_FOUND ? this.stack[i] : null); + } + + public T getOrThrow(Atom atom) { + int i = this.valueIndex(atom); + if (i == NOT_FOUND) { + throw new IllegalArgumentException("No value for atom " + atom); + } else { + return (T)this.stack[i]; + } + } + + public T getOrDefault(Atom atom, T defaultValue) { + int i = this.valueIndex(atom); + return (T)(i != NOT_FOUND ? this.stack[i] : defaultValue); + } + + @Nullable + @SafeVarargs + public final T getAny(Atom... atoms) { + int i = this.valueIndexForAny(atoms); + return (T)(i != NOT_FOUND ? this.stack[i] : null); + } + + @SafeVarargs + public final T getAnyOrThrow(Atom... atoms) { + int i = this.valueIndexForAny(atoms); + if (i == NOT_FOUND) { + throw new IllegalArgumentException("No value for atoms " + Arrays.toString(atoms)); + } else { + return (T)this.stack[i]; + } + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + boolean flag = true; + + for (int i = 0; i <= this.topEntryKeyIndex; i += ENTRY_STRIDE) { + Object object = this.stack[i]; + Object object1 = this.stack[i + 1]; + if (object == FRAME_START_MARKER) { + stringBuilder.append('|'); + flag = true; + } else { + if (!flag) { + stringBuilder.append(','); + } + + flag = false; + stringBuilder.append(object).append(':').append(object1); + } + } + + return stringBuilder.toString(); + } + + @VisibleForTesting + public Map, ?> lastFrame() { + HashMap, Object> map = new HashMap<>(); + + for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) { + Object object = this.stack[i]; + Object object1 = this.stack[i + 1]; + map.put((Atom)object, object1); + } + + return map; + } + + 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"); + } else { + 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 parseState, final Scope scope, final Control control) { + if (++scope.depth > 512) { + parseState.errorCollector().store(parseState.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 parseState, 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..ed88ccd42 --- /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 parseState); + + static SuggestionSupplier empty() { + return parseState -> 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..ceab78ef0 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/Term.java @@ -0,0 +1,237 @@ +package net.momirealms.craftengine.core.util.snbt.parse; + +import java.util.ArrayList; +import java.util.List; + +public interface Term { + boolean parse(ParseState parseState, Scope scope, Control control); + + static Term marker(Atom name, T value) { + return new Marker<>(name, value); + } + + @SafeVarargs + static Term sequence(Term... elements) { + return new Sequence<>(elements); + } + + @SafeVarargs + static Term alternative(Term... elements) { + return new Alternative<>(elements); + } + + 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 seperator) { + return repeatedWithTrailingSeparator(element, listName, seperator, 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 parseState, Scope scope, Control control) { + control.cut(); + return true; + } + + @Override + public String toString() { + return "↑"; + } + }; + } + + static Term empty() { + return new Term<>() { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + return true; + } + + @Override + public String toString() { + return "ε"; + } + }; + } + + static Term fail(final Object reason) { + return new Term<>() { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + parseState.errorCollector().store(parseState.mark(), reason); + return false; + } + + @Override + public String toString() { + return "fail"; + } + }; + } + + record Alternative(Term[] elements) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + Control control1 = parseState.acquireControl(); + + try { + int i = parseState.mark(); + scope.splitFrame(); + + for (Term term : this.elements) { + if (term.parse(parseState, scope, control1)) { + scope.mergeFrame(); + return true; + } + + scope.clearFrameValues(); + parseState.restore(i); + if (control1.hasCut()) { + break; + } + } + + scope.popFrame(); + return false; + } finally { + parseState.releaseControl(); + } + } + } + + record LookAhead(Term term, boolean positive) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + int i = parseState.mark(); + boolean flag = this.term.parse(parseState.silent(), scope, control); + parseState.restore(i); + return this.positive == flag; + } + } + + record Marker(Atom name, T value) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + scope.put(this.name, this.value); + return true; + } + } + + record Maybe(Term term) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + int i = parseState.mark(); + if (!this.term.parse(parseState, scope, control)) { + parseState.restore(i); + } + + return true; + } + } + + record Repeated(NamedRule element, Atom> listName, int minRepetitions) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + int i = parseState.mark(); + List list = new ArrayList<>(this.minRepetitions); + + while (true) { + int i1 = parseState.mark(); + T object = parseState.parse(this.element); + if (object == null) { + parseState.restore(i1); + if (list.size() < this.minRepetitions) { + parseState.restore(i); + return false; + } else { + scope.put(this.listName, list); + return true; + } + } + + list.add(object); + } + } + } + + record RepeatedWithSeparator( + NamedRule element, Atom> listName, Term separator, int minRepetitions, boolean allowTrailingSeparator + ) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + int i = parseState.mark(); + List list = new ArrayList<>(this.minRepetitions); + boolean flag = true; + + while (true) { + int i1 = parseState.mark(); + if (!flag && !this.separator.parse(parseState, scope, control)) { + parseState.restore(i1); + break; + } + + int i2 = parseState.mark(); + T object = parseState.parse(this.element); + if (object == null) { + if (flag) { + parseState.restore(i2); + } else { + if (!this.allowTrailingSeparator) { + parseState.restore(i); + return false; + } + + parseState.restore(i2); + } + break; + } + + list.add(object); + flag = false; + } + + if (list.size() < this.minRepetitions) { + parseState.restore(i); + return false; + } else { + scope.put(this.listName, list); + return true; + } + } + } + + record Sequence(Term[] elements) implements Term { + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + int i = parseState.mark(); + + for (Term term : this.elements) { + if (!term.parse(parseState, scope, control)) { + parseState.restore(i); + 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..2f13a7448 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/Grammar.java @@ -0,0 +1,53 @@ +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 parseState) { + return parseState.parseTopRule(this.top); + } + + public T parse(StringReader reader) throws CommandSyntaxException { + ErrorCollector.LongestOnly longestOnly = new ErrorCollector.LongestOnly<>(); + StringReaderParserState stringReaderParserState = new StringReaderParserState(longestOnly, reader); + Optional optional = this.parse(stringReaderParserState); + if (optional.isPresent()) { + T result = optional.get(); + if (CachedParseState.JAVA_NULL_VALUE_MARKER.equals(result)) { + result = null; + } + return result; + } else { + List> list = longestOnly.entries(); + List list1 = list.stream().mapMulti((errorEntry, consumer) -> { + if (errorEntry.reason() instanceof DelayedException delayedException) { + consumer.accept(delayedException.create(reader.getString(), errorEntry.cursor())); + } else if (errorEntry.reason() instanceof Exception exception1) { + consumer.accept(exception1); + } + }).toList(); + + for (Exception exception : list1) { + if (exception instanceof CommandSyntaxException commandSyntaxException) { + throw commandSyntaxException; + } + } + + if (list1.size() == 1 && list1.getFirst() instanceof RuntimeException runtimeException) { + throw runtimeException; + } else { + throw new IllegalStateException("Failed to parse: " + list.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..9d5e07b91 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPatternParseRule.java @@ -0,0 +1,34 @@ +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 parseState) { + StringReader stringReader = parseState.input(); + String string = stringReader.getString(); + Matcher matcher = this.pattern.matcher(string).region(stringReader.getCursor(), string.length()); + if (!matcher.lookingAt()) { + parseState.errorCollector().store(parseState.mark(), this.error); + return null; + } else { + stringReader.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..d3c8d8870 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/GreedyPredicateParseRule.java @@ -0,0 +1,49 @@ +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 parseState) { + StringReader stringReader = parseState.input(); + String string = stringReader.getString(); + int cursor = stringReader.getCursor(); + int i = cursor; + + while (i < string.length() && this.isAccepted(string.charAt(i)) && i - cursor < this.maxSize) { + i++; + } + + int i1 = i - cursor; + if (i1 < this.minSize) { + parseState.errorCollector().store(parseState.mark(), this.error); + return null; + } else { + stringReader.setCursor(i); + return string.substring(cursor, i); + } + } + + 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..71a919544 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/NumberRunParseRule.java @@ -0,0 +1,47 @@ +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 parseState) { + StringReader stringReader = parseState.input(); + stringReader.skipWhitespace(); + String string = stringReader.getString(); + int cursor = stringReader.getCursor(); + int i = cursor; + + while (i < string.length() && this.isAccepted(string.charAt(i))) { + i++; + } + + int i1 = i - cursor; + if (i1 == 0) { + parseState.errorCollector().store(parseState.mark(), this.noValueError); + return null; + } else if (string.charAt(cursor) != '_' && string.charAt(i - 1) != '_') { + stringReader.setCursor(i); + return string.substring(cursor, i); + } else { + parseState.errorCollector().store(parseState.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..9bf0a87b9 --- /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 cursor) { + this.input.setCursor(cursor); + } +} 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..12cb7def3 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/StringReaderTerms.java @@ -0,0 +1,65 @@ +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 c) { + return value == c; + } + }; + } + + static Term characters(final char value1, final char value2) { + return new TerminalCharacters(CharList.of(value1, value2)) { + @Override + protected boolean isAccepted(char c) { + return c == value1 || c == value2; + } + }; + } + + static StringReader createReader(String input, int cursor) { + StringReader stringReader = new StringReader(input); + stringReader.setCursor(cursor); + return stringReader; + } + + abstract class TerminalCharacters implements Term { + private final DelayedException error; + private final SuggestionSupplier suggestions; + + public TerminalCharacters(CharList characters) { + String string = characters.intStream().mapToObj(Character::toString).collect(Collectors.joining("|")); + this.error = DelayedException.create(LITERAL_INCORRECT, string); + this.suggestions = parseState -> characters.intStream().mapToObj(Character::toString); + } + + @Override + public boolean parse(ParseState parseState, Scope scope, Control control) { + parseState.input().skipWhitespace(); + int i = parseState.mark(); + if (parseState.input().canRead() && this.isAccepted(parseState.input().read())) { + return true; + } else { + parseState.errorCollector().store(i, this.suggestions, this.error); + return false; + } + } + + protected abstract boolean isAccepted(char c); + } + +} 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..1909633c8 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/snbt/parse/grammar/UnquotedStringParseRule.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 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 parseState) { + parseState.input().skipWhitespace(); + int i = parseState.mark(); + String unquotedString = parseState.input().readUnquotedString(); + if (unquotedString.length() < this.minSize) { + parseState.errorCollector().store(i, this.error); + return null; + } else { + return unquotedString; + } + } +} diff --git a/gradle.properties b/gradle.properties index 46db05f12..a82325cc5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G # Project settings project_version=0.0.65.15 config_version=60 -lang_version=41 +lang_version=42 project_group=net.momirealms latest_supported_version=1.21.10