Files
tianmo d9ea449b64 面板增删改查 + Inspector + 可识别源码版(src-named)
面板(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 新增第六节(工具链 + 已验证 + 已知限制)。
2026-10-16 15:25:02 +08:00

157 lines
6.1 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}
}
}
}