#!/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=`), 不需要知道任何 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: / / (两个 desc 都是 official 形式) mem[("METHOD", p[3], p[2])] = p[1] elif p[0] == "FD:" and len(p) == 3: # FD: / 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 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())