From e223be25714cc98fec86f5af379997ba1df3f4a4 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 15:39:41 +0800 Subject: [PATCH] =?UTF-8?q?P8:=20scaffold=20=E2=80=94=E2=80=94=20=E5=8F=8D?= =?UTF-8?q?=E6=B7=B7=E6=B7=86=20+=20MC=20=E6=B8=B2=E6=9F=93=E6=A0=88?= =?UTF-8?q?=E7=A6=BB=E5=B1=8F=E5=88=9D=E5=A7=8B=E5=8C=96=20+=20=E7=9C=9F?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=87=BA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测链路(全绿): 1. 官方 ProGuard 映射 → SRG → SpecialSource 重映射 → client-named.jar 2. 反编译确认 MC 渲染器挂在 Minecraft 单例上,不能独立 new 3. 权威序列:tryDetectVersion → Bootstrap → RenderSystem → new Minecraft(cfg) 4. 渲 3 帧 + glReadPixels → PNG,画面是 Mojang 加载屏(真渲染,非清屏) 三个坑(实测证据): - 漏 tryDetectVersion → Game version not set - FolderData 参数顺序错 → icons/ 404 - assetIndex 用 id(5) 而非落盘名(1.20.1) → 资源全找不到 下一步:ClientLevel 构造只需 listener.registryAccess(), ClientChunkCache.replaceWithPacketData 接收 map_chunk 原始字节 (与 chunk.py 同格式)⇒ 这就是数据喂渲染器的落点。 --- scaffold/GlProbe.java | 21 ++++++++ scaffold/README.md | 75 +++++++++++++++++++++++++++ scaffold/RenderProbe.java | 105 ++++++++++++++++++++++++++++++++++++++ scaffold/Scaffold.java | 91 +++++++++++++++++++++++++++++++++ scaffold/build.sh | 76 +++++++++++++++++++++++++++ scaffold/pg2srg.py | 104 +++++++++++++++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 scaffold/GlProbe.java create mode 100644 scaffold/README.md create mode 100644 scaffold/RenderProbe.java create mode 100644 scaffold/Scaffold.java create mode 100755 scaffold/build.sh create mode 100644 scaffold/pg2srg.py diff --git a/scaffold/GlProbe.java b/scaffold/GlProbe.java new file mode 100644 index 0000000..80afc7b --- /dev/null +++ b/scaffold/GlProbe.java @@ -0,0 +1,21 @@ +import org.lwjgl.glfw.*; +import org.lwjgl.opengl.*; +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.system.MemoryUtil.NULL; + +public class GlProbe { + public static void main(String[] a) { + GLFWErrorCallback.createPrint(System.err).set(); + if (!glfwInit()) { System.out.println("FAIL: glfwInit"); return; } + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + long win = glfwCreateWindow(320, 240, "probe", NULL, NULL); + if (win == NULL) { System.out.println("FAIL: createWindow"); return; } + glfwMakeContextCurrent(win); + GL.createCapabilities(); + System.out.println("OK gl=" + glGetString(GL_VERSION)); + System.out.println("OK renderer=" + glGetString(GL_RENDERER)); + glfwDestroyWindow(win); glfwTerminate(); + } +} diff --git a/scaffold/README.md b/scaffold/README.md new file mode 100644 index 0000000..c249072 --- /dev/null +++ b/scaffold/README.md @@ -0,0 +1,75 @@ +# mcfold scaffold —— 用官方客户端渲染,但不跑客户端 + +**目标**(用户 2026-09-25 定向):不起完整客户端,把 MC 的渲染能力抽出来, +用我们自己的世界数据驱动它出图。 + +## 实测结论(2026-09-25 全绿) + +客户端 jar 是 **ProGuard 全量混淆**的(`LevelRenderer` 叫 `fjv`), +所以「抽渲染器」的第一步必须是**反混淆**。三层关卡全部验证通过: + +| 关卡 | 证据 | 结果 | +|---|---|---| +| 反混淆 | `client-named.jar` 25MB;`javap` 读出 `net.minecraft.client.renderer.LevelRenderer` | ✅ | +| 编译期引用 | `javac` 编译引用 `Minecraft`/`LevelRenderer` 的代码 | ✅ | +| GL 离屏 | Xvfb + llvmpipe → OpenGL 4.5 core | ✅ | +| **渲染栈初始化** | `new Minecraft(cfg)` 完成 GL/资源/渲染器初始化,**不进主循环** | ✅ | +| **真渲染出图** | 渲 3 帧 + 读像素 → PNG,画面是 Mojang 加载屏(logo 贴图 + 进度条) | ✅ | + +## 为什么渲染器不能独立 new + +反编译(Vineflower)给出的实测签名: + +```java +LevelRenderer(Minecraft, EntityRenderDispatcher, BlockEntityRenderDispatcher, RenderBuffers) +renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f) +``` + +全部挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。 +正确形态是「初始化 MC 渲染栈,但不跑主循环」。 + +## 权威序列(反编译 `net.minecraft.client.main.Main` 得到) + +```java +SharedConstants.tryDetectVersion(); // ← 漏了会 "Game version not set" +SharedConstants.enableDataFixerOptimizations(); +Bootstrap.bootStrap(); +RenderSystem.initRenderThread(); +RenderSystem.beginInitialization(); +Minecraft mc = new Minecraft(gameConfig); // ← 构造即完成渲染栈初始化 +RenderSystem.finishInitialization(); +// 此处官方会起 Game thread 跑主循环 —— 我们不做 +``` + +渲染一帧(反编译 `Minecraft.render()` 得到): + +```java +RenderSystem.clear(16640, ON_OSX); +mainRenderTarget.bindWrite(true); +gameRenderer.render(partialTick, nanos, renderLevel); +mainRenderTarget.unbindWrite(); +mainRenderTarget.blitToScreen(w, h); +window.updateDisplay(); +``` + +## 踩过的三个坑(都有实测证据) + +1. **漏 `tryDetectVersion()`** → `DataFixers.` 抛 `Game version not set` +2. **`FolderData` 参数顺序** + `(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)` + —— 顺序写错会让 `icons/icon_16x16.png` 404 +3. **assetIndex 名要用落盘名** + version.json 里 `assetIndex.id = "5"`,但磁盘是 `assets/indexes/1.20.1.json` + (MCLC 按 `version.number` 落盘)—— 传 `"5"` 则索引加载失败、资源全找不到 + +## 下一步:渲染真实世界 + +`ClientLevel` 构造只需 `listener.registryAccess()`(其余字段不用 listener), +且 `ClientChunkCache.replaceWithPacketData(...)` 接收的正是 **`map_chunk` 包的原始字节** +—— 与 mcfold `chunk.py` 解码器同一格式。这就是设计文档说的桥梁落点。 + +## 构建 + +```bash +./build.sh /path/to/store/vanilla-1.20.1 1.20.1 +``` diff --git a/scaffold/RenderProbe.java b/scaffold/RenderProbe.java new file mode 100644 index 0000000..6bc48e8 --- /dev/null +++ b/scaffold/RenderProbe.java @@ -0,0 +1,105 @@ +import com.mojang.blaze3d.pipeline.MainTarget; +import com.mojang.blaze3d.platform.DisplayData; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.client.main.GameConfig; +import net.minecraft.server.Bootstrap; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.net.Proxy; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * mcfold render probe —— 起 MC 渲染栈,渲一帧,读像素写 PNG。 + * 全程:不连服、不登录、不进主循环、不 gameplay。 + */ +public class RenderProbe { + + public static void main(String[] args) throws Exception { + String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); + String assetIdx = arg(args, "--assetIndex", "1.20.1"); + String out = arg(args, "--out", "/tmp/scaffold-frame.png"); + int width = Integer.parseInt(arg(args, "--width", "854")); + int height = Integer.parseInt(arg(args, "--height", "480")); + + SharedConstants.tryDetectVersion(); + SharedConstants.enableDataFixerOptimizations(); + Bootstrap.bootStrap(); + + File runDir = new File(gameDir); + User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", + Optional.empty(), Optional.empty(), User.Type.LEGACY); + PropertyMap props = new PropertyMap(); + + GameConfig cfg = new GameConfig( + new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), + new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false), + new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), + new GameConfig.GameData(false, "mcfold", "release", false, false), + new GameConfig.QuickPlayData(null, null, null, null) + ); + + RenderSystem.initRenderThread(); + RenderSystem.beginInitialization(); + Minecraft mc = new Minecraft(cfg); + RenderSystem.finishInitialization(); + + System.out.println("[probe] constructed. w=" + mc.getWindow().getWidth() + + " h=" + mc.getWindow().getHeight()); + + // ★ 渲染一帧:照抄 Minecraft.render() 的实测序列(反编译 nout2/.../Minecraft.java:1110-1140) + MainTarget target = (MainTarget) mc.getMainRenderTarget(); + for (int frame = 0; frame < 3; frame++) { + RenderSystem.clear(16640, Minecraft.ON_OSX); + target.bindWrite(true); + mc.gameRenderer.render(0.0F, 0L, true); + target.unbindWrite(); + target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + mc.getWindow().updateDisplay(); + System.out.println("[probe] frame " + frame + " rendered"); + } + + // ★ 读像素:从 GL 后台缓冲直接读(比截屏更可靠,且不依赖 X) + GL11.glReadBuffer(GL11.GL_BACK); + int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight(); + ByteBuffer buf = org.lwjgl.BufferUtils.createByteBuffer(w * h * 4); + GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf); + + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + // GL 原点在左下,PNG 在左上,需翻转 + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int i = ((h - 1 - y) * w + x) * 4; + int r = buf.get(i) & 0xFF, g = buf.get(i + 1) & 0xFF, b = buf.get(i + 2) & 0xFF; + img.setRGB(x, y, (r << 16) | (g << 8) | b); + } + } + ImageIO.write(img, "png", new File(out)); + System.out.println("[probe] wrote " + out + " (" + w + "x" + h + ")"); + + // 统计非黑像素,证明真的画了东西(而非空缓冲) + long nonBlack = 0; int total = w * h; + for (int p : img.getRGB(0, 0, w, h, null, 0, w)) if ((p & 0xFFFFFF) != 0) nonBlack++; + System.out.println("[probe] non-black pixels = " + nonBlack + "/" + total + + " (" + (100.0 * nonBlack / total) + "%)"); + + GLFW.glfwTerminate(); + System.out.println("[probe] DONE"); + System.exit(0); + } + + private static String arg(String[] a, String key, String def) { + for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; + return def; + } +} diff --git a/scaffold/Scaffold.java b/scaffold/Scaffold.java new file mode 100644 index 0000000..b1d255d --- /dev/null +++ b/scaffold/Scaffold.java @@ -0,0 +1,91 @@ +import com.mojang.blaze3d.platform.DisplayData; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.client.main.GameConfig; +import net.minecraft.server.Bootstrap; + +import java.io.File; +import java.net.Proxy; +import java.util.OptionalInt; + +/** + * mcfold scaffold —— 只起 MC 的**渲染栈**,不进游戏主循环。 + * + * 依据(反编译 net.minecraft.client.main.Main 的实测序列): + * Bootstrap.bootStrap() + * RenderSystem.initRenderThread(); beginInitialization(); + * new Minecraft(gameConfig) ← 构造即完成 GL/资源/渲染器初始化 + * RenderSystem.finishInitialization() + * 之后 Main 才起 Game thread 跑主循环 —— 我们**不**走那步。 + */ +public class Scaffold { + + public static void main(String[] args) throws Exception { + String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); + // ★ 索引名必须是 assets/indexes/.json 中真实存在的那个。 + // version.json 的 assetIndex.id 是 "5",但 MCLC 落盘用 version.number(1.20.1) + // —— 传 "5" 会导致索引加载失败,所有资源/icons 全部找不到(实测踩过)。 + String assetIdx = arg(args, "--assetIndex", "1.20.1"); + int width = Integer.parseInt(arg(args, "--width", "1280")); + int height = Integer.parseInt(arg(args, "--height", "720")); + + System.out.println("[scaffold] bootstrap..."); + // ★ 官方 Main 的实测序列:tryDetectVersion 必须在 bootStrap 之前, + // 否则 DataFixers. 抛 "Game version not set" + SharedConstants.tryDetectVersion(); + SharedConstants.enableDataFixerOptimizations(); + Bootstrap.bootStrap(); + System.out.println("[scaffold] bootstrap ok, mcVersion=" + SharedConstants.getCurrentVersion().getName()); + + File runDir = new File(gameDir); + File assetsDir = new File(assets); + + User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", + java.util.Optional.empty(), java.util.Optional.empty(), User.Type.LEGACY); + PropertyMap props = new PropertyMap(); + + GameConfig cfg = new GameConfig( + new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), + new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false), + new GameConfig.FolderData( + // ★ 顺序是 (gameDirectory, resourcePackDirectory, assetDirectory, assetIndex) + // 反着写会把 assets 当游戏目录,导致资源全找不到(实测踩过:icons/icon_16x16.png 404) + runDir, new File(runDir, "resourcepacks"), assetsDir, assetIdx), + new GameConfig.GameData( + false, // demo + "mcfold", // version + "release", // versionType + false, // disableMultiplayer + false), // disableChat + new GameConfig.QuickPlayData(null, null, null, null) + ); + + System.out.println("[scaffold] RenderSystem init..."); + RenderSystem.initRenderThread(); + RenderSystem.beginInitialization(); + + System.out.println("[scaffold] new Minecraft(cfg) ..."); + Minecraft mc = new Minecraft(cfg); + + RenderSystem.finishInitialization(); + System.out.println("[scaffold] OK Minecraft constructed"); + + // 检查渲染栈关键对象是否就绪(不启主循环) + System.out.println("[scaffold] window=" + mc.getWindow().getWidth() + "x" + mc.getWindow().getHeight()); + System.out.println("[scaffold] levelRenderer=" + (mc.levelRenderer != null ? "present" : "null")); + System.out.println("[scaffold] gameRenderer=" + (mc.gameRenderer != null ? "present" : "null")); + System.out.println("[scaffold] level=" + (mc.level != null ? "present" : "null (не connected)")); + + System.out.println("[scaffold] DONE (no main loop)"); + } + + private static String arg(String[] a, String key, String def) { + for (int i = 0; i < a.length - 1; i++) + if (a[i].equals(key)) return a[i + 1]; + return def; + } +} diff --git a/scaffold/build.sh b/scaffold/build.sh new file mode 100755 index 0000000..0e8298b --- /dev/null +++ b/scaffold/build.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# mcfold scaffold 构建脚本 —— 把「官方客户端 jar」变成「可直接调用的渲染库」 +# +# 实测链路(2026-09-25 全绿): +# 1. 下官方 ProGuard 映射(client_mappings.txt) +# 2. 转成 SRG(SpecialSource 只吃 SRG) +# 3. 重映射 client.jar → client-named.jar(类名可读,javap/javac 可用) +# 4. 编译脚手架 +# +# 用法: ./build.sh [实例store目录] [MC版本] +set -euo pipefail + +STORE="${1:-/home/newqqagent/mcview/store/vanilla-1.20.1}" +MCVER="${2:-1.20.1}" +HERE="$(cd "$(dirname "$0")" && pwd)" +WORK="${MCFOLD_SCAFFOLD_WORK:-/tmp/mcfold-scaffold}" + +mkdir -p "$WORK" +cd "$WORK" + +CLIENT_JAR="$STORE/versions/$MCVER/$MCVER.jar" +[ -f "$CLIENT_JAR" ] || { echo "找不到客户端 jar: $CLIENT_JAR"; exit 1; } + +# ── 1. 映射文件 ──────────────────────────────────────────────── +MAPPINGS_URL=$(python3 - "$STORE/versions/$MCVER/$MCVER.json" <<'PY' +import json,sys +d=json.load(open(sys.argv[1])) +print(d['downloads']['client_mappings']['url']) +PY +) +if [ ! -s client_mappings.txt ]; then + echo "[1/4] 下载官方映射..." + curl -sL -o client_mappings.txt "$MAPPINGS_URL" +fi +echo "[1/4] 映射: $(wc -l < client_mappings.txt) 行" + +# ── 2. ProGuard → SRG ───────────────────────────────────────── +echo "[2/4] 转换映射格式..." +python3 "$HERE/pg2srg.py" client_mappings.txt mappings.srg + +# ── 3. 重映射 jar ───────────────────────────────────────────── +if [ ! -f client-named.jar ] || [ "$CLIENT_JAR" -nt client-named.jar ]; then + echo "[3/4] 重映射客户端 jar(约 1-2 分钟)..." + SS=$(find /home/minecraft/libraries -name "SpecialSource-*.jar" 2>/dev/null | head -1) + [ -n "$SS" ] || { echo "找不到 SpecialSource"; exit 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 "$a.jar" ] || curl -sL -o "$a.jar" "https://repo1.maven.org/maven2/org/ow2/asm/$a/9.7/$a-9.7.jar" + done + [ -f joptsimple.jar ] || curl -sL -o joptsimple.jar \ + "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar" + java -Xmx3G -cp "$SS:joptsimple.jar:asm.jar:asm-commons.jar:asm-tree.jar:$GUAVA" \ + net.md_5.specialsource.SpecialSource \ + -i "$CLIENT_JAR" -o client-named.jar -m mappings.srg +else + echo "[3/4] client-named.jar 已是最新" +fi +ls -la client-named.jar + +# ── classpath ───────────────────────────────────────────────── +find "$STORE/libraries" -name "*.jar" ! -name "*-sources.jar" ! -name "*natives*" 2>/dev/null \ + | grep -vE "minecraftforge|neoforge" > cp.txt +CP="$(paste -sd: cp.txt):$WORK/client-named.jar" + +# ── 4. 编译脚手架 ───────────────────────────────────────────── +echo "[4/4] 编译脚手架..." +mkdir -p classes +javac -nowarn -proc:none -cp "$CP" -d classes "$HERE"/*.java +echo +echo "✅ 构建完成" +echo " named jar : $WORK/client-named.jar" +echo " classes : $WORK/classes" +echo +echo "运行(无头,需 Xvfb):" +echo " find $STORE/libraries -name '*natives-linux*.jar' | while read j; do unzip -o -q \$j -d $WORK/nativ; done" +echo " DISPLAY=:98 java -cp \"$CP:$WORK/classes\" -Djava.library.path=$WORK/nativ RenderProbe" diff --git a/scaffold/pg2srg.py b/scaffold/pg2srg.py new file mode 100644 index 0000000..c3d3236 --- /dev/null +++ b/scaffold/pg2srg.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""ProGuard mappings (Mojang官方) -> SRG (SpecialSource 可读) + +ProGuard 格式: + net.minecraft.client.Minecraft -> enn: + 187:307:void render(...) -> a + org.slf4j.Logger LOGGER -> c + +SRG 格式: + CL: enn net/minecraft/client/Minecraft + MD: enn/a (FJZ)V net/minecraft/client/Minecraft/render (FJZ)V + FD: fjv/c net/minecraft/client/renderer/LevelRenderer/LOGGER +""" +import re, sys + +PRIM = {'void':'V','boolean':'Z','byte':'B','char':'C','short':'S', + 'int':'I','long':'J','float':'F','double':'D'} + +def main(pg_path, out_path): + classes = {} # named -> obf + class_lines = [] # (obf, named) + members = [] # ('MD'|'FD', obfOwner, obfName, namedOwner, namedName, [namedDesc]) + cur_named = cur_obf = None + + class_re = re.compile(r'^(\S+) -> (\S+):$') + meth_re = re.compile(r'^\s*\d+:\d+:(.*) -> (\S+)$') + field_re = re.compile(r'^\s+(\S+)\s+(\S+) -> (\S+)$') + + with open(pg_path, encoding='utf-8') as f: + for line in f: + line = line.rstrip('\n') + if not line or line.startswith('#'): + continue + m = class_re.match(line) + if m: + cur_named, cur_obf = m.group(1), m.group(2).rstrip(':') + classes[cur_named] = cur_obf + class_lines.append((cur_obf, cur_named)) + continue + if cur_obf is None: + continue + # ★ ProGuard 的方法行「可以没有」行号前缀(如 `Foo acquire() -> a`), + # 所以判据是「-> 左侧以 ) 结尾」而不是「行首有 N:M:」。靠行号判会漏成字段。 + m = re.match(r'^\s*(?:\d+:\d+:)?(.*?)\s+->\s+(\S+)$', line) + if not m: + continue + sig, obf_name = m.group(1).strip(), m.group(2) + if sig.endswith(')'): + members.append(('MD', cur_obf, obf_name, cur_named, sig)) + else: + # 字段:"type name" + parts = sig.rsplit(' ', 1) + if len(parts) != 2: + continue + ftype, fname = parts[0], parts[1] + members.append(('FD', cur_obf, obf_name, cur_named, ftype + ' ' + fname)) + + def to_obf_desc(t): + """named 类型 -> obf JVM 描述符""" + t = t.strip() + if t in PRIM: + return PRIM[t] + if t.endswith('[]'): + return '[' + to_obf_desc(t[:-2]) + # 内部类在 ProGuard 里用 $ 写,obf 侧也是 $ 形式(已包含在 classes 里) + obf = classes.get(t) + if obf is None: + # 可能是混淆后的内部类写法差异,或非 MC 类(java/*, netty 等)-> 转 / + return 'L' + t.replace('.', '/') + ';' + return 'L' + obf + ';' + + def split_params(s): + """按顶层逗号切分(泛型里不会有逗号,ProGuard 已擦除泛型)""" + if not s.strip(): + return [] + return [p.strip() for p in s.split(',')] + + with open(out_path, 'w', encoding='utf-8') as out: + for obf, named in class_lines: + out.write(f'CL: {obf} {named.replace(".", "/")}\n') + + for kind, obf_owner, obf_name, named_owner, payload in members: + named_owner_s = named_owner.replace('.', '/') + if kind == 'FD': + ftype, fname = payload.split(' ', 1) + out.write(f'FD: {obf_owner}/{obf_name} {named_owner_s}/{fname}\n') + else: + # payload = "retType name(args)" + mm = re.match(r'^(.*?)\s+([\w$<>]+)\((.*)\)$', payload) + if not mm: + continue + ret, name, argstr = mm.group(1), mm.group(2), mm.group(3) + try: + params = split_params(argstr) + obf_desc = '(' + ''.join(to_obf_desc(p) for p in params) + ')' + to_obf_desc(ret) + except Exception: + continue + out.write(f'MD: {obf_owner}/{obf_name} {obf_desc} ' + f'{named_owner_s}/{name} {obf_desc}\n') + + sys.stderr.write(f'CL={len(class_lines)} members={len(members)}\n') + +if __name__ == '__main__': + main(sys.argv[1], sys.argv[2])