9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-30 20:39:10 +00:00

Merge branch 'Xiao-MoMi:dev' into dev

This commit is contained in:
jhqwqmc
2025-06-07 09:28:06 +08:00
committed by GitHub
23 changed files with 775 additions and 626 deletions

View File

@@ -53,7 +53,7 @@ public class DisableResourceCommand extends BukkitCommandFeature<CommandSender>
return;
}
}
YamlDocument document = plugin().config().loadYamlData(packMetaPath.toFile());
YamlDocument document = plugin().config().loadYamlData(packMetaPath);
document.set("enable", false);
try {
document.save(packMetaPath.toFile());

View File

@@ -52,7 +52,7 @@ public class EnableResourceCommand extends BukkitCommandFeature<CommandSender> {
return;
}
}
YamlDocument document = plugin().config().loadYamlData(packMetaPath.toFile());
YamlDocument document = plugin().config().loadYamlData(packMetaPath);
document.set("enable", true);
try {
document.save(packMetaPath.toFile());

View File

@@ -132,6 +132,7 @@ warning.config.recipe.smithing_transform.post_processor.keep_component.missing_c
warning.config.recipe.smithing_transform.post_processor.keep_component.missing_tags: "<yellow>Issue found in file <arg:0> - The smithing transform recipe '<arg:1>' is missing the required argument 'tags' for post-processors 'keep_tags'.</yellow>"
warning.config.i18n.unknown_locale: "<yellow>Issue found in file <arg:0> - Unknown locale '<arg:1>'.</yellow>"
warning.config.template.duplicate: "<yellow>Issue found in file <arg:0> - Duplicated template '<arg:1>'. Please check if there is the same configuration in other files.</yellow>"
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.vanilla_loot.missing_type: "<yellow>Issue found in file <arg:0> - The vanilla loot '<arg:1>' is missing the required 'type' argument.</yellow>"

View File

@@ -133,6 +133,7 @@ warning.config.recipe.smithing_transform.post_processor.keep_component.missing_t
warning.config.i18n.unknown_locale: "<yellow>在文件 <arg:0> 发现问题 - 未知的语言环境 '<arg:1>'</yellow>"
warning.config.template.duplicate: "<yellow>在文件 <arg:0> 发现问题 - 重复的模板 '<arg:1>' 请检查其他文件中是否存在相同配置</yellow>"
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.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>"

View File

@@ -286,7 +286,7 @@ public abstract class AbstractFontManager implements FontManager {
}
for (int i = -256; i <= 256; i++) {
String shiftTag = "<shift:" + i + ">";
this.tagMapper.put(shiftTag, AdventureHelper.miniMessage().deserialize(this.offsetFont.createOffset(i, FormatUtils::miniMessageFont)));
this.tagMapper.put(shiftTag, this.offsetFont.createOffset(i));
this.tagMapper.put("\\" + shiftTag, Component.text(shiftTag));
}
this.imageTagTrie = Trie.builder()

View File

@@ -266,7 +266,7 @@ public abstract class AbstractPackManager implements PackManager {
String author = null;
boolean enable = true;
if (Files.exists(metaFile) && Files.isRegularFile(metaFile)) {
YamlDocument metaYML = Config.instance().loadYamlData(metaFile.toFile());
YamlDocument metaYML = Config.instance().loadYamlData(metaFile);
enable = metaYML.getBoolean("enable", true);
namespace = metaYML.getString("namespace", namespace);
description = metaYML.getString("description");
@@ -467,7 +467,8 @@ public abstract class AbstractPackManager implements PackManager {
}
for (Map.Entry<String, Object> entry : cachedFile.config().entrySet()) {
processConfigEntry(entry, path, cachedFile.pack(), (p, c) ->
cachedConfigs.computeIfAbsent(p, k -> new ArrayList<>()).add(c));
cachedConfigs.computeIfAbsent(p, k -> new ArrayList<>()).add(c)
);
}
}
return FileVisitResult.CONTINUE;
@@ -494,12 +495,13 @@ public abstract class AbstractPackManager implements PackManager {
Key id = Key.withDefaultNamespace(key, cached.pack().namespace());
try {
if (parser.supportsParsingObject()) {
// do not apply templates
parser.parseObject(cached.pack(), cached.filePath(), id, configEntry.getValue());
} else if (predicate.test(parser)) {
if (configEntry.getValue() instanceof Map<?, ?> configSection0) {
Map<String, Object> configSection1 = castToMap(configSection0, false);
if ((boolean) configSection1.getOrDefault("enable", true)) {
parser.parseSection(cached.pack(), cached.filePath(), id, plugin.templateManager().applyTemplates(id, configSection1));
Map<String, Object> config = castToMap(configSection0, false);
if ((boolean) config.getOrDefault("enable", true)) {
parser.parseSection(cached.pack(), cached.filePath(), id, MiscUtils.castToMap(this.plugin.templateManager().applyTemplates(id, config), false));
}
} else {
TranslationManager.instance().log("warning.config.structure.not_section", cached.filePath().toString(), cached.prefix() + "." + key, configEntry.getValue().getClass().getSimpleName());
@@ -835,8 +837,8 @@ public abstract class AbstractPackManager implements PackManager {
}
private void generateBlockOverrides(Path generatedPackPath) {
File blockStatesFile = new File(plugin.dataFolderFile(), "blockstates.yml");
if (!blockStatesFile.exists()) plugin.saveResource("blockstates.yml");
Path blockStatesFile = this.plugin.dataFolderPath().resolve("blockstates.yml");
if (!Files.exists(blockStatesFile)) this.plugin.saveResource("blockstates.yml");
YamlDocument preset = Config.instance().loadYamlData(blockStatesFile);
for (Map.Entry<Key, Map<String, JsonElement>> entry : plugin.blockManager().blockOverrides().entrySet()) {
Key key = entry.getKey();

View File

@@ -25,12 +25,10 @@ import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.InjectionTarget;
import net.momirealms.craftengine.core.world.chunk.storage.CompressionMethod;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
@@ -40,6 +38,8 @@ public class Config {
private final Path configFilePath;
private final String configVersion;
private YamlDocument config;
private long lastModified;
private long size;
protected boolean firstTime = true;
protected boolean debug;
@@ -152,49 +152,63 @@ public class Config {
}
public void load() {
if (Files.exists(this.configFilePath)) {
this.config = this.loadYamlData(this.configFilePath.toFile());
String configVersion = config.getString("config-version");
if (configVersion.equals(this.configVersion)) {
loadSettings();
return;
}
// 文件不存在,则保存
if (!Files.exists(this.configFilePath)) {
this.plugin.saveResource("config.yml");
}
try {
BasicFileAttributes attributes = Files.readAttributes(this.configFilePath, BasicFileAttributes.class);
long lastModified = attributes.lastModifiedTime().toMillis();
long size = attributes.size();
if (lastModified != this.lastModified || size != this.size) {
byte[] configFileBytes = Files.readAllBytes(this.configFilePath);
try (InputStream inputStream = new ByteArrayInputStream(configFileBytes)) {
this.config = YamlDocument.create(inputStream);
String configVersion = this.config.getString("config-version");
if (!configVersion.equals(this.configVersion)) {
this.updateConfigVersion(configFileBytes);
}
}
// 加载配置文件
this.loadSettings();
this.lastModified = lastModified;
this.size = size;
}
} catch (IOException e) {
this.plugin.logger().severe("Failed to load config.yml", e);
}
this.updateConfigVersion();
loadSettings();
}
private void updateConfigVersion() {
this.config = this.loadYamlConfig(
"config.yml",
GeneralSettings.builder()
.setRouteSeparator('.')
.setUseDefaults(false)
.build(),
LoaderSettings
.builder()
.setAutoUpdate(true)
.build(),
DumperSettings.builder()
.setEscapeUnprintable(false)
.setScalarFormatter((tag, value, role, def) -> {
if (role == NodeRole.KEY) {
return ScalarStyle.PLAIN;
} else {
return tag == Tag.STR ? ScalarStyle.DOUBLE_QUOTED : ScalarStyle.PLAIN;
}
})
.build(),
UpdaterSettings
.builder()
.setVersioning(new BasicVersioning("config-version"))
.addIgnoredRoute(PluginProperties.getValue("config"), "resource-pack.delivery.hosting", '.')
.build()
);
private void updateConfigVersion(byte[] bytes) throws IOException {
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
this.config = YamlDocument.create(inputStream, this.plugin.resourceStream("config.yml"), GeneralSettings.builder()
.setRouteSeparator('.')
.setUseDefaults(false)
.build(),
LoaderSettings
.builder()
.setAutoUpdate(true)
.build(),
DumperSettings.builder()
.setEscapeUnprintable(false)
.setScalarFormatter((tag, value, role, def) -> {
if (role == NodeRole.KEY) {
return ScalarStyle.PLAIN;
} else {
return tag == Tag.STR ? ScalarStyle.DOUBLE_QUOTED : ScalarStyle.PLAIN;
}
})
.build(),
UpdaterSettings
.builder()
.setVersioning(new BasicVersioning("config-version"))
.addIgnoredRoute(PluginProperties.getValue("config"), "resource-pack.delivery.hosting", '.')
.build());
}
try {
config.save(new File(plugin.dataFolderFile(), "config.yml"));
this.config.save(new File(plugin.dataFolderFile(), "config.yml"));
} catch (IOException e) {
throw new RuntimeException(e);
this.plugin.logger().warn("Could not save config.yml", e);
}
}
@@ -714,11 +728,11 @@ public class Config {
}
public YamlDocument loadOrCreateYamlData(String fileName) {
File file = new File(this.plugin.dataFolderFile(), fileName);
if (!file.exists()) {
Path path = this.plugin.dataFolderPath().resolve(fileName);
if (!Files.exists(path)) {
this.plugin.saveResource(fileName);
}
return this.loadYamlData(file);
return this.loadYamlData(path);
}
public YamlDocument loadYamlConfig(String filePath, GeneralSettings generalSettings, LoaderSettings loaderSettings, DumperSettings dumperSettings, UpdaterSettings updaterSettings) {
@@ -730,8 +744,8 @@ public class Config {
}
}
public YamlDocument loadYamlData(File file) {
try (InputStream inputStream = new FileInputStream(file)) {
public YamlDocument loadYamlData(Path file) {
try (InputStream inputStream = Files.newInputStream(file)) {
return YamlDocument.create(inputStream);
} catch (IOException e) {
this.plugin.logger().severe("Failed to load config " + file, e);

View File

@@ -0,0 +1,41 @@
package net.momirealms.craftengine.core.plugin.config;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
public class TranslationConfigConstructor extends SafeConstructor {
public TranslationConfigConstructor(LoaderOptions loaderOptions) {
super(loaderOptions);
}
@Override
protected Map<Object, Object> constructMapping(MappingNode node) {
Map<Object, Object> map = new LinkedHashMap<>();
for (NodeTuple tuple : node.getValue()) {
Node keyNode = tuple.getKeyNode();
Node valueNode = tuple.getValueNode();
String key = constructScalar((ScalarNode) keyNode);
Object value = constructObject(valueNode);
if (value instanceof List<?> list) {
StringJoiner stringJoiner = new StringJoiner("<reset><newline>");
for (Object str : list) {
stringJoiner.add(String.valueOf(str));
}
map.put(key, stringJoiner.toString());
} else {
map.put(key, value.toString());
}
}
return map;
}
}

View File

@@ -0,0 +1,66 @@
package net.momirealms.craftengine.core.plugin.config.template;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.data.EvaluationValue;
import net.momirealms.craftengine.core.util.Key;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class ExpressionTemplateArgument implements TemplateArgument {
public static final Factory FACTORY = new Factory();
private final TemplateManager.ArgumentString expression;
private final ValueType valueType;
protected ExpressionTemplateArgument(String expression, ValueType valueType) {
this.expression = TemplateManager.preParse(expression);
this.valueType = valueType;
}
@Override
public Object get(Map<String, TemplateArgument> arguments) {
String expression = Optional.ofNullable(this.expression.get(arguments)).map(String::valueOf).orElse(null);
if (expression == null) return null;
try {
return this.valueType.formatter().apply(new Expression(expression).evaluate());
} catch (Exception e) {
throw new RuntimeException("Failed to process expression argument: " + this.expression, e);
}
}
@Override
public Key type() {
return TemplateArguments.EXPRESSION;
}
protected enum ValueType {
INT(e -> e.getNumberValue().intValue()),
LONG(e -> e.getNumberValue().longValue()),
SHORT(e -> e.getNumberValue().shortValueExact()),
DOUBLE(e -> e.getNumberValue().doubleValue()),
FLOAT(e -> e.getNumberValue().floatValue()),
BOOLEAN(EvaluationValue::getBooleanValue),;
private final Function<EvaluationValue, Object> formatter;
ValueType(Function<EvaluationValue, Object> formatter) {
this.formatter = formatter;
}
public Function<EvaluationValue, Object> formatter() {
return formatter;
}
}
public static class Factory implements TemplateArgumentFactory {
@Override
public TemplateArgument create(Map<String, Object> arguments) {
return new ExpressionTemplateArgument(
arguments.getOrDefault("expression", "").toString(),
ValueType.valueOf(arguments.getOrDefault("value-type", "double").toString().toUpperCase(Locale.ENGLISH))
);
}
}
}

View File

@@ -16,7 +16,7 @@ public class ListTemplateArgument implements TemplateArgument {
}
@Override
public List<Object> get() {
public List<Object> get(Map<String, TemplateArgument> arguments) {
return value;
}

View File

@@ -14,7 +14,7 @@ public class MapTemplateArgument implements TemplateArgument {
}
@Override
public Map<String, Object> get() {
public Map<String, Object> get(Map<String, TemplateArgument> arguments) {
return value;
}

View File

@@ -17,7 +17,7 @@ public class NullTemplateArgument implements TemplateArgument {
}
@Override
public Object get() {
public Object get(Map<String, TemplateArgument> arguments) {
return null;
}

View File

@@ -2,6 +2,8 @@ package net.momirealms.craftengine.core.plugin.config.template;
import net.momirealms.craftengine.core.util.Key;
import java.util.Map;
public class ObjectTemplateArgument implements TemplateArgument {
private final Object value;
@@ -9,13 +11,17 @@ public class ObjectTemplateArgument implements TemplateArgument {
this.value = value;
}
public static ObjectTemplateArgument of(Object value) {
return new ObjectTemplateArgument(value);
}
@Override
public Key type() {
return TemplateArguments.OBJECT;
}
@Override
public Object get() {
public Object get(Map<String, TemplateArgument> arguments) {
return this.value;
}
}

View File

@@ -17,7 +17,7 @@ public class PlainStringTemplateArgument implements TemplateArgument {
}
@Override
public String get() {
public String get(Map<String, TemplateArgument> arguments) {
return value;
}

View File

@@ -19,7 +19,7 @@ public class SelfIncreaseIntTemplateArgument implements TemplateArgument {
}
@Override
public String get() {
public String get(Map<String, TemplateArgument> arguments) {
String value = String.valueOf(this.current);
if (this.current < this.max) this.current += 1;
return value;

View File

@@ -2,9 +2,11 @@ package net.momirealms.craftengine.core.plugin.config.template;
import net.momirealms.craftengine.core.util.Key;
import java.util.function.Supplier;
import java.util.Map;
public interface TemplateArgument extends Supplier<Object> {
public interface TemplateArgument {
Key type();
Object get(Map<String, TemplateArgument> arguments);
}

View File

@@ -15,6 +15,7 @@ public class TemplateArguments {
public static final Key MAP = Key.of("craftengine:map");
public static final Key LIST = Key.of("craftengine:list");
public static final Key NULL = Key.of("craftengine:null");
public static final Key EXPRESSION = Key.of("craftengine:expression");
public static final Key OBJECT = Key.of("craftengine:object"); // No Factory, internal use
public static void register(Key key, TemplateArgumentFactory factory) {
@@ -29,6 +30,7 @@ public class TemplateArguments {
register(MAP, MapTemplateArgument.FACTORY);
register(LIST, ListTemplateArgument.FACTORY);
register(NULL, NullTemplateArgument.FACTORY);
register(EXPRESSION, ExpressionTemplateArgument.FACTORY);
}
public static TemplateArgument fromMap(Map<String, Object> map) {

View File

@@ -4,19 +4,274 @@ import net.momirealms.craftengine.core.plugin.Manageable;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.util.Key;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public interface TemplateManager extends Manageable {
Pattern ARGUMENT_PATTERN = Pattern.compile("\\{[^{}]+}");
String LEFT_BRACKET = "{";
String RIGHT_BRACKET = "}";
String TEMPLATE = "template";
String OVERRIDES = "overrides";
String ARGUMENTS = "arguments";
String MERGES = "merges";
ConfigParser parser();
Map<String, Object> applyTemplates(Key id, Map<String, Object> input);
Object applyTemplates(Key id, Object input);
interface ArgumentString {
String rawValue();
Object get(Map<String, TemplateArgument> arguments);
}
final class Literal implements ArgumentString {
private final String value;
public Literal(String value) {
this.value = value;
}
public static Literal literal(String value) {
return new Literal(value);
}
@Override
public String rawValue() {
return this.value;
}
@Override
public Object get(Map<String, TemplateArgument> arguments) {
return this.value;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Literal literal)) return false;
return this.value.equals(literal.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public String toString() {
return "Literal(" + this.value + ")";
}
}
final class Placeholder implements ArgumentString {
private final String placeholder;
private final String rawText;
public Placeholder(String placeholder) {
this.placeholder = placeholder;
this.rawText = "{" + this.placeholder + "}";
}
public static Placeholder placeholder(String placeholder) {
return new Placeholder(placeholder);
}
@Override
public Object get(Map<String, TemplateArgument> arguments) {
TemplateArgument replacement = arguments.get(this.placeholder);
if (replacement != null) {
return replacement.get(arguments);
}
return rawValue();
}
@Override
public String rawValue() {
return this.rawText;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Placeholder that)) return false;
return this.placeholder.equals(that.placeholder);
}
@Override
public int hashCode() {
return this.placeholder.hashCode();
}
@Override
public String toString() {
return "Placeholder(" + this.placeholder + ")";
}
}
final class Complex2 implements ArgumentString {
private final String rawText;
private final ArgumentString arg1;
private final ArgumentString arg2;
public Complex2(String rawText, ArgumentString arg1, ArgumentString arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
this.rawText = rawText;
}
@Override
public Object get(Map<String, TemplateArgument> arguments) {
Object arg1 = this.arg1.get(arguments);
Object arg2 = this.arg2.get(arguments);
if (arg1 == null && arg2 == null) return null;
if (arg1 == null) return String.valueOf(arg2);
if (arg2 == null) return String.valueOf(arg1);
return String.valueOf(arg1) + arg2;
}
@Override
public String rawValue() {
return this.rawText;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Complex that)) return false;
return this.rawText.equals(that.rawText);
}
@Override
public int hashCode() {
return this.rawText.hashCode();
}
@Override
public String toString() {
return "Complex2(" + this.rawText + ")";
}
}
final class Complex implements ArgumentString {
private final List<ArgumentString> parts;
private final String rawText;
public Complex(String rawText, List<ArgumentString> parts) {
this.parts = parts;
this.rawText = rawText;
}
@Override
public Object get(Map<String, TemplateArgument> arguments) {
StringBuilder result = new StringBuilder();
boolean hasValue = false;
for (ArgumentString part : this.parts) {
Object arg = part.get(arguments);
if (arg != null) {
result.append(arg);
hasValue = true;
}
}
if (!hasValue) return null;
return result.toString();
}
@Override
public String rawValue() {
return this.rawText;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Complex that)) return false;
return this.rawText.equals(that.rawText);
}
@Override
public int hashCode() {
return this.rawText.hashCode();
}
@Override
public String toString() {
return "Complex(" + this.rawText + ")";
}
}
static ArgumentString preParse(String input) {
if (input == null || input.isEmpty()) {
return Literal.literal("");
}
int n = input.length();
int lastAppendPosition = 0; // 追踪上一次追加操作结束的位置
int i = 0;
List<ArgumentString> arguments = new ArrayList<>();
while (i < n) {
// 检查当前字符是否为未转义的 '{'
int backslashes = 0;
int temp_i = i - 1;
while (temp_i >= 0 && input.charAt(temp_i) == '\\') {
backslashes++;
temp_i--;
}
if (input.charAt(i) == '{' && backslashes % 2 == 0) {
// 发现占位符起点
int placeholderStartIndex = i;
// 追加从上一个位置到当前占位符之前的文本
if (lastAppendPosition < i) {
arguments.add(Literal.literal(input.substring(lastAppendPosition, i)));
}
// --- 开始解析占位符内部 ---
StringBuilder keyBuilder = new StringBuilder();
int depth = 1;
int j = i + 1;
boolean foundMatch = false;
while (j < n) {
char c = input.charAt(j);
if (c == '\\') { // 处理转义
if (j + 1 < n) {
keyBuilder.append(input.charAt(j + 1));
j += 2;
} else {
keyBuilder.append(c);
j++;
}
} else if (c == '{') {
depth++;
keyBuilder.append(c);
j++;
} else if (c == '}') {
depth--;
if (depth == 0) { // 找到匹配的结束括号
String key = keyBuilder.toString();
arguments.add(Placeholder.placeholder(key));
// 更新位置指针
i = j + 1;
lastAppendPosition = i;
foundMatch = true;
break;
}
keyBuilder.append(c); // 嵌套的 '}'
j++;
} else {
keyBuilder.append(c);
j++;
}
}
// --- 占位符解析结束 ---
if (!foundMatch) {
// 如果内层循环结束仍未找到匹配的 '}',则不进行任何特殊处理
// 外层循环的 i 会自然递增
i++;
}
} else {
i++;
}
}
// 追加最后一个占位符之后的所有剩余文本
if (lastAppendPosition < n) {
arguments.add(Literal.literal(input.substring(lastAppendPosition)));
}
return switch (arguments.size()) {
case 1 -> arguments.getFirst();
case 2 -> new Complex2(input, arguments.get(0), arguments.get(1));
default -> new Complex(input, arguments);
};
}
}

View File

@@ -4,30 +4,38 @@ import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
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.MiscUtils;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
@SuppressWarnings("DuplicatedCode")
public class TemplateManagerImpl implements TemplateManager {
/*
* 此类仍需要一次重构,对模板预解析,避免每次调用时候重新判断值是否含有参数
*/
private static final ArgumentString TEMPLATE = Literal.literal("template");
private static final ArgumentString OVERRIDES = Literal.literal("overrides");
private static final ArgumentString ARGUMENTS = Literal.literal("arguments");
private static final ArgumentString MERGES = Literal.literal("merges");
private final static Set<ArgumentString> NON_TEMPLATE_ARGUMENTS = new HashSet<>(Set.of(TEMPLATE, ARGUMENTS, OVERRIDES, MERGES));
private final Map<Key, Object> templates = new HashMap<>();
private final static Set<String> NON_TEMPLATE_KEY = new HashSet<>(Set.of(TEMPLATE, ARGUMENTS, OVERRIDES, MERGES));
private final TemplateParser templateParser;
public TemplateManagerImpl() {
this.templateParser = new TemplateParser();
}
@Override
public void unload() {
this.templates.clear();
}
@Override
public ConfigParser parser() {
return this.templateParser;
}
public class TemplateParser implements ConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"templates", "template"};
@@ -51,413 +59,270 @@ public class TemplateManagerImpl implements TemplateManager {
if (templates.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.template.duplicate", path.toString(), id.toString());
}
templates.put(id, obj);
// 预处理会将 string类型的键或值解析为ArgumentString以加速模板应用。所以处理后不可能存在String类型。
templates.put(id, preprocessUnknownValue(obj));
}
}
@Override
public void unload() {
this.templates.clear();
public Object applyTemplates(Key id, Object input) {
Object preprocessedInput = preprocessUnknownValue(input);
return processUnknownValue(preprocessedInput, Map.of(
"__NAMESPACE__", PlainStringTemplateArgument.plain(id.namespace()),
"__ID__", PlainStringTemplateArgument.plain(id.value())
));
}
@Override
public ConfigParser parser() {
return this.templateParser;
}
@Override
public Map<String, Object> applyTemplates(Key id, Map<String, Object> input) {
Objects.requireNonNull(input, "Input must not be null");
Map<String, Object> result = new LinkedHashMap<>();
processMap(input,
Map.of("__ID__", PlainStringTemplateArgument.plain(id.value()),
"__NAMESPACE__", PlainStringTemplateArgument.plain(id.namespace())),
(obj) -> {
// 当前位于根节点下如果下一级就是模板则应把模板结果与当前map合并
// 如果模板结果不是map则为非法值因为不可能出现类似于下方的配置
// items:
// test:invalid: 111
if (obj instanceof Map<?,?> mapResult) {
result.putAll(MiscUtils.castToMap(mapResult, false));
} else {
throw new IllegalArgumentException("Invalid template used. Input: " + GsonHelper.get().toJson(input) + ". Template: " + GsonHelper.get().toJson(obj));
}
});
return result;
private Object preprocessUnknownValue(Object value) {
switch (value) {
case Map<?, ?> map -> {
Map<String, Object> in = MiscUtils.castToMap(map, false);
Map<ArgumentString, Object> out = new LinkedHashMap<>(map.size());
for (Map.Entry<String, Object> entry : in.entrySet()) {
out.put(TemplateManager.preParse(entry.getKey()), preprocessUnknownValue(entry.getValue()));
}
return out;
}
case List<?> list -> {
List<Object> objList = new ArrayList<>(list.size());
for (Object o : list) {
objList.add(preprocessUnknownValue(o));
}
return objList;
}
case String string -> {
return TemplateManager.preParse(string);
}
case null, default -> {
return value;
}
}
}
// 对于处理map只有input是已知map而返回值可能并不是
private void processMap(Map<String, Object> input,
Map<String, TemplateArgument> parentArguments,
// 只有当前为模板的时候才会调用callback
Consumer<Object> processCallBack) {
private Object processMap(Map<ArgumentString, Object> input,
Map<String, TemplateArgument> arguments) {
// 传入的input是否含有template这种情况下返回值有可能是非map
if (input.containsKey(TEMPLATE)) {
TemplateProcessingResult processingResult = processTemplates(input, parentArguments);
List<Object> templates = processingResult.templates();
// 你敢保证template里没有template吗
List<Object> processedTemplates = new ArrayList<>();
// 先递归处理后再合并
for (Object template : templates) {
processUnknownTypeMember(template, processingResult.arguments(), processedTemplates::add);
}
if (processedTemplates.isEmpty()) {
return;
}
Object firstTemplate = processedTemplates.get(0);
// 如果是map应当深度合并
if (firstTemplate instanceof Map<?,?>) {
Map<String, Object> results = new LinkedHashMap<>();
for (Object processedTemplate : processedTemplates) {
if (processedTemplate instanceof Map<?, ?> anotherMap) {
deepMergeMaps(results, MiscUtils.castToMap(anotherMap, false));
TemplateProcessingResult processingResult = processTemplates(input, arguments);
List<Object> processedTemplates = processingResult.templates();
if (!processedTemplates.isEmpty()) {
// 先获取第一个模板的类型
Object firstTemplate = processedTemplates.getFirst();
// 如果是map应当深度合并
if (firstTemplate instanceof Map<?,?>) {
Map<String, Object> results = new LinkedHashMap<>();
for (Object processedTemplate : processedTemplates) {
if (processedTemplate instanceof Map<?, ?> map) {
deepMergeMaps(results, MiscUtils.castToMap(map, false));
}
}
}
if (processingResult.overrides() instanceof Map<?, ?> overrides) {
results.putAll(MiscUtils.castToMap(overrides, false));
}
if (processingResult.merges() instanceof Map<?, ?> merges) {
deepMergeMaps(results, MiscUtils.castToMap(merges, false));
}
processCallBack.accept(results);
} else if (firstTemplate instanceof List<?>) {
List<Object> results = new ArrayList<>();
// 仅仅合并list
for (Object processedTemplate : processedTemplates) {
if (processedTemplate instanceof List<?> anotherList) {
results.addAll(anotherList);
if (processingResult.overrides() instanceof Map<?, ?> overrides) {
results.putAll(MiscUtils.castToMap(overrides, false));
}
}
if (processingResult.overrides() instanceof List<?> overrides) {
results.clear();
results.addAll(overrides);
}
if (processingResult.merges() instanceof List<?> merges) {
results.addAll(merges);
}
processCallBack.accept(results);
} else {
Object overrides = processingResult.overrides();
if (overrides != null) {
processCallBack.accept(overrides);
if (processingResult.merges() instanceof Map<?, ?> merges) {
deepMergeMaps(results, MiscUtils.castToMap(merges, false));
}
return results;
} else if (firstTemplate instanceof List<?>) {
List<Object> results = new ArrayList<>();
// 仅仅合并list
for (Object processedTemplate : processedTemplates) {
if (processedTemplate instanceof List<?> anotherList) {
results.addAll(anotherList);
}
}
if (processingResult.overrides() instanceof List<?> overrides) {
results.clear();
results.addAll(overrides);
}
if (processingResult.merges() instanceof List<?> merges) {
results.addAll(merges);
}
return results;
} else {
// 其他情况下应当忽略其他的template
processCallBack.accept(firstTemplate);
// 有覆写用覆写,无覆写返回最后一个模板值
if (processingResult.overrides() != null) {
return processingResult.overrides();
}
if (processingResult.merges() != null) {
return processingResult.merges();
}
return processedTemplates.getLast();
}
} else {
// 模板为空啦如果是map则合并
if (processingResult.overrides() instanceof Map<?,?> overrides) {
Map<String, Object> output = new LinkedHashMap<>(MiscUtils.castToMap(overrides, false));
if (processingResult.merges() instanceof Map<?,?> merges) {
deepMergeMaps(output, MiscUtils.castToMap(merges, false));
}
return output;
} else if (processingResult.overrides() instanceof List<?> overrides) {
List<Object> output = new ArrayList<>(overrides);
if (processingResult.merges() instanceof List<?> merges) {
output.addAll(merges);
}
return output;
}
// 否则有overrides就返回overrides
if (processingResult.overrides() != null) {
return processingResult.overrides();
}
// 否则有merges就返回merges
if (processingResult.merges() != null) {
return processingResult.merges();
}
return null;
}
} else {
// 如果不是模板则返回值一定是map
// 依次处理map下的每个参数
Map<String, Object> result = new LinkedHashMap<>();
for (Map.Entry<String, Object> inputEntry : input.entrySet()) {
String key = applyArgument(inputEntry.getKey(), parentArguments).toString();
processUnknownTypeMember(inputEntry.getValue(), parentArguments, (processed) -> result.put(key, processed));
Map<String, Object> result = new LinkedHashMap<>(input.size());
for (Map.Entry<ArgumentString, Object> inputEntry : input.entrySet()) {
Object key = inputEntry.getKey().get(arguments);
// 如果key为null说明不插入此键
if (key != null) {
result.put(key.toString(), processUnknownValue(inputEntry.getValue(), arguments));
}
}
processCallBack.accept(result);
return result;
}
}
// 处理一个类型未知的值本方法只管将member处理好后传递回调用者
private void processUnknownTypeMember(Object member,
Map<String, TemplateArgument> parentArguments,
Consumer<Object> processCallback) {
if (member instanceof Map<?,?> innerMap) {
// 处理一个类型未知的值本方法只管将member处理好后传递回调用者a
@SuppressWarnings("unchecked")
private Object processUnknownValue(Object value,
Map<String, TemplateArgument> arguments) {
switch (value) {
case Map<?, ?> innerMap ->
// map下面还是个map吗这并不一定
// 比如
// a:
// template: xxx
// 这时候a并不一定是map最终类型取决于template那么应当根据template的结果进行调整所以我们继续交给上方方法处理
processMap(MiscUtils.castToMap(innerMap, false), parentArguments, processCallback);
} else if (member instanceof List<?> innerList) {
// map 下面是个list那么对下面的每个成员再次处理
List<Object> result = new ArrayList<>();
for (Object item : innerList) {
// 处理完以后加入到list内
processUnknownTypeMember(item, parentArguments, result::add);
// 这时候并不一定是map最终类型取决于template那么应当根据template的结果进行调整所以我们继续交给上方方法处理
{
return processMap((Map<ArgumentString, Object>) innerMap, arguments);
}
case List<?> innerList -> {
List<Object> result = new ArrayList<>();
for (Object item : innerList) {
result.add(processUnknownValue(item, arguments));
}
return result;
}
case ArgumentString arg -> {
return arg.get(arguments);
}
case null, default -> {
return value;
}
processCallback.accept(result);
} else if (member instanceof String possibleArgument) {
// 如果是个string其可能是 {xxx} 的参数,那么就尝试应用参数后再返回
processCallback.accept(applyArgument(possibleArgument, parentArguments));
} else {
// 对于其他值,直接处理
processCallback.accept(member);
}
}
private TemplateProcessingResult processTemplates(Map<String, Object> input,
@SuppressWarnings("unchecked")
private TemplateProcessingResult processTemplates(Map<ArgumentString, Object> input,
Map<String, TemplateArgument> parentArguments) {
int knownKeys = 1;
// 先获取template节点下所有的模板
List<String> templateIds = MiscUtils.getAsStringList(input.get(TEMPLATE));
List<ArgumentString> templateIds = MiscUtils.getAsList(input.get(TEMPLATE), ArgumentString.class);
List<Object> templateList = new ArrayList<>(templateIds.size());
for (String templateId : templateIds) {
// 如果模板id被用了参数则应先应用参数后再查询模板
Object actualTemplate = applyArgument(templateId, parentArguments);
if (actualTemplate == null) continue; // 忽略被null掉的模板
Object template = Optional.ofNullable(this.templates.get(Key.of(actualTemplate.toString())))
.orElseThrow(() -> new IllegalArgumentException("Template not found: " + actualTemplate));
templateList.add(template);
}
// 获取arguments
Object argument = input.get(ARGUMENTS);
boolean hasArgument = argument != null;
if (hasArgument) knownKeys++;
// 将本节点下的参数与父参数合并
Map<String, TemplateArgument> arguments = !hasArgument ? parentArguments : mergeArguments(
MiscUtils.castToMap(argument, false),
Map<String, TemplateArgument> arguments = hasArgument ? mergeArguments(
(Map<ArgumentString, Object>) argument,
parentArguments
);
) : parentArguments;
// 获取处理后的template
for (ArgumentString templateId : templateIds) {
// 如果模板id被用了参数则应先应用参数后再查询模板
Object actualTemplate = templateId.get(parentArguments);
if (actualTemplate == null) continue; // 忽略被null掉的模板
Object template = Optional.ofNullable(this.templates.get(Key.of(actualTemplate.toString())))
.orElseThrow(() -> new LocalizedResourceConfigException("warning.config.template.invalid", actualTemplate.toString()));
Object processedTemplate = processUnknownValue(template, arguments);
if (processedTemplate != null) templateList.add(processedTemplate);
}
// 获取overrides
Object override = input.get(OVERRIDES);
if (override instanceof Map<?, ?> rawOverrides) {
// 对overrides参数应用 本节点 + 父节点 参数
Map<String, Object> overrides = new LinkedHashMap<>();
processMap(MiscUtils.castToMap(rawOverrides, false), arguments, (obj) -> {
// 如果overrides的下一级就是一个模板则模板必须为map类型
if (obj instanceof Map<?,?> mapResult) {
overrides.putAll(MiscUtils.castToMap(mapResult, false));
} else {
throw new IllegalArgumentException("Invalid template used. Input: " + GsonHelper.get().toJson(input) + ". Template: " + GsonHelper.get().toJson(obj));
}
});
// overrides是map了merges也只能是map
if (input.get(MERGES) instanceof Map<?, ?> rawMerges) {
Map<String, Object> merges = new LinkedHashMap<>();
processMap(MiscUtils.castToMap(rawMerges, false), arguments, (obj) -> {
// 如果merges的下一级就是一个模板则模板必须为map类型
if (obj instanceof Map<?,?> mapResult) {
merges.putAll(MiscUtils.castToMap(mapResult, false));
} else {
throw new IllegalArgumentException("Invalid template used. Input: " + GsonHelper.get().toJson(input) + ". Template: " + GsonHelper.get().toJson(obj));
}
});
// 已有templatemergesoverrides 和可选的arguments
if (input.size() > (hasArgument ? 4 : 3)) {
// 会不会有一种可能有笨比用户把模板和普通配置混合在了一起再次遍历input后处理
for (Map.Entry<String, Object> inputEntry : input.entrySet()) {
String inputKey = inputEntry.getKey();
if (NON_TEMPLATE_KEY.contains(inputKey)) continue;
processUnknownTypeMember(inputEntry.getValue(), arguments, (processed) -> merges.put(inputKey, processed));
}
}
// 返回处理结果
return new TemplateProcessingResult(
templateList,
overrides,
merges,
arguments
);
} else {
// 已有templateoverrides 和可选的arguments
if (input.size() > (hasArgument ? 3 : 2)) {
Map<String, Object> merges = new LinkedHashMap<>();
// 会不会有一种可能有笨比用户把模板和普通配置混合在了一起再次遍历input后处理
for (Map.Entry<String, Object> inputEntry : input.entrySet()) {
String inputKey = inputEntry.getKey();
if (NON_TEMPLATE_KEY.contains(inputKey)) continue;
processUnknownTypeMember(inputEntry.getValue(), arguments, (processed) -> merges.put(inputKey, processed));
}
return new TemplateProcessingResult(
templateList,
overrides,
merges,
arguments
);
} else {
return new TemplateProcessingResult(
templateList,
overrides,
null,
arguments
);
boolean hasOverrides = override != null;
if (hasOverrides) {
knownKeys++;
override = processUnknownValue(override, arguments);
}
// 获取merges
Object merge = input.get(MERGES);
boolean hasMerges = merge != null;
if (hasMerges) {
knownKeys++;
merge = processUnknownValue(merge, arguments);
}
// 有其他意外参数
if (input.size() > knownKeys) {
Map<String, Object> merges = new LinkedHashMap<>();
// 会不会有一种可能有笨比用户把模板和普通配置混合在了一起再次遍历input后处理。
for (Map.Entry<ArgumentString, Object> inputEntry : input.entrySet()) {
ArgumentString inputKey = inputEntry.getKey();
if (NON_TEMPLATE_ARGUMENTS.contains(inputKey)) continue;
Object key = inputKey.get(parentArguments);
if (key != null) {
merges.put(key.toString(), processUnknownValue(inputEntry.getValue(), arguments));
}
}
} else if (override instanceof List<?> overrides) {
// overrides不为空且不是map
List<Object> processedOverrides = new ArrayList<>(overrides.size());
for (Object item : overrides) {
processUnknownTypeMember(item, arguments, processedOverrides::add);
}
if (input.get(MERGES) instanceof List<?> rawMerges) {
List<Object> merges = new ArrayList<>(rawMerges.size());
for (Object item : rawMerges) {
processUnknownTypeMember(item, arguments, merges::add);
if (hasMerges && merge instanceof Map<?, ?> rawMerges) {
Map<ArgumentString, Object> mergeMap = (Map<ArgumentString, Object>) rawMerges;
for (Map.Entry<ArgumentString, Object> inputEntry : mergeMap.entrySet()) {
ArgumentString inputKey = inputEntry.getKey();
Object key = inputKey.get(parentArguments);
if (key != null) {
merges.put(key.toString(), processUnknownValue(inputEntry.getValue(), arguments));
}
}
return new TemplateProcessingResult(
templateList,
processedOverrides,
merges,
arguments
);
} else {
return new TemplateProcessingResult(
templateList,
processedOverrides,
null,
arguments
);
}
} else if (override instanceof String rawOverride) {
return new TemplateProcessingResult(
templateList,
applyArgument(rawOverride, arguments),
null,
arguments
);
} else if (override != null) {
// overrides不为空且不是map,list。此情况不用再考虑merge了
return new TemplateProcessingResult(
templateList,
override,
null,
merges,
arguments
);
} else {
// 获取merges
Object merge = input.get(MERGES);
if (merge instanceof Map<?, ?> rawMerges) {
Map<String, Object> merges = new LinkedHashMap<>();
processMap(MiscUtils.castToMap(rawMerges, false), arguments, (obj) -> {
// 如果merges的下一级就是一个模板则模板必须为map类型
if (obj instanceof Map<?,?> mapResult) {
merges.putAll(MiscUtils.castToMap(mapResult, false));
} else {
throw new IllegalArgumentException("Invalid template used. Input: " + GsonHelper.get().toJson(input) + ". Template: " + GsonHelper.get().toJson(obj));
}
});
// 已有template和merges 和可选的arguments
if (input.size() > (hasArgument ? 3 : 2)) {
// 会不会有一种可能有笨比用户把模板和普通配置混合在了一起再次遍历input后处理
for (Map.Entry<String, Object> inputEntry : input.entrySet()) {
String inputKey = inputEntry.getKey();
if (NON_TEMPLATE_KEY.contains(inputKey)) continue;
processUnknownTypeMember(inputEntry.getValue(), arguments, (processed) -> merges.put(inputKey, processed));
}
}
return new TemplateProcessingResult(
templateList,
null,
merges,
arguments
);
} else if (merge instanceof List<?> rawMerges) {
List<Object> merges = new ArrayList<>(rawMerges.size());
for (Object item : rawMerges) {
processUnknownTypeMember(item, arguments, merges::add);
}
return new TemplateProcessingResult(
templateList,
null,
merges,
arguments
);
} else if (merge instanceof String rawMerge) {
// merge是个string
return new TemplateProcessingResult(
templateList,
null,
applyArgument(rawMerge, arguments),
arguments
);
} else if (merge != null) {
// merge是个普通的类型
return new TemplateProcessingResult(
templateList,
null,
merge,
arguments
);
} else {
// 无overrides和merges
// 会不会有一种可能有笨比用户不会使用merges把模板和普通配置混合在了一起再次遍历input后处理
if (input.size() > (hasArgument ? 2 : 1)) {
Map<String, Object> merges = new LinkedHashMap<>();
for (Map.Entry<String, Object> inputEntry : input.entrySet()) {
String inputKey = inputEntry.getKey();
if (NON_TEMPLATE_KEY.contains(inputKey)) continue;
processUnknownTypeMember(inputEntry.getValue(), arguments, (processed) -> merges.put(inputKey, processed));
}
return new TemplateProcessingResult(
templateList,
null,
merges,
arguments
);
} else {
return new TemplateProcessingResult(
templateList,
null,
null,
arguments
);
}
}
return new TemplateProcessingResult(
templateList,
override,
merge,
arguments
);
}
}
// 合并参数
private Map<String, TemplateArgument> mergeArguments(@NotNull Map<String, Object> rawChildArguments,
@SuppressWarnings("unchecked")
private Map<String, TemplateArgument> mergeArguments(@NotNull Map<ArgumentString, Object> childArguments,
@NotNull Map<String, TemplateArgument> parentArguments) {
Map<String, TemplateArgument> result = new HashMap<>(parentArguments);
// 我们遍历一下当前节点下的所有参数这些参数可能含有内嵌参数。所以需要对参数map先处理一次后再合并
// arguments:
// argument_1: "{parent_argument}"
for (Map.Entry<String, Object> argumentEntry : rawChildArguments.entrySet()) {
// 获取最终的string形式参数
String placeholder = applyArgument(argumentEntry.getKey(), parentArguments).toString();
Map<String, TemplateArgument> result = new LinkedHashMap<>(parentArguments);
for (Map.Entry<ArgumentString, Object> argumentEntry : childArguments.entrySet()) {
Object placeholderObj = argumentEntry.getKey().get(result);
if (placeholderObj == null) continue;
String placeholder = placeholderObj.toString();
// 父亲参数最大
if (result.containsKey(placeholder)) continue;
Object rawArgument = argumentEntry.getValue();
if (rawArgument instanceof Map<?,?> mapArgument) {
// 此参数是一个map那么对map应用模板然后再根据map是否含有type等参数判别其是否为带名特殊参数
Map<String, Object> nestedResult = new LinkedHashMap<>();
processMap(MiscUtils.castToMap(mapArgument, false), parentArguments, (obj) -> {
// 如果有人往arguments下塞了一个模板则模板类型应为map
if (obj instanceof Map<?,?> mapResult) {
nestedResult.putAll(MiscUtils.castToMap(mapResult, false));
} else {
throw new IllegalArgumentException("Invalid template used. Input: " + GsonHelper.get().toJson(mapArgument) + ". Template: " + GsonHelper.get().toJson(obj));
}
});
result.put(placeholder, TemplateArguments.fromMap(nestedResult));
} else if (rawArgument instanceof List<?> listArgument) {
// 此参数是一个list那么只需要应用模板即可
List<Object> nestedResult = new ArrayList<>();
for (Object item : listArgument) {
processUnknownTypeMember(item, parentArguments, nestedResult::add);
}
result.put(placeholder, new ListTemplateArgument(nestedResult));
} else if (rawArgument == null) {
// 使用 null 覆写其父参数内容
result.put(placeholder, NullTemplateArgument.INSTANCE);
} else if (rawArgument instanceof Number number) {
result.put(placeholder, new ObjectTemplateArgument(number));
} else if (rawArgument instanceof Boolean booleanValue) {
result.put(placeholder, new ObjectTemplateArgument(booleanValue));
} else {
// 将参数字符串化后,应用参数再放入
Object applied = applyArgument(rawArgument.toString(), parentArguments);
result.put(placeholder, new ObjectTemplateArgument(applied));
Object processedPlaceholderValue = processUnknownValue(argumentEntry.getValue(), result);
switch (processedPlaceholderValue) {
case Map<?, ?> map -> result.put(placeholder, TemplateArguments.fromMap(MiscUtils.castToMap(map, false)));
case List<?> listArgument -> result.put(placeholder, new ListTemplateArgument((List<Object>) listArgument));
case null -> result.put(placeholder, NullTemplateArgument.INSTANCE);
default -> result.put(placeholder, new ObjectTemplateArgument(processedPlaceholderValue));
}
}
return result;
}
// 将某个输入变成最终的结果可以是string->string也可以是string->map/list
private Object applyArgument(String input, Map<String, TemplateArgument> arguments) {
// 如果字符串长度连3都没有那么肯定没有{}啊
if (input.length() < 3) return input;
if (input.charAt(0) == '{' && input.charAt(input.length() - 1) == '}') {
String key = input.substring(1, input.length() - 1);
TemplateArgument argument = arguments.get(key);
if (argument != null) {
return argument.get();
}
}
return replacePlaceholders(input, arguments);
}
private record TemplateProcessingResult(
List<Object> templates,
Object overrides,
@@ -488,86 +353,4 @@ public class TemplateManagerImpl implements TemplateManager {
}
}
}
public static String replacePlaceholders(String input, Map<String, TemplateArgument> replacements) {
if (input == null || input.isEmpty()) {
return input;
}
StringBuilder finalResult = new StringBuilder();
int n = input.length();
int lastAppendPosition = 0; // 追踪上一次追加操作结束的位置
int i = 0;
while (i < n) {
// 检查当前字符是否为未转义的 '{'
int backslashes = 0;
int temp_i = i - 1;
while (temp_i >= 0 && input.charAt(temp_i) == '\\') {
backslashes++;
temp_i--;
}
if (input.charAt(i) == '{' && backslashes % 2 == 0) {
// 发现占位符起点
int placeholderStartIndex = i;
// 追加从上一个位置到当前占位符之前的文本
finalResult.append(input, lastAppendPosition, placeholderStartIndex);
// --- 开始解析占位符内部 ---
StringBuilder keyBuilder = new StringBuilder();
int depth = 1;
int j = i + 1;
boolean foundMatch = false;
while (j < n) {
char c = input.charAt(j);
if (c == '\\') { // 处理转义
if (j + 1 < n) {
keyBuilder.append(input.charAt(j + 1));
j += 2;
} else {
keyBuilder.append(c);
j++;
}
} else if (c == '{') {
depth++;
keyBuilder.append(c);
j++;
} else if (c == '}') {
depth--;
if (depth == 0) { // 找到匹配的结束括号
String key = keyBuilder.toString();
TemplateArgument value = replacements.get(key);
if (value != null) {
// 如果在 Map 中找到值,则进行替换
finalResult.append(value.get());
} else {
// 否则,保留原始占位符(包括 '{}'
finalResult.append(input, placeholderStartIndex, j + 1);
}
// 更新位置指针
i = j + 1;
lastAppendPosition = i;
foundMatch = true;
break;
}
keyBuilder.append(c); // 嵌套的 '}'
j++;
} else {
keyBuilder.append(c);
j++;
}
}
// --- 占位符解析结束 ---
if (!foundMatch) {
// 如果内层循环结束仍未找到匹配的 '}',则不进行任何特殊处理
// 外层循环的 i 会自然递增
i++;
}
} else {
i++;
}
}
// 追加最后一个占位符之后的所有剩余文本
if (lastAppendPosition < n) {
finalResult.append(input, lastAppendPosition, n);
}
return finalResult.toString();
}
}

View File

@@ -21,9 +21,5 @@ public interface MiniMessageTranslator extends Translator, Examinable {
return renderer().render(component, locale);
}
@NotNull Iterable<? extends Translator> sources();
boolean addSource(final @NotNull Translator source);
boolean removeSource(final @NotNull Translator source);
boolean setSource(final @NotNull Translator source);
}

View File

@@ -11,17 +11,14 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
public class MiniMessageTranslatorImpl implements MiniMessageTranslator {
private static final Key NAME = Key.key(net.momirealms.craftengine.core.util.Key.DEFAULT_NAMESPACE, "main");
static final MiniMessageTranslatorImpl INSTANCE = new MiniMessageTranslatorImpl();
final TranslatableComponentRenderer<Locale> renderer = TranslatableComponentRenderer.usingTranslationSource(this);
private final Set<Translator> sources = Collections.newSetFromMap(new ConcurrentHashMap<>());
protected final TranslatableComponentRenderer<Locale> renderer = TranslatableComponentRenderer.usingTranslationSource(this);
private Translator source;
@Override
public @NotNull Key name() {
@@ -30,7 +27,7 @@ public class MiniMessageTranslatorImpl implements MiniMessageTranslator {
@Override
public @NotNull TriState hasAnyTranslations() {
if (!this.sources.isEmpty()) {
if (this.source != null) {
return TriState.TRUE;
}
return TriState.FALSE;
@@ -44,33 +41,20 @@ public class MiniMessageTranslatorImpl implements MiniMessageTranslator {
@Override
public @Nullable Component translate(@NotNull TranslatableComponent component, @NotNull Locale locale) {
for (final Translator source : this.sources) {
final Component translation = source.translate(component, locale);
if (translation != null) {
return translation;
}
if (this.source != null) {
return this.source.translate(component, locale);
}
return null;
}
@Override
public @NotNull Iterable<? extends Translator> sources() {
return Collections.unmodifiableSet(this.sources);
}
@Override
public boolean addSource(final @NotNull Translator source) {
if (source == this) throw new IllegalArgumentException("MiniMessageTranslationSource");
return this.sources.add(source);
}
@Override
public boolean removeSource(final @NotNull Translator source) {
return this.sources.remove(source);
public boolean setSource(@NotNull Translator source) {
this.source = source;
return true;
}
@Override
public @NotNull Stream<? extends ExaminableProperty> examinableProperties() {
return Stream.of(ExaminableProperty.of("sources", this.sources));
return Stream.of(ExaminableProperty.of("source", this.source));
}
}

View File

@@ -9,26 +9,28 @@ import net.momirealms.craftengine.core.plugin.Plugin;
import net.momirealms.craftengine.core.plugin.PluginProperties;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.StringKeyConstructor;
import net.momirealms.craftengine.core.plugin.config.TranslationConfigConstructor;
import net.momirealms.craftengine.core.plugin.text.minimessage.IndexedArgumentTag;
import net.momirealms.craftengine.core.util.AdventureHelper;
import net.momirealms.craftengine.core.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TranslationManagerImpl implements TranslationManager {
private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
@@ -38,13 +40,14 @@ public class TranslationManagerImpl implements TranslationManager {
private final Path translationsDirectory;
private final String langVersion;
private final String[] supportedLanguages;
private final Map<String, Object> translationFallback = new LinkedHashMap<>();
private final Map<String, String> translationFallback = new LinkedHashMap<>();
private Locale forcedLocale = null;
private Locale selectedLocale = DEFAULT_LOCALE;
private MiniMessageTranslationRegistry registry;
private final Map<String, I18NData> clientLangData = new HashMap<>();
private final LangParser langParser;
private final I18NParser i18nParser;
private Map<String, CachedTranslation> cachedTranslations = Map.of();
public TranslationManagerImpl(Plugin plugin) {
instance = this;
@@ -54,7 +57,7 @@ public class TranslationManagerImpl implements TranslationManager {
this.supportedLanguages = PluginProperties.getValue("supported-languages").split(",");
this.langParser = new LangParser();
this.i18nParser = new I18NParser();
Yaml yaml = new Yaml(new StringKeyConstructor(new LoaderOptions()));
Yaml yaml = new Yaml(new TranslationConfigConstructor(new LoaderOptions()));
try (InputStream is = plugin.resourceStream("translations/en.yml")) {
this.translationFallback.putAll(yaml.load(is));
} catch (IOException e) {
@@ -81,12 +84,7 @@ public class TranslationManagerImpl implements TranslationManager {
public void reload() {
// clear old data
this.clientLangData.clear();
// remove any previous registry
if (this.registry != null) {
MiniMessageTranslator.translator().removeSource(this.registry);
this.installed.clear();
}
this.installed.clear();
// save resources
for (String lang : this.supportedLanguages) {
@@ -95,8 +93,10 @@ public class TranslationManagerImpl implements TranslationManager {
this.registry = MiniMessageTranslationRegistry.create(Key.key(net.momirealms.craftengine.core.util.Key.DEFAULT_NAMESPACE, "main"), AdventureHelper.miniMessage());
this.registry.defaultLocale(DEFAULT_LOCALE);
this.loadFromFileSystem(this.translationsDirectory, false);
MiniMessageTranslator.translator().addSource(this.registry);
this.loadFromFileSystem(this.translationsDirectory);
this.loadFromCache();
MiniMessageTranslator.translator().setSource(this.registry);
this.setSelectedLocale();
}
@@ -138,89 +138,65 @@ public class TranslationManagerImpl implements TranslationManager {
return MiniMessageTranslator.render(component, locale);
}
public void loadFromFileSystem(Path directory, boolean suppressDuplicatesError) {
List<Path> translationFiles;
try (Stream<Path> stream = Files.list(directory)) {
translationFiles = stream.filter(TranslationManagerImpl::isTranslationFile).collect(Collectors.toList());
} catch (IOException e) {
translationFiles = Collections.emptyList();
}
if (translationFiles.isEmpty()) {
return;
}
Map<Locale, Map<String, String>> loaded = new HashMap<>();
for (Path translationFile : translationFiles) {
try {
Pair<Locale, Map<String, String>> result = loadTranslationFile(translationFile);
loaded.put(result.left(), result.right());
} catch (Exception e) {
if (!suppressDuplicatesError || !isAdventureDuplicatesException(e)) {
this.plugin.logger().warn("Error loading locale file: " + translationFile.getFileName(), e);
}
private void loadFromCache() {
for (Map.Entry<String, CachedTranslation> entry : this.cachedTranslations.entrySet()) {
Locale locale = TranslationManager.parseLocale(entry.getKey());
if (locale == null) {
this.plugin.logger().warn("Unknown locale '" + entry.getKey() + "' - unable to register.");
continue;
}
}
// try registering the locale without a country code - if we don't already have a registration for that
loaded.forEach((locale, bundle) -> {
Map<String, String> translations = entry.getValue().translations();
this.registry.registerAll(locale, translations);
this.installed.add(locale);
Locale localeWithoutCountry = Locale.of(locale.getLanguage());
if (!locale.equals(localeWithoutCountry) && !localeWithoutCountry.equals(DEFAULT_LOCALE) && this.installed.add(localeWithoutCountry)) {
try {
this.registry.registerAll(localeWithoutCountry, bundle);
this.registry.registerAll(localeWithoutCountry, translations);
} catch (IllegalArgumentException e) {
// ignore
}
}
});
}
public static boolean isTranslationFile(Path path) {
return path.getFileName().toString().endsWith(".yml");
}
private static boolean isAdventureDuplicatesException(Exception e) {
return e instanceof IllegalArgumentException && (e.getMessage().startsWith("Invalid key") || e.getMessage().startsWith("Translation already exists"));
}
@SuppressWarnings("unchecked")
private Pair<Locale, Map<String, String>> loadTranslationFile(Path translationFile) {
String fileName = translationFile.getFileName().toString();
String localeString = fileName.substring(0, fileName.length() - ".yml".length());
Locale locale = TranslationManager.parseLocale(localeString);
if (locale == null) {
throw new IllegalStateException("Unknown locale '" + localeString + "' - unable to register.");
}
}
Map<String, String> bundle = new HashMap<>();
Yaml yaml = new Yaml(new StringKeyConstructor(new LoaderOptions()));
try (InputStreamReader inputStream = new InputStreamReader(new FileInputStream(translationFile.toFile()), StandardCharsets.UTF_8)) {
Map<String, Object> map = yaml.load(inputStream);
String langVersion = map.getOrDefault("lang-version", "").toString();
if (!langVersion.equals(this.langVersion)) {
map = updateLangFile(map, translationFile);
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof String str) {
bundle.put(entry.getKey(), str);
} else if (entry.getValue() instanceof List<?> list) {
List<String> strList = (List<String>) list;
StringJoiner stringJoiner = new StringJoiner("<reset><newline>");
for (String str : strList) {
stringJoiner.add(str);
public void loadFromFileSystem(Path directory) {
Map<String, CachedTranslation> previousTranslations = this.cachedTranslations;
this.cachedTranslations = new HashMap<>();
try {
Files.walkFileTree(directory, new SimpleFileVisitor<>() {
@Override
public @NotNull FileVisitResult visitFile(@NotNull Path path, @NotNull BasicFileAttributes attrs) {
String fileName = path.getFileName().toString();
if (Files.isRegularFile(path) && fileName.endsWith(".yml")) {
String localeName = fileName.substring(0, fileName.length() - ".yml".length());
CachedTranslation cachedFile = previousTranslations.get(localeName);
long lastModifiedTime = attrs.lastModifiedTime().toMillis();
long size = attrs.size();
if (cachedFile != null && cachedFile.lastModified() == lastModifiedTime && cachedFile.size() == size) {
TranslationManagerImpl.this.cachedTranslations.put(localeName, cachedFile);
} else {
try (InputStreamReader inputStream = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) {
Yaml yaml = new Yaml(new TranslationConfigConstructor(new LoaderOptions()));
Map<String, String> data = yaml.load(inputStream);
if (data == null) return FileVisitResult.CONTINUE;
String langVersion = data.getOrDefault("lang-version", "");
if (!langVersion.equals(TranslationManagerImpl.this.langVersion)) {
data = updateLangFile(data, path);
}
cachedFile = new CachedTranslation(data, lastModifiedTime, size);
TranslationManagerImpl.this.cachedTranslations.put(localeName, cachedFile);
} catch (IOException e) {
TranslationManagerImpl.this.plugin.logger().severe("Error while reading translation file: " + path, e);
return FileVisitResult.CONTINUE;
}
}
}
bundle.put(entry.getKey(), stringJoiner.toString());
return FileVisitResult.CONTINUE;
}
}
this.registry.registerAll(locale, bundle);
this.installed.add(locale);
});
} catch (IOException e) {
this.plugin.logger().warn(translationFile, "Error loading translation file", e);
this.plugin.logger().warn("Failed to load translation file from folder", e);
}
return Pair.of(locale, bundle);
}
@Override
@@ -230,7 +206,7 @@ public class TranslationManagerImpl implements TranslationManager {
this.plugin.senderFactory().console().sendMessage(AdventureHelper.miniMessage().deserialize(translation, new IndexedArgumentTag(Arrays.stream(args).map(Component::text).toList())));
}
private Map<String, Object> updateLangFile(Map<String, Object> previous, Path translationFile) throws IOException {
private Map<String, String> updateLangFile(Map<String, String> previous, Path translationFile) throws IOException {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setPrettyFlow(true);
@@ -238,11 +214,12 @@ public class TranslationManagerImpl implements TranslationManager {
options.setSplitLines(false);
options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED);
Yaml yaml = new Yaml(new StringKeyConstructor(new LoaderOptions()), new Representer(options), options);
LinkedHashMap<String, Object> newFileContents = new LinkedHashMap<>();
try (InputStream is = plugin.resourceStream("translations/" + translationFile.getFileName())) {
Map<String, Object> newMap = yaml.load(is);
LinkedHashMap<String, String> newFileContents = new LinkedHashMap<>();
try (InputStream is = this.plugin.resourceStream("translations/" + translationFile.getFileName())) {
Map<String, String> newMap = yaml.load(is);
newFileContents.putAll(this.translationFallback);
newFileContents.putAll(newMap);
// 思考是否值得特殊处理list类型的dump似乎并没有这个必要。用户很少会使用list类型且dump后只改变YAML结构而不影响游戏内效果。
newFileContents.putAll(previous);
newFileContents.put("lang-version", this.langVersion);
String yamlString = yaml.dump(newFileContents);
@@ -332,4 +309,7 @@ public class TranslationManagerImpl implements TranslationManager {
TranslationManagerImpl.this.addClientTranslation(langId, sectionData);
}
}
private record CachedTranslation(Map<String, String> translations, long lastModified, long size) {
}
}

View File

@@ -50,6 +50,22 @@ public class MiscUtils {
return list;
}
@SuppressWarnings("unchecked")
public static <T> List<T> getAsList(Object o, Class<T> clazz) {
if (o instanceof List<?> list) {
if (list.isEmpty()) {
return List.of();
}
if (clazz.isInstance(list.getFirst())) {
return (List<T>) list;
}
}
if (clazz.isInstance(o)) {
return List.of((T) o);
}
return List.of();
}
public static Vector3f getAsVector3f(Object o, String option) {
if (o == null) return new Vector3f();
if (o instanceof List<?> list && list.size() == 3) {