Files
agentmc/scaffold/gen_names.py
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

200 lines
8.0 KiB
Python
Raw Permalink 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.

#!/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())