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:
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class ListTemplateArgument implements TemplateArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Object> get() {
|
||||
public List<Object> get(Map<String, TemplateArgument> arguments) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public class NullTemplateArgument implements TemplateArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
public Object get(Map<String, TemplateArgument> arguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public class PlainStringTemplateArgument implements TemplateArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
public String get(Map<String, TemplateArgument> arguments) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
// 已有template,merges,overrides 和可选的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 {
|
||||
// 已有template,overrides 和可选的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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user