M1(构建): - build.sh 编译 CP 加入 $OUT/fabric/*.jar,解决 net.fabricmc.* 找不到 - gen_1_21 内联 Sodium 通知改为委托共用 RenderCompat,补 reportRenderCompat - 1.21.1 全量七步构建产物齐备(classes-intermediary.jar / names*.json / srg / inheritance.map) M2(协议): - login_start 按协议数据发 UUID(1.21 必填;1.20 可选) - login_acknowledged / serverbound 包 id 改为查表,不再写死 0x00 - send() 实现压缩帧格式(varint(dataLen)+数据,小包不压) - configuration 双向协商:发 settings、回 select_known_packs、拒 resource_pack - protodef 补 bitflags / entityMetadataLoop / count(ctx引用) / compareTo 路径表达式 - NBT 读取器由占位符改为真实现(anonymousNbt 支撑 registry_data) - 坏包容错:长度前缀是帧层权威,解不开整包丢弃并布告,不断流 实测:login→configuration(11 注册表)→play,位置 (5.5,-60.0,-0.5),map_chunk×9 判据:protodef 6→13 项(含线上真实 entity_metadata 字节回归);全量 77 项通过
201 lines
11 KiB
Bash
Executable File
201 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# mcfold 构建脚本 —— 把官方客户端 jar 变成「可直接调用的渲染库」。
|
||
#
|
||
# ★ 版本隔离策略(用户 2026-09-25 强调)
|
||
# 产物写入**内容根** store/<loader>-<version>/.mcfold/,不写 /tmp:
|
||
# 内容根/.mcfold/client-named.jar 反混淆后的客户端(可只读共享)
|
||
# 内容根/.mcfold/classes/ 我们的渲染服务
|
||
# 内容根/.mcfold/natives/ LWJGL .so
|
||
# 理由:这些都由该版本的 client jar 派生,属「可共享内容」;
|
||
# /tmp 是 tmpfs,重启即丢(实测踩过)。实例独占的东西(mods/config/options)
|
||
# 一律不进这里。
|
||
#
|
||
# 用法: ./build.sh <内容根|实例名> <MC版本> [java模式目录]
|
||
# ./build.sh /home/newqqagent/mcview/store/vanilla-1.20.1 1.20.1
|
||
# ./build.sh /home/newqqagent/mcview/store/vanilla-1.21.1 1.21.1 java/gen_1_21
|
||
set -euo pipefail
|
||
|
||
STORE="${1:?用法: build.sh <内容根> <MC版本> [java目录]}"
|
||
MCVER="${2:?需要 MC 版本}"
|
||
JAVA_SUB="${3:-}"
|
||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||
|
||
# 按版本挑世代目录(除非显式指定)
|
||
if [ -z "$JAVA_SUB" ]; then
|
||
case "$MCVER" in
|
||
1.21*) JAVA_SUB="java/gen_1_21" ;;
|
||
*) JAVA_SUB="java/gen_1_20" ;;
|
||
esac
|
||
fi
|
||
|
||
OUT="$STORE/.mcfold"
|
||
WORK="${MCFOLD_BUILD_WORK:-$(mktemp -d /tmp/mcfold-build-XXXXXX)}"
|
||
CLIENT_JAR="$STORE/versions/$MCVER/$MCVER.jar"
|
||
[ -f "$CLIENT_JAR" ] || { echo "找不到客户端 jar: $CLIENT_JAR"; exit 1; }
|
||
|
||
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'
|
||
import json,sys
|
||
print(json.load(open(sys.argv[1]))['downloads']['client_mappings']['url'])
|
||
PY
|
||
)
|
||
echo "[1/7] 下载官方映射..."
|
||
curl -sL -o "$WORK/client_mappings.txt" "$URL"
|
||
fi
|
||
echo "[1/7] 映射 $(wc -l < "$WORK/client_mappings.txt") 行"
|
||
|
||
# ── 2. ProGuard → SRG ─────────────────────────────────────────
|
||
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/7] 重映射(1-3 分钟)..."
|
||
[ -n "$SS" ] || { echo "找不到 SpecialSource"; exit 1; }
|
||
java -Xmx3G -cp "$SS_CP" net.md_5.specialsource.SpecialSource \
|
||
-i "$CLIENT_JAR" -o "$NAMED" -m "$WORK/mappings.srg"
|
||
else
|
||
echo "[3/7] client-named.jar 已是最新"
|
||
fi
|
||
|
||
# ── 4. classpath(去 asm 重复,Fabric 需要) + natives ────────
|
||
# classpath.txt **只含第三方 libraries** —— client-named.jar 与 classes/ 由使用方拼接
|
||
# (build.sh 自己在下面拼,测试/后端则从各自路径取)。
|
||
# ★ 别在这里把 named/classes 也写进去:那会让「libraries 清单」与「运行 classpath」
|
||
# 两个概念混成一列,而 Fabric 路线需要把 asm 从 libraries 里剔掉后**另行**拼 8 个库。
|
||
find "$STORE/libraries" -name "*.jar" ! -name "*-sources.jar" ! -name "*natives*" 2>/dev/null \
|
||
| grep -vE "minecraftforge|neoforge" > "$OUT/classpath.txt"
|
||
NATLIST=$(find "$STORE/libraries" -name "*natives-linux*.jar" 2>/dev/null)
|
||
if [ -n "$NATLIST" ]; then
|
||
rm -rf "$OUT/natives"; mkdir -p "$OUT/natives"
|
||
# ★ 必须**平铺**。natives jar 里的路径是 linux/x64/org/lwjgl/<module>/lib*.so,
|
||
# 而 LWJGL 是经 java.library.path/LD_LIBRARY_PATH 按**文件名**找库的 ——
|
||
# 嵌套一层的目录它**找不到**(实测 UnsatisfiedLinkError: Failed to locate library: liblwjgl.so)。
|
||
while read -r j; do
|
||
[ -f "$j" ] || continue
|
||
unzip -o -q "$j" -d "$OUT/natives.tmp" 2>/dev/null || true
|
||
done <<< "$NATLIST"
|
||
find "$OUT/natives.tmp" -name '*.so' -exec cp {} "$OUT/natives/" \; 2>/dev/null
|
||
rm -rf "$OUT/natives.tmp"
|
||
fi
|
||
echo "[4/7] 库 $(wc -l < "$OUT/classpath.txt") 个, natives $(find "$OUT/natives" -name '*.so' | wc -l) 个"
|
||
|
||
# ── 5. 编译渲染服务 ───────────────────────────────────────────
|
||
echo "[5/7] 编译 $JAVA_SUB ..."
|
||
# ★ 编译 CP 必须含 **Fabric 库**:loader_fabric/LoaderBoot 引用
|
||
# net.fabricmc.api / net.fabricmc.loader.impl.launch.knot,
|
||
# 缺了就是 "package net.fabricmc.api does not exist"(1.21.1 实测报过)。
|
||
# 1.20.1 那次能过是因为**手工拼的运行 CP** 里恰好带了它,
|
||
# 而 build.sh 自己的编译 CP 一直没加 —— 侥幸而已。
|
||
CP="$(paste -sd: "$OUT/classpath.txt"):$NAMED:$OUT/classes"
|
||
if [ -d "$OUT/fabric" ]; then
|
||
FAB_CP=$(find "$OUT/fabric" -name '*.jar' | paste -sd:)
|
||
[ -n "$FAB_CP" ] && CP="$CP:$FAB_CP"
|
||
fi
|
||
find "$HERE/$JAVA_SUB" "$HERE/java/common" -name "*.java" > "$WORK/srcs.txt"
|
||
# ★ LoaderBoot(加载器启动器)也要编进来:它是 launcher="fabric" 的主类,
|
||
# 少了它运行期报 ClassNotFoundException: LoaderBoot。
|
||
find "$HERE/java/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/java/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/java/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 (裸启动 / named)"
|
||
echo " int : $OUT/classes-intermediary.jar(加载器 / intermediary)"
|
||
echo " 次表 : $OUT/names.json · $OUT/names-intermediary.json"
|
||
echo
|
||
echo "运行(渲染服务从 stdin 读 NDJSON):"
|
||
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)⇒ 实例目录与内容根**同级**。"
|