## 问题
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 处)
- 加载器路线端到端跑通(见后续提交)
200 lines
8.0 KiB
Python
200 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
||
"""生成 names.json —— 反射用的「语义名 → 物理名」映射表(extern 参数)。
|
||
|
||
## 背景(为什么需要它)
|
||
|
||
Java 后端起 MC 渲染栈时要访问若干**私有字段**(`Minecraft.resourceManager` 等),
|
||
只能走反射。而反射参数是**字符串**:
|
||
|
||
Minecraft.class.getDeclaredField("resourceManager")
|
||
|
||
remap 工具只改常量池里的**符号引用**,**改不了字符串** ⇒ 把产物从 named
|
||
remap 到 intermediary 时,`Xxx.class` 都对了,`"resourceManager"` 原样留着
|
||
⇒ 运行期 `NoSuchFieldException`(实测撞到过)。
|
||
|
||
## 做法
|
||
|
||
源码里写**语义路径**(全路径,跨命名空间稳定):
|
||
|
||
McfoldNames.field(Minecraft.class, "net/minecraft/client/Minecraft/resourceManager")
|
||
|
||
本脚本扫出这些路径,经两份映射复合解析成当前命名空间的物理名:
|
||
|
||
mappings.srg official → named (ProGuard,我们做 client-named.jar 用的)
|
||
intermediary mappings.tiny official → intermediary(Fabric)
|
||
|
||
两者键在**同一套 official 名**上,所以能连起来:
|
||
named ──(srg 反查)──> official ──(tiny)──> intermediary
|
||
|
||
## 输出
|
||
|
||
`names.json`:`{"net/minecraft/client/Minecraft/resourceManager": "field_1745", ...}`
|
||
|
||
★ 调用方(Python)只需要传**路径**(`-Dmcfold.names=<path>`),
|
||
不需要知道任何 MC 名字 —— 命名空间知识留在构建产物里。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
|
||
def load_srg(path: Path):
|
||
"""official → named 的 CL / MD / FD 三张表。
|
||
|
||
mem 的键统一成 (kind, namedPath, desc):
|
||
METHOD (namedPath, officialDesc)
|
||
FIELD (namedPath, None) ← 字段没有 desc(类型)可用,靠 owner+name 定位
|
||
"""
|
||
cls, mem = {}, {}
|
||
for line in path.read_text(errors="replace").splitlines():
|
||
p = line.split()
|
||
if not p:
|
||
continue
|
||
if p[0] == "CL:" and len(p) == 3:
|
||
cls[p[1]] = p[2]
|
||
elif p[0] == "MD:" and len(p) == 5:
|
||
# MD: <obfOwner>/<name> <offDesc> <namedOwner>/<name> <namedDesc> (两个 desc 都是 official 形式)
|
||
mem[("METHOD", p[3], p[2])] = p[1]
|
||
elif p[0] == "FD:" and len(p) == 3:
|
||
# FD: <obfOwner>/<name> <namedPath>
|
||
mem[("FIELD", p[2], None)] = p[1]
|
||
return cls, mem
|
||
|
||
|
||
def load_tiny(path: Path):
|
||
"""official → intermediary。"""
|
||
cls, mem = {}, {}
|
||
with zipfile.ZipFile(path) as z:
|
||
text = z.read("mappings/mappings.tiny").decode("utf-8", "replace")
|
||
for line in text.splitlines():
|
||
p = line.split("\t")
|
||
if not p:
|
||
continue
|
||
if p[0] == "CLASS" and len(p) >= 3:
|
||
cls[p[1]] = p[2]
|
||
elif p[0] in ("METHOD", "FIELD") and len(p) >= 5:
|
||
# METHOD/FIELD <offOwner> <desc> <name> <intName>
|
||
mem[(p[0], p[1], p[3], p[2])] = p[4]
|
||
return cls, mem
|
||
|
||
|
||
# 语义路径的字面量:`net/...` 或 `com/...`,末段是成员名。
|
||
# 不依赖调用形态(`field(x, "...")` / `method(y.getClass(),\n "...")` 都可能跨行),
|
||
# 只要它出现在 McfoldNames 调用的附近。
|
||
CALL_RE = re.compile(
|
||
r'McfoldNames\.(\w+)\s*\([^;]*?' # 捕获**调用形式**(field/method/get/set/of)
|
||
r'"((?:net|com)/[A-Za-z0-9_$/]+)"', # 到第一个 MC 路径字面量
|
||
re.S,
|
||
)
|
||
|
||
|
||
def scan_semantics(java_files: list[Path]) -> list[tuple[str, str]]:
|
||
"""返回 [(调用形式, 语义路径)]。
|
||
|
||
★ 为什么要带上调用形式:MC 里存在**同名的字段与方法**,例如
|
||
ClientPacketListener.registryAccess
|
||
FD: fex/B registryAccess (字段)
|
||
MD: fex/r registryAccess()Lhs; (方法)
|
||
只看语义路径无法区分该取哪个。
|
||
⇒ 用 McfoldNames.field / method 的**调用形式**做 kind 判定,
|
||
与运行期实际用法一致。
|
||
"""
|
||
seen: list[tuple[str, str]] = []
|
||
for f in java_files:
|
||
for m in CALL_RE.finditer(f.read_text(errors="replace")):
|
||
form, s = m.group(1), m.group(2)
|
||
kind = "FIELD" if form in ("field", "get", "set") else "METHOD"
|
||
if (kind, s) not in seen:
|
||
seen.append((kind, s))
|
||
return seen
|
||
|
||
|
||
def split_semantic(s: str):
|
||
"""'net/minecraft/client/Minecraft/resourceManager' → ('net/minecraft/client/Minecraft', 'resourceManager')"""
|
||
i = s.rfind("/")
|
||
return s[:i], s[i + 1:]
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--srg", required=True, help="official→named 的 mappings.srg")
|
||
ap.add_argument("--intermediary", required=True, help="含 mappings/mappings.tiny 的 jar")
|
||
ap.add_argument("--java", nargs="+", required=True, help="要扫的 .java 文件/目录")
|
||
ap.add_argument("--out", required=True, help="输出 names.json 路径")
|
||
ap.add_argument("--namespace", default="intermediary",
|
||
choices=["intermediary", "named"],
|
||
help="目标命名空间;named = 直接取末段(不需要表)")
|
||
args = ap.parse_args()
|
||
|
||
files: list[Path] = []
|
||
for p in args.java:
|
||
pp = Path(p)
|
||
files.extend(sorted(pp.rglob("*.java")) if pp.is_dir() else [pp])
|
||
sem = scan_semantics(files)
|
||
print(f" 扫到 {len(sem)} 个语义引用(来自 {len(files)} 个 .java)")
|
||
|
||
if args.namespace == "named":
|
||
table = {s: split_semantic(s)[1] for _, s in sem}
|
||
else:
|
||
srg_cls, srg_mem = load_srg(Path(args.srg))
|
||
int_cls, int_mem = load_tiny(Path(args.intermediary))
|
||
# ★ 键必须**带 desc**:同一个 obf 名(如 fjq/b)可能有十几个重载,
|
||
# 只按 (kind, owner, name) 做键会取到**第一个碰到的重载** ——
|
||
# 实测症状:reloadShaders(Lala;)V 被解成 method_3207()V(无参重载),
|
||
# 运行期 NoSuchMethodException(参数不匹配)。
|
||
int_by_key = {}
|
||
for (kind, off, name, desc), intname in int_mem.items():
|
||
int_by_key[(kind, off, name, desc)] = intname
|
||
# ★ 字段单独建一张 (owner,name) → intName 的表:
|
||
# tiny 的 FIELD 行带字段类型描述符,而 srg 的 FD 行没有 ⇒
|
||
# 拿 None 去查带 desc 的键永远不命中(实测:7 个字段全漏)。
|
||
int_fld_by_name = {}
|
||
for (kind, off, name, desc), intname in int_mem.items():
|
||
if kind == "FIELD":
|
||
int_fld_by_name[(off, name)] = intname
|
||
|
||
table, unresolved = {}, []
|
||
for want_kind, s in sem:
|
||
owner, member = split_semantic(s)
|
||
named_path = f"{owner}/{member}"
|
||
# ① named 全路径 → official(按调用形式限定 kind)
|
||
off = off_desc = None
|
||
for kk, v in srg_mem.items():
|
||
if kk[0] == want_kind and kk[1] == named_path:
|
||
off, off_desc = v, kk[2]
|
||
break
|
||
if off is None:
|
||
unresolved.append((s, f"named 侧未找到({want_kind})"))
|
||
continue
|
||
off_owner, off_name = off.rsplit("/", 1)
|
||
# ② official → intermediary
|
||
if want_kind == "FIELD":
|
||
hit = int_fld_by_name.get((off_owner, off_name))
|
||
else:
|
||
hit = int_by_key.get((want_kind, off_owner, off_name, off_desc))
|
||
if hit is None:
|
||
unresolved.append((s, f"official {off} ({off_desc}) 未映射到 intermediary"))
|
||
continue
|
||
table[s] = hit
|
||
|
||
if unresolved:
|
||
print(f" ⚠ {len(unresolved)} 项未解析:")
|
||
for s, why in unresolved[:12]:
|
||
print(f" {s} ← {why}")
|
||
print(f" 解析成功 {len(table)}/{len(sem)}")
|
||
|
||
out = Path(args.out)
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
out.write_text(json.dumps(table, indent=2, ensure_ascii=False) + "\n")
|
||
print(f" ✅ 写入 {out}({len(table)} 项, namespace={args.namespace})")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|