package mcfold; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * 【统一接口的 extern 侧】反射用的符号名解析。 * * ## 为什么需要它 * * 我们的 Java 后端要访问 MC 的**私有字段**(Minecraft.resourceManager 等), * 只能走反射。而反射的参数是**字符串**: * * Minecraft.class.getDeclaredField("resourceManager") * * ★ 关键问题:remap 工具(SpecialSource / tiny-remapper)只改**常量池里的符号引用** * (类名、字段描述符、方法引用),**改不了字符串字面量**。 * * ⇒ 于是「把我们的产物从 named 命名空间 remap 到 intermediary」时, * 所有 `Xxx.class` 都被正确改名,而 `"resourceManager"` 原样留下 ⇒ * 运行期抛 `NoSuchFieldException: resourceManager`(实测撞到过)。 * * ## 解法:语义名 → 物理名,表由外部传入 * * 代码里写**语义路径**(跨命名空间稳定),物理名运行时查表: * * McfoldNames.field(Minecraft.class, "net/minecraft/client/Minecraft/resourceManager") * * - named 模式(表缺失):`of()` 退化为取末段 ⇒ `"resourceManager"` ✅ 照常工作 * - intermediary 模式:表里有 `{...: "field_1745"}` ⇒ `"field_1745"` ✅ * * ⇒ **同一份 class 文件在两种命名空间下都能跑**,切换命名空间不必重编。 * 这正是「加载器路线」(mod 要 intermediary)与「我们的代码」(本来对着 named 编译) * 能共存的技术前提。 * * ## 表的来源与落点(extern 参数) * * 表由 `scaffold/gen_names.py` 在构建时生成: * 读源码里出现的语义路径 → 经 mappings.srg + intermediary tiny 复合解析 → 写 JSON。 * * 运行时按**这个顺序**找: * 1. 系统属性 `-Dmcfold.names=` ← ★ 显式 extern 参数(调用方传,只传路径) * 2. `<类自身的 code source 同级>/names.json` * 3. 都没有 ⇒ 退化为 named 模式(末段) * * ★ 调用方(Python)**不需要知道任何 MC 名字** —— 它只传一个路径。 * 命名空间的知识全部留在 jar 与构建产物里。这就是「接口统一、非通用数据走 extern」。 */ public final class McfoldNames { private McfoldNames() {} /** 语义路径 → 物理简单名。空表 = named 模式。 */ private static final Map TABLE = load(); private static Map load() { Map m = new HashMap<>(); Path p = locate(); if (p == null) { System.err.println("[mcfold] names.json 未找到 ⇒ named 模式(符号名按末段取)"); return m; } try { String text = new String(Files.readAllBytes(p), StandardCharsets.UTF_8); parseFlatJson(text, m); System.err.println("[mcfold] names.json 已加载: " + p + " (" + m.size() + " 项)"); } catch (IOException e) { System.err.println("[mcfold] names.json 读取失败(" + p + "): " + e + " ⇒ named 模式"); } return m; } private static Path locate() { // ① 显式 extern 参数 String prop = System.getProperty("mcfold.names"); if (prop != null && !prop.isEmpty()) { Path p = Paths.get(prop); if (Files.isReadable(p)) return p; System.err.println("[mcfold] -Dmcfold.names 指向的文件不可读: " + prop); } // ② 与自身 class 同级的 names.json try { var src = McfoldNames.class.getProtectionDomain().getCodeSource(); if (src != null && src.getLocation() != null) { Path base = Paths.get(src.getLocation().toURI()); Path cand = Files.isDirectory(base) ? base.resolve("names.json") : base.getParent().resolve("names.json"); if (Files.isReadable(cand)) return cand; } } catch (Throwable ignored) { } return null; } /** 极简平铺 JSON 解析({"k":"v",...})—— 避免引入 JSON 依赖。 */ private static void parseFlatJson(String text, Map out) { int i = 0, n = text.length(); while (i < n) { int k1 = text.indexOf('"', i); if (k1 < 0) break; int k2 = text.indexOf('"', k1 + 1); if (k2 < 0) break; String key = unescape(text.substring(k1 + 1, k2)); int c = text.indexOf(':', k2); if (c < 0) break; int v1 = text.indexOf('"', c); if (v1 < 0) break; int v2 = text.indexOf('"', v1 + 1); if (v2 < 0) break; String val = unescape(text.substring(v1 + 1, v2)); out.put(key, val); i = v2 + 1; } } private static String unescape(String s) { if (s.indexOf('\\') < 0) return s; return s.replace("\\\"", "\"").replace("\\\\", "\\") .replace("\\n", "\n").replace("\\/", "/"); } /** * 语义路径 → 当前命名空间下的物理简单名。 * * @param semantic 全路径,如 `net/minecraft/client/Minecraft/resourceManager` */ public static String of(String semantic) { String hit = TABLE.get(semantic); if (hit != null) return hit; // named 模式(或表里没这一项):末段就是物理名 int i = semantic.lastIndexOf('/'); return i < 0 ? semantic : semantic.substring(i + 1); } /** 是否处于映射模式(诊断用)。 */ public static boolean mapped() { return !TABLE.isEmpty(); } /** 读字段:owner 已被 remap 处理,语义路径给出字段归属。 */ public static Field field(Class owner, String semantic) throws NoSuchFieldException { String name = of(semantic); for (Class c = owner; c != null; c = c.getSuperclass()) { try { Field f = c.getDeclaredField(name); f.setAccessible(true); return f; } catch (NoSuchFieldException ignored) { } } throw new NoSuchFieldException(semantic + " → " + name + " (owner=" + owner.getName() + ", mapped=" + mapped() + ")"); } /** 取方法:先本类声明,再沿父类/接口找(等价 getMethod 但用解析后的名字)。 */ public static Method method(Class owner, String semantic, Class... params) throws NoSuchMethodException { String name = of(semantic); for (Class c = owner; c != null; c = c.getSuperclass()) { try { Method m = c.getDeclaredMethod(name, params); m.setAccessible(true); return m; } catch (NoSuchMethodException ignored) { } } try { Method m = owner.getMethod(name, params); m.setAccessible(true); return m; } catch (NoSuchMethodException e) { throw new NoSuchMethodException(semantic + " → " + name + "(owner=" + owner.getName() + ", mapped=" + mapped() + ")"); } } /** 字段当前值(省一次 setAccessible 样板)。 */ public static Object get(Class owner, String semantic, Object instance) throws Exception { return field(owner, semantic).get(instance); } /** 设置字段值。 */ public static void set(Class owner, String semantic, Object instance, Object value) throws Exception { field(owner, semantic).set(instance, value); } }