阶段四: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

+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""把 3.8.99 载荷里所有方法的访问标志加上 public。
为什么敢改访问标志:解密函数算 key 用的是 **(调用方类)** 的常量池大小
key = (cpSize + 调用类名 + 调用方法名 + cpSize).hashCode()
其中 cpSize 每次都取 st[2](调用解密函数的那个类),跟解密函数所在类无关。
而 access_flags 是方法头里的两个字节,改它不会动常量池,密文一个字节都不受影响。
改 public 的目的:我们可以造一个合成类,用**原始调用方的类名和方法名**去调解密函数,
从而让 key 自然对上,不需要自己实现 AES —— 载荷里有好几个内联副本,全开 public 最省事。
"""
import sys
import zipfile
ACC_PUBLIC = 0x0001
ACC_PRIVATE = 0x0002
ACC_PROTECTED = 0x0004
class Reader:
def __init__(self, data):
self.d = data
self.p = 0
def u1(self):
v = self.d[self.p]
self.p += 1
return v
def u2(self):
v = (self.d[self.p] << 8) | self.d[self.p + 1]
self.p += 2
return v
def u4(self):
v = int.from_bytes(self.d[self.p:self.p + 4], "big")
self.p += 4
return v
def skip(self, n):
self.p += n
def parse_constant_pool(r):
"""返回 (utf8 列表, 索引->绝对偏移) 以及常量池结束位置。"""
count = r.u2()
utf8 = {}
offsets = {}
i = 1
while i < count:
offsets[i] = r.p
tag = r.u1()
if tag == 1:
length = r.u2()
raw = r.d[r.p:r.p + length]
r.skip(length)
utf8[i] = raw
elif tag in (3, 4):
r.skip(4)
elif tag in (5, 6):
r.skip(8)
i += 1
elif tag in (7, 8, 16, 19, 20):
r.skip(2)
elif tag in (9, 10, 11, 12, 17, 18):
r.skip(4)
elif tag == 15:
r.skip(3)
else:
raise ValueError("未知常量池 tag %d @%d" % (tag, r.p - 1))
i += 1
return utf8, offsets, count
def skip_attributes(r):
n = r.u2()
for _ in range(n):
r.skip(2)
r.skip(r.u4())
def patch(data):
"""把类里所有方法的访问标志加上 ACC_PUBLIC,就地修改 data(bytearray)。"""
r = Reader(data)
if r.u4() != 0xCAFEBABE:
raise ValueError("不是 class 文件")
r.u2()
r.u2()
utf8, offsets, cp_count = parse_constant_pool(r)
r.skip(2) # access_flags(类的)
r.skip(2) # this_class
r.skip(2) # super_class
r.skip(2 * r.u2()) # interfaces
for _ in range(r.u2()): # fields
r.skip(6)
skip_attributes(r)
changed = 0
for _ in range(r.u2()): # methods
access_pos = r.p
access = r.u2()
r.skip(4) # name_index / descriptor_index
skip_attributes(r)
# 不能直接 OR:private static final 是 0x1A,OR 上 0x01 会变成 0x1B(public+private,非法)
new_access = (access & ~(ACC_PRIVATE | ACC_PROTECTED)) | ACC_PUBLIC
if new_access != access:
data[access_pos] = (new_access >> 8) & 0xFF
data[access_pos + 1] = new_access & 0xFF
changed += 1
return changed, cp_count
def main():
source, target = sys.argv[1], sys.argv[2]
patched_classes = 0
patched_methods = 0
with zipfile.ZipFile(source) as zin:
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
raw = zin.read(item.filename)
if item.filename.endswith(".class"):
raw = bytearray(raw)
try:
changed, cp_count = patch(raw)
patched_classes += 1
patched_methods += changed
except Exception as error:
# 载荷里有几个不以 CAFEBABE 开头的 .class(比如 wvv.class 是签名数据),跳过
print("跳过 %s : %s" % (item.filename, error))
zout.writestr(item, raw)
print("已把 %d 个类里的 %d 个方法改成 public" % (patched_classes, patched_methods))
print("已写出 " + target)
if __name__ == "__main__":
main()