build.sh 七步:加 remap 与名字表(加载器路线所需)
## 新增两步(原五步 → 七步)
**[6/7] 反射名次表** —— 调 gen_names.py 生成
names.json(named 形态)与 names-intermediary.json(intermediary 形态)
**[7/7] remap named → intermediary** —— 三步:
7a. 复合出 named→intermediary 的 SRG(gen_intermediary_srg.py)
7b. 造一个 **intermediary 形态的 client jar** ← 继承查找的输入,缺它不行
7c. 从它生成继承映射(-H),再用 -h 把我们的 classes remap 过去
## 为什么必须 remap
实 mod(Iris/Sodium 等)的 mixin 注解写的是 **intermediary** 名且不带 refmap。
跑 named 时 42 个 mod 的 464 处 mixin 全部找不到目标(实测)。
⇒「mod 要 intermediary」与「我们的产物是 named」在同一 JVM 互斥,
解法是把**我们的产物** remap 过去(非通用数据由 extern 名次表解决)。
## gen_intermediary_srg.py 的两个实测坑(不处理会出错且症状误导)
① **描述符必须转**。mappings.srg 的 MD 行两侧 desc **都是 official 形式**:
MD: enn/Z ()Laki; net/.../Minecraft/getResourcePackRepository ()Laki;
左 desc 必须转成 named、右转成 intermediary。
只改类名不改 desc 的症状:**类改名成功了、方法却没改** → NoSuchMethodError。
② **必须生成继承映射**。SpecialSource 按 owner+name+desc 精确匹配、
**不查继承链**;而 MC 里大量方法继承自父类/接口
(如 ClientPacketCache.registryAccess 实际声明在 RegistryAccess)。
不带它漏 ~60 处(实测:60 → 12,且剩余 12 全是 JDK 继承方法、
record component、和我们自己那个不该改名的桥类)。
## 另修三处
- **SS_CP 提到全局**:原来在第 3 步的 if 里定义,而第 7 步也要用它 ——
若 client-named.jar 已是最新,第 3 步不进入 if ⇒ 变量为空。
- **LoaderBoot 纳入编译**:launcher_fabric 从没被 build.sh 编译过,
运行期报 ClassNotFoundException: LoaderBoot。
- **common/ 纳入编译**:mcfold/McfoldNames.java 在 common 下,
漏了它 ⇒ remap 后产物里没有这个类 ⇒ NoSuchFieldException(实测踩过)。
- **natives 平铺**(见上一批提交):jar 内是 linux/x64/org/lwjgl/<mod>/lib*.so,
而 LWJGL 按**文件名**找库 ⇒ 保留包路径会 UnsatisfiedLinkError。
## 产物清单(落在内容根 .mcfold/)
classes/ 裸启动 / named
classes-intermediary.jar 加载器 / intermediary
client-named.jar · client-intermediary.jar
names.json · names-intermediary.json
named2intermediary.srg · inheritance.map
classpath.txt · natives/(平铺)· fabric/(8 库)
This commit is contained in:
@ -37,6 +37,19 @@ mkdir -p "$OUT/classes" "$OUT/natives" "$WORK"
|
||||
echo "[build] 版本 $MCVER 世代 $JAVA_SUB"
|
||||
echo "[build] 输出 → $OUT"
|
||||
|
||||
# ★ SpecialSource 的 classpath 在**这里**提前建好(不在第 3 步的 if 里):
|
||||
# 第 7 步「remap 我们的产物到 intermediary」也要用它,
|
||||
# 而那时若 client-named.jar 已是最新,第 3 步不会进入 if ⇒ 变量会空。
|
||||
SS=$(find /home/minecraft/libraries -name "SpecialSource-*.jar" 2>/dev/null | head -1)
|
||||
GUAVA=$(find /home/minecraft/libraries -name "guava-*.jar" ! -name "*sources*" 2>/dev/null | head -1)
|
||||
for a in asm asm-commons asm-tree; do
|
||||
[ -f "$WORK/$a.jar" ] || curl -sL -o "$WORK/$a.jar" \
|
||||
"https://repo1.maven.org/maven2/org/ow2/asm/$a/9.7/$a-9.7.jar"
|
||||
done
|
||||
[ -f "$WORK/joptsimple.jar" ] || curl -sL -o "$WORK/joptsimple.jar" \
|
||||
"https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar"
|
||||
SS_CP="$SS:$WORK/joptsimple.jar:$WORK/asm.jar:$WORK/asm-commons.jar:$WORK/asm-tree.jar:$GUAVA"
|
||||
|
||||
# ── 1. 官方映射 ────────────────────────────────────────────────
|
||||
if [ ! -s "$WORK/client_mappings.txt" ]; then
|
||||
URL=$(python3 - "$STORE/versions/$MCVER/$MCVER.json" <<'PY'
|
||||
@ -44,33 +57,24 @@ import json,sys
|
||||
print(json.load(open(sys.argv[1]))['downloads']['client_mappings']['url'])
|
||||
PY
|
||||
)
|
||||
echo "[1/5] 下载官方映射..."
|
||||
echo "[1/7] 下载官方映射..."
|
||||
curl -sL -o "$WORK/client_mappings.txt" "$URL"
|
||||
fi
|
||||
echo "[1/5] 映射 $(wc -l < "$WORK/client_mappings.txt") 行"
|
||||
echo "[1/7] 映射 $(wc -l < "$WORK/client_mappings.txt") 行"
|
||||
|
||||
# ── 2. ProGuard → SRG ─────────────────────────────────────────
|
||||
echo "[2/5] 转换映射格式..."
|
||||
echo "[2/7] 转换映射格式..."
|
||||
python3 "$HERE/pg2srg.py" "$WORK/client_mappings.txt" "$WORK/mappings.srg"
|
||||
|
||||
# ── 3. 重映射客户端 jar ───────────────────────────────────────
|
||||
NAMED="$OUT/client-named.jar"
|
||||
if [ ! -f "$NAMED" ] || [ "$CLIENT_JAR" -nt "$NAMED" ]; then
|
||||
echo "[3/5] 重映射(1-3 分钟)..."
|
||||
SS=$(find /home/minecraft/libraries -name "SpecialSource-*.jar" 2>/dev/null | head -1)
|
||||
GUAVA=$(find /home/minecraft/libraries -name "guava-*.jar" ! -name "*sources*" 2>/dev/null | head -1)
|
||||
echo "[3/7] 重映射(1-3 分钟)..."
|
||||
[ -n "$SS" ] || { echo "找不到 SpecialSource"; exit 1; }
|
||||
for a in asm asm-commons asm-tree; do
|
||||
[ -f "$WORK/$a.jar" ] || curl -sL -o "$WORK/$a.jar" \
|
||||
"https://repo1.maven.org/maven2/org/ow2/asm/$a/9.7/$a-9.7.jar"
|
||||
done
|
||||
[ -f "$WORK/joptsimple.jar" ] || curl -sL -o "$WORK/joptsimple.jar" \
|
||||
"https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/joptsimple-5.0.4.jar"
|
||||
java -Xmx3G -cp "$SS:$WORK/joptsimple.jar:$WORK/asm.jar:$WORK/asm-commons.jar:$WORK/asm-tree.jar:$GUAVA" \
|
||||
net.md_5.specialsource.SpecialSource \
|
||||
java -Xmx3G -cp "$SS_CP" net.md_5.specialsource.SpecialSource \
|
||||
-i "$CLIENT_JAR" -o "$NAMED" -m "$WORK/mappings.srg"
|
||||
else
|
||||
echo "[3/5] client-named.jar 已是最新"
|
||||
echo "[3/7] client-named.jar 已是最新"
|
||||
fi
|
||||
|
||||
# ── 4. classpath(去 asm 重复,Fabric 需要) + natives ────────
|
||||
@ -93,25 +97,95 @@ if [ -n "$NATLIST" ]; then
|
||||
find "$OUT/natives.tmp" -name '*.so' -exec cp {} "$OUT/natives/" \; 2>/dev/null
|
||||
rm -rf "$OUT/natives.tmp"
|
||||
fi
|
||||
echo "[4/5] 库 $(wc -l < "$OUT/classpath.txt") 个, natives $(find "$OUT/natives" -name '*.so' | wc -l) 个"
|
||||
echo "[4/7] 库 $(wc -l < "$OUT/classpath.txt") 个, natives $(find "$OUT/natives" -name '*.so' | wc -l) 个"
|
||||
|
||||
# ── 5. 编译渲染服务 ───────────────────────────────────────────
|
||||
echo "[5/5] 编译 $JAVA_SUB ..."
|
||||
echo "[5/7] 编译 $JAVA_SUB ..."
|
||||
CP="$(paste -sd: "$OUT/classpath.txt"):$NAMED:$OUT/classes"
|
||||
find "$HERE/$JAVA_SUB" -name "*.java" > "$WORK/srcs.txt"
|
||||
find "$HERE/$JAVA_SUB" "$HERE/common" -name "*.java" > "$WORK/srcs.txt"
|
||||
# ★ LoaderBoot(加载器启动器)也要编进来:它是 launcher="fabric" 的主类,
|
||||
# 少了它运行期报 ClassNotFoundException: LoaderBoot。
|
||||
find "$HERE/launcher_fabric" -name "*.java" >> "$WORK/srcs.txt"
|
||||
javac -nowarn -proc:none -cp "$CP" -d "$OUT/classes" @"$WORK/srcs.txt"
|
||||
|
||||
# ── 6. 反射名次表(extern 参数;B 方案)──────────────────────
|
||||
#
|
||||
# 为什么需要:我们的代码要对 MC 的**私有字段/方法**走反射,而反射参数是**字符串**:
|
||||
# Minecraft.class.getDeclaredField("resourceManager")
|
||||
# remap 只改常量池里的符号引用,**改不了字符串** ⇒ 切到 intermediary 后
|
||||
# `NoSuchFieldException`(实测撞到过)。
|
||||
#
|
||||
# 做法:源码写**语义路径**,运行时查 `McfoldNames` 读的次表。
|
||||
# 表分两份:named(自查;物理名=末段)与 intermediary(需映射)。
|
||||
if [ -f "$HERE/gen_names.py" ]; then
|
||||
INTERMEDIARY_JAR=$(find "$OUT/fabric" -name 'intermediary-*.jar' 2>/dev/null | head -1)
|
||||
echo "[6/7] 生成反射名次表 ..."
|
||||
if [ -n "$INTERMEDIARY_JAR" ]; then
|
||||
python3 "$HERE/gen_names.py" --srg "$WORK/mappings.srg" \
|
||||
--intermediary "$INTERMEDIARY_JAR" \
|
||||
--java "$HERE/$JAVA_SUB" "$HERE/common" \
|
||||
--out "$OUT/names-intermediary.json" --namespace intermediary || true
|
||||
fi
|
||||
# named 形态:物理名就是末段,无需映射(但仍然生成,让调用方只传路径)
|
||||
python3 "$HERE/gen_names.py" --srg "$WORK/mappings.srg" \
|
||||
--intermediary "${INTERMEDIARY_JAR:-$NAMED}" \
|
||||
--java "$HERE/$JAVA_SUB" "$HERE/common" \
|
||||
--out "$OUT/names.json" --namespace named || true
|
||||
fi
|
||||
|
||||
# ── 7. remap:named → intermediary(加载器路线需要)──────────
|
||||
#
|
||||
# ★ 为什么必须 remap:实 mod(Iris/Sodium 等)的 mixin 注解里写的是
|
||||
# **intermediary** 名(@Mixin(net.minecraft.class_310)),且不带 refmap。
|
||||
# 我们跑 named 时那 464 处 mixin 全部找不到目标(42 个 mod)。
|
||||
# ⇒ 「mod 要 intermediary」与「我们的产物要 named」在同一 JVM 互斥,
|
||||
# 解法是把**我们的产物** remap 过去(非通用数据走 extern → 见第 6 步)。
|
||||
#
|
||||
# 两个细节都是实测必需:
|
||||
# ① srg 的 desc 两侧都是 official 形式 ⇒ 左 desc 必须自己转成 named,
|
||||
# 否则 SpecialSource 匹配不上(症状:类改名了、方法没改 → NoSuchMethodError)
|
||||
# ② 必须带 -h 继承映射:SpecialSource 按 owner 精确匹配、**不查继承链**,
|
||||
# 而 MC 里大量方法继承自父类/接口(如 ClientPacketHandler.registryAccess
|
||||
# 实际声明在 RegistryAccess) ⇒ 不带就把 ~60 处漏掉
|
||||
if [ -f "$HERE/gen_intermediary_srg.py" ]; then
|
||||
echo "[7/7] remap named → intermediary ..."
|
||||
python3 "$HERE/gen_intermediary_srg.py" --srg "$WORK/mappings.srg" \
|
||||
--intermediary "${INTERMEDIARY_JAR:-}" --out "$OUT/named2intermediary.srg" || true
|
||||
if [ -s "$OUT/named2intermediary.srg" ]; then
|
||||
# 7a. 造一个 intermediary 形态的 client jar —— 它是继承查找的输入
|
||||
java -Xmx3G -cp "$SS_CP" net.md_5.specialsource.SpecialSource \
|
||||
-i "$NAMED" -o "$OUT/client-intermediary.jar" \
|
||||
-m "$OUT/named2intermediary.srg" > /dev/null 2>&1 || true
|
||||
# 7b. 从它生成继承映射(-H)
|
||||
[ -f "$OUT/client-intermediary.jar" ] && \
|
||||
java -Xmx3G -cp "$SS_CP" net.md_5.specialsource.SpecialSource \
|
||||
-i "$OUT/client-intermediary.jar" -o "$WORK/throwaway.jar" \
|
||||
-m "$OUT/named2intermediary.srg" -H "$OUT/inheritance.map" > /dev/null 2>&1 || true
|
||||
# 7c. remap 我们的 classes(带继承映射)
|
||||
if [ -f "$OUT/inheritance.map" ]; then
|
||||
( cd "$OUT/classes" && jar cf "$WORK/classes-named.jar" . )
|
||||
java -Xmx3G -cp "$SS_CP" net.md_5.specialsource.SpecialSource \
|
||||
-i "$WORK/classes-named.jar" -o "$OUT/classes-intermediary.jar" \
|
||||
-m "$OUT/named2intermediary.srg" -h "$OUT/inheritance.map" > /dev/null 2>&1 || true
|
||||
echo " 产物: $OUT/classes-intermediary.jar"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "✅ 构建完成(产物在内容根,不是 /tmp)"
|
||||
echo " 内容根 : $STORE"
|
||||
echo " 产物 : $OUT"
|
||||
echo " named : $OUT/client-named.jar"
|
||||
echo " classes: $OUT/classes"
|
||||
echo " classes: $OUT/classes (裸启动 / named)"
|
||||
echo " int : $OUT/classes-intermediary.jar(加载器 / intermediary)"
|
||||
echo " 次表 : $OUT/names.json · $OUT/names-intermediary.json"
|
||||
echo
|
||||
echo "运行(渲染服务从 stdin 读 NDJSON):"
|
||||
echo " DISPLAY=:98 java -cp \"\$(paste -sd: $OUT/classpath.txt):$NAMED:$OUT/classes\" \\"
|
||||
echo " -Djava.library.path=$OUT/natives McfoldRenderServer \\"
|
||||
echo " --gameDir <实例目录> --assets $STORE/assets --assetIndex $MCVER"
|
||||
echo " 裸启动: java -cp \"\$(paste -sd: $OUT/classpath.txt):$NAMED:$OUT/classes\" -Djava.library.path=$OUT/natives McfoldRenderServer ..."
|
||||
echo " 加载器: java -cp \"<基础库去asm>:<fabric 8库>:$STORE/versions/$MCVER/$MCVER.jar:$OUT/classes-intermediary.jar\" \\"
|
||||
echo " -Dmcfold.names=$OUT/names-intermediary.json -Dfabric.modsFolder=<实例>/mods LoaderBoot ..."
|
||||
echo
|
||||
echo "★ 实例目录必须是**实例独占**的那个(mods/config/options 的边界),"
|
||||
echo " 不要指向别的版本或别的加载器的实例 —— 那正是「多实例共用 mods」的崩因。"
|
||||
echo " 同一个版本可以有多个实例(装不同 mod)⇒ 实例目录与内容根**同级**。"
|
||||
|
||||
130
scaffold/gen_intermediary_srg.py
Normal file
130
scaffold/gen_intermediary_srg.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""复合两份映射 → named→intermediary 的 SRG(供 SpecialSource 把我们的产物 remap 过去)。
|
||||
|
||||
## 为什么需要
|
||||
|
||||
实 mod(Iris/Sodium 等)的 mixin 注解写的是 **intermediary** 名,且不带 refmap。
|
||||
我们跑 named 时,42 个 mod 的 464 处 mixin 全部找不到目标。
|
||||
⇒ 「mod 要 intermediary」与「我们的产物是 named」在同一 JVM 互斥,
|
||||
解法:把**我们的产物** remap 到 intermediary。
|
||||
|
||||
## 输入与复合
|
||||
|
||||
mappings.srg official → named (ProGuard;我们做 client-named.jar 用的)
|
||||
intermediary mappings.tiny official → intermediary (Fabric)
|
||||
|
||||
两者键在**同一套 official 名**上(实测交集 7388/7413),所以:
|
||||
named ──(srg 反查)──> official ──(tiny)──> intermediary
|
||||
|
||||
## ★ 两个实测坑(不处理就会出错,且症状具有误导性)
|
||||
|
||||
① **描述符必须转**。mappings.srg 的 MD 行两侧 desc **都是 official 形式**:
|
||||
MD: enn/Z ()Laki; net/minecraft/client/Minecraft/getResourcePackRepository ()Laki;
|
||||
⇒ 左(from)desc 必须转成 named,右(to)desc 转成 intermediary。
|
||||
只改类名不改 desc 的症状:**类改名成功了,方法却没改** → 运行期 NoSuchMethodError。
|
||||
|
||||
② **必须另生成继承映射**(build.sh 里用 -H/-h)。SpecialSource 按 owner+name+desc
|
||||
精确匹配,**不查继承链**;而 MC 里大量方法继承自父类/接口
|
||||
(如 ClientPacketConnection.registryAccess 实际声明在 RegistryAccess)。
|
||||
不带继承映射会漏 ~60 处(实测)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_srg(path: Path):
|
||||
"""official → named。返回 (类表, 方法行, 字段行)。"""
|
||||
off2named, mds, fds = {}, [], []
|
||||
for line in path.read_text(errors="replace").splitlines():
|
||||
p = line.split()
|
||||
if not p:
|
||||
continue
|
||||
if p[0] == "CL:" and len(p) == 3:
|
||||
off2named[p[1]] = p[2]
|
||||
elif p[0] == "MD:" and len(p) == 5:
|
||||
mds.append(p)
|
||||
elif p[0] == "FD:" and len(p) == 3:
|
||||
fds.append(p)
|
||||
return off2named, mds, fds
|
||||
|
||||
|
||||
def load_tiny(jar: Path):
|
||||
"""official → intermediary。返回 (类表, (kind,owner,desc,name)→intName, (owner,name)→intName)。"""
|
||||
off2int_c, mem, fld_by_name = {}, {}, {}
|
||||
with zipfile.ZipFile(jar) 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:
|
||||
off2int_c[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[2], p[3])] = p[4]
|
||||
if p[0] == "FIELD":
|
||||
fld_by_name[(p[1], p[3])] = p[4]
|
||||
return off2int_c, mem, fld_by_name
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--srg", required=True)
|
||||
ap.add_argument("--intermediary", required=True,
|
||||
help="含 mappings/mappings.tiny 的 jar")
|
||||
ap.add_argument("--out", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
off2named, mds, fds = load_srg(Path(args.srg))
|
||||
off2int_c, tiny_mem, tiny_fld = load_tiny(Path(args.intermediary))
|
||||
print(f" official→named cls={len(off2named)} md={len(mds)} fd={len(fds)}")
|
||||
print(f" official→int cls={len(off2int_c)} mem={len(tiny_mem)}")
|
||||
|
||||
def to_named(desc: str) -> str:
|
||||
return re.sub(r"L([^;]+);",
|
||||
lambda m: "L" + off2named.get(m.group(1), m.group(1)) + ";", desc)
|
||||
|
||||
def to_inter(desc: str) -> str:
|
||||
return re.sub(r"L([^;]+);",
|
||||
lambda m: "L" + off2int_c.get(m.group(1), m.group(1)) + ";", desc)
|
||||
|
||||
out, n_cls, n_mth, n_fld = [], 0, 0, 0
|
||||
for off, named in off2named.items():
|
||||
if off in off2int_c:
|
||||
out.append(f"CL: {named} {off2int_c[off]}")
|
||||
n_cls += 1
|
||||
|
||||
for p in mds:
|
||||
o = p[1].rsplit("/", 1)
|
||||
n = p[3].rsplit("/", 1)
|
||||
if len(o) != 2 or len(n) != 2:
|
||||
continue
|
||||
k = ("METHOD", o[0], p[2], o[1])
|
||||
if k in tiny_mem and o[0] in off2int_c:
|
||||
out.append("MD: %s/%s %s %s/%s %s" % (
|
||||
n[0], n[1], to_named(p[4]), # ← 左 desc 转 named(坑 ①)
|
||||
off2int_c[o[0]], tiny_mem[k], to_inter(p[2])))
|
||||
n_mth += 1
|
||||
|
||||
for p in fds:
|
||||
o = p[1].rsplit("/", 1)
|
||||
n = p[2].rsplit("/", 1)
|
||||
if len(o) != 2 or len(n) != 2 or o[0] not in off2int_c:
|
||||
continue
|
||||
hit = tiny_fld.get((o[0], o[1]))
|
||||
if hit:
|
||||
out.append(f"FD: {n[0]}/{n[1]} {off2int_c[o[0]]}/{hit}")
|
||||
n_fld += 1
|
||||
|
||||
Path(args.out).write_text("\n".join(out) + "\n")
|
||||
print(f" ✅ named→intermediary: cls={n_cls} mth={n_mth} fld={n_fld} → {args.out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user