面板(viewpanel/src/com/spineview/) - ObjectPanel:Refresh / New Bone / New Slot / New BBox / New Path / Delete / Rename / Inspect + 名字输入框 + 对象列表(点行=选中)+ 状态行,全英文。 - SceneObjects:查(Tree 控件按层级/扁平列全部对象)、选中(VZ.D(Tree.Node,boolean) —— VZ.D(Object) 内部只是 Selection.L(),不真选中)、读当前选择(VZ.B())。 - SceneActions:增/删/改 + 撤销包装(un.D()/un.B())+ 树节点重建(VZ.D(GP,true,true)); 统一在编辑器渲染线程执行(OG.B(Runnable);OG.I() 有线程断言,非渲染线程会抛 RuntimeException); 输入名字对话框自动化:找到『有 TextField 字段 + L(Object)V 回调』的对话框对象 → 填名字 → 调 L(true)。 - 实测:骨骼新建自动编号(bone7/bone8);模型层允许重名、按名字查只返回第一个;删父骨骼连子节点一起删; 不能在渲染线程里 sleep(有些创建是下一帧做的)。 - 探针:-Dspine.probe=objects|panels|select|crud|matrix|panel。 可识别源码版(工具) - tools/deobf/Renamer.java:ASM Remapper 只改成员名不改类名(indy 运行时要用加密串里的类名反射查)。 - tools/deobf/anchors.tsv(手动 14 条有证据的锚点)+ anchors-libgdx.tsv(MapMethods 对 gdx-1.11.0 指纹自动生成 178 条)→ names.tsv 192 条。 - 产物 ~/Desktop/spine398/src-named(4176 类全量,原 src-readable 不动);已验证 Table.add/clearChildren、SelectBox.getSelected/setSelected/setSelectedIndex、Gp.name。 - 文档:混淆名对照表.md 新增第六节(工具链 + 已验证 + 已知限制)。
157 lines
6.1 KiB
Java
157 lines
6.1 KiB
Java
package com.spineview.deobf;
|
||
|
||
import java.io.BufferedWriter;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.InputStream;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.nio.file.Files;
|
||
import java.nio.file.Path;
|
||
import java.nio.file.Paths;
|
||
import java.util.Enumeration;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.zip.ZipEntry;
|
||
import java.util.zip.ZipFile;
|
||
import java.util.zip.ZipOutputStream;
|
||
import org.objectweb.asm.ClassReader;
|
||
import org.objectweb.asm.ClassWriter;
|
||
import org.objectweb.asm.commons.ClassRemapper;
|
||
import org.objectweb.asm.commons.Remapper;
|
||
|
||
/**
|
||
* 按"名字映射表"重命名载荷里的**成员**(字段/方法),产出一份新的载荷 zip,
|
||
* 专门用来反编译 —— 这样反编译出来的代码里,声明和直接调用都带可识别的名字。
|
||
*
|
||
* ⚠️ 只改成员名,**不改类名**:载荷的 invokedynamic 是在运行时用加密串解出
|
||
* "类名 + 方法名 + 描述符"再反射查找的,类名必须保持不变(成员名改了以后这份 zip
|
||
* 只用于反编译,不能拿去运行)。
|
||
*
|
||
* 映射表(TSV,4 列,`#` 开头是注释):
|
||
* owner<TAB>name<TAB>desc<TAB>newName
|
||
* - owner:内部类名,如 `et`、`com/badlogic/gdx/scenes/scene2d/ui/Tree`
|
||
* - name :混淆成员名(字段或方法)
|
||
* - desc :描述符;字段如 `F`(float)、`Ljava/lang/String;`;
|
||
* 方法如 `(Ldl;)LdR;`。**写 `*` 表示"该 owner 下所有同名成员"**(仅在
|
||
* 确定这一层不会撞车时用)
|
||
* - newName:要换成的新名字
|
||
*
|
||
* 用法:
|
||
* java -cp "asm.jar:asm-tree.jar:asm-commons.jar:build" com.spineview.deobf.Renamer <in.zip> <map.tsv> <out.zip>
|
||
*/
|
||
public final class Renamer {
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
Path in = Paths.get(args[0]);
|
||
Path map = Paths.get(args[1]);
|
||
Path out = Paths.get(args[2]);
|
||
|
||
Map<String, String> exact = new HashMap<String, String>();
|
||
Map<String, String> wildcard = new HashMap<String, String>();
|
||
int loaded = 0;
|
||
for (String line : Files.readAllLines(map, StandardCharsets.UTF_8)) {
|
||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||
String[] parts = line.split("\t");
|
||
if (parts.length < 4 || parts[0].isEmpty() || parts[1].isEmpty()) continue;
|
||
String owner = parts[0];
|
||
String name = parts[1];
|
||
String desc = parts[2];
|
||
String renamed = parts[3];
|
||
if (renamed.isEmpty()) continue;
|
||
if (desc.equals("*")) {
|
||
wildcard.put(owner + "#" + name, renamed);
|
||
} else {
|
||
exact.put(owner + "#" + name + "#" + desc, renamed);
|
||
}
|
||
loaded++;
|
||
}
|
||
System.err.println("映射条目 " + loaded + "(具体 " + exact.size() + ",通配 " + wildcard.size() + ")");
|
||
|
||
int classes = 0;
|
||
int renamedMembers = 0;
|
||
try (ZipFile zip = new ZipFile(in.toFile());
|
||
ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(out))) {
|
||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||
while (entries.hasMoreElements()) {
|
||
ZipEntry entry = entries.nextElement();
|
||
byte[] raw = readAll(zip.getInputStream(entry));
|
||
if (!entry.getName().endsWith(".class")) {
|
||
zipOut.putNextEntry(new ZipEntry(entry.getName()));
|
||
zipOut.write(raw);
|
||
zipOut.closeEntry();
|
||
continue;
|
||
}
|
||
try {
|
||
ClassReader reader = new ClassReader(raw);
|
||
ClassWriter writer = new ClassWriter(0);
|
||
final Map<String, String> exactMap = exact;
|
||
final Map<String, String> wildcardMap = wildcard;
|
||
final int[] counter = new int[1];
|
||
Remapper remapper = new Remapper() {
|
||
@Override
|
||
public String mapFieldName(String owner, String name, String descriptor) {
|
||
String renamed = lookup(owner, name, descriptor, exactMap, wildcardMap);
|
||
if (renamed == null) return name;
|
||
counter[0]++;
|
||
return renamed;
|
||
}
|
||
|
||
@Override
|
||
public String mapMethodName(String owner, String name, String descriptor) {
|
||
String renamed = lookup(owner, name, descriptor, exactMap, wildcardMap);
|
||
if (renamed == null) return name;
|
||
counter[0]++;
|
||
return renamed;
|
||
}
|
||
};
|
||
reader.accept(new ClassRemapper(writer, remapper), 0);
|
||
byte[] rewritten = writer.toByteArray();
|
||
zipOut.putNextEntry(new ZipEntry(entry.getName()));
|
||
zipOut.write(rewritten);
|
||
zipOut.closeEntry();
|
||
classes++;
|
||
renamedMembers += counter[0];
|
||
} catch (Throwable error) {
|
||
zipOut.putNextEntry(new ZipEntry(entry.getName()));
|
||
zipOut.write(raw);
|
||
zipOut.closeEntry();
|
||
}
|
||
}
|
||
}
|
||
System.err.println("处理 " + classes + " 个类,改名成员引用 " + renamedMembers + " 处 -> " + out);
|
||
}
|
||
|
||
private static String lookup(String owner, String name, String descriptor,
|
||
Map<String, String> exact, Map<String, String> wildcard) {
|
||
String found = exact.get(owner + "#" + name + "#" + descriptor);
|
||
if (found != null) return found;
|
||
return wildcard.get(owner + "#" + name);
|
||
}
|
||
|
||
private static byte[] readAll(InputStream in) throws java.io.IOException {
|
||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||
byte[] chunk = new byte[65536];
|
||
int n;
|
||
while ((n = in.read(chunk)) > 0) buffer.write(chunk, 0, n);
|
||
return buffer.toByteArray();
|
||
}
|
||
|
||
/** 生成"没被用到"的映射条目清单,方便发现写错的 owner/desc。 */
|
||
static List<String> unused(Map<String, Integer> hits) {
|
||
List<String> out = new java.util.ArrayList<String>();
|
||
for (Map.Entry<String, Integer> entry : hits.entrySet()) {
|
||
if (entry.getValue() == 0) out.add(entry.getKey());
|
||
}
|
||
return out;
|
||
}
|
||
|
||
static void writeReport(Path path, Map<String, Integer> hits) throws Exception {
|
||
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
|
||
for (Map.Entry<String, Integer> entry : hits.entrySet()) {
|
||
writer.write(entry.getValue() + "\t" + entry.getKey());
|
||
writer.write('\n');
|
||
}
|
||
}
|
||
}
|
||
}
|