阶段四:Spine 3.8.99 编辑器里挂上自建视图面板

目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示),
点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。

本轮打通:
1. 编辑器在我们自己的进程里完整启动
   - 类加载器必须是 URLClassLoader 本身(自检要求)
   - Editor.main 必须在 AWT EDT 上调用
   - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava
   - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身
2. 3.8.99 全量解密 → 可读源码
   - 字符串 17251/17461(伪造同名调用帧对齐 key)
   - 混淆动态派发还原 53871 处(调引导方法取 MemberName)
3. 自建视图
   - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检)
   - 面板 Actor 由外部反射填进 us.spineView.D
   - 复用已有图标做别名(不新建图片资源)
   最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开

另外:
- ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩
- DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成
- 阶段四-视图面板挂载.md:完整记录做法与踩过的坑

不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、
Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
This commit is contained in:
tianmo committed 2026-10-16 13:34:18 +08:00
commit 2e7ab7c33c
113 files changed
+25327

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Minimal Java .class parser.
Reads every .class under a directory and prints a JSON model with the pieces we
need to identify obfuscated classes by *structure* rather than by name:
this class, super class, declared fields (name + type) and declared methods
(name + parameter/return types).
Usage:
python3 tools/classdump.py <classes-dir> [--out model.json] [--only TextButton]
"""
import argparse
import json
import os
import struct
import sys
TYPE_CHARS = {
"B": "byte", "C": "char", "D": "double", "F": "float", "I": "int",
"J": "long", "S": "short", "Z": "boolean", "V": "void",
}
def parse_descriptor(desc, at=0):
"""Turn one JVM type descriptor into (readable name, next index)."""
arrays = 0
while at < len(desc) and desc[at] == "[":
arrays += 1
at += 1
if at >= len(desc):
return "?", at
ch = desc[at]
if ch == "L":
end = desc.find(";", at)
if end < 0:
return "?", len(desc)
name = desc[at + 1:end].replace("/", ".")
at = end + 1
else:
name = TYPE_CHARS.get(ch, ch)
at += 1
return name + "[]" * arrays, at
def parse_method_descriptor(desc):
params = []
at = 0
assert desc[at] == "("
at += 1
while desc[at] != ")":
name, at = parse_descriptor(desc, at)
params.append(name)
at += 1
result, _ = parse_descriptor(desc, at)
return params, result
class Reader:
def __init__(self, data):
self.data = data
self.pos = 0
def u1(self):
value = self.data[self.pos]
self.pos += 1
return value
def u2(self):
value = struct.unpack_from(">H", self.data, self.pos)[0]
self.pos += 2
return value
def u4(self):
value = struct.unpack_from(">I", self.data, self.pos)[0]
self.pos += 4
return value
def skip(self, count):
self.pos += count
def read_constant_pool(reader):
count = reader.u2()
pool = [None] * count
index = 1
while index < count:
tag = reader.u1()
if tag == 1: # Utf8
length = reader.u2()
raw = reader.data[reader.pos:reader.pos + length]
reader.skip(length)
pool[index] = raw.decode("utf-8", "replace")
elif tag in (3, 4): # Integer, Float
reader.skip(4)
elif tag in (5, 6): # Long, Double
reader.skip(8)
index += 1
elif tag == 7: # Class -> index of the Utf8 holding the internal name
pool[index] = ("class", reader.u2())
elif tag in (8, 16, 19, 20): # String, MethodType, Module, Package
reader.skip(2)
elif tag in (9, 10, 11, 12, 17, 18): # Field/Method/Interface refs, NameAndType, Dynamic
reader.skip(4)
elif tag == 15: # MethodHandle
reader.skip(3)
else:
raise ValueError("unknown constant pool tag %d" % tag)
index += 1
return pool
def parse_class(path):
with open(path, "rb") as handle:
data = handle.read()
if len(data) < 10 or data[:4] != b"\xca\xfe\xba\xbe":
return None
reader = Reader(data)
reader.skip(4)
reader.u2() # minor
reader.u2() # major
pool = read_constant_pool(reader)
def utf8(index):
value = pool[index] if 0 < index < len(pool) else None
return value if isinstance(value, str) else None
def class_name(index):
entry = pool[index] if 0 < index < len(pool) else None
if isinstance(entry, tuple) and entry[0] == "class":
entry = utf8(entry[1])
if isinstance(entry, str):
return entry.replace("/", ".")
return None
access_flags = reader.u2()
this_class = class_name(reader.u2())
super_class = class_name(reader.u2())
interfaces = [class_name(reader.u2()) for _ in range(reader.u2())]
fields = []
for _ in range(reader.u2()):
flags = reader.u2()
name = utf8(reader.u2())
desc = utf8(reader.u2())
for _ in range(reader.u2()):
reader.u2()
reader.skip(reader.u4())
fields.append({
"name": name,
"type": parse_descriptor(desc)[0] if desc else "?",
"flags": flags,
})
methods = []
for _ in range(reader.u2()):
flags = reader.u2()
name = utf8(reader.u2())
desc = utf8(reader.u2())
for _ in range(reader.u2()):
reader.u2()
reader.skip(reader.u4())
if desc and desc.startswith("("):
params, result = parse_method_descriptor(desc)
else:
params, result = [], "?"
methods.append({"name": name, "params": params, "returns": result, "flags": flags})
return {
"name": this_class,
"super": super_class,
"interfaces": interfaces,
"flags": access_flags,
"fields": fields,
"methods": methods,
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("directory")
parser.add_argument("--out")
parser.add_argument("--only", help="only keep classes whose name contains this")
args = parser.parse_args()
model = {}
failed = 0
for dirpath, _, filenames in os.walk(args.directory):
for filename in filenames:
if not filename.endswith(".class"):
continue
path = os.path.join(dirpath, filename)
try:
entry = parse_class(path)
except Exception:
failed += 1
continue
if not entry or not entry["name"]:
failed += 1
continue
if args.only and args.only not in entry["name"]:
continue
entry["path"] = path
model[entry["name"]] = entry
print("解析成功 %d 个类,失败 %d" % (len(model), failed), file=sys.stderr)
if args.out:
with open(args.out, "w") as handle:
json.dump(model, handle)
print("已写入 %s" % args.out, file=sys.stderr)
if __name__ == "__main__":
main()