9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-19 15:09:15 +00:00

Merge pull request #483 from jhqwqmc/refactor/snbt

更贴切原版的snbt实现
This commit is contained in:
XiaoMoMi
2025-12-05 01:04:25 +08:00
committed by GitHub
35 changed files with 2908 additions and 319 deletions

View File

@@ -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<String, Object> map = (Map<String, Object>) 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);

View File

@@ -105,6 +105,32 @@ warning.config.type.vec3d: "<yellow>Issue found in file <arg:0> - Failed to load
warning.config.type.map: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Map type for option '<arg:3>'.</yellow>"
warning.config.type.aabb: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to AABB type for option '<arg:3>'.</yellow>"
warning.config.type.snbt.invalid_syntax: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Invalid snbt syntax '<arg:2>'.</yellow>"
warning.config.type.snbt.invalid_syntax.parse_error: "<yellow><arg:0> at position <arg:1>: <arg:2></yellow>"
warning.config.type.snbt.invalid_syntax.here: "<yellow><--[HERE]</yellow>"
warning.config.type.snbt.parser.expected_string_uuid: "<yellow>Expected a string representing a valid UUID</yellow>"
warning.config.type.snbt.parser.expected_number_or_boolean: "<yellow>Expected a number or a boolean</yellow>"
warning.config.type.snbt.parser.trailing: "<yellow>Unexpected trailing data</yellow>"
warning.config.type.snbt.parser.expected.compound: "<yellow>Expected compound tag</yellow>"
warning.config.type.snbt.parser.number_parse_failure: "<yellow>Failed to parse number: <arg:0></yellow>"
warning.config.type.snbt.parser.expected_hex_escape: "<yellow>Expected a character literal of length <arg:0></yellow>"
warning.config.type.snbt.parser.invalid_codepoint: "<yellow>Invalid Unicode character value: <arg:0></yellow>"
warning.config.type.snbt.parser.no_such_operation: "<yellow>No such operation: <arg:0></yellow>"
warning.config.type.snbt.parser.expected_integer_type: "<yellow>Expected an integer number</yellow>"
warning.config.type.snbt.parser.expected_float_type: "<yellow>Expected a floating point number</yellow>"
warning.config.type.snbt.parser.expected_non_negative_number: "<yellow>Expected a non-negative number</yellow>"
warning.config.type.snbt.parser.invalid_character_name: "<yellow>Invalid Unicode character name</yellow>"
warning.config.type.snbt.parser.invalid_array_element_type: "<yellow>Invalid array element type</yellow>"
warning.config.type.snbt.parser.invalid_unquoted_start: "<yellow>Unquoted strings can't start with digits 0-9, + or -</yellow>"
warning.config.type.snbt.parser.expected_unquoted_string: "<yellow>Expected a valid unquoted string</yellow>"
warning.config.type.snbt.parser.invalid_string_contents: "<yellow>Invalid string contents</yellow>"
warning.config.type.snbt.parser.expected_binary_numeral: "<yellow>Expected a binary number</yellow>"
warning.config.type.snbt.parser.underscore_not_allowed: "<yellow>Underscore is not allowed in binary numerals</yellow>"
warning.config.type.snbt.parser.expected_decimal_numeral: "<yellow>Expected a decimal number</yellow>"
warning.config.type.snbt.parser.expected_hex_numeral: "<yellow>Expected a hexadecimal number</yellow>"
warning.config.type.snbt.parser.empty_key: "<yellow>Key cannot be empty</yellow>"
warning.config.type.snbt.parser.leading_zero_not_allowed: "<yellow>Decimal numbers can't start with 0</yellow>"
warning.config.type.snbt.parser.infinity_not_allowed: "<yellow>Non-finite numbers are not allowed</yellow>"
warning.config.type.snbt.parser.incorrect: "<yellow>Expected literal <arg:0></yellow>"
warning.config.number.missing_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'type' argument for number argument.</yellow>"
warning.config.number.invalid_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid number argument type '<arg:2>'.</yellow>"
warning.config.number.missing_argument: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the argument for 'number'.</yellow>"

View File

@@ -102,6 +102,32 @@ warning.config.type.vector3f: "<yellow>在文件 <arg:0> 发现问题 - 无法
warning.config.type.vec3d: "<yellow>在文件 <arg:0> 发现问题 - 无法加载 '<arg:1>': 无法将 '<arg:2>' 转换为双精度浮点数三维向量类型 (选项 '<arg:3>')</yellow>"
warning.config.type.map: "<yellow>在文件 <arg:0> 发现问题 - 无法加载 '<arg:1>': 无法将 '<arg:2>' 转换为映射类型 (选项 '<arg:3>')</yellow>"
warning.config.type.snbt.invalid_syntax: "<yellow>在文件 <arg:0> 发现问题 - 无法加载 '<arg:1>': 无效的 SNBT 语法 '<arg:2>'</yellow>"
warning.config.type.snbt.invalid_syntax.parse_error: "<yellow><arg:0>,位于第<arg:1>个字符:<arg:2></yellow>"
warning.config.type.snbt.invalid_syntax.here: "<yellow><--[此处]</yellow>"
warning.config.type.snbt.parser.expected_string_uuid: "<yellow>应为表示有效UUID的字符串</yellow>"
warning.config.type.snbt.parser.expected_number_or_boolean: "<yellow>应为数字或布尔型</yellow>"
warning.config.type.snbt.parser.trailing: "<yellow>多余的尾随数据</yellow>"
warning.config.type.snbt.parser.expected.compound: "<yellow>应为复合标签</yellow>"
warning.config.type.snbt.parser.number_parse_failure: "<yellow>解析数字失败:<arg:0></yellow>"
warning.config.type.snbt.parser.expected_hex_escape: "<yellow>字符字面量长度应为<arg:0></yellow>"
warning.config.type.snbt.parser.invalid_codepoint: "<yellow>无效的Unicode字符码位<arg:0></yellow>"
warning.config.type.snbt.parser.no_such_operation: "<yellow>不存在的操作: <arg:0></yellow>"
warning.config.type.snbt.parser.expected_integer_type: "<yellow>应为整数</yellow>"
warning.config.type.snbt.parser.expected_float_type: "<yellow>应为浮点数</yellow>"
warning.config.type.snbt.parser.expected_non_negative_number: "<yellow>应为非负数</yellow>"
warning.config.type.snbt.parser.invalid_character_name: "<yellow>无效的Unicode字符名称</yellow>"
warning.config.type.snbt.parser.invalid_array_element_type: "<yellow>无效的数组元素类型</yellow>"
warning.config.type.snbt.parser.invalid_unquoted_start: "<yellow>无引号字符串不能以数字0-9、+或-开头</yellow>"
warning.config.type.snbt.parser.expected_unquoted_string: "<yellow>应为有效的无引号字符串</yellow>"
warning.config.type.snbt.parser.invalid_string_contents: "<yellow>无效的字符串内容</yellow>"
warning.config.type.snbt.parser.expected_binary_numeral: "<yellow>应为二进制数</yellow>"
warning.config.type.snbt.parser.underscore_not_allowed: "<yellow>数字的开头和结尾不允许使用下划线字符</yellow>"
warning.config.type.snbt.parser.expected_decimal_numeral: "<yellow>应为十进制数</yellow>"
warning.config.type.snbt.parser.expected_hex_numeral: "<yellow>应为十六进制数</yellow>"
warning.config.type.snbt.parser.empty_key: "<yellow>键不能为空</yellow>"
warning.config.type.snbt.parser.leading_zero_not_allowed: "<yellow>十进制数不能以0开头</yellow>"
warning.config.type.snbt.parser.infinity_not_allowed: "<yellow>不允许使用非有限数的数值</yellow>"
warning.config.type.snbt.parser.incorrect: "<yellow>应为字面量<arg:0></yellow>"
warning.config.number.missing_type: "<yellow>在文件 <arg:0> 发现问题 - 配置项 '<arg:1>' 缺少数字类型所需的 'type' 参数</yellow>"
warning.config.number.invalid_type: "<yellow>在文件 <arg:0> 发现问题 - 配置项 '<arg:1>' 使用了无效的数字类型 '<arg:2>'</yellow>"
warning.config.number.missing_argument: "<yellow>在文件 <arg:0> 发现问题 - 配置项 '<arg:1>' 缺少数字参数</yellow>"

View File

@@ -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<I> implements ItemDataModifier<I> {
if (string.startsWith("(json) ")) {
return CraftEngine.instance().platform().jsonToSparrowNBT(GsonHelper.get().fromJson(string.substring("(json) ".length()), JsonElement.class));
} else if (string.startsWith("(snbt) ")) {
return CraftEngine.instance().platform().snbtToSparrowNBT(string.substring("(snbt) ".length()));
String snbt = string.substring("(snbt) ".length());
try {
return TagParser.parseTagFully(snbt);
} catch (CommandSyntaxException e) {
throw new LocalizedResourceConfigException("warning.config.type.snbt.invalid_syntax", e.getMessage());
}
}
}
return CraftEngine.instance().platform().javaToSparrowNBT(value);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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<String, Object> parseCompound() {
skip(); // 跳过 '{'
skipWhitespace();
Map<String, Object> 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<Object> 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<List<Number>, Object> convertor) {
skipWhitespace();
// 用来暂存解析出的数字
List<Number> 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
package net.momirealms.craftengine.core.util.snbt;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.serialization.DynamicOps;
import net.momirealms.craftengine.core.util.snbt.parse.*;
import net.momirealms.sparrow.nbt.util.UUIDUtil;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class SnbtOperations {
static final DelayedException<CommandSyntaxException> ERROR_EXPECTED_STRING_UUID = DelayedException.create(
new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_string_uuid"))
);
static final DelayedException<CommandSyntaxException> ERROR_EXPECTED_NUMBER_OR_BOOLEAN = DelayedException.create(
new LocalizedSimpleCommandExceptionType(new LocalizedMessage("warning.config.type.snbt.parser.expected_number_or_boolean"))
);
public static final String BUILTIN_TRUE = "true";
public static final String BUILTIN_FALSE = "false";
public static final String BUILTIN_NULL = "null";
public static final Map<BuiltinKey, BuiltinOperation> BUILTIN_OPERATIONS = Map.of(
new BuiltinKey("bool", 1), new BuiltinOperation() {
@Override
public <T> T run(DynamicOps<T> ops, List<T> arguments, ParseState<StringReader> state) {
Boolean result = convert(ops, arguments.getFirst());
if (result == null) {
state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_NUMBER_OR_BOOLEAN);
return null;
}
return ops.createBoolean(result);
}
@Nullable
private static <T> Boolean convert(DynamicOps<T> ops, T arg) {
Optional<Boolean> asBoolean = ops.getBooleanValue(arg).result();
if (asBoolean.isPresent()) {
return asBoolean.get();
} else {
Optional<Number> asNumber = ops.getNumberValue(arg).result();
return asNumber.isPresent() ? asNumber.get().doubleValue() != 0.0 : null;
}
}
}, new BuiltinKey("uuid", 1), new BuiltinOperation() {
@Override
public <T> T run(DynamicOps<T> ops, List<T> arguments, ParseState<StringReader> state) {
Optional<String> arg = ops.getStringValue(arguments.getFirst()).result();
if (arg.isEmpty()) {
state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_STRING_UUID);
return null;
}
UUID uuid;
try {
uuid = UUID.fromString(arg.get());
} catch (IllegalArgumentException var7) {
state.errorCollector().store(state.mark(), SnbtOperations.ERROR_EXPECTED_STRING_UUID);
return null;
}
return ops.createIntList(IntStream.of(UUIDUtil.uuidToIntArray(uuid)));
}
}
);
public static final SuggestionSupplier<StringReader> BUILTIN_IDS = new SuggestionSupplier<>() {
private final Set<String> keys = Stream.concat(
Stream.of(BUILTIN_FALSE, BUILTIN_TRUE, BUILTIN_NULL), SnbtOperations.BUILTIN_OPERATIONS.keySet().stream().map(BuiltinKey::id)
)
.collect(Collectors.toSet());
@Override
public Stream<String> possibleValues(ParseState<StringReader> state) {
return this.keys.stream();
}
};
public record BuiltinKey(String id, int argCount) {
@Override
public @NotNull String toString() {
return this.id + "/" + this.argCount;
}
}
public interface BuiltinOperation {
@Nullable
<T> T run(DynamicOps<T> ops, List<T> arguments, ParseState<StringReader> state);
}
}

View File

@@ -0,0 +1,97 @@
package net.momirealms.craftengine.core.util.snbt;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JavaOps;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.snbt.parse.LocalizedMessage;
import net.momirealms.craftengine.core.util.snbt.parse.LocalizedSimpleCommandExceptionType;
import net.momirealms.craftengine.core.util.snbt.parse.grammar.Grammar;
import net.momirealms.sparrow.nbt.CompoundTag;
import net.momirealms.sparrow.nbt.Tag;
import net.momirealms.sparrow.nbt.codec.LegacyJavaOps;
import net.momirealms.sparrow.nbt.codec.LegacyNBTOps;
import net.momirealms.sparrow.nbt.codec.NBTOps;
@SuppressWarnings("unused")
public class TagParser<T> {
public static final SimpleCommandExceptionType ERROR_TRAILING_DATA = new LocalizedSimpleCommandExceptionType(
new LocalizedMessage("warning.config.type.snbt.parser.trailing")
);
public static final SimpleCommandExceptionType ERROR_EXPECTED_COMPOUND = new LocalizedSimpleCommandExceptionType(
new LocalizedMessage("warning.config.type.snbt.parser.expected.compound")
);
public static final char ELEMENT_SEPARATOR = ',';
public static final char NAME_VALUE_SEPARATOR = ':';
public static final TagParser<Tag> NBT_OPS_PARSER = create(VersionHelper.isOrAbove1_20_5() ? NBTOps.INSTANCE : LegacyNBTOps.INSTANCE);
public static final TagParser<Object> JAVA_OPS_PARSER = create(VersionHelper.isOrAbove1_20_5() ? JavaOps.INSTANCE : LegacyJavaOps.INSTANCE);
private final DynamicOps<T> ops;
private final Grammar<T> grammar;
private TagParser(DynamicOps<T> ops, Grammar<T> grammar) {
this.ops = ops;
this.grammar = grammar;
}
public DynamicOps<T> ops() {
return this.ops;
}
public static <T> TagParser<T> create(DynamicOps<T> ops) {
return new TagParser<>(ops, SnbtGrammar.createParser(ops));
}
private static CompoundTag castToCompoundOrThrow(StringReader reader, Tag result) throws CommandSyntaxException {
if (result instanceof CompoundTag compoundTag) {
return compoundTag;
}
throw ERROR_EXPECTED_COMPOUND.createWithContext(reader);
}
public static CompoundTag parseCompoundFully(String input) throws CommandSyntaxException {
StringReader reader = new StringReader(input);
Tag result = NBT_OPS_PARSER.parseFully(reader);
return castToCompoundOrThrow(reader, result);
}
public static Tag parseTagFully(String input) throws CommandSyntaxException {
StringReader reader = new StringReader(input);
return NBT_OPS_PARSER.parseFully(reader);
}
public static Object parseObjectFully(String input) throws CommandSyntaxException {
StringReader reader = new StringReader(input);
return JAVA_OPS_PARSER.parseFully(reader);
}
public T parseFully(String input) throws CommandSyntaxException {
return this.parseFully(new StringReader(input));
}
public T parseFully(StringReader reader) throws CommandSyntaxException {
T result = this.grammar.parse(reader);
reader.skipWhitespace();
if (reader.canRead()) {
throw ERROR_TRAILING_DATA.createWithContext(reader);
}
return result;
}
public T parseAsArgument(StringReader reader) throws CommandSyntaxException {
return this.grammar.parse(reader);
}
public static CompoundTag parseCompoundAsArgument(StringReader reader) throws CommandSyntaxException {
Tag result = parseTagAsArgument(reader);
return castToCompoundOrThrow(reader, result);
}
public static Tag parseTagAsArgument(StringReader reader) throws CommandSyntaxException {
return NBT_OPS_PARSER.parseAsArgument(reader);
}
public static Object parseObjectAsArgument(StringReader reader) throws CommandSyntaxException {
return JAVA_OPS_PARSER.parseAsArgument(reader);
}
}

View File

@@ -0,0 +1,15 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public record Atom<T>(String name) {
@Override
public @NotNull String toString() {
return "<" + this.name + ">";
}
public static <T> Atom<T> of(String name) {
return new Atom<>(name);
}
}

View File

@@ -0,0 +1,235 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import net.momirealms.craftengine.core.util.MiscUtils;
import javax.annotation.Nullable;
@SuppressWarnings("unchecked")
public abstract class CachedParseState<S> implements ParseState<S> {
private PositionCache[] positionCache = new PositionCache[256];
private final ErrorCollector<S> errorCollector;
private final Scope scope = new Scope();
private SimpleControl[] controlCache = new SimpleControl[16];
private int nextControlToReturn;
private final Silent silent = new Silent();
public static final Object JAVA_NULL_VALUE_MARKER = new Object() {
@Override
public String toString() {
return "null";
}
};
protected CachedParseState(ErrorCollector<S> errorCollector) {
this.errorCollector = errorCollector;
}
@Override
public Scope scope() {
return this.scope;
}
@Override
public ErrorCollector<S> errorCollector() {
return this.errorCollector;
}
@Nullable
@Override
public <T> T parse(NamedRule<S, T> rule) {
int markBeforeParse = this.mark();
PositionCache positionCache = this.getCacheForPosition(markBeforeParse);
int entryIndex = positionCache.findKeyIndex(rule.name());
if (entryIndex != -1) {
CacheEntry<T> value = positionCache.getValue(entryIndex);
if (value != null) {
if (value == CachedParseState.CacheEntry.NEGATIVE) {
return null;
}
this.restore(value.markAfterParse);
return value.value;
}
} else {
entryIndex = positionCache.allocateNewEntry(rule.name());
}
T result = rule.value().parse(this);
CacheEntry<T> entry;
if (result == null) {
entry = CacheEntry.negativeEntry();
} else {
int markAfterParse = this.mark();
entry = new CacheEntry<>(result, markAfterParse);
}
positionCache.setValue(entryIndex, entry);
return result;
}
private PositionCache getCacheForPosition(int index) {
int currentSize = this.positionCache.length;
if (index >= currentSize) {
int newSize = MiscUtils.growByHalf(currentSize, index + 1);
PositionCache[] newCache = new PositionCache[newSize];
System.arraycopy(this.positionCache, 0, newCache, 0, currentSize);
this.positionCache = newCache;
}
PositionCache result = this.positionCache[index];
if (result == null) {
result = new PositionCache();
this.positionCache[index] = result;
}
return result;
}
@Override
public Control acquireControl() {
int currentSize = this.controlCache.length;
if (this.nextControlToReturn >= currentSize) {
int newSize = MiscUtils.growByHalf(currentSize, this.nextControlToReturn + 1);
SimpleControl[] newControlCache = new SimpleControl[newSize];
System.arraycopy(this.controlCache, 0, newControlCache, 0, currentSize);
this.controlCache = newControlCache;
}
int controlIndex = this.nextControlToReturn++;
SimpleControl entry = this.controlCache[controlIndex];
if (entry == null) {
entry = new SimpleControl();
this.controlCache[controlIndex] = entry;
} else {
entry.reset();
}
return entry;
}
@Override
public void releaseControl() {
this.nextControlToReturn--;
}
@Override
public ParseState<S> silent() {
return this.silent;
}
record CacheEntry<T>(@Nullable T value, int markAfterParse) {
public static final CacheEntry<?> NEGATIVE = new CacheEntry<>(null, -1);
public static <T> CacheEntry<T> negativeEntry() {
return (CacheEntry<T>) NEGATIVE;
}
}
static class PositionCache {
public static final int ENTRY_STRIDE = 2;
private static final int NOT_FOUND = -1;
private Object[] atomCache = new Object[16];
private int nextKey;
public int findKeyIndex(Atom<?> key) {
for (int i = 0; i < this.nextKey; i += ENTRY_STRIDE) {
if (this.atomCache[i] == key) {
return i;
}
}
return NOT_FOUND;
}
public int allocateNewEntry(Atom<?> key) {
int newKeyIndex = this.nextKey;
this.nextKey += ENTRY_STRIDE;
int newValueIndex = newKeyIndex + 1;
int currentSize = this.atomCache.length;
if (newValueIndex >= currentSize) {
int newSize = MiscUtils.growByHalf(currentSize, newValueIndex + 1);
Object[] newCache = new Object[newSize];
System.arraycopy(this.atomCache, 0, newCache, 0, currentSize);
this.atomCache = newCache;
}
this.atomCache[newKeyIndex] = key;
return newKeyIndex;
}
@Nullable
public <T> CacheEntry<T> getValue(int keyIndex) {
return (CacheEntry<T>) this.atomCache[keyIndex + 1];
}
public void setValue(int keyIndex, CacheEntry<?> entry) {
this.atomCache[keyIndex + 1] = entry;
}
}
class Silent implements ParseState<S> {
private final ErrorCollector<S> silentCollector = new ErrorCollector.Nop<>();
@Override
public ErrorCollector<S> errorCollector() {
return this.silentCollector;
}
@Override
public Scope scope() {
return CachedParseState.this.scope();
}
@Nullable
@Override
public <T> T parse(NamedRule<S, T> rule) {
return CachedParseState.this.parse(rule);
}
@Override
public S input() {
return CachedParseState.this.input();
}
@Override
public int mark() {
return CachedParseState.this.mark();
}
@Override
public void restore(int mark) {
CachedParseState.this.restore(mark);
}
@Override
public Control acquireControl() {
return CachedParseState.this.acquireControl();
}
@Override
public void releaseControl() {
CachedParseState.this.releaseControl();
}
@Override
public ParseState<S> silent() {
return this;
}
}
static class SimpleControl implements Control {
private boolean hasCut;
@Override
public void cut() {
this.hasCut = true;
}
@Override
public boolean hasCut() {
return this.hasCut;
}
public void reset() {
this.hasCut = false;
}
}
}

View File

@@ -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();
}

View File

@@ -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 extends Exception> {
T create(String contents, int position);
static DelayedException<CommandSyntaxException> create(SimpleCommandExceptionType type) {
return (contents, position) -> type.createWithContext(StringReaderTerms.createReader(contents, position));
}
static DelayedException<CommandSyntaxException> create(DynamicCommandExceptionType type, String argument) {
return (contents, position) -> type.createWithContext(StringReaderTerms.createReader(contents, position), argument);
}
}

View File

@@ -0,0 +1,93 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import javax.annotation.Nullable;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
@SuppressWarnings("unchecked")
public class Dictionary<S> {
private final Map<Atom<?>, Entry<S, ?>> terms = new IdentityHashMap<>();
public <T> NamedRule<S, T> put(Atom<T> name, Rule<S, T> entry) {
Entry<S, T> holder = (Entry<S, T>)this.terms.computeIfAbsent(name, Entry::new);
if (holder.value != null) {
throw new IllegalArgumentException("Trying to override rule: " + name);
}
holder.value = entry;
return holder;
}
public <T> NamedRule<S, T> putComplex(Atom<T> name, Term<S> term, Rule.RuleAction<S, T> action) {
return this.put(name, Rule.fromTerm(term, action));
}
public <T> NamedRule<S, T> put(Atom<T> name, Term<S> term, Rule.SimpleRuleAction<S, T> action) {
return this.put(name, Rule.fromTerm(term, action));
}
public void checkAllBound() {
List<? extends Atom<?>> unboundNames = this.terms.entrySet().stream()
.filter(entry -> entry.getValue() == null)
.map(Map.Entry::getKey)
.toList();
if (!unboundNames.isEmpty()) {
throw new IllegalStateException("Unbound names: " + unboundNames);
}
}
public <T> NamedRule<S, T> forward(Atom<T> name) {
return this.getOrCreateEntry(name);
}
private <T> Entry<S, T> getOrCreateEntry(Atom<T> name) {
return (Entry<S, T>)this.terms.computeIfAbsent(name, Entry::new);
}
public <T> Term<S> named(Atom<T> name) {
return new Reference<>(this.getOrCreateEntry(name), name);
}
public <T> Term<S> namedWithAlias(Atom<T> nameToParse, Atom<T> nameToStore) {
return new Reference<>(this.getOrCreateEntry(nameToParse), nameToStore);
}
static class Entry<S, T> implements NamedRule<S, T>, Supplier<String> {
private final Atom<T> name;
@Nullable
Rule<S, T> value;
private Entry(Atom<T> name) {
this.name = name;
}
@Override
public Atom<T> name() {
return this.name;
}
@Override
public Rule<S, T> value() {
return Objects.requireNonNull(this.value, this);
}
@Override
public String get() {
return "Unbound rule " + this.name;
}
}
record Reference<S, T>(Entry<S, T> ruleToParse, Atom<T> nameToStore) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
T result = state.parse(this.ruleToParse);
if (result == null) {
return false;
}
scope.put(this.nameToStore, result);
return true;
}
}
}

View File

@@ -0,0 +1,97 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import net.momirealms.craftengine.core.util.MiscUtils;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("unchecked")
public interface ErrorCollector<S> {
void store(int cursor, SuggestionSupplier<S> suggestions, Object reason);
default void store(int cursor, Object reason) {
this.store(cursor, SuggestionSupplier.empty(), reason);
}
void finish(int finalCursor);
class LongestOnly<S> implements ErrorCollector<S> {
private MutableErrorEntry<S>[] entries = new MutableErrorEntry[16];
private int nextErrorEntry;
private int lastCursor = -1;
private void discardErrorsFromShorterParse(int cursor) {
if (cursor > this.lastCursor) {
this.lastCursor = cursor;
this.nextErrorEntry = 0;
}
}
@Override
public void finish(int finalCursor) {
this.discardErrorsFromShorterParse(finalCursor);
}
@Override
public void store(int cursor, SuggestionSupplier<S> suggestions, Object reason) {
this.discardErrorsFromShorterParse(cursor);
if (cursor == this.lastCursor) {
this.addErrorEntry(suggestions, reason);
}
}
private void addErrorEntry(SuggestionSupplier<S> suggestions, Object reason) {
int currentSize = this.entries.length;
if (this.nextErrorEntry >= currentSize) {
int newSize = MiscUtils.growByHalf(currentSize, this.nextErrorEntry + 1);
MutableErrorEntry<S>[] newEntries = new MutableErrorEntry[newSize];
System.arraycopy(this.entries, 0, newEntries, 0, currentSize);
this.entries = newEntries;
}
int entryIndex = this.nextErrorEntry++;
MutableErrorEntry<S> entry = this.entries[entryIndex];
if (entry == null) {
entry = new MutableErrorEntry<>();
this.entries[entryIndex] = entry;
}
entry.suggestions = suggestions;
entry.reason = reason;
}
public List<ErrorEntry<S>> entries() {
int errorCount = this.nextErrorEntry;
if (errorCount == 0) {
return List.of();
}
List<ErrorEntry<S>> result = new ArrayList<>(errorCount);
for (int i = 0; i < errorCount; i++) {
MutableErrorEntry<S> mutableErrorEntry = this.entries[i];
result.add(new ErrorEntry<>(this.lastCursor, mutableErrorEntry.suggestions, mutableErrorEntry.reason));
}
return result;
}
public int cursor() {
return this.lastCursor;
}
static class MutableErrorEntry<S> {
SuggestionSupplier<S> suggestions = SuggestionSupplier.empty();
Object reason = "empty";
}
}
class Nop<S> implements ErrorCollector<S> {
@Override
public void store(int cursor, SuggestionSupplier<S> suggestions, Object reason) {
}
@Override
public void finish(int finalCursor) {
}
}
}

View File

@@ -0,0 +1,4 @@
package net.momirealms.craftengine.core.util.snbt.parse;
public record ErrorEntry<S>(int cursor, SuggestionSupplier<S> suggestions, Object reason) {
}

View File

@@ -0,0 +1,84 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import com.mojang.brigadier.Message;
import com.mojang.brigadier.exceptions.CommandExceptionType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.util.AdventureHelper;
import java.util.Optional;
import java.util.function.Supplier;
public class LocalizedCommandSyntaxException extends CommandSyntaxException {
public static final int CONTEXT_AMOUNT = 50;
public static final String PARSE_ERROR_NODE = "warning.config.type.snbt.invalid_syntax.parse_error";
public static final String HERE_NODE = "warning.config.type.snbt.invalid_syntax.here";
private final Message message;
private final String input;
private final int cursor;
public LocalizedCommandSyntaxException(CommandExceptionType type, Message message) {
super(type, message);
this.message = message;
this.input = null;
this.cursor = -1;
}
public LocalizedCommandSyntaxException(CommandExceptionType type, Message message, String input, int cursor) {
super(type, message, input, cursor);
this.message = message;
this.input = input;
this.cursor = cursor;
}
@Override
public String getMessage() {
String message = this.message.getString();
final String context = getContext();
if (context == null) {
return message;
}
return generateLocalizedMessage(
PARSE_ERROR_NODE,
() -> message + " at position " + this.cursor + ": " + context,
message, String.valueOf(this.cursor), context
);
}
@Override
public String getContext() {
if (this.input == null || this.cursor < 0) {
return null;
}
final StringBuilder builder = new StringBuilder();
final int cursor = Math.min(this.input.length(), this.cursor);
if (cursor > CONTEXT_AMOUNT) {
builder.append("...");
}
builder.append(this.input, Math.max(0, cursor - CONTEXT_AMOUNT), cursor);
builder.append(generateLocalizedMessage(HERE_NODE, () -> "<--[HERE]"));
return builder.toString();
}
private String generateLocalizedMessage(String node, Supplier<String> 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(
"<arg:" + i + ">",
arguments[i] != null ? arguments[i] : "null"
);
}
return cleanMessage;
} catch (Exception e) {
return fallback.get();
}
}
}

View File

@@ -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<Object, Message> function;
public LocalizedDynamicCommandExceptionType(Function<Object, Message> 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());
}
}

View File

@@ -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(
"<arg:" + i + ">",
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()
);
}
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,7 @@
package net.momirealms.craftengine.core.util.snbt.parse;
public interface NamedRule<S, T> {
Atom<T> name();
Rule<S, T> value();
}

View File

@@ -0,0 +1,37 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import javax.annotation.Nullable;
import java.util.Optional;
public interface ParseState<S> {
Scope scope();
ErrorCollector<S> errorCollector();
default <T> Optional<T> parseTopRule(NamedRule<S, T> rule) {
T result = this.parse(rule);
if (result != null) {
this.errorCollector().finish(this.mark());
}
if (!this.scope().hasOnlySingleFrame()) {
throw new IllegalStateException("Malformed scope: " + this.scope());
}
return Optional.ofNullable(result);
}
@Nullable
<T> T parse(NamedRule<S, T> rule);
S input();
int mark();
void restore(int mark);
Control acquireControl();
void releaseControl();
ParseState<S> silent();
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import javax.annotation.Nullable;
public interface Rule<S, T> {
@Nullable
T parse(ParseState<S> state);
static <S, T> Rule<S, T> fromTerm(Term<S> child, RuleAction<S, T> action) {
return new WrappedTerm<>(action, child);
}
static <S, T> Rule<S, T> fromTerm(Term<S> child, SimpleRuleAction<S, T> action) {
return new WrappedTerm<>(action, child);
}
@FunctionalInterface
interface RuleAction<S, T> {
@Nullable
T run(ParseState<S> state);
}
@FunctionalInterface
interface SimpleRuleAction<S, T> extends RuleAction<S, T> {
T run(Scope ruleScope);
@Override
default T run(ParseState<S> state) {
return this.run(state.scope());
}
}
record WrappedTerm<S, T>(RuleAction<S, T> action, Term<S> child) implements Rule<S, T> {
@Nullable
@Override
public T parse(ParseState<S> state) {
Scope scope = state.scope();
scope.pushFrame();
try {
if (!this.child.parse(state, scope, Control.UNBOUND)) {
return null;
}
return this.action.run(state);
} finally {
scope.popFrame();
}
}
}
}

View File

@@ -0,0 +1,315 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import com.google.common.annotations.VisibleForTesting;
import net.momirealms.craftengine.core.util.MiscUtils;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unchecked")
public final class Scope {
private static final int NOT_FOUND = -1;
private static final Object FRAME_START_MARKER = new Object() {
@Override
public String toString() {
return "frame";
}
};
private static final int ENTRY_STRIDE = 2;
private Object[] stack = new Object[128];
private int topEntryKeyIndex = 0;
private int topMarkerKeyIndex = 0;
private int depth;
public Scope() {
this.stack[0] = FRAME_START_MARKER;
this.stack[1] = null;
}
private int valueIndex(Atom<?> atom) {
for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) {
Object key = this.stack[i];
assert key instanceof Atom;
if (key == atom) {
return i + 1;
}
}
return NOT_FOUND;
}
public int valueIndexForAny(Atom<?>... atoms) {
for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) {
Object key = this.stack[i];
assert key instanceof Atom;
for (Atom<?> atom : atoms) {
if (atom == key) {
return i + 1;
}
}
}
return NOT_FOUND;
}
private void ensureCapacity(int additionalEntryCount) {
int currentSize = this.stack.length;
int currentLastValueIndex = this.topEntryKeyIndex + 1;
int newLastValueIndex = currentLastValueIndex + additionalEntryCount * 2;
if (newLastValueIndex >= currentSize) {
int newSize = MiscUtils.growByHalf(currentSize, newLastValueIndex + 1);
Object[] newStack = new Object[newSize];
System.arraycopy(this.stack, 0, newStack, 0, currentSize);
this.stack = newStack;
}
assert this.validateStructure();
}
private void setupNewFrame() {
this.topEntryKeyIndex += ENTRY_STRIDE;
this.stack[this.topEntryKeyIndex] = FRAME_START_MARKER;
this.stack[this.topEntryKeyIndex + 1] = this.topMarkerKeyIndex;
this.topMarkerKeyIndex = this.topEntryKeyIndex;
}
public void pushFrame() {
this.ensureCapacity(1);
this.setupNewFrame();
assert this.validateStructure();
}
private int getPreviousMarkerIndex(int markerKeyIndex) {
return (Integer) this.stack[markerKeyIndex + 1];
}
public void popFrame() {
assert this.topMarkerKeyIndex != 0;
this.topEntryKeyIndex = this.topMarkerKeyIndex - ENTRY_STRIDE;
this.topMarkerKeyIndex = this.getPreviousMarkerIndex(this.topMarkerKeyIndex);
assert this.validateStructure();
}
public void splitFrame() {
int currentFrameMarkerIndex = this.topMarkerKeyIndex;
int nonMarkerEntriesInFrame = (this.topEntryKeyIndex - this.topMarkerKeyIndex) / ENTRY_STRIDE;
this.ensureCapacity(nonMarkerEntriesInFrame + 1);
this.setupNewFrame();
int sourceCursor = currentFrameMarkerIndex + ENTRY_STRIDE;
int targetCursor = this.topEntryKeyIndex;
for (int i = 0; i < nonMarkerEntriesInFrame; i++) {
targetCursor += ENTRY_STRIDE;
Object key = this.stack[sourceCursor];
assert key != null;
this.stack[targetCursor] = key;
this.stack[targetCursor + 1] = null;
sourceCursor += ENTRY_STRIDE;
}
this.topEntryKeyIndex = targetCursor;
assert this.validateStructure();
}
public void clearFrameValues() {
for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) {
assert this.stack[i] instanceof Atom;
this.stack[i + 1] = null;
}
assert this.validateStructure();
}
public void mergeFrame() {
int previousMarkerIndex = this.getPreviousMarkerIndex(this.topMarkerKeyIndex);
int previousFrameCursor = previousMarkerIndex;
int currentFrameCursor = this.topMarkerKeyIndex;
while (currentFrameCursor < this.topEntryKeyIndex) {
previousFrameCursor += ENTRY_STRIDE;
currentFrameCursor += ENTRY_STRIDE;
Object newKey = this.stack[currentFrameCursor];
assert newKey instanceof Atom;
Object newValue = this.stack[currentFrameCursor + 1];
Object oldKey = this.stack[previousFrameCursor];
if (oldKey != newKey) {
this.stack[previousFrameCursor] = newKey;
this.stack[previousFrameCursor + 1] = newValue;
} else if (newValue != null) {
this.stack[previousFrameCursor + 1] = newValue;
}
}
this.topEntryKeyIndex = previousFrameCursor;
this.topMarkerKeyIndex = previousMarkerIndex;
assert this.validateStructure();
}
public <T> void put(Atom<T> name, @Nullable T value) {
int valueIndex = this.valueIndex(name);
if (valueIndex != NOT_FOUND) {
this.stack[valueIndex] = value;
} else {
this.ensureCapacity(1);
this.topEntryKeyIndex += ENTRY_STRIDE;
this.stack[this.topEntryKeyIndex] = name;
this.stack[this.topEntryKeyIndex + 1] = value;
}
assert this.validateStructure();
}
@Nullable
public <T> T get(Atom<T> name) {
int valueIndex = this.valueIndex(name);
return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : null);
}
public <T> T getOrThrow(Atom<T> name) {
int valueIndex = this.valueIndex(name);
if (valueIndex == NOT_FOUND) {
throw new IllegalArgumentException("No value for atom " + name);
}
return (T) this.stack[valueIndex];
}
public <T> T getOrDefault(Atom<T> name, T fallback) {
int valueIndex = this.valueIndex(name);
return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : fallback);
}
@Nullable
@SafeVarargs
public final <T> T getAny(Atom<? extends T>... names) {
int valueIndex = this.valueIndexForAny(names);
return (T) (valueIndex != NOT_FOUND ? this.stack[valueIndex] : null);
}
@SafeVarargs
public final <T> T getAnyOrThrow(Atom<? extends T>... names) {
int valueIndex = this.valueIndexForAny(names);
if (valueIndex == NOT_FOUND) {
throw new IllegalArgumentException("No value for atoms " + Arrays.toString(names));
}
return (T) this.stack[valueIndex];
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
boolean afterFrame = true;
for (int i = 0; i <= this.topEntryKeyIndex; i += ENTRY_STRIDE) {
Object key = this.stack[i];
Object value = this.stack[i + 1];
if (key == FRAME_START_MARKER) {
result.append('|');
afterFrame = true;
} else {
if (!afterFrame) {
result.append(',');
}
afterFrame = false;
result.append(key).append(':').append(value);
}
}
return result.toString();
}
@VisibleForTesting
public Map<Atom<?>, ?> lastFrame() {
HashMap<Atom<?>, Object> result = new HashMap<>();
for (int i = this.topEntryKeyIndex; i > this.topMarkerKeyIndex; i -= ENTRY_STRIDE) {
Object key = this.stack[i];
Object value = this.stack[i + 1];
result.put((Atom<?>) key, value);
}
return result;
}
public boolean hasOnlySingleFrame() {
for (int i = this.topEntryKeyIndex; i > 0; i--) {
if (this.stack[i] == FRAME_START_MARKER) {
return false;
}
}
if (this.stack[0] != FRAME_START_MARKER) {
throw new IllegalStateException("Corrupted stack");
}
return true;
}
private boolean validateStructure() {
assert this.topMarkerKeyIndex >= 0;
assert this.topEntryKeyIndex >= this.topMarkerKeyIndex;
for (int i = 0; i <= this.topEntryKeyIndex; i += ENTRY_STRIDE) {
Object object = this.stack[i];
if (object != FRAME_START_MARKER && !(object instanceof Atom)) {
return false;
}
}
for (int ix = this.topMarkerKeyIndex; ix != 0; ix = this.getPreviousMarkerIndex(ix)) {
Object object = this.stack[ix];
if (object != FRAME_START_MARKER) {
return false;
}
}
return true;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static <S> Term<S> increaseDepth() {
class IncreasingDepthTerm<W> implements Term<W> {
public static final IncreasingDepthTerm INSTANCE = new IncreasingDepthTerm();
@Override
public boolean parse(final ParseState<W> state, final Scope scope, final Control control) {
if (++scope.depth > 512) {
state.errorCollector().store(state.mark(), new IllegalStateException("Too deep"));
return false;
}
return true;
}
}
return (Term<S>) IncreasingDepthTerm.INSTANCE;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static <S> Term<S> decreaseDepth() {
class DecreasingDepthTerm<W> implements Term<W> {
public static final DecreasingDepthTerm INSTANCE = new DecreasingDepthTerm();
@Override
public boolean parse(final ParseState<W> state, final Scope scope, final Control control) {
scope.depth--;
return true;
}
}
return (Term<S>) DecreasingDepthTerm.INSTANCE;
}
}

View File

@@ -0,0 +1,11 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import java.util.stream.Stream;
public interface SuggestionSupplier<S> {
Stream<String> possibleValues(ParseState<S> state);
static <S> SuggestionSupplier<S> empty() {
return state -> Stream.empty();
}
}

View File

@@ -0,0 +1,235 @@
package net.momirealms.craftengine.core.util.snbt.parse;
import java.util.ArrayList;
import java.util.List;
public interface Term<S> {
boolean parse(ParseState<S> state, Scope scope, Control control);
static <S, T> Term<S> marker(Atom<T> name, T value) {
return new Marker<>(name, value);
}
@SafeVarargs
static <S> Term<S> sequence(Term<S>... terms) {
return new Sequence<>(terms);
}
@SafeVarargs
static <S> Term<S> alternative(Term<S>... terms) {
return new Alternative<>(terms);
}
static <S> Term<S> optional(Term<S> term) {
return new Maybe<>(term);
}
static <S, T> Term<S> repeated(NamedRule<S, T> element, Atom<List<T>> listName) {
return repeated(element, listName, 0);
}
static <S, T> Term<S> repeated(NamedRule<S, T> element, Atom<List<T>> listName, int minRepetitions) {
return new Repeated<>(element, listName, minRepetitions);
}
static <S, T> Term<S> repeatedWithTrailingSeparator(NamedRule<S, T> element, Atom<List<T>> listName, Term<S> separator) {
return repeatedWithTrailingSeparator(element, listName, separator, 0);
}
static <S, T> Term<S> repeatedWithTrailingSeparator(NamedRule<S, T> element, Atom<List<T>> listName, Term<S> seperator, int minRepetitions) {
return new RepeatedWithSeparator<>(element, listName, seperator, minRepetitions, true);
}
static <S> Term<S> positiveLookahead(Term<S> term) {
return new LookAhead<>(term, true);
}
static <S> Term<S> cut() {
return new Term<>() {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
control.cut();
return true;
}
@Override
public String toString() {
return "";
}
};
}
static <S> Term<S> empty() {
return new Term<>() {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
return true;
}
@Override
public String toString() {
return "ε";
}
};
}
static <S> Term<S> fail(final Object message) {
return new Term<>() {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
state.errorCollector().store(state.mark(), message);
return false;
}
@Override
public String toString() {
return "fail";
}
};
}
record Alternative<S>(Term<S>[] elements) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
Control controlForThis = state.acquireControl();
try {
int mark = state.mark();
scope.splitFrame();
for (Term<S> element : this.elements) {
if (element.parse(state, scope, controlForThis)) {
scope.mergeFrame();
return true;
}
scope.clearFrameValues();
state.restore(mark);
if (controlForThis.hasCut()) {
break;
}
}
scope.popFrame();
return false;
} finally {
state.releaseControl();
}
}
}
record LookAhead<S>(Term<S> term, boolean positive) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
int mark = state.mark();
boolean result = this.term.parse(state.silent(), scope, control);
state.restore(mark);
return this.positive == result;
}
}
record Marker<S, T>(Atom<T> name, T value) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
scope.put(this.name, this.value);
return true;
}
}
record Maybe<S>(Term<S> term) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
int mark = state.mark();
if (!this.term.parse(state, scope, control)) {
state.restore(mark);
}
return true;
}
}
record Repeated<S, T>(NamedRule<S, T> element, Atom<List<T>> listName, int minRepetitions) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
int mark = state.mark();
List<T> elements = new ArrayList<>(this.minRepetitions);
while (true) {
int entryMark = state.mark();
T parsedElement = state.parse(this.element);
if (parsedElement == null) {
state.restore(entryMark);
if (elements.size() < this.minRepetitions) {
state.restore(mark);
return false;
}
scope.put(this.listName, elements);
return true;
}
elements.add(parsedElement);
}
}
}
record RepeatedWithSeparator<S, T>(
NamedRule<S, T> element, Atom<List<T>> listName, Term<S> separator, int minRepetitions, boolean allowTrailingSeparator
) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
int listMark = state.mark();
List<T> elements = new ArrayList<>(this.minRepetitions);
boolean first = true;
while (true) {
int markBeforeSeparator = state.mark();
if (!first && !this.separator.parse(state, scope, control)) {
state.restore(markBeforeSeparator);
break;
}
int markAfterSeparator = state.mark();
T parsedElement = state.parse(this.element);
if (parsedElement == null) {
if (first) {
state.restore(markAfterSeparator);
} else {
if (!this.allowTrailingSeparator) {
state.restore(listMark);
return false;
}
state.restore(markAfterSeparator);
}
break;
}
elements.add(parsedElement);
first = false;
}
if (elements.size() < this.minRepetitions) {
state.restore(listMark);
return false;
}
scope.put(this.listName, elements);
return true;
}
}
record Sequence<S>(Term<S>[] elements) implements Term<S> {
@Override
public boolean parse(ParseState<S> state, Scope scope, Control control) {
int mark = state.mark();
for (Term<S> element : this.elements) {
if (!element.parse(state, scope, control)) {
state.restore(mark);
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.util.snbt.parse.*;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public record Grammar<T>(Dictionary<StringReader> rules, NamedRule<StringReader, T> top) {
public Grammar {
rules.checkAllBound();
}
public Optional<T> parse(ParseState<StringReader> state) {
return state.parseTopRule(this.top);
}
public T parse(StringReader reader) throws CommandSyntaxException {
ErrorCollector.LongestOnly<StringReader> errorCollector = new ErrorCollector.LongestOnly<>();
StringReaderParserState stringReaderParserState = new StringReaderParserState(errorCollector, reader);
Optional<T> optionalResult = this.parse(stringReaderParserState);
if (optionalResult.isPresent()) {
T result = optionalResult.get();
if (CachedParseState.JAVA_NULL_VALUE_MARKER.equals(result)) {
result = null;
}
return result;
}
List<ErrorEntry<StringReader>> errorEntries = errorCollector.entries();
List<Exception> exceptions = errorEntries.stream().<Exception>mapMulti((entry, output) -> {
if (entry.reason() instanceof DelayedException<?> delayedException) {
output.accept(delayedException.create(reader.getString(), entry.cursor()));
} else if (entry.reason() instanceof Exception exception1) {
output.accept(exception1);
}
}).toList();
for (Exception exception : exceptions) {
if (exception instanceof CommandSyntaxException cse) {
throw cse;
}
}
if (exceptions.size() == 1 && exceptions.getFirst() instanceof RuntimeException re) {
throw re;
}
throw new IllegalStateException("Failed to parse: " + errorEntries.stream().map(ErrorEntry::toString).collect(Collectors.joining(", ")));
}
}

View File

@@ -0,0 +1,33 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.util.snbt.parse.DelayedException;
import net.momirealms.craftengine.core.util.snbt.parse.ParseState;
import net.momirealms.craftengine.core.util.snbt.parse.Rule;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class GreedyPatternParseRule implements Rule<StringReader, String> {
private final Pattern pattern;
private final DelayedException<CommandSyntaxException> error;
public GreedyPatternParseRule(Pattern pattern, DelayedException<CommandSyntaxException> error) {
this.pattern = pattern;
this.error = error;
}
@Override
public String parse(ParseState<StringReader> state) {
StringReader input = state.input();
String fullString = input.getString();
Matcher matcher = this.pattern.matcher(fullString).region(input.getCursor(), fullString.length());
if (!matcher.lookingAt()) {
state.errorCollector().store(state.mark(), this.error);
return null;
}
input.setCursor(matcher.end());
return matcher.group(0);
}
}

View File

@@ -0,0 +1,48 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.util.snbt.parse.DelayedException;
import net.momirealms.craftengine.core.util.snbt.parse.ParseState;
import net.momirealms.craftengine.core.util.snbt.parse.Rule;
import javax.annotation.Nullable;
public abstract class GreedyPredicateParseRule implements Rule<StringReader, String> {
private final int minSize;
private final int maxSize;
private final DelayedException<CommandSyntaxException> error;
public GreedyPredicateParseRule(int minSize, DelayedException<CommandSyntaxException> error) {
this(minSize, Integer.MAX_VALUE, error);
}
public GreedyPredicateParseRule(int minSize, int maxSize, DelayedException<CommandSyntaxException> error) {
this.minSize = minSize;
this.maxSize = maxSize;
this.error = error;
}
@Nullable
@Override
public String parse(ParseState<StringReader> state) {
StringReader input = state.input();
String fullString = input.getString();
int start = input.getCursor();
int pos = start;
while (pos < fullString.length() && this.isAccepted(fullString.charAt(pos)) && pos - start < this.maxSize) {
pos++;
}
int length = pos - start;
if (length < this.minSize) {
state.errorCollector().store(state.mark(), this.error);
return null;
}
input.setCursor(pos);
return fullString.substring(start, pos);
}
protected abstract boolean isAccepted(char c);
}

View File

@@ -0,0 +1,46 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.util.snbt.parse.DelayedException;
import net.momirealms.craftengine.core.util.snbt.parse.ParseState;
import net.momirealms.craftengine.core.util.snbt.parse.Rule;
import javax.annotation.Nullable;
public abstract class NumberRunParseRule implements Rule<StringReader, String> {
private final DelayedException<CommandSyntaxException> noValueError;
private final DelayedException<CommandSyntaxException> underscoreNotAllowedError;
public NumberRunParseRule(DelayedException<CommandSyntaxException> noValueError, DelayedException<CommandSyntaxException> underscoreNotAllowedError) {
this.noValueError = noValueError;
this.underscoreNotAllowedError = underscoreNotAllowedError;
}
@Nullable
@Override
public String parse(ParseState<StringReader> state) {
StringReader input = state.input();
input.skipWhitespace();
String fullString = input.getString();
int start = input.getCursor();
int pos = start;
while (pos < fullString.length() && this.isAccepted(fullString.charAt(pos))) {
pos++;
}
int length = pos - start;
if (length == 0) {
state.errorCollector().store(state.mark(), this.noValueError);
return null;
} else if (fullString.charAt(start) != '_' && fullString.charAt(pos - 1) != '_') {
input.setCursor(pos);
return fullString.substring(start, pos);
}
state.errorCollector().store(state.mark(), this.underscoreNotAllowedError);
return null;
}
protected abstract boolean isAccepted(char c);
}

View File

@@ -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<StringReader> {
private final StringReader input;
public StringReaderParserState(ErrorCollector<StringReader> errorCollector, StringReader input) {
super(errorCollector);
this.input = input;
}
@Override
public StringReader input() {
return this.input;
}
@Override
public int mark() {
return this.input.getCursor();
}
@Override
public void restore(int mark) {
this.input.setCursor(mark);
}
}

View File

@@ -0,0 +1,64 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import it.unimi.dsi.fastutil.chars.CharList;
import net.momirealms.craftengine.core.util.snbt.parse.*;
import java.util.stream.Collectors;
public interface StringReaderTerms {
DynamicCommandExceptionType LITERAL_INCORRECT = new LocalizedDynamicCommandExceptionType(
expected -> new LocalizedMessage("warning.config.type.snbt.parser.incorrect", String.valueOf(expected))
);
static Term<StringReader> character(final char value) {
return new TerminalCharacters(CharList.of(value)) {
@Override
protected boolean isAccepted(char v) {
return value == v;
}
};
}
static Term<StringReader> characters(final char v1, final char v2) {
return new TerminalCharacters(CharList.of(v1, v2)) {
@Override
protected boolean isAccepted(char v) {
return v == v1 || v == v2;
}
};
}
static StringReader createReader(String contents, int cursor) {
StringReader reader = new StringReader(contents);
reader.setCursor(cursor);
return reader;
}
abstract class TerminalCharacters implements Term<StringReader> {
private final DelayedException<CommandSyntaxException> error;
private final SuggestionSupplier<StringReader> suggestions;
public TerminalCharacters(CharList values) {
String joinedValues = values.intStream().mapToObj(Character::toString).collect(Collectors.joining("|"));
this.error = DelayedException.create(LITERAL_INCORRECT, joinedValues);
this.suggestions = s -> values.intStream().mapToObj(Character::toString);
}
@Override
public boolean parse(ParseState<StringReader> state, Scope scope, Control control) {
state.input().skipWhitespace();
int cursor = state.mark();
if (state.input().canRead() && this.isAccepted(state.input().read())) {
return true;
}
state.errorCollector().store(cursor, this.suggestions, this.error);
return false;
}
protected abstract boolean isAccepted(char value);
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.core.util.snbt.parse.grammar;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.core.util.snbt.parse.DelayedException;
import net.momirealms.craftengine.core.util.snbt.parse.ParseState;
import net.momirealms.craftengine.core.util.snbt.parse.Rule;
import javax.annotation.Nullable;
public class UnquotedStringParseRule implements Rule<StringReader, String> {
private final int minSize;
private final DelayedException<CommandSyntaxException> error;
public UnquotedStringParseRule(int minSize, DelayedException<CommandSyntaxException> error) {
this.minSize = minSize;
this.error = error;
}
@Nullable
@Override
public String parse(ParseState<StringReader> state) {
state.input().skipWhitespace();
int cursor = state.mark();
String value = state.input().readUnquotedString();
if (value.length() < this.minSize) {
state.errorCollector().store(cursor, this.error);
return null;
}
return value;
}
}