P8: scaffold —— 反混淆 + MC 渲染栈离屏初始化 + 真渲染出图
实测链路(全绿): 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 同格式)⇒ 这就是数据喂渲染器的落点。
This commit is contained in:
21
scaffold/GlProbe.java
Normal file
21
scaffold/GlProbe.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
75
scaffold/README.md
Normal file
75
scaffold/README.md
Normal file
@ -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.<clinit>` 抛 `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
|
||||
```
|
||||
105
scaffold/RenderProbe.java
Normal file
105
scaffold/RenderProbe.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
91
scaffold/Scaffold.java
Normal file
91
scaffold/Scaffold.java
Normal file
@ -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/<name>.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.<clinit> 抛 "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;
|
||||
}
|
||||
}
|
||||
76
scaffold/build.sh
Executable file
76
scaffold/build.sh
Executable file
@ -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"
|
||||
104
scaffold/pg2srg.py
Normal file
104
scaffold/pg2srg.py
Normal file
@ -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])
|
||||
Reference in New Issue
Block a user