反射名的 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 处)
- 加载器路线端到端跑通(见后续提交)
This commit is contained in:
197
scaffold/java/common/mcfold/McfoldNames.java
Normal file
197
scaffold/java/common/mcfold/McfoldNames.java
Normal file
@ -0,0 +1,197 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user