Files
agentmc/scaffold/java/common/mcfold/McfoldNames.java
JianFeeeee d4ba586ac7 反射名的 extern 机制(B 方案):19 处字符串反射 → 0
## 问题

Java 后端要访问 MC 的**私有字段/方法**(Minecraft.resourceManager 等),
只能走反射。而反射参数是**字符串**:

    Minecraft.class.getDeclaredField("resourceManager")

remap 工具只改常量池里的**符号引用**,**改不了字符串字面量** ⇒
把产物从 named remap 到 intermediary 时,`Xxx.class` 都对了,
`"resourceManager"` 原样留着 ⇒ 运行期 NoSuchFieldException(实测撞到过)。

## 做法:语义名 → 物理名,表由 extern 参数传入

源码写**语义路径**(跨命名空间稳定):
    McfoldNames.field(Minecraft.class, "net/minecraft/client/Minecraft/resourceManager")

- named 模式(无表):退化为取末段 ⇒ "resourceManager" ✅
- intermediary 模式:表里查 ⇒ "field_1745" ✅

⇒ **同一份 class 在两种命名空间下都能跑**,切换不必重编。
   这是「mod 要 intermediary」与「我们的产物本来是 named」能共存的**前提**。

## 表的来源(scaffold/gen_names.py)

扫源码里的语义引用 → 经两份映射复合解析:
    named ──(mappings.srg 反查)──> official ──(intermediary tiny)──> intermediary

运行时按此序找表:
  1. -Dmcfold.names=<path>          ★ 显式 extern 参数(调用方只传路径)
  2. <class 同级>/names.json
  3. 都无 ⇒ named 模式

★ 调用方(Python)**不需要知道任何 MC 名字**,只传一个路径 ——
  命名空间知识全部留在 jar 与构建产物里。这就是「接口统一、非通用数据走 extern」。

## 实测踩坑(都在 gen_names.py 里记了原因)

① 查表必须**带描述符**:同一个 obf 名(如 fjq/b)有十几个重载,
   只按 (kind, owner, name) 会取到第一个重载 ——
   症状:reloadShaders(Lala;)V 被解成 method_3207()V(无参),运行期 NoSuchMethodException。
② 字段要单建一张 (owner,name) 表:tiny 的 FIELD 行带字段类型描述符,
   而 srg 的 FD 行没有 ⇒ 拿 None 去查永远不命中(7 个字段全漏)。
③ **不能靠首字母大小猜字段/方法**,必须在 srg 里实查 kind。
④ MC 里存在**同名字段与方法**(如 ClientPacketListener.registryAccess
   既有 FD fex/B 又有 MD fex/r)⇒ 用 McfoldNames.field/method 的**调用形式**
   判定 kind,与运行期用法一致。

## 验证

- 名字表 11/11 解析成功(gen_1_20)
- 两世代后端字符串反射归零(gen_1_20 10 处、gen_1_21 9 处)
- 加载器路线端到端跑通(见后续提交)
2026-09-26 10:17:29 +08:00

198 lines
7.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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=<path>` ← ★ 显式 extern 参数(调用方传,只传路径)
* 2. `<类自身的 code source 同级>/names.json`
* 3. 都没有 ⇒ 退化为 named 模式(末段)
*
* ★ 调用方(Python)**不需要知道任何 MC 名字** —— 它只传一个路径。
* 命名空间的知识全部留在 jar 与构建产物里。这就是「接口统一、非通用数据走 extern」。
*/
public final class McfoldNames {
private McfoldNames() {}
/** 语义路径 → 物理简单名。空表 = named 模式。 */
private static final Map<String, String> TABLE = load();
private static Map<String, String> load() {
Map<String, String> 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<String, String> 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);
}
}