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

Merge branch 'Xiao-MoMi:dev' into dev

This commit is contained in:
jhqwqmc
2025-06-24 04:00:18 +08:00
committed by GitHub
8 changed files with 396 additions and 31 deletions

View File

@@ -1,11 +1,14 @@
package net.momirealms.craftengine.bukkit.plugin;
import com.google.gson.JsonElement;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.momirealms.craftengine.bukkit.nms.FastNMS;
import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.MRegistryOps;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.Platform;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.sparrow.nbt.CompoundTag;
import net.momirealms.sparrow.nbt.Tag;
import org.bukkit.Bukkit;
import java.util.Map;
@@ -19,14 +22,36 @@ public class BukkitPlatform implements Platform {
@SuppressWarnings("unchecked")
@Override
public Object nbt2Java(String nbt) {
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) {
CraftEngine.instance().debug(e::getMessage);
throw new LocalizedResourceConfigException("warning.config.template.argument.default_value.invalid_syntax", e, nbt);
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) {
CraftEngine.instance().debug(e::getMessage);
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

@@ -42,7 +42,7 @@ public final class MRegistryOps {
// 1.20.1-1.20.4
JAVA = (DynamicOps<Object>) CoreReflections.method$RegistryOps$create.invoke(null, LegacyJavaOps.INSTANCE, FastNMS.INSTANCE.registryAccess());
} else {
JAVA = null;
throw new ReflectionInitException("Could not find JavaOps");
}
NBT = (DynamicOps<Object>) CoreReflections.method$RegistryOps$create.invoke(null, ReflectionUtils.getDeclaredField(clazz$NbtOps, clazz$NbtOps, 0).get(null), FastNMS.INSTANCE.registryAccess());
JSON = (DynamicOps<JsonElement>) CoreReflections.method$RegistryOps$create.invoke(null, JsonOps.INSTANCE, FastNMS.INSTANCE.registryAccess());

View File

@@ -71,6 +71,7 @@ warning.config.type.float: "<yellow>Issue found in file <arg:0> - Failed to load
warning.config.type.double: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to double type for option '<arg:3>'.</yellow>"
warning.config.type.quaternionf: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Quaternionf type for option '<arg:3>'.</yellow>"
warning.config.type.vector3f: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Vector3f 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.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>"
@@ -137,7 +138,6 @@ warning.config.template.duplicate: "<yellow>Issue found in file <arg:0> - Duplic
warning.config.template.invalid: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid template '<arg:2>'.</yellow>"
warning.config.template.argument.self_increase_int.invalid_range: "<yellow>Issue found in file <arg:0> - The template '<arg:1>' is using a 'from' '<arg:2>' larger than 'to' '<arg:3>' in 'self_increase_int' argument.</yellow>"
warning.config.template.argument.list.invalid_type: "<yellow>Issue found in file <arg:0> - The template '<arg:1>' is using a 'list' argument which expects a 'List' as argument while the input argument is a(n) '<arg:2>'.</yellow>"
warning.config.template.argument.default_value.invalid_syntax: "<yellow>Issue found in file <arg:0> - The template '<arg:1>' is using an invalid default value '<arg:2>' for argument '<arg:3>'.</yellow>"
warning.config.vanilla_loot.missing_type: "<yellow>Issue found in file <arg:0> - The vanilla loot '<arg:1>' is missing the required 'type' argument.</yellow>"
warning.config.vanilla_loot.invalid_type: "<yellow>Issue found in file <arg:0> - The vanilla loot '<arg:1>' is using an invalid type '<arg:2>'. Allowed types: [<arg:3>].</yellow>"
warning.config.vanilla_loot.block.invalid_target: "<yellow>Issue found in file <arg:0> - Invalid block target '<arg:2>' in vanilla loot '<arg:1>'.</yellow>"

View File

@@ -71,6 +71,7 @@ warning.config.type.boolean: "<yellow>在文件 <arg:0> 发现问题 - 无法加
warning.config.type.double: "<yellow>在文件 <arg:0> 发现问题 - 无法加载 '<arg:1>': 无法将 '<arg:2>' 转换为双精度类型 (选项 '<arg:3>')</yellow>"
warning.config.type.quaternionf: "<yellow>在文件 <arg:0> 发现问题 - 无法加载 '<arg:1>': 无法将 '<arg:2>' 转换为四元数类型 (选项 '<arg:3>')</yellow>"
warning.config.type.vector3f: "<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.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>"
@@ -137,7 +138,6 @@ warning.config.template.duplicate: "<yellow>在文件 <arg:0> 发现问题 - 重
warning.config.template.argument.self_increase_int.invalid_range: "<yellow>在文件 <arg:0> 发现问题 - 模板 '<arg:1>' 在 'self_increase_int' 参数中使用了一个起始值 '<arg:2>' 大于终止值 '<arg:3>'</yellow>"
warning.config.template.invalid: "<yellow>在文件 <arg:0> 发现问题 - 配置 '<arg:1>' 使用了无效的模板 '<arg:2>'.</yellow>"
warning.config.template.argument.list.invalid_type: "<yellow>在文件 <arg:0> 发现问题 - 模板 '<arg:1>' 的 'list' 参数需要列表类型 但输入参数类型为 '<arg:2>'</yellow>"
warning.config.template.argument.default_value.invalid_syntax: "<yellow>在文件 <arg:0> 发现问题 - 模板 '<arg:1>' 在参数 '<arg:3>' 中使用了无效的默认值 '<arg:2>'.</yellow>"
warning.config.vanilla_loot.missing_type: "<yellow>在文件 <arg:0> 发现问题 - 原版战利品 '<arg:1>' 缺少必需的 'type' 参数</yellow>"
warning.config.vanilla_loot.invalid_type: "<yellow>在文件 <arg:0> 发现问题 - 原版战利品 '<arg:1>' 使用了无效类型 '<arg:2>' 允许的类型: [<arg:3>]</yellow>"
warning.config.vanilla_loot.block.invalid_target: "<yellow>在文件 <arg:0> 发现问题 - 原版战利品 '<arg:1>' 中存在无效的方块目标 '<arg:2>'</yellow>"

View File

@@ -6,6 +6,7 @@ import net.momirealms.craftengine.core.item.ComponentKeys;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.NetworkItemHandler;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.Pair;
@@ -17,15 +18,15 @@ import java.util.List;
import java.util.Map;
public class ComponentModifier<I> implements ItemDataModifier<I> {
private final List<Pair<Key, Object>> arguments;
private JsonObject customData = null;
private final List<Pair<Key, Tag>> arguments;
private CompoundTag customData = null;
public ComponentModifier(Map<String, Object> arguments) {
List<Pair<Key, Object>> pairs = new ArrayList<>(arguments.size());
List<Pair<Key, Tag>> pairs = new ArrayList<>(arguments.size());
for (Map.Entry<String, Object> entry : arguments.entrySet()) {
Key key = Key.of(entry.getKey());
if (key.equals(ComponentKeys.CUSTOM_DATA)) {
this.customData = parseJsonObjectValue(entry.getValue());
this.customData = (CompoundTag) parseValue(entry.getValue());
} else {
pairs.add(new Pair<>(key, parseValue(entry.getValue())));
}
@@ -33,28 +34,19 @@ public class ComponentModifier<I> implements ItemDataModifier<I> {
this.arguments = pairs;
}
public List<Pair<Key, Object>> arguments() {
public List<Pair<Key, Tag>> arguments() {
return arguments;
}
private Object parseValue(Object value) {
private Tag parseValue(Object value) {
if (value instanceof String string) {
if (string.startsWith("(json) ")) {
return GsonHelper.get().fromJson(string.substring("(json) ".length()), JsonElement.class);
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()));
}
}
return value;
}
private JsonObject parseJsonObjectValue(Object value) {
if (value instanceof String string) {
if (string.startsWith("(json) ")) {
return GsonHelper.get().fromJson(string.substring("(json) ".length()), JsonObject.class);
}
} else if (value instanceof Map<?,?> map) {
return (JsonObject) GsonHelper.get().toJsonTree(map, Map.class);
}
throw new UnsupportedOperationException("Invalid minecraft:custom_data value: " + value.toString());
return CraftEngine.instance().platform().javaToSparrowNBT(value);
}
@Override
@@ -64,13 +56,16 @@ public class ComponentModifier<I> implements ItemDataModifier<I> {
@Override
public Item<I> apply(Item<I> item, ItemBuildContext context) {
for (Pair<Key, Object> entry : this.arguments) {
item.setComponent(entry.left(), entry.right());
for (Pair<Key, Tag> entry : this.arguments) {
item.setNBTComponent(entry.left(), entry.right());
}
if (this.customData != null) {
JsonObject tag = (JsonObject) item.getJsonComponent(ComponentKeys.CUSTOM_DATA);
CompoundTag tag = (CompoundTag) item.getNBTTag(ComponentKeys.CUSTOM_DATA);
if (tag != null) {
item.setComponent(ComponentKeys.CUSTOM_DATA, GsonHelper.shallowMerge(this.customData, tag));
for (Map.Entry<String, Tag> entry : this.customData.entrySet()) {
tag.put(entry.getKey(), entry.getValue());
}
item.setComponent(ComponentKeys.CUSTOM_DATA, tag);
} else {
item.setComponent(ComponentKeys.CUSTOM_DATA, this.customData);
}
@@ -80,7 +75,7 @@ public class ComponentModifier<I> implements ItemDataModifier<I> {
@Override
public Item<I> prepareNetworkItem(Item<I> item, ItemBuildContext context, CompoundTag networkData) {
for (Pair<Key, Object> entry : this.arguments) {
for (Pair<Key, Tag> entry : this.arguments) {
Tag previous = item.getNBTComponent(entry.left());
if (previous != null) {
networkData.put(entry.left().asString(), NetworkItemHandler.pack(NetworkItemHandler.Operation.ADD, previous));

View File

@@ -1,8 +1,17 @@
package net.momirealms.craftengine.core.plugin;
import com.google.gson.JsonElement;
import net.momirealms.sparrow.nbt.Tag;
public interface Platform {
void dispatchCommand(String command);
Object nbt2Java(String nbt);
Object snbtToJava(String nbt);
Tag jsonToSparrowNBT(JsonElement json);
Tag snbtToSparrowNBT(String nbt);
Tag javaToSparrowNBT(Object object);
}

View File

@@ -76,7 +76,7 @@ public interface TemplateManager extends Manageable {
this.placeholder = placeholderContent.substring(0, separatorIndex);
String defaultValueString = placeholderContent.substring(separatorIndex + 2);
try {
this.defaultValue = CraftEngine.instance().platform().nbt2Java(defaultValueString);
this.defaultValue = CraftEngine.instance().platform().snbtToJava(defaultValueString);
} catch (LocalizedResourceConfigException e) {
e.appendTailArgument(this.placeholder);
throw e;

View File

@@ -0,0 +1,336 @@
package net.momirealms.craftengine.core.util;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class SNBTDeserializer {
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 KEY_VALUE_SEPARATOR = ':';
private static final char ELEMENT_SEPARATOR = ',';
private static final char ESCAPE_CHAR = '\\';
// 数字类型后缀
private static final char BYTE_SUFFIX = 'b';
private static final char SHORT_SUFFIX = 's';
private static final char LONG_SUFFIX = 'l';
private static final char FLOAT_SUFFIX = 'f';
private static final char DOUBLE_SUFFIX = 'd';
private static final char BOOLEAN_SUFFIX = 'B';
// 布尔值常量
private static final String TRUE_LITERAL = "true";
private static final String FALSE_LITERAL = "false";
private static final int TRUE_LENGTH = 4;
private static final int FALSE_LENGTH = 5;
// 使用 char[] 处理获得更高的性能
private final char[] sourceContent;
private final int length;
private int position = 0;
private SNBTDeserializer(String content) {
this.sourceContent = content.toCharArray();
this.length = sourceContent.length;
}
// 入口API
public static Object parse(String input) throws IllegalArgumentException {
SNBTDeserializer parser = new SNBTDeserializer(input);
Object result = parser.parseValue();
parser.skipWhitespace();
if (parser.position != parser.length) {
throw new IllegalArgumentException("Extra content at end: " +
new String(parser.sourceContent, parser.position,
parser.length - parser.position));
}
return result;
}
// 开始解析, 步进字符.
private Object parseValue() {
skipWhitespace();
return switch (peekCurrentChar()) {
case COMPOUND_START -> parseCompound();
case LIST_START -> parseList();
case STRING_DELIMITER -> parseString();
default -> parsePrimitive();
};
}
// 解析包小肠 {}
private Map<String, Object> parseCompound() {
position++; // 跳过 '{'
skipWhitespace();
Map<String, Object> compoundMap = new LinkedHashMap<>(16); // 避免一次扩容, 应该有一定的性能提升
if (position < length && sourceContent[position] != COMPOUND_END) {
do {
String key = parseKey();
if (position >= length || sourceContent[position] != KEY_VALUE_SEPARATOR) {
throw new IllegalArgumentException("Expected ':' at position " + position);
}
position++; // 跳过 ':'
Object value = parseValue();
compoundMap.put(key, value);
skipWhitespace();
} while (position < length && sourceContent[position] == ELEMENT_SEPARATOR && ++position > 0);
}
if (position >= length || sourceContent[position] != COMPOUND_END) {
throw new IllegalArgumentException("Expected '}' at position " + position);
}
position++; // 跳过 '}'
return compoundMap;
}
// 解析列表值 [1, 2, 3]
private List<Object> parseList() {
position++; // 跳过 '['
skipWhitespace();
List<Object> elementList = new ArrayList<>();
if (position < length && sourceContent[position] != LIST_END) {
do {
elementList.add(parseValue());
skipWhitespace();
} while (position < length && sourceContent[position] == ELEMENT_SEPARATOR && ++position > 0);
}
if (position >= length || sourceContent[position] != LIST_END) {
throw new IllegalArgumentException("Expected ']' at position " + position);
}
position++; // 跳过 ']'
return elementList;
}
// 解析字符串
private String parseString() {
position++; // 跳过开始的引号
int start = position;
// 扫一次字符串, 如果没有发现转义就直接返回, 发现了就再走转义解析.
// 这样可以避免创建一次 StringBuilder.
while (position < length) {
char c = sourceContent[position];
if (c == STRING_DELIMITER) {
String result = new String(sourceContent, start, position - start);
position++; // 跳过结束引号
return result; // 没有转义直接返回字符串.
}
// 如果发现转义字符,
else if (c == ESCAPE_CHAR) {
return parseStringWithEscape(start);
}
position++;
}
// 没有扫描到结束引号
throw new IllegalArgumentException("Unterminated string at " + start);
}
// 处理含转义的字符串
private String parseStringWithEscape(int start) {
StringBuilder sb = new StringBuilder(position - start + 16);
sb.append(sourceContent, start, position - start);
while (position < length) {
char c = sourceContent[position++];
if (c == ESCAPE_CHAR && position < length) {
sb.append(getEscapedChar(sourceContent[position++]));
} else if (c == STRING_DELIMITER) { // 字符
return sb.toString();
} else {
sb.append(c);
}
}
// 没有扫描到结束引号
throw new IllegalArgumentException("Unterminated string at " + start);
}
// 解析Key值
private String parseKey() {
skipWhitespace();
// 如果有双引号就委托给string解析处理.
if (position < length && sourceContent[position] == STRING_DELIMITER) {
return parseString();
}
int start = position;
while (position < length) {
char c = sourceContent[position];
if (Character.isJavaIdentifierPart(c)) {
position++;
} else {
break;
}
}
skipWhitespace();
return new String(sourceContent, start, position - start);
}
// 解析原生值
private Object parsePrimitive() {
skipWhitespace();
int tokenStart = position;
// 先解析获取值的长度
while (position < length) {
char c = sourceContent[position];
if (c <= ' ' || c == ',' || c == ']' || c == '}') break;
position++;
}
int tokenLength = position - tokenStart;
if (tokenLength == 0) {
throw new IllegalArgumentException("Empty value at position " + tokenStart);
}
// 布尔值快速检查
if (tokenLength == TRUE_LENGTH && matchesAt(tokenStart, TRUE_LITERAL)) {
return Boolean.TRUE;
}
if (tokenLength == FALSE_LENGTH && matchesAt(tokenStart, FALSE_LITERAL)) {
return Boolean.FALSE;
}
// 带后缀的值处理
try {
char lastChar = sourceContent[tokenStart + tokenLength - 1];
if (tokenLength > 1 && isTypeSuffix(lastChar)) {
return switch (lastChar) {
case BYTE_SUFFIX -> parseByte(tokenStart, tokenLength - 1);
case SHORT_SUFFIX -> parseShort(tokenStart, tokenLength - 1);
case LONG_SUFFIX -> parseLong(tokenStart, tokenLength - 1);
case FLOAT_SUFFIX -> Float.parseFloat(new String(sourceContent, tokenStart, tokenLength - 1));
case DOUBLE_SUFFIX -> Double.parseDouble(new String(sourceContent, tokenStart, tokenLength - 1));
case BOOLEAN_SUFFIX -> parseBoolean(new String(sourceContent, tokenStart, tokenLength - 1));
default -> throw new IllegalArgumentException("Invalid suffixed value " + new String(sourceContent, tokenStart, tokenLength - 1));
};
}
// 没有后缀就默认为 double 喵
return Double.parseDouble(new String(sourceContent, tokenStart, tokenLength));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid number value at position " + tokenStart, e);
}
}
// 工具函数: 快速检查布尔值字符串匹配, 忽略大小写.
private boolean matchesAt(int start, String target) {
for (int i = 0; i < target.length(); i++) {
char c1 = sourceContent[start + i];
char c2 = target.charAt(i);
if (c1 != c2 && c1 != (c2 ^ 32)) return false; // 忽略大小写比较
}
return true;
}
// 工具函数: 合法后缀检查
private boolean isTypeSuffix(char c) {
return c == BYTE_SUFFIX || c == SHORT_SUFFIX || c == LONG_SUFFIX ||
c == FLOAT_SUFFIX || c == DOUBLE_SUFFIX || c == BOOLEAN_SUFFIX;
}
// 手动解析值
private byte parseByte(int start, int length) {
int value = parseInteger(start, length);
if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE)
throw new IllegalArgumentException("Byte value out of range");
return (byte) value;
}
private short parseShort(int start, int length) {
int value = parseInteger(start, length);
if (value < Short.MIN_VALUE || value > Short.MAX_VALUE)
throw new NumberFormatException("Short value out of range");
return (short) value;
}
private Boolean parseBoolean(String content) {
if ("1".equals(content)) return Boolean.TRUE;
if ("0".equals(content)) return Boolean.FALSE;
throw new NumberFormatException("Invalid boolean value");
}
private int parseInteger(int start, int length) {
int result = 0;
boolean negative = false;
int i = 0;
if (sourceContent[start] == '-') {
negative = true;
i = 1;
}
for (; i < length; i++) {
char c = sourceContent[start + i];
if (c < '0' || c > '9') throw new NumberFormatException("Invalid integer");
result = result * 10 + (c - '0');
}
return negative ? -result : result;
}
private long parseLong(int start, int length) {
long result = 0;
boolean negative = false;
int i = 0;
if (sourceContent[start] == '-') {
negative = true;
i = 1;
}
for (; i < length; i++) {
char c = sourceContent[start + i];
if (c < '0' || c > '9') throw new NumberFormatException("Invalid long");
result = result * 10 + (c - '0');
}
return negative ? -result : result;
}
// 转义字符处理
private char getEscapedChar(char escapedChar) {
return switch (escapedChar) {
case STRING_DELIMITER -> '"';
case ESCAPE_CHAR -> '\\';
case COMPOUND_START -> '{';
case COMPOUND_END -> '}';
case LIST_START -> '[';
case LIST_END -> ']';
case KEY_VALUE_SEPARATOR -> ':';
case ELEMENT_SEPARATOR -> ',';
default -> escapedChar;
};
}
// 获取当前字符
private char peekCurrentChar() {
if (position >= length) throw new IllegalArgumentException("Unexpected end of input at position " + position);
return sourceContent[position];
}
// 跳过空格
private void skipWhitespace() {
while (position < length) {
char c = sourceContent[position];
if (c > ' ') { break; } // 大于空格的字符都不是空白字符
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
position++;
} else {
break;
}
}
}
}