This commit is contained in:
C-H716 2025-10-25 02:01:52 +08:00
parent e109bd9ef9
commit cfeffd958d

View File

@ -9,23 +9,24 @@ import net.minecraft.world.item.crafting.RecipeType;
import net.minecraftforge.fml.loading.FMLPaths;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static com.extendedae_plus.util.GlobalSendMessage.sendPlayerMessage;
import static com.extendedae_plus.util.Logger.EAP$LOGGER;
/**
* 负责配置文件 extendedae_plus/recipe_type_names.json 的加载与写入
* 以及 recipeType -> 中文名称 / 搜索关键字 的映射逻辑
*/
public final class RecipeTypeNameConfig {
private RecipeTypeNameConfig() {}
private static final String CONFIG_RELATIVE = "extendedae_plus/recipe_type_names.json";
private static final String CONFIG_PATH = "extendedae_plus/recipe_type_names.json";
private static final Map<ResourceLocation, String> CUSTOM_NAMES = new ConcurrentHashMap<>();
// 允许使用最终搜索关键字通常为 path 或自定义短语作为键例如"assembler": "组装机"
private static final Map<String, String> CUSTOM_ALIASES = new ConcurrentHashMap<>();
@ -35,228 +36,236 @@ public final class RecipeTypeNameConfig {
try {
loadRecipeTypeNames();
} catch (Throwable t) {
// 安静失败使用内置映射
sendPlayerMessage(Component.literal("ExtendedAE_Plus: 配置文件解析失败, " + t.getMessage()));
EAP$LOGGER.warn("ExtendedAE_Plus: 映射文件解析失败, {}", t.getMessage());
}
}
/**
* 从配置文件加载 RecipeType 中文名称映射文件不存在则生成模板
* 同时支持别名形式不含冒号的键会被视为最终搜索关键字大小写不敏感
* {
* "assembler": "组装机"
* }
*/
public static synchronized void loadRecipeTypeNames() {
try {
Path cfgDir = FMLPaths.CONFIGDIR.get();
Path cfgPath = cfgDir.resolve(CONFIG_RELATIVE);
if (!Files.exists(cfgPath)) {
// 创建目录并写入模板
Files.createDirectories(cfgPath.getParent());
JsonObject tmpl = new JsonObject();
// 提供一些常见原版默认仅作为示例实际仍以内置 switch 为兜底
tmpl.addProperty("minecraft:smelting", "熔炉");
tmpl.addProperty("minecraft:blasting", "高炉");
tmpl.addProperty("minecraft:smoking", "烟熏");
tmpl.addProperty("minecraft:campfire_cooking", "营火");
// GTCEu 示例占位
tmpl.addProperty("gtceu:assembler", "组装机");
tmpl.addProperty("gtceu:arc_furnace", "电弧炉");
tmpl.addProperty("gtceu:chemical_reactor", "化学反应器");
// 也支持别名最终搜索关键字形式例如
tmpl.addProperty("assembler", "组装机");
Files.writeString(cfgPath, GSON.toJson(tmpl));
}
String json = Files.readString(cfgPath);
JsonObject obj = GSON.fromJson(json, JsonObject.class);
Map<ResourceLocation, String> map = new HashMap<>();
Map<String, String> alias = new HashMap<>();
if (obj != null) {
for (Map.Entry<String, JsonElement> e : obj.entrySet()) {
String k = e.getKey();
JsonElement v = e.getValue();
if (v != null && v.isJsonPrimitive()) {
String name = v.getAsString();
if (name == null || name.isBlank()) continue;
if (k.contains(":")) {
// 形如 namespace:path
try {
ResourceLocation rl = new ResourceLocation(k);
map.put(rl, name);
} catch (Exception ignored) {}
} else {
// 视为别名最终搜索关键字大小写不敏感
alias.put(k.toLowerCase(), name);
}
}
}
}
CUSTOM_NAMES.clear();
CUSTOM_NAMES.putAll(map);
CUSTOM_ALIASES.clear();
CUSTOM_ALIASES.putAll(alias);
} catch (IOException ignored) {
}
}
private RecipeTypeNameConfig() {}
// 最近一次通过 JEI 填充到编码终端的处理配方的中文名称烧炼/高炉/烟熏...
public static volatile String lastProcessingName = null;
public static void setLastProcessingName(String name) {
lastProcessingName = name;
}
/**
* 向配置中新增或更新别名 -> 中文映射并刷新内存映射
* 仅用于非原版或希望使用最终搜索关键字场景
* 生成默认的配方类型映射用于配置文件模板
*
* @return 默认映射
*/
private static Map<String, String> getDefaultMappings() {
Map<String, String> mappings = new HashMap<>();
// 添加原版和常见模组的默认映射
mappings.put("minecraft:smelting", "熔炉");
mappings.put("minecraft:blasting", "高炉");
mappings.put("minecraft:smoking", "烟熏");
mappings.put("minecraft:campfire_cooking", "营火");
mappings.put("gtceu:assembler", "组装机");
mappings.put("assembler", "组装机");
return mappings;
}
/**
* 创建默认配置文件模板
*
* @return 默认的 JSON 对象
*/
private static JsonObject createDefaultTemplate() {
JsonObject tmpl = new JsonObject();
// 将默认映射写入 JSON
getDefaultMappings().forEach(tmpl::addProperty);
return tmpl;
}
/**
* 加载 JSON 配置文件若文件不存在返回空对象
*
* @param cfgPath 文件路径
* @return JSON 对象
* @throws IOException 如果文件读取失败
* @throws JsonSyntaxException 如果 JSON 解析失败
*/
private static JsonObject loadJsonConfig(Path cfgPath) throws IOException, JsonSyntaxException {
// 文件不存在返回空对象
if (!Files.exists(cfgPath)) return new JsonObject();
String json = Files.readString(cfgPath);
JsonObject obj = GSON.fromJson(json, JsonObject.class);
// 确保返回非 null 对象
return obj != null ? obj : new JsonObject();
}
/**
* 保存 JSON 配置到文件
*
* @param cfgPath 文件路径
* @param config JSON 对象
* @throws IOException 如果文件写入失败
*/
private static void saveJsonConfig(Path cfgPath, JsonObject config) throws IOException {
Files.createDirectories(cfgPath.getParent());
// 写入格式化 JSON
Files.writeString(cfgPath, GSON.toJson(config));
}
/**
* 加载配方类型名称映射如果配置文件不存在则生成默认模板
* 支持 ResourceLocation 格式namespace:path和别名格式 path
*
* @throws IOException 如果文件读写失败
*/
public static synchronized void loadRecipeTypeNames() throws IOException {
// 获取配置文件路径
Path cfgPath = FMLPaths.CONFIGDIR.get().resolve(CONFIG_PATH);
JsonObject config = loadJsonConfig(cfgPath);
if (config.entrySet().isEmpty()) {
// 文件为空或不存在时生成默认模板
config = createDefaultTemplate();
saveJsonConfig(cfgPath, config);
}
Map<ResourceLocation, String> nameMap = new HashMap<>();
Map<String, String> alias = new HashMap<>();
for (Map.Entry<String, JsonElement> entry : config.entrySet()) {
String key = entry.getKey();
JsonElement value = entry.getValue();
if (value != null && value.isJsonPrimitive()) {
String name = value.getAsString();
if (name == null || name.isBlank()) continue;
if (key.contains(":")) {
try {
// 解析完整 ID
ResourceLocation rl = new ResourceLocation(key);
nameMap.put(rl, name);
} catch (Exception ignored) {}
} else {
// 存入别名映射小写
alias.put(key.toLowerCase(), name);
}
}
}
CUSTOM_NAMES.clear();
// 批量更新 ResourceLocation 映射
CUSTOM_NAMES.putAll(nameMap);
CUSTOM_ALIASES.clear();
// 批量更新别名映射
CUSTOM_ALIASES.putAll(alias);
}
/**
* 新增或更新别名到名称的映射并保存到配置文件
*
* @param aliasKey 最终搜索关键字不含冒号大小写不敏感
* @param cnValue 中文名称
* @param value 名称
* @return 是否写入成功
*/
public static synchronized boolean addOrUpdateAliasMapping(String aliasKey, String cnValue) {
if (aliasKey == null || aliasKey.isBlank() || cnValue == null || cnValue.isBlank()) {
return false;
public static synchronized boolean addOrUpdateAliasMapping(String aliasKey, String value) {
if (aliasKey == null || aliasKey.isBlank() || value == null || value.isBlank()) {
return false; // 输入验证
}
try {
Path cfgDir = FMLPaths.CONFIGDIR.get();
Path cfgPath = cfgDir.resolve(CONFIG_RELATIVE);
if (!Files.exists(cfgPath)) {
// 若文件不存在先创建模板
loadRecipeTypeNames();
}
JsonObject obj;
if (Files.exists(cfgPath)) {
String json = Files.readString(cfgPath);
obj = GSON.fromJson(json, JsonObject.class);
if (obj == null) obj = new JsonObject();
} else {
obj = new JsonObject();
}
Path cfgPath = FMLPaths.CONFIGDIR.get().resolve(CONFIG_PATH); // 获取配置文件路径
JsonObject config = loadJsonConfig(cfgPath); // 加载现有配置
String key = aliasKey.trim();
// 仅允许作为别名写入不含冒号如包含冒号仍按原样写入但推荐别名
obj.addProperty(key, cnValue);
Files.createDirectories(cfgPath.getParent());
Files.writeString(cfgPath, GSON.toJson(obj));
config.addProperty(key, value); // 更新或添加映射
saveJsonConfig(cfgPath, config); // 保存到文件
// 更新内存映射
if (key.contains(":")) {
try {
ResourceLocation rl = new ResourceLocation(key);
CUSTOM_NAMES.put(rl, cnValue);
ResourceLocation rl = new ResourceLocation(key); // 解析完整 ID
CUSTOM_NAMES.put(rl, value); // 更新 ResourceLocation 映射
} catch (Exception ignored) {}
} else {
CUSTOM_ALIASES.put(key.toLowerCase(), cnValue);
CUSTOM_ALIASES.put(key.toLowerCase(), value); // 更新别名映射小写
}
return true;
} catch (JsonSyntaxException e) {
sendPlayerMessage(Component.literal("ExtendedAE_Plus: 配置文件解析失败, " + e.getMessage()));
} catch (IOException e) {
} catch (IOException | JsonSyntaxException e) {
sendPlayerMessage(Component.literal("ExtendedAE_Plus: 配置文件更新失败: " + e.getMessage()));
return false;
}
return false;
}
/**
* 按中文值精确匹配删除映射支持别名与完整ID
* 返回删除的条目数量
* 按值精确匹配删除映射支持别名与完整ID
*
* @param delValue 名称
* @return 删除的条目数量
*/
public static synchronized int removeMappingsByCnValue(String cnValue) {
if (cnValue == null) return 0;
String target = cnValue.trim();
if (target.isEmpty()) return 0;
public static synchronized int removeMappingsByCnValue(String delValue) {
if (delValue == null || delValue.trim().isEmpty()) return 0; // 输入验证
try {
Path cfgDir = FMLPaths.CONFIGDIR.get();
Path cfgPath = cfgDir.resolve(CONFIG_RELATIVE);
if (!Files.exists(cfgPath)) {
return 0;
}
String json = Files.readString(cfgPath);
JsonObject obj = GSON.fromJson(json, JsonObject.class);
if (obj == null) return 0;
Path cfgPath = FMLPaths.CONFIGDIR.get().resolve(CONFIG_PATH); // 获取配置文件路径
JsonObject config = loadJsonConfig(cfgPath); // 加载现有配置
java.util.List<String> toRemove = new java.util.ArrayList<>();
for (java.util.Map.Entry<String, JsonElement> e : obj.entrySet()) {
JsonElement v = e.getValue();
if (v != null && v.isJsonPrimitive()) {
String name = v.getAsString();
if (target.equals(name)) {
toRemove.add(e.getKey());
}
List<String> toRemove = new ArrayList<>();
for (Map.Entry<String, JsonElement> entry : config.entrySet()) {
JsonElement value = entry.getValue();
if (value != null && value.isJsonPrimitive() && delValue.equals(value.getAsString())) {
toRemove.add(entry.getKey()); // 收集匹配中文名称的键
}
}
if (toRemove.isEmpty()) return 0;
// JSON 中移除
for (String k : toRemove) {
obj.remove(k);
}
Files.createDirectories(cfgPath.getParent());
Files.writeString(cfgPath, GSON.toJson(obj));
toRemove.forEach(config::remove); // 移除匹配的键
saveJsonConfig(cfgPath, config); // 保存更新后的配置
// 同步移除内存映射
for (String k : toRemove) {
if (k.contains(":")) {
// 更新内存映射
for (String key : toRemove) {
if (key.contains(":")) {
try {
ResourceLocation rl = new ResourceLocation(k);
// 仅当值匹配才移除双重保险
String cur = CUSTOM_NAMES.get(rl);
if (target.equals(cur)) {
CUSTOM_NAMES.remove(rl);
ResourceLocation rl = new ResourceLocation(key); // 解析完整 ID
if (delValue.equals(CUSTOM_NAMES.get(rl))) {
CUSTOM_NAMES.remove(rl); // 移除匹配的 ResourceLocation 映射
}
} catch (Exception ignored) {}
} else {
// 别名按小写存放
String lower = k.toLowerCase();
String cur = CUSTOM_ALIASES.get(lower);
if (target.equals(cur)) {
CUSTOM_ALIASES.remove(lower);
String lower = key.toLowerCase();
if (delValue.equals(CUSTOM_ALIASES.get(lower))) {
CUSTOM_ALIASES.remove(lower); // 移除匹配的别名映射
}
}
}
return toRemove.size();
} catch (JsonSyntaxException e) {
sendPlayerMessage(Component.literal("ExtendedAE_Plus: 配置文件解析失败, " + e.getMessage()));
} catch (IOException e) {
} catch (IOException | JsonSyntaxException e) {
sendPlayerMessage(Component.literal("ExtendedAE_Plus: 配置文件删除失败: " + e.getMessage()));
return 0;
}
return 0;
}
/**
* 供搜索使用的关键字映射
* - 有中文映射则返回中文
* - 否则返回配方类型的 path不含命名空间例如 assembler
* 映射配方类型到搜索关键字优先使用别名或自定义名称
*
* @param recipe 配方对象
* @return 搜索关键字自定义名称别名或类型路径 null 如果无效
*/
public static String mapRecipeTypeToSearchKey(Recipe<?> recipe) {
if (recipe == null) return null;
RecipeType<?> type = recipe.getType();
ResourceLocation key = BuiltInRegistries.RECIPE_TYPE.getKey(type);
if (key == null) return null;
// 先查别名 path 匹配
String alias = CUSTOM_ALIASES.get(key.getPath().toLowerCase());
if (alias != null && !alias.isBlank()) return alias;
// 再查完整ID映射
String custom = CUSTOM_NAMES.get(key);
if (custom != null && !custom.isBlank()) {
return custom;
}
return key.getPath();
String path = key.getPath().toLowerCase();
// 优先查别名再查完整 ID最后用路径
return CUSTOM_ALIASES.getOrDefault(path, CUSTOM_NAMES.getOrDefault(key, path));
}
/**
* 仅使用反射的 GTCEu GTRecipe -> 搜索关键字避免在运行时直接引用 GTCEu
* 通过反射映射 GTCEu 配方到搜索关键字
*
* @param gtRecipeObj GTCEu 配方对象
* @return 搜索关键字 null 如果映射失败
*/
public static String mapGTCEuRecipeToSearchKey(Object gtRecipeObj) {
if (gtRecipeObj == null) return null;
try {
// 通过反射调用 getType() toString() 应返回 registryName namespace:path
java.lang.reflect.Method mGetType = gtRecipeObj.getClass().getMethod("getType");
// 获取配方类型
Method mGetType = gtRecipeObj.getClass().getMethod("getType");
Object typeObj = mGetType.invoke(gtRecipeObj);
String idStr = String.valueOf(typeObj);
if (idStr == null || idStr.isBlank()) return null;
// 解析类型 ID
ResourceLocation rl = new ResourceLocation(idStr);
// 1) 别名优先使用 path 作为最终搜索关键字
String path = rl.getPath();
@ -266,17 +275,18 @@ public final class RecipeTypeNameConfig {
}
// 2) 再查完整ID映射
String custom = CUSTOM_NAMES.get(rl);
if (custom != null && !custom.isBlank()) return custom;
// 3) 默认返回 path 作为搜索关键字
return (path != null && !path.isBlank()) ? path : idStr;
// 3) 默认返回自定义名称或路径
return custom != null && !custom.isBlank() ? custom : path;
} catch (Throwable t) {
return null;
}
}
/**
* JEI 传入的 recipeBase 不是原版 Recipe<?> 根据类的包名/类名推导一个尽量可用的搜索关键字
* 例如"moe.gregtech.recipe.SomeAssemblerRecipe" -> "gtceu assembler"
* 从未知配方类推导搜索关键字
*
* @param recipeBase 配方对象
* @return 推导的搜索关键字 null 如果失败
*/
public static String deriveSearchKeyFromUnknownRecipe(Object recipeBase) {
if (recipeBase == null) return null;
@ -285,39 +295,45 @@ public final class RecipeTypeNameConfig {
String simple = cls.getSimpleName();
String pkg = cls.getName();
String ns = null;
String namespace = null;
String lower = pkg.toLowerCase();
if (lower.contains("gtceu")) ns = "gtceu";
else if (lower.contains("gregtech")) ns = "gregtech";
else if (lower.contains("projecte")) ns = "projecte";
else if (lower.contains("create")) ns = "create";
else if (lower.contains("immersiveengineering")) ns = "immersive";
// 检测模组命名空间
if (lower.contains("gtceu")) namespace = "gtceu";
else if (lower.contains("gregtech")) namespace = "gregtech";
else if (lower.contains("projecte")) namespace = "projecte";
else if (lower.contains("create")) namespace = "create";
else if (lower.contains("immersiveengineering")) namespace = "immersive";
String token = toSearchToken(simple);
String key;
if (ns != null && token != null && !token.isBlank()) key = ns + " " + token;
else key = token != null && !token.isBlank() ? token : ns;
String token = toSearchToken(simple); // 转换类名为关键字
String key = (namespace != null && token != null && !token.isBlank()) ?
namespace + " " + token :
token;
if (key == null || key.isBlank()) return null;
// 尝试别名映射大小写不敏感
String alias = CUSTOM_ALIASES.get(key.toLowerCase());
return (alias != null && !alias.isBlank()) ? alias : key;
// 返回别名或推导的键
return alias != null && !alias.isBlank() ? alias : key;
} catch (Throwable ignored) {
return null;
}
}
/**
* 将类名转换为搜索关键字
*
* @param simpleName 类简单名称
* @return 转换后的关键字 null 如果无效
*/
private static String toSearchToken(String simpleName) {
if (simpleName == null || simpleName.isBlank()) return null;
// 去掉常见后缀
String s = simpleName
.replaceAll("Recipe$", "")
.replaceAll("Recipes$", "")
.replaceAll("Recipe(s)?$", "")
.replaceAll("Category$", "")
.replaceAll("JEI$", "");
// 驼峰转空格并小写
s = s.replaceAll("(?<!^)([A-Z])", " $1").toLowerCase();
// 取首个关键词
s = s.trim();
return s;
.replaceAll("JEI$", "")
.replaceAll("(?<!^)([A-Z])", " $1") // 驼峰转空格
.toLowerCase()
.trim();
return s.isBlank() ? null : s;
}
}