面板增删改查 + 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 新增第六节(工具链 + 已验证 + 已知限制)。
This commit is contained in:
tianmo committed 2026-10-16 15:25:02 +08:00
1 parent 7fbb042aa6
commit d9ea449b64
10 files changed
+2225 -43

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
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');
}
}
}
}
+180
View File
@@ -0,0 +1,180 @@
# 由 tools/obfmap/MapMethods 指纹比对自动生成(上游 libGDX 1.11.0)
# owner name desc newName
KP B (I)V setSelectedIndex
KP D (Ldt;)V setStage
com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
com/badlogic/gdx/scenes/scene2d/ui/Label D (II)V setAlignment
com/badlogic/gdx/scenes/scene2d/ui/Label D (Lcom/badlogic/gdx/scenes/scene2d/ui/Label$LabelStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)Z textEquals
com/badlogic/gdx/scenes/scene2d/ui/Label I (Z)V setEllipsis
com/badlogic/gdx/scenes/scene2d/ui/Label L (Z)V setWrap
com/badlogic/gdx/scenes/scene2d/ui/Label SourceFile ()V invalidate
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane B (ZZ)V setScrollingDisabled
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (FFZ)Ldl; hit
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (ILdl;)V addActorAt
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Lcom/badlogic/gdx/scenes/scene2d/ui/ScrollPane$ScrollPaneStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;)V addActor
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;Z)Z removeActor
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (FFFF)V scrollTo
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (Ldl;)Z removeActor
com/badlogic/gdx/scenes/scene2d/ui/Slider D ()Lcom/badlogic/gdx/scenes/scene2d/ui/Slider$SliderStyle; getStyle
com/badlogic/gdx/scenes/scene2d/ui/Slider D ([FF)V setSnapToValues
com/badlogic/gdx/scenes/scene2d/ui/Slider init ()Z isDragging
com/badlogic/gdx/scenes/scene2d/ui/TextButton L (Ljava/lang/String;)V setText
com/badlogic/gdx/scenes/scene2d/ui/TextField B (I)Z withinMaxLength
com/badlogic/gdx/scenes/scene2d/ui/TextField D ()Ldr; createInputListener
com/badlogic/gdx/scenes/scene2d/ui/TextField D (ILjava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String; insert
com/badlogic/gdx/scenes/scene2d/ui/TextField LineNumberTable ()V selectAll
com/badlogic/gdx/scenes/scene2d/ui/TextField StackMapTable ()F getPrefWidth
com/badlogic/gdx/scenes/scene2d/ui/TextField String ()V clearSelection
com/badlogic/gdx/scenes/scene2d/ui/TextField init ()F getPrefHeight
com/badlogic/gdx/scenes/scene2d/ui/TextField init ()V copy
com/badlogic/gdx/scenes/scene2d/ui/Tree D ()LeM; getSelectedNode
com/badlogic/gdx/scenes/scene2d/ui/Tree D (LfI;)V findExpandedValues
com/badlogic/gdx/scenes/scene2d/ui/Tree D (Ljava/lang/Object;)LeM; findNode
com/badlogic/gdx/scenes/scene2d/ui/Tree I (LfI;)V expandAll
com/badlogic/gdx/scenes/scene2d/ui/Tree L (LeM;)V setOverNode
com/badlogic/gdx/scenes/scene2d/ui/Tree b_ ()V invalidate
com/badlogic/gdx/scenes/scene2d/ui/Tree java ()F plusMinusWidth
com/badlogic/gdx/scenes/scene2d/ui/Window B (I)V setResizeBorder
com/badlogic/gdx/scenes/scene2d/ui/Window D (LJ;FFFFF)V drawStageBackground
com/badlogic/gdx/scenes/scene2d/ui/Window D (Lcom/badlogic/gdx/scenes/scene2d/ui/Window$WindowStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/Window StackMapTable ()F getPrefWidth
dR B ()V row
dR B (LdR;)V merge
dR B (LeN;)LdR; height
dR B (Z)LdR; uniform
dR C (LeN;)LdR; prefWidth
dR Code (LeN;)LdR; spaceRight
dR D ()LdR; fill
dR D ()V reset
dR D (LdR;)V set
dR D (Ldl;)LdR; setActor
dR D (LeN;)LdR; width
dR D (LeN;LeN;)LdR; size
dR D (Z)LdR; fill
dR D (ZZ)LdR; uniform
dR F ()LdR; grow
dR F (LeN;)LdR; spaceBottom
dR I (LeN;)LdR; minHeight
dR L ()V clear
dR L (LeN;)LdR; minWidth
dR SourceFile (LeN;)LdR; padRight
dR StackMapTable ()LdR; expand
dR String ()LdR; uniform
dR V (LeN;)LdR; space
dR Z (LeN;)LdR; prefHeight
dR toString ()Ljava/lang/String; toString
dl B ()V clearListeners
dl B ()Z hasActions
dl B (FFFF)V setColor
dl B (FI)V setY
dl B (I)F getY
dl B (Lde;)Lde; stageToLocalCoordinates
dl B (Ldl;Lde;)Lde; localToActorCoordinates
dl B (Z)V setDebug
dl C (FF)V setOrigin
dl D ()Ldl; debug
dl D ()Ljava/lang/Object; getUserObject
dl D ()Ljava/lang/String; getName
dl D ()V clearActions
dl D (FFFF)V setBounds
dl D (FFI)V setPosition
dl D (FFZ)Ldl; hit
dl D (FI)V setX
dl D (I)F getX
dl D (LJ;F)V draw
dl D (Lca;)V drawDebug
dl D (Lcom/badlogic/gdx/graphics/Color;)V setColor
dl D (Ldn;)Z addListener
dl D (Ljava/lang/Object;)V setUserObject
dl D (Ljava/lang/String;)V setName
dl D (Z)V setVisible
dl F ()V clipEnd
dl I ()Z isTouchable
dl L ()V clear
dl L ()Z hasParent
dl L (Ldn;)Z addCaptureListener
dl S (FF)V setOrigin
dl S (Lde;)Lde; localToStageCoordinates
dl V ()V toBack
dl Z ()V toFront
dl b_ ()Z remove
do B ()Ldo; debugAll
do Code ()V childrenChanged
do D (F)V act
do D (I)Ldl; getChild
do D (II)Z swapActor
do D (ILdl;)V addActorAt
do D (LJ;)V resetTransform
do D (LJ;F)V draw
do D (LJ;Lcom/badlogic/gdx/math/Matrix4;)V applyTransform
do D (Lca;)V drawDebug
do D (Ldd;)V setCullingArea
do D (Ldl;)V addActor
do D (Ldt;)V setStage
do I (Lca;)V resetTransform
do L (Ldl;)Z removeActor
do L (Z)V setTransform
do LineNumberTable ()Z isTransform
do init ()Z hasChildren
eM C ()V expandTo
eM D ()F getHeight
eM D ()Z hasChildren
eM D (Ljava/lang/Object;)LeM; findNode
eM I ()V collapseAll
eM S ()V expandAll
eX D (LJ;F)V draw
eX Object ()Z needsLayout
eX StackMapTable ()V layout
eX V (Z)V setFillParent
eX b_ ()V invalidate
et B (Lca;)V drawDebugBounds
et B (LeN;)Let; padTop
et B (Z)V setDebug
et C ()Let; bottom
et C (Z)V setClip
et D (F)Let; pad
et D (FFFF)Let; pad
et D (FFZ)Ldl; hit
et D (I)Let; align
et D (LJ;F)V draw
et D (LJ;FFF)V drawBackground
et D (Lca;)V drawDebug
et D (Lcom/badlogic/gdx/scenes/scene2d/ui/Skin;)V setSkin
et D (LeN;)Let; pad
et D (LeN;LeN;LeN;LeN;)Let; pad
et D (Lez;)Let; debug
et D (Lfu;)Let; background
et D (Ljava/lang/CharSequence;)LdR; add
et D (Ljava/lang/CharSequence;Ljava/lang/String;)LdR; add
et D (Ljava/lang/CharSequence;Ljava/lang/String;Lcom/badlogic/gdx/graphics/Color;)LdR; add
et D (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)LdR; add
et D (Ljava/lang/String;)Let; background
et D ([Ldl;)LdR; stack
et D ([Ldl;)Let; add
et I ()Let; top
et I (LeN;)Let; padBottom
et L ()LdR; add
et L ()Let; center
et L (Ldl;)Z removeActor
et L (LeN;)Let; padLeft
et S ()Let; left
et S (LeN;)Let; padRight
et Z ()Let; right
et Z (Z)V setRound
et b_ ()V invalidate
et lang ()Z getClip
fI B ()Ljava/lang/Object; peek
fI B ()Z isEmpty
fI B (I)[Ljava/lang/Object; setSize
fI D ()Ljava/lang/Object; pop
fI D ()Z notEmpty
fI D (LfI;)V addAll
fI D (Ljava/lang/Object;)V add
fI D (Ljava/lang/Object;Z)Z contains
fI D (Ljava/util/Comparator;)V sort
fI D ([Ljava/lang/Object;)LfI; with
fI D ([Ljava/lang/Object;)V addAll
fI L ()Ljava/lang/Object; first
fI L ()V reverse
1 # 由 tools/obfmap/MapMethods 指纹比对自动生成(上游 libGDX 1.11.0)
2 # owner name desc newName
3 KP B (I)V setSelectedIndex
4 KP D (Ldt;)V setStage
5 com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
6 com/badlogic/gdx/scenes/scene2d/ui/Label D (II)V setAlignment
7 com/badlogic/gdx/scenes/scene2d/ui/Label D (Lcom/badlogic/gdx/scenes/scene2d/ui/Label$LabelStyle;)V setStyle
8 com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)Z textEquals
9 com/badlogic/gdx/scenes/scene2d/ui/Label I (Z)V setEllipsis
10 com/badlogic/gdx/scenes/scene2d/ui/Label L (Z)V setWrap
11 com/badlogic/gdx/scenes/scene2d/ui/Label SourceFile ()V invalidate
12 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane B (ZZ)V setScrollingDisabled
13 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (FFZ)Ldl; hit
14 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (ILdl;)V addActorAt
15 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Lcom/badlogic/gdx/scenes/scene2d/ui/ScrollPane$ScrollPaneStyle;)V setStyle
16 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;)V addActor
17 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;Z)Z removeActor
18 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (FFFF)V scrollTo
19 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (Ldl;)Z removeActor
20 com/badlogic/gdx/scenes/scene2d/ui/Slider D ()Lcom/badlogic/gdx/scenes/scene2d/ui/Slider$SliderStyle; getStyle
21 com/badlogic/gdx/scenes/scene2d/ui/Slider D ([FF)V setSnapToValues
22 com/badlogic/gdx/scenes/scene2d/ui/Slider init ()Z isDragging
23 com/badlogic/gdx/scenes/scene2d/ui/TextButton L (Ljava/lang/String;)V setText
24 com/badlogic/gdx/scenes/scene2d/ui/TextField B (I)Z withinMaxLength
25 com/badlogic/gdx/scenes/scene2d/ui/TextField D ()Ldr; createInputListener
26 com/badlogic/gdx/scenes/scene2d/ui/TextField D (ILjava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String; insert
27 com/badlogic/gdx/scenes/scene2d/ui/TextField LineNumberTable ()V selectAll
28 com/badlogic/gdx/scenes/scene2d/ui/TextField StackMapTable ()F getPrefWidth
29 com/badlogic/gdx/scenes/scene2d/ui/TextField String ()V clearSelection
30 com/badlogic/gdx/scenes/scene2d/ui/TextField init ()F getPrefHeight
31 com/badlogic/gdx/scenes/scene2d/ui/TextField init ()V copy
32 com/badlogic/gdx/scenes/scene2d/ui/Tree D ()LeM; getSelectedNode
33 com/badlogic/gdx/scenes/scene2d/ui/Tree D (LfI;)V findExpandedValues
34 com/badlogic/gdx/scenes/scene2d/ui/Tree D (Ljava/lang/Object;)LeM; findNode
35 com/badlogic/gdx/scenes/scene2d/ui/Tree I (LfI;)V expandAll
36 com/badlogic/gdx/scenes/scene2d/ui/Tree L (LeM;)V setOverNode
37 com/badlogic/gdx/scenes/scene2d/ui/Tree b_ ()V invalidate
38 com/badlogic/gdx/scenes/scene2d/ui/Tree java ()F plusMinusWidth
39 com/badlogic/gdx/scenes/scene2d/ui/Window B (I)V setResizeBorder
40 com/badlogic/gdx/scenes/scene2d/ui/Window D (LJ;FFFFF)V drawStageBackground
41 com/badlogic/gdx/scenes/scene2d/ui/Window D (Lcom/badlogic/gdx/scenes/scene2d/ui/Window$WindowStyle;)V setStyle
42 com/badlogic/gdx/scenes/scene2d/ui/Window StackMapTable ()F getPrefWidth
43 dR B ()V row
44 dR B (LdR;)V merge
45 dR B (LeN;)LdR; height
46 dR B (Z)LdR; uniform
47 dR C (LeN;)LdR; prefWidth
48 dR Code (LeN;)LdR; spaceRight
49 dR D ()LdR; fill
50 dR D ()V reset
51 dR D (LdR;)V set
52 dR D (Ldl;)LdR; setActor
53 dR D (LeN;)LdR; width
54 dR D (LeN;LeN;)LdR; size
55 dR D (Z)LdR; fill
56 dR D (ZZ)LdR; uniform
57 dR F ()LdR; grow
58 dR F (LeN;)LdR; spaceBottom
59 dR I (LeN;)LdR; minHeight
60 dR L ()V clear
61 dR L (LeN;)LdR; minWidth
62 dR SourceFile (LeN;)LdR; padRight
63 dR StackMapTable ()LdR; expand
64 dR String ()LdR; uniform
65 dR V (LeN;)LdR; space
66 dR Z (LeN;)LdR; prefHeight
67 dR toString ()Ljava/lang/String; toString
68 dl B ()V clearListeners
69 dl B ()Z hasActions
70 dl B (FFFF)V setColor
71 dl B (FI)V setY
72 dl B (I)F getY
73 dl B (Lde;)Lde; stageToLocalCoordinates
74 dl B (Ldl;Lde;)Lde; localToActorCoordinates
75 dl B (Z)V setDebug
76 dl C (FF)V setOrigin
77 dl D ()Ldl; debug
78 dl D ()Ljava/lang/Object; getUserObject
79 dl D ()Ljava/lang/String; getName
80 dl D ()V clearActions
81 dl D (FFFF)V setBounds
82 dl D (FFI)V setPosition
83 dl D (FFZ)Ldl; hit
84 dl D (FI)V setX
85 dl D (I)F getX
86 dl D (LJ;F)V draw
87 dl D (Lca;)V drawDebug
88 dl D (Lcom/badlogic/gdx/graphics/Color;)V setColor
89 dl D (Ldn;)Z addListener
90 dl D (Ljava/lang/Object;)V setUserObject
91 dl D (Ljava/lang/String;)V setName
92 dl D (Z)V setVisible
93 dl F ()V clipEnd
94 dl I ()Z isTouchable
95 dl L ()V clear
96 dl L ()Z hasParent
97 dl L (Ldn;)Z addCaptureListener
98 dl S (FF)V setOrigin
99 dl S (Lde;)Lde; localToStageCoordinates
100 dl V ()V toBack
101 dl Z ()V toFront
102 dl b_ ()Z remove
103 do B ()Ldo; debugAll
104 do Code ()V childrenChanged
105 do D (F)V act
106 do D (I)Ldl; getChild
107 do D (II)Z swapActor
108 do D (ILdl;)V addActorAt
109 do D (LJ;)V resetTransform
110 do D (LJ;F)V draw
111 do D (LJ;Lcom/badlogic/gdx/math/Matrix4;)V applyTransform
112 do D (Lca;)V drawDebug
113 do D (Ldd;)V setCullingArea
114 do D (Ldl;)V addActor
115 do D (Ldt;)V setStage
116 do I (Lca;)V resetTransform
117 do L (Ldl;)Z removeActor
118 do L (Z)V setTransform
119 do LineNumberTable ()Z isTransform
120 do init ()Z hasChildren
121 eM C ()V expandTo
122 eM D ()F getHeight
123 eM D ()Z hasChildren
124 eM D (Ljava/lang/Object;)LeM; findNode
125 eM I ()V collapseAll
126 eM S ()V expandAll
127 eX D (LJ;F)V draw
128 eX Object ()Z needsLayout
129 eX StackMapTable ()V layout
130 eX V (Z)V setFillParent
131 eX b_ ()V invalidate
132 et B (Lca;)V drawDebugBounds
133 et B (LeN;)Let; padTop
134 et B (Z)V setDebug
135 et C ()Let; bottom
136 et C (Z)V setClip
137 et D (F)Let; pad
138 et D (FFFF)Let; pad
139 et D (FFZ)Ldl; hit
140 et D (I)Let; align
141 et D (LJ;F)V draw
142 et D (LJ;FFF)V drawBackground
143 et D (Lca;)V drawDebug
144 et D (Lcom/badlogic/gdx/scenes/scene2d/ui/Skin;)V setSkin
145 et D (LeN;)Let; pad
146 et D (LeN;LeN;LeN;LeN;)Let; pad
147 et D (Lez;)Let; debug
148 et D (Lfu;)Let; background
149 et D (Ljava/lang/CharSequence;)LdR; add
150 et D (Ljava/lang/CharSequence;Ljava/lang/String;)LdR; add
151 et D (Ljava/lang/CharSequence;Ljava/lang/String;Lcom/badlogic/gdx/graphics/Color;)LdR; add
152 et D (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)LdR; add
153 et D (Ljava/lang/String;)Let; background
154 et D ([Ldl;)LdR; stack
155 et D ([Ldl;)Let; add
156 et I ()Let; top
157 et I (LeN;)Let; padBottom
158 et L ()LdR; add
159 et L ()Let; center
160 et L (Ldl;)Z removeActor
161 et L (LeN;)Let; padLeft
162 et S ()Let; left
163 et S (LeN;)Let; padRight
164 et Z ()Let; right
165 et Z (Z)V setRound
166 et b_ ()V invalidate
167 et lang ()Z getClip
168 fI B ()Ljava/lang/Object; peek
169 fI B ()Z isEmpty
170 fI B (I)[Ljava/lang/Object; setSize
171 fI D ()Ljava/lang/Object; pop
172 fI D ()Z notEmpty
173 fI D (LfI;)V addAll
174 fI D (Ljava/lang/Object;)V add
175 fI D (Ljava/lang/Object;Z)Z contains
176 fI D (Ljava/util/Comparator;)V sort
177 fI D ([Ljava/lang/Object;)LfI; with
178 fI D ([Ljava/lang/Object;)V addAll
179 fI L ()Ljava/lang/Object; first
180 fI L ()V reverse
+29
View File
@@ -0,0 +1,29 @@
# 名字映射锚点(只放**已确定**的;每条都要能说清依据,依据写进 混淆名对照表.md)
# 列:owner name desc newName (desc 写 * 表示该 owner 下所有同名成员)
#
# ---- libGDX:Table(载荷名 et);依据 Obf.java + 直读字节码 ----
et D (Ldl;)LdR; add
et D ([Ldl;)Let; addAll
et I ()LdR; row
et Z (Z)V setFillParent
et SourceFile ()V clearChildren
et D (Ljava/lang/CharSequence;)LdR; addLabel
# ---- libGDX:Cell(载荷名 dR)----
dR StackMapTable ()LdR; expand
dR D ()LdR; fill
dR C ()LdR; left
dR S ()LdR; top
# ---- libGDX:Button / Label(保名类)----
com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
com/badlogic/gdx/scenes/scene2d/ui/Button B (ZZ)V setCheckedAndFire
com/badlogic/gdx/scenes/scene2d/ui/Button a_ (Z)V setDisabled
com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)V setText
com/badlogic/gdx/scenes/scene2d/ui/Label D ()Ljava/lang/StringBuilder; getTextBuilder
# ---- libGDX:SelectBox(载荷名 KP);依据 DumpSites 解出的 (Ljava/lang/String;)V ----
KP B (Ljava/lang/String;)V setSelected
KP B ()Ljava/lang/String; getSelected
KP B (I)V setSelectedIndex
# ---- 场景模型:Gp = 骨骼;依据 gp_2.java 的 toString()/D(String,boolean)V ----
Gp D Ljava/lang/String; name
Gp D (Ljava/lang/String;Z)V setName
1 # 名字映射锚点(只放**已确定**的;每条都要能说清依据,依据写进 混淆名对照表.md)
2 # 列:owner name desc newName (desc 写 * 表示该 owner 下所有同名成员)
3 #
4 # ---- libGDX:Table(载荷名 et);依据 Obf.java + 直读字节码 ----
5 et D (Ldl;)LdR; add
6 et D ([Ldl;)Let; addAll
7 et I ()LdR; row
8 et Z (Z)V setFillParent
9 et SourceFile ()V clearChildren
10 et D (Ljava/lang/CharSequence;)LdR; addLabel
11 # ---- libGDX:Cell(载荷名 dR)----
12 dR StackMapTable ()LdR; expand
13 dR D ()LdR; fill
14 dR C ()LdR; left
15 dR S ()LdR; top
16 # ---- libGDX:Button / Label(保名类)----
17 com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
18 com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
19 com/badlogic/gdx/scenes/scene2d/ui/Button B (ZZ)V setCheckedAndFire
20 com/badlogic/gdx/scenes/scene2d/ui/Button a_ (Z)V setDisabled
21 com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)V setText
22 com/badlogic/gdx/scenes/scene2d/ui/Label D ()Ljava/lang/StringBuilder; getTextBuilder
23 # ---- libGDX:SelectBox(载荷名 KP);依据 DumpSites 解出的 (Ljava/lang/String;)V ----
24 KP B (Ljava/lang/String;)V setSelected
25 KP B ()Ljava/lang/String; getSelected
26 KP B (I)V setSelectedIndex
27 # ---- 场景模型:Gp = 骨骼;依据 gp_2.java 的 toString()/D(String,boolean)V ----
28 Gp D Ljava/lang/String; name
29 Gp D (Ljava/lang/String;Z)V setName
+194
View File
@@ -0,0 +1,194 @@
# 合并后的名字映射表:先由 MapMethods 指纹自动生成,再用手动锚点覆盖
# owner name desc newName
Gp D (Ljava/lang/String;Z)V setName
Gp D Ljava/lang/String; name
KP B ()Ljava/lang/String; getSelected
KP B (I)V setSelectedIndex
KP B (Ljava/lang/String;)V setSelected
KP D (Ldt;)V setStage
com/badlogic/gdx/scenes/scene2d/ui/Button B (ZZ)V setCheckedAndFire
com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
com/badlogic/gdx/scenes/scene2d/ui/Button a_ (Z)V setDisabled
com/badlogic/gdx/scenes/scene2d/ui/Label D ()Ljava/lang/StringBuilder; getTextBuilder
com/badlogic/gdx/scenes/scene2d/ui/Label D (II)V setAlignment
com/badlogic/gdx/scenes/scene2d/ui/Label D (Lcom/badlogic/gdx/scenes/scene2d/ui/Label$LabelStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)V setText
com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)Z textEquals
com/badlogic/gdx/scenes/scene2d/ui/Label I (Z)V setEllipsis
com/badlogic/gdx/scenes/scene2d/ui/Label L (Z)V setWrap
com/badlogic/gdx/scenes/scene2d/ui/Label SourceFile ()V invalidate
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane B (ZZ)V setScrollingDisabled
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (FFZ)Ldl; hit
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (ILdl;)V addActorAt
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Lcom/badlogic/gdx/scenes/scene2d/ui/ScrollPane$ScrollPaneStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;)V addActor
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;Z)Z removeActor
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (FFFF)V scrollTo
com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (Ldl;)Z removeActor
com/badlogic/gdx/scenes/scene2d/ui/Slider D ()Lcom/badlogic/gdx/scenes/scene2d/ui/Slider$SliderStyle; getStyle
com/badlogic/gdx/scenes/scene2d/ui/Slider D ([FF)V setSnapToValues
com/badlogic/gdx/scenes/scene2d/ui/Slider init ()Z isDragging
com/badlogic/gdx/scenes/scene2d/ui/TextButton L (Ljava/lang/String;)V setText
com/badlogic/gdx/scenes/scene2d/ui/TextField B (I)Z withinMaxLength
com/badlogic/gdx/scenes/scene2d/ui/TextField D ()Ldr; createInputListener
com/badlogic/gdx/scenes/scene2d/ui/TextField D (ILjava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String; insert
com/badlogic/gdx/scenes/scene2d/ui/TextField LineNumberTable ()V selectAll
com/badlogic/gdx/scenes/scene2d/ui/TextField StackMapTable ()F getPrefWidth
com/badlogic/gdx/scenes/scene2d/ui/TextField String ()V clearSelection
com/badlogic/gdx/scenes/scene2d/ui/TextField init ()F getPrefHeight
com/badlogic/gdx/scenes/scene2d/ui/TextField init ()V copy
com/badlogic/gdx/scenes/scene2d/ui/Tree D ()LeM; getSelectedNode
com/badlogic/gdx/scenes/scene2d/ui/Tree D (LfI;)V findExpandedValues
com/badlogic/gdx/scenes/scene2d/ui/Tree D (Ljava/lang/Object;)LeM; findNode
com/badlogic/gdx/scenes/scene2d/ui/Tree I (LfI;)V expandAll
com/badlogic/gdx/scenes/scene2d/ui/Tree L (LeM;)V setOverNode
com/badlogic/gdx/scenes/scene2d/ui/Tree b_ ()V invalidate
com/badlogic/gdx/scenes/scene2d/ui/Tree java ()F plusMinusWidth
com/badlogic/gdx/scenes/scene2d/ui/Window B (I)V setResizeBorder
com/badlogic/gdx/scenes/scene2d/ui/Window D (LJ;FFFFF)V drawStageBackground
com/badlogic/gdx/scenes/scene2d/ui/Window D (Lcom/badlogic/gdx/scenes/scene2d/ui/Window$WindowStyle;)V setStyle
com/badlogic/gdx/scenes/scene2d/ui/Window StackMapTable ()F getPrefWidth
dR B ()V row
dR B (LdR;)V merge
dR B (LeN;)LdR; height
dR B (Z)LdR; uniform
dR C ()LdR; left
dR C (LeN;)LdR; prefWidth
dR Code (LeN;)LdR; spaceRight
dR D ()LdR; fill
dR D ()V reset
dR D (LdR;)V set
dR D (Ldl;)LdR; setActor
dR D (LeN;)LdR; width
dR D (LeN;LeN;)LdR; size
dR D (Z)LdR; fill
dR D (ZZ)LdR; uniform
dR F ()LdR; grow
dR F (LeN;)LdR; spaceBottom
dR I (LeN;)LdR; minHeight
dR L ()V clear
dR L (LeN;)LdR; minWidth
dR S ()LdR; top
dR SourceFile (LeN;)LdR; padRight
dR StackMapTable ()LdR; expand
dR String ()LdR; uniform
dR V (LeN;)LdR; space
dR Z (LeN;)LdR; prefHeight
dR toString ()Ljava/lang/String; toString
dl B ()V clearListeners
dl B ()Z hasActions
dl B (FFFF)V setColor
dl B (FI)V setY
dl B (I)F getY
dl B (Lde;)Lde; stageToLocalCoordinates
dl B (Ldl;Lde;)Lde; localToActorCoordinates
dl B (Z)V setDebug
dl C (FF)V setOrigin
dl D ()Ldl; debug
dl D ()Ljava/lang/Object; getUserObject
dl D ()Ljava/lang/String; getName
dl D ()V clearActions
dl D (FFFF)V setBounds
dl D (FFI)V setPosition
dl D (FFZ)Ldl; hit
dl D (FI)V setX
dl D (I)F getX
dl D (LJ;F)V draw
dl D (Lca;)V drawDebug
dl D (Lcom/badlogic/gdx/graphics/Color;)V setColor
dl D (Ldn;)Z addListener
dl D (Ljava/lang/Object;)V setUserObject
dl D (Ljava/lang/String;)V setName
dl D (Z)V setVisible
dl F ()V clipEnd
dl I ()Z isTouchable
dl L ()V clear
dl L ()Z hasParent
dl L (Ldn;)Z addCaptureListener
dl S (FF)V setOrigin
dl S (Lde;)Lde; localToStageCoordinates
dl V ()V toBack
dl Z ()V toFront
dl b_ ()Z remove
do B ()Ldo; debugAll
do Code ()V childrenChanged
do D (F)V act
do D (I)Ldl; getChild
do D (II)Z swapActor
do D (ILdl;)V addActorAt
do D (LJ;)V resetTransform
do D (LJ;F)V draw
do D (LJ;Lcom/badlogic/gdx/math/Matrix4;)V applyTransform
do D (Lca;)V drawDebug
do D (Ldd;)V setCullingArea
do D (Ldl;)V addActor
do D (Ldt;)V setStage
do I (Lca;)V resetTransform
do L (Ldl;)Z removeActor
do L (Z)V setTransform
do LineNumberTable ()Z isTransform
do init ()Z hasChildren
eM C ()V expandTo
eM D ()F getHeight
eM D ()Z hasChildren
eM D (Ljava/lang/Object;)LeM; findNode
eM I ()V collapseAll
eM S ()V expandAll
eX D (LJ;F)V draw
eX Object ()Z needsLayout
eX StackMapTable ()V layout
eX V (Z)V setFillParent
eX b_ ()V invalidate
et B (Lca;)V drawDebugBounds
et B (LeN;)Let; padTop
et B (Z)V setDebug
et C ()Let; bottom
et C (Z)V setClip
et D (F)Let; pad
et D (FFFF)Let; pad
et D (FFZ)Ldl; hit
et D (I)Let; align
et D (LJ;F)V draw
et D (LJ;FFF)V drawBackground
et D (Lca;)V drawDebug
et D (Lcom/badlogic/gdx/scenes/scene2d/ui/Skin;)V setSkin
et D (Ldl;)LdR; add
et D (LeN;)Let; pad
et D (LeN;LeN;LeN;LeN;)Let; pad
et D (Lez;)Let; debug
et D (Lfu;)Let; background
et D (Ljava/lang/CharSequence;)LdR; addLabel
et D (Ljava/lang/CharSequence;Ljava/lang/String;)LdR; add
et D (Ljava/lang/CharSequence;Ljava/lang/String;Lcom/badlogic/gdx/graphics/Color;)LdR; add
et D (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)LdR; add
et D (Ljava/lang/String;)Let; background
et D ([Ldl;)LdR; stack
et D ([Ldl;)Let; addAll
et I ()LdR; row
et I ()Let; top
et I (LeN;)Let; padBottom
et L ()LdR; add
et L ()Let; center
et L (Ldl;)Z removeActor
et L (LeN;)Let; padLeft
et S ()Let; left
et S (LeN;)Let; padRight
et SourceFile ()V clearChildren
et Z ()Let; right
et Z (Z)V setFillParent
et b_ ()V invalidate
et lang ()Z getClip
fI B ()Ljava/lang/Object; peek
fI B ()Z isEmpty
fI B (I)[Ljava/lang/Object; setSize
fI D ()Ljava/lang/Object; pop
fI D ()Z notEmpty
fI D (LfI;)V addAll
fI D (Ljava/lang/Object;)V add
fI D (Ljava/lang/Object;Z)Z contains
fI D (Ljava/util/Comparator;)V sort
fI D ([Ljava/lang/Object;)LfI; with
fI D ([Ljava/lang/Object;)V addAll
fI L ()Ljava/lang/Object; first
fI L ()V reverse
1 # 合并后的名字映射表:先由 MapMethods 指纹自动生成,再用手动锚点覆盖
2 # owner name desc newName
3 Gp D (Ljava/lang/String;Z)V setName
4 Gp D Ljava/lang/String; name
5 KP B ()Ljava/lang/String; getSelected
6 KP B (I)V setSelectedIndex
7 KP B (Ljava/lang/String;)V setSelected
8 KP D (Ldt;)V setStage
9 com/badlogic/gdx/scenes/scene2d/ui/Button B (ZZ)V setCheckedAndFire
10 com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
11 com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
12 com/badlogic/gdx/scenes/scene2d/ui/Button a_ (Z)V setDisabled
13 com/badlogic/gdx/scenes/scene2d/ui/Label D ()Ljava/lang/StringBuilder; getTextBuilder
14 com/badlogic/gdx/scenes/scene2d/ui/Label D (II)V setAlignment
15 com/badlogic/gdx/scenes/scene2d/ui/Label D (Lcom/badlogic/gdx/scenes/scene2d/ui/Label$LabelStyle;)V setStyle
16 com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)V setText
17 com/badlogic/gdx/scenes/scene2d/ui/Label D (Ljava/lang/CharSequence;)Z textEquals
18 com/badlogic/gdx/scenes/scene2d/ui/Label I (Z)V setEllipsis
19 com/badlogic/gdx/scenes/scene2d/ui/Label L (Z)V setWrap
20 com/badlogic/gdx/scenes/scene2d/ui/Label SourceFile ()V invalidate
21 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane B (ZZ)V setScrollingDisabled
22 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (FFZ)Ldl; hit
23 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (ILdl;)V addActorAt
24 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Lcom/badlogic/gdx/scenes/scene2d/ui/ScrollPane$ScrollPaneStyle;)V setStyle
25 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;)V addActor
26 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane D (Ldl;Z)Z removeActor
27 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (FFFF)V scrollTo
28 com/badlogic/gdx/scenes/scene2d/ui/ScrollPane L (Ldl;)Z removeActor
29 com/badlogic/gdx/scenes/scene2d/ui/Slider D ()Lcom/badlogic/gdx/scenes/scene2d/ui/Slider$SliderStyle; getStyle
30 com/badlogic/gdx/scenes/scene2d/ui/Slider D ([FF)V setSnapToValues
31 com/badlogic/gdx/scenes/scene2d/ui/Slider init ()Z isDragging
32 com/badlogic/gdx/scenes/scene2d/ui/TextButton L (Ljava/lang/String;)V setText
33 com/badlogic/gdx/scenes/scene2d/ui/TextField B (I)Z withinMaxLength
34 com/badlogic/gdx/scenes/scene2d/ui/TextField D ()Ldr; createInputListener
35 com/badlogic/gdx/scenes/scene2d/ui/TextField D (ILjava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String; insert
36 com/badlogic/gdx/scenes/scene2d/ui/TextField LineNumberTable ()V selectAll
37 com/badlogic/gdx/scenes/scene2d/ui/TextField StackMapTable ()F getPrefWidth
38 com/badlogic/gdx/scenes/scene2d/ui/TextField String ()V clearSelection
39 com/badlogic/gdx/scenes/scene2d/ui/TextField init ()F getPrefHeight
40 com/badlogic/gdx/scenes/scene2d/ui/TextField init ()V copy
41 com/badlogic/gdx/scenes/scene2d/ui/Tree D ()LeM; getSelectedNode
42 com/badlogic/gdx/scenes/scene2d/ui/Tree D (LfI;)V findExpandedValues
43 com/badlogic/gdx/scenes/scene2d/ui/Tree D (Ljava/lang/Object;)LeM; findNode
44 com/badlogic/gdx/scenes/scene2d/ui/Tree I (LfI;)V expandAll
45 com/badlogic/gdx/scenes/scene2d/ui/Tree L (LeM;)V setOverNode
46 com/badlogic/gdx/scenes/scene2d/ui/Tree b_ ()V invalidate
47 com/badlogic/gdx/scenes/scene2d/ui/Tree java ()F plusMinusWidth
48 com/badlogic/gdx/scenes/scene2d/ui/Window B (I)V setResizeBorder
49 com/badlogic/gdx/scenes/scene2d/ui/Window D (LJ;FFFFF)V drawStageBackground
50 com/badlogic/gdx/scenes/scene2d/ui/Window D (Lcom/badlogic/gdx/scenes/scene2d/ui/Window$WindowStyle;)V setStyle
51 com/badlogic/gdx/scenes/scene2d/ui/Window StackMapTable ()F getPrefWidth
52 dR B ()V row
53 dR B (LdR;)V merge
54 dR B (LeN;)LdR; height
55 dR B (Z)LdR; uniform
56 dR C ()LdR; left
57 dR C (LeN;)LdR; prefWidth
58 dR Code (LeN;)LdR; spaceRight
59 dR D ()LdR; fill
60 dR D ()V reset
61 dR D (LdR;)V set
62 dR D (Ldl;)LdR; setActor
63 dR D (LeN;)LdR; width
64 dR D (LeN;LeN;)LdR; size
65 dR D (Z)LdR; fill
66 dR D (ZZ)LdR; uniform
67 dR F ()LdR; grow
68 dR F (LeN;)LdR; spaceBottom
69 dR I (LeN;)LdR; minHeight
70 dR L ()V clear
71 dR L (LeN;)LdR; minWidth
72 dR S ()LdR; top
73 dR SourceFile (LeN;)LdR; padRight
74 dR StackMapTable ()LdR; expand
75 dR String ()LdR; uniform
76 dR V (LeN;)LdR; space
77 dR Z (LeN;)LdR; prefHeight
78 dR toString ()Ljava/lang/String; toString
79 dl B ()V clearListeners
80 dl B ()Z hasActions
81 dl B (FFFF)V setColor
82 dl B (FI)V setY
83 dl B (I)F getY
84 dl B (Lde;)Lde; stageToLocalCoordinates
85 dl B (Ldl;Lde;)Lde; localToActorCoordinates
86 dl B (Z)V setDebug
87 dl C (FF)V setOrigin
88 dl D ()Ldl; debug
89 dl D ()Ljava/lang/Object; getUserObject
90 dl D ()Ljava/lang/String; getName
91 dl D ()V clearActions
92 dl D (FFFF)V setBounds
93 dl D (FFI)V setPosition
94 dl D (FFZ)Ldl; hit
95 dl D (FI)V setX
96 dl D (I)F getX
97 dl D (LJ;F)V draw
98 dl D (Lca;)V drawDebug
99 dl D (Lcom/badlogic/gdx/graphics/Color;)V setColor
100 dl D (Ldn;)Z addListener
101 dl D (Ljava/lang/Object;)V setUserObject
102 dl D (Ljava/lang/String;)V setName
103 dl D (Z)V setVisible
104 dl F ()V clipEnd
105 dl I ()Z isTouchable
106 dl L ()V clear
107 dl L ()Z hasParent
108 dl L (Ldn;)Z addCaptureListener
109 dl S (FF)V setOrigin
110 dl S (Lde;)Lde; localToStageCoordinates
111 dl V ()V toBack
112 dl Z ()V toFront
113 dl b_ ()Z remove
114 do B ()Ldo; debugAll
115 do Code ()V childrenChanged
116 do D (F)V act
117 do D (I)Ldl; getChild
118 do D (II)Z swapActor
119 do D (ILdl;)V addActorAt
120 do D (LJ;)V resetTransform
121 do D (LJ;F)V draw
122 do D (LJ;Lcom/badlogic/gdx/math/Matrix4;)V applyTransform
123 do D (Lca;)V drawDebug
124 do D (Ldd;)V setCullingArea
125 do D (Ldl;)V addActor
126 do D (Ldt;)V setStage
127 do I (Lca;)V resetTransform
128 do L (Ldl;)Z removeActor
129 do L (Z)V setTransform
130 do LineNumberTable ()Z isTransform
131 do init ()Z hasChildren
132 eM C ()V expandTo
133 eM D ()F getHeight
134 eM D ()Z hasChildren
135 eM D (Ljava/lang/Object;)LeM; findNode
136 eM I ()V collapseAll
137 eM S ()V expandAll
138 eX D (LJ;F)V draw
139 eX Object ()Z needsLayout
140 eX StackMapTable ()V layout
141 eX V (Z)V setFillParent
142 eX b_ ()V invalidate
143 et B (Lca;)V drawDebugBounds
144 et B (LeN;)Let; padTop
145 et B (Z)V setDebug
146 et C ()Let; bottom
147 et C (Z)V setClip
148 et D (F)Let; pad
149 et D (FFFF)Let; pad
150 et D (FFZ)Ldl; hit
151 et D (I)Let; align
152 et D (LJ;F)V draw
153 et D (LJ;FFF)V drawBackground
154 et D (Lca;)V drawDebug
155 et D (Lcom/badlogic/gdx/scenes/scene2d/ui/Skin;)V setSkin
156 et D (Ldl;)LdR; add
157 et D (LeN;)Let; pad
158 et D (LeN;LeN;LeN;LeN;)Let; pad
159 et D (Lez;)Let; debug
160 et D (Lfu;)Let; background
161 et D (Ljava/lang/CharSequence;)LdR; addLabel
162 et D (Ljava/lang/CharSequence;Ljava/lang/String;)LdR; add
163 et D (Ljava/lang/CharSequence;Ljava/lang/String;Lcom/badlogic/gdx/graphics/Color;)LdR; add
164 et D (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)LdR; add
165 et D (Ljava/lang/String;)Let; background
166 et D ([Ldl;)LdR; stack
167 et D ([Ldl;)Let; addAll
168 et I ()LdR; row
169 et I ()Let; top
170 et I (LeN;)Let; padBottom
171 et L ()LdR; add
172 et L ()Let; center
173 et L (Ldl;)Z removeActor
174 et L (LeN;)Let; padLeft
175 et S ()Let; left
176 et S (LeN;)Let; padRight
177 et SourceFile ()V clearChildren
178 et Z ()Let; right
179 et Z (Z)V setFillParent
180 et b_ ()V invalidate
181 et lang ()Z getClip
182 fI B ()Ljava/lang/Object; peek
183 fI B ()Z isEmpty
184 fI B (I)[Ljava/lang/Object; setSize
185 fI D ()Ljava/lang/Object; pop
186 fI D ()Z notEmpty
187 fI D (LfI;)V addAll
188 fI D (Ljava/lang/Object;)V add
189 fI D (Ljava/lang/Object;Z)Z contains
190 fI D (Ljava/util/Comparator;)V sort
191 fI D ([Ljava/lang/Object;)LfI; with
192 fI D ([Ljava/lang/Object;)V addAll
193 fI L ()Ljava/lang/Object; first
194 fI L ()V reverse
+10
View File
@@ -143,6 +143,16 @@ public final class Launcher {
System.err.println("[probe] ==== " + probe + " ====");
if ("objects".equals(probe)) {
System.err.println(SceneObjects.describe(viewLoader));
} else if ("panels".equals(probe)) {
System.err.println(SceneActions.describePanels(viewLoader));
} else if ("matrix".equals(probe)) {
System.err.println(SceneActions.probeMatrix(viewLoader));
} else if ("panel".equals(probe)) {
System.err.println(ObjectPanel.probePanel(viewLoader));
} else if ("select".equals(probe)) {
System.err.println(SceneActions.probeSelect(viewLoader));
} else if ("crud".equals(probe)) {
System.err.println(SceneActions.probeCrud(viewLoader));
} else {
System.err.println("[probe] 未知探针: " + probe);
}
+5
View File
@@ -64,6 +64,11 @@ public final class Obf {
public static final String TABLE_ADD_VARARGS = "D";
/** `row() -> Cell` —— 换行的关键,不调用的话所有控件都挤在第一行 */
public static final String TABLE_ROW = "I";
/**
* `clearChildren() -> void`(体 = 逐个 actor.remove() + cells.clear() + rows/columns 归零)。
* 重建列表时用它把旧行清掉。
*/
public static final String TABLE_CLEAR_CHILDREN = "SourceFile";
/** `setFillParent(boolean)`(WidgetGroup 的,让面板铺满停靠框) */
public static final String TABLE_SET_FILL_PARENT = "Z";
/** `setClip(boolean)` */
@@ -0,0 +1,650 @@
package com.spineview;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
/**
* "Spine View" 面板的内容:**场景对象的增删改查**。
*
* 布局(都是英文,编辑器缺中文字形):
* ```
* [Refresh] [New Bone]
* [New Slot] [Delete] [Rename]
* [新名字 TextField]
* ┌ 对象列表(每个对象一行,点一下就选中它)┐
* │ skeleton skeleton │
* │ bone root │
* │ ... │
* └─────────────────────────────────────────┘
* 状态行
* ```
*
* 两条硬约束(都在 `编辑器交互与数据API.md` 里):
* 1. **凡是要动编辑器模型/控件的代码,必须在编辑器的渲染线程上跑** ——
* 统一走 {@link SceneActions#onEditorThread}(它用编辑器的 `OG.B(Runnable)`)。
* 2. **所有动作都作用在"当前选择"上** —— 新建/删除/改名之前先 {@link SceneActions#select}。
*/
public final class ObjectPanel {
private ObjectPanel() {
}
/** 面板状态(静态:按钮回调是另一条线程进来的,靠它记住"当前对着谁操作")。 */
private static final class State {
ClassLoader loader;
Object skin;
Object listTable;
Object status;
Object nameField;
List<Object> objects = new ArrayList<Object>();
Object target;
}
/**
* 建面板(由 {@link ViewInstaller#buildPanel} 调用)。返回的 Table 会被塞进 us.spineView。
*/
public static Object build(ClassLoader loader, Object skin) throws Exception {
final State state = new State();
state.loader = loader;
state.skin = skin;
Class<?> tableClass = Class.forName(Obf.TABLE, true, loader);
Class<?> cellClass = Class.forName(Obf.CELL, true, loader);
Class<?> actorClass = Class.forName(Obf.ACTOR, true, loader);
Class<?> scrollClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.ScrollPane", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
Class<?> fieldClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextField", true, loader);
Object root = tableClass.getConstructor().newInstance();
Object rows = tableClass.getConstructor().newInstance();
state.listTable = tableClass.getConstructor().newInstance();
Object scroll = scrollClass.getConstructor(actorClass, skinClass).newInstance(state.listTable, skin);
state.status = label(loader, skin, "Press Refresh to list scene objects.");
state.nameField = fieldClass.getConstructor(String.class, skinClass)
.newInstance("particle_bone", skin);
// 让面板铺满停靠框(WidgetGroup.setFillParent)。
call(root, Obf.TABLE_SET_FILL_PARENT, new Class<?>[] {boolean.class}, void.class, Boolean.TRUE);
Object refresh = button(loader, skin, "Refresh");
Object newBone = button(loader, skin, "New Bone");
Object newSlot = button(loader, skin, "New Slot");
Object newBBox = button(loader, skin, "New BBox");
Object newPath = button(loader, skin, "New Path");
Object delete = button(loader, skin, "Delete");
Object rename = button(loader, skin, "Rename");
Object inspect = button(loader, skin, "Inspect");
Object cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, refresh);
layoutCell(cell, Obf.CELL_LEFT);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newBone);
layoutCell(cell, Obf.CELL_LEFT);
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newSlot);
layoutCell(cell, Obf.CELL_LEFT);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, delete);
layoutCell(cell, Obf.CELL_LEFT);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, rename);
layoutCell(cell, Obf.CELL_LEFT);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, inspect);
layoutCell(cell, Obf.CELL_LEFT);
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newBBox);
layoutCell(cell, Obf.CELL_LEFT);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newPath);
layoutCell(cell, Obf.CELL_LEFT);
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_EXPAND);
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
// 第 1 行:按钮区;第 2 行:对象列表(撑满);第 3 行:状态行。
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, rows);
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_FILL);
call(root, Obf.TABLE_ROW, new Class<?>[0], cellClass);
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, scroll);
layoutCell(cell, Obf.CELL_EXPAND, Obf.CELL_FILL, Obf.CELL_TOP);
call(root, Obf.TABLE_ROW, new Class<?>[0], cellClass);
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.status);
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_FILL);
wire(loader, refresh, new Runnable() {
@Override
public void run() {
refreshList(state);
}
});
wire(loader, newBone, new Runnable() {
@Override
public void run() {
create(state, "Bone");
}
});
wire(loader, newSlot, new Runnable() {
@Override
public void run() {
create(state, "Slot");
}
});
wire(loader, newBBox, new Runnable() {
@Override
public void run() {
create(state, "Bounding Box");
}
});
wire(loader, newPath, new Runnable() {
@Override
public void run() {
create(state, "Path");
}
});
wire(loader, inspect, new Runnable() {
@Override
public void run() {
inspect(state);
}
});
wire(loader, delete, new Runnable() {
@Override
public void run() {
if (state.target == null) {
setStatus(state, "Delete: select an object first.");
return;
}
Object victim = state.target;
String what = SceneObjects.kind(victim) + " " + SceneObjects.nameOf(victim);
state.target = null;
boolean ok = SceneActions.delete(state.loader, victim);
setStatus(state, (ok ? "Deleted " : "Delete failed: ") + what);
refreshList(state);
}
});
wire(loader, rename, new Runnable() {
@Override
public void run() {
if (state.target == null) {
setStatus(state, "Rename: select an object first.");
return;
}
String name = textFieldText(state.nameField);
boolean ok = SceneActions.rename(state.loader, state.target, name);
setStatus(state, (ok ? "Renamed to " : "Rename failed: ") + name);
refreshList(state);
}
});
System.err.println("[panel] 增删改查面板已建好");
refreshList(state);
return root;
}
/** 重新读一遍树、重建对象列表(全部在编辑器渲染线程上做)。 */
private static void refreshList(final State state) {
SceneActions.onEditorThread(state.loader, new Runnable() {
@Override
public void run() {
List<Object> objects;
try {
objects = SceneObjects.listAllNodes(state.loader);
} catch (Throwable error) {
setStatus(state, "List failed: " + error);
return;
}
state.objects = objects;
try {
// Table.clearChildren()(载荷名 `SourceFile()V`):把列表里的行全清掉重建。
call(state.listTable, Obf.TABLE_CLEAR_CHILDREN, new Class<?>[0], void.class);
Class<?> cellClass = Class.forName(Obf.CELL, true, state.loader);
Class<?> actorClass = Class.forName(Obf.ACTOR, true, state.loader);
for (int i = 0; i < objects.size() && i < 300; i++) {
final Object object = objects.get(i);
Object row = button(state.loader, state.skin,
SceneObjects.kind(object) + " " + SceneObjects.nameOf(object));
Object cell = call(state.listTable, Obf.TABLE_ADD,
new Class<?>[] {actorClass}, cellClass, row);
layoutCell(cell, Obf.CELL_LEFT);
call(state.listTable, Obf.TABLE_ROW, new Class<?>[0], cellClass);
wire(state.loader, row, new Runnable() {
@Override
public void run() {
selectTarget(state, object);
}
});
}
} catch (Throwable error) {
setStatus(state, "List build failed: " + error);
return;
}
setStatus(state, objects.size() + " scene objects"
+ (state.target == null ? "" : "; target = " + SceneObjects.kind(state.target)
+ " " + SceneObjects.nameOf(state.target)));
}
});
}
/** 点列表里的某一行:在编辑器层级树里选中它,并把它记成"当前操作对象"。 */
private static void selectTarget(final State state, final Object object) {
state.target = object;
boolean ok = SceneActions.select(state.loader, object);
setStatus(state, (ok ? "Selected " : "Select failed: ")
+ SceneObjects.kind(object) + " " + SceneObjects.nameOf(object));
}
/** 新建:以当前对象为父级跑编辑器的 `New <type>`。 */
/**
* 新建:以当前对象为父级跑编辑器的 `New <type>`。
* 名称输入框里**有字就用它**,没字就让编辑器那套自动化自己生成(`New Slot` 会弹输入框,
* 面板会自动把名字填进去并点 OK,见 SceneActions#fillDialogWhenReady)。
*/
private static void create(final State state, final String type) {
if (state.target == null) {
setStatus(state, "New " + type + ": select a parent object first.");
return;
}
Object parent = state.target;
String typed = textFieldText(state.nameField);
SceneActions.nextName(typed == null ? "" : typed.trim());
boolean ok = SceneActions.create(state.loader, parent, type);
String used = SceneActions.lastName();
setStatus(state, (ok ? "New " + type + " under " : "New " + type + " failed: ")
+ SceneObjects.kind(parent) + " " + SceneObjects.nameOf(parent)
+ (used == null ? "" : " name=" + used));
refreshList(state);
}
/**
* Inspector:把**编辑器里当前选中对象**的字段与方法列到列表区。
* 字段名是被混淆的(一堆 D/B/L),所以每行都带上类型;对象引用只显示类名。
*/
private static void inspect(final State state) {
SceneActions.onEditorThread(state.loader, new Runnable() {
@Override
public void run() {
Object object;
try {
object = SceneObjects.selected(state.loader);
} catch (Throwable error) {
setStatus(state, "Inspect failed: " + error);
return;
}
if (object == null) {
setStatus(state, "Inspect: nothing selected (click a row, or select in the editor tree).");
return;
}
List<String> lines = new ArrayList<String>();
lines.add("selected: " + SceneObjects.kind(object) + " \"" + SceneObjects.nameOf(object)
+ "\" (" + object.getClass().getName() + ")");
lines.add("--- fields (" + countFields(object) + ") ---");
for (Class<?> type = object.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
String value;
try {
field.setAccessible(true);
Object raw = field.get(object);
value = raw == null ? "null"
: (raw instanceof Number || raw instanceof Boolean || raw instanceof String)
? String.valueOf(raw) : raw.getClass().getName();
} catch (Throwable error) {
value = "<" + error.getClass().getSimpleName() + ">";
}
lines.add(" " + shortType(field.getType().getName()) + " " + field.getName()
+ " = " + value + " @" + type.getName());
}
}
lines.add("--- methods ---");
for (Method method : object.getClass().getMethods()) {
if (method.getDeclaringClass() == Object.class) continue;
StringBuilder params = new StringBuilder();
for (Class<?> parameter : method.getParameterTypes()) {
if (params.length() > 0) params.append(", ");
params.append(shortType(parameter.getName()));
}
lines.add(" " + shortType(method.getReturnType().getName()) + " " + method.getName()
+ "(" + params + ")");
}
fillTextRows(state, lines);
setStatus(state, "Inspected " + SceneObjects.kind(object) + " \""
+ SceneObjects.nameOf(object) + "\": " + (lines.size() - 2) + " entries");
}
});
}
private static int countFields(Object object) {
int count = 0;
for (Class<?> type = object.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) count++;
}
}
return count;
}
private static String shortType(String name) {
int index = name.lastIndexOf('.');
String simple = index < 0 ? name : name.substring(index + 1);
if (simple.length() == 1) return name; // D/B/L 这种混淆名:带上包名才分得清
return simple;
}
/** 用纯文本行填列表区(行不可点)。 */
private static void fillTextRows(final State state, List<String> lines) {
try {
call(state.listTable, Obf.TABLE_CLEAR_CHILDREN, new Class<?>[0], void.class);
Class<?> cellClass = Class.forName(Obf.CELL, true, state.loader);
Class<?> actorClass = Class.forName(Obf.ACTOR, true, state.loader);
for (String line : lines) {
Object row = button(state.loader, state.skin, line);
Object cell = call(state.listTable, Obf.TABLE_ADD, new Class<?>[] {actorClass},
cellClass, row);
layoutCell(cell, Obf.CELL_LEFT);
call(state.listTable, Obf.TABLE_ROW, new Class<?>[0], cellClass);
}
} catch (Throwable error) {
System.err.println("[panel] inspector 填充失败: " + error);
}
}
private static void setStatus(final State state, final String text) {
if (state.status == null) return;
SceneActions.onEditorThread(state.loader, new Runnable() {
@Override
public void run() {
try {
call(state.status, Obf.LABEL_SET_TEXT, new Class<?>[] {CharSequence.class},
void.class, text);
} catch (Throwable error) {
System.err.println("[panel] 状态行更新失败: " + error);
}
}
});
}
/** TextField 里的文字(名字被混淆了,按"无参 + 返回 String"取)。 */
private static String textFieldText(Object field) {
for (Method method : field.getClass().getMethods()) {
if (method.getParameterTypes().length != 0) continue;
if (method.getReturnType() != String.class) continue;
try {
method.setAccessible(true);
Object value = method.invoke(field);
if (value instanceof String) return (String) value;
} catch (Throwable ignored) {
// 换下一个
}
}
return "particle_bone";
}
private static Object button(ClassLoader loader, Object skin, String text) throws Exception {
Class<?> buttonClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
return buttonClass.getConstructor(String.class, skinClass).newInstance(text, skin);
}
private static Object label(ClassLoader loader, Object skin, String text) throws Exception {
Class<?> labelClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Label", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
return labelClass.getConstructor(CharSequence.class, skinClass).newInstance(text, skin);
}
// ==================== 按钮点击:一个轮询线程 + 一张路由表 ====================
//
// libGDX 的 Button 自己带 ClickListener,点一下会翻转 isChecked,所以轮询 isChecked 就等于监听点击。
// 用轮询而不是 addListener,是因为监听器接口的实例没法用反射 new 出来(Scene2dPanel 里已经踩过)。
// 一条路由 = { button, action, isCheckedMethod, setCheckedQuietMethod }。
private static final List<Object[]> ROUTES = new ArrayList<Object[]>();
private static volatile boolean routerStarted;
/** 验证用:记住面板上的按钮(标签 → 按钮),探针可以按标签"点"它们。 */
private static final java.util.Map<String, Object> BUTTONS =
new java.util.LinkedHashMap<String, Object>();
/** 建面板的顺序(验证用):0 Refresh 1 New Bone 2 New Slot 3 New BBox 4 New Path 5 Delete 6 Rename。 */
private static final List<Object> ORDER = new ArrayList<Object>();
static Object buttonAt(int index) {
synchronized (ORDER) {
return index >= 0 && index < ORDER.size() ? ORDER.get(index) : null;
}
}
static void remember(String label, Object button) {
synchronized (BUTTONS) {
BUTTONS.put(label, button);
ORDER.add(button);
if (ROUTES.size() > 100000) BUTTONS.clear();
}
}
static Object panelButton(String label) {
synchronized (BUTTONS) {
return BUTTONS.get(label);
}
}
static void wire(ClassLoader loader, Object button, Runnable action) {
try {
Method isChecked = null;
Method resetQuiet = null;
Class<?> buttonClass = button.getClass();
for (Method method : buttonClass.getMethods()) {
if (!method.getName().equals(Obf.BUTTON_IS_CHECKED)) continue;
if (method.getParameterTypes().length != 0) continue;
if (method.getReturnType() != boolean.class) continue;
isChecked = method;
break;
}
for (Method method : buttonClass.getMethods()) {
if (!method.getName().equals(Obf.BUTTON_SET_CHECKED_FIRE)) continue;
if (method.getParameterTypes().length != 2) continue;
if (method.getParameterTypes()[0] != boolean.class
|| method.getParameterTypes()[1] != boolean.class) continue;
resetQuiet = method;
break;
}
if (isChecked == null) {
System.err.println("[panel] 找不到 isChecked,按钮不响应: " + button);
return;
}
isChecked.setAccessible(true);
if (resetQuiet != null) resetQuiet.setAccessible(true);
synchronized (ROUTES) {
ROUTES.add(new Object[] {button, action, isChecked, resetQuiet});
}
remember(buttonLabel(button), button);
startRouter();
} catch (Throwable error) {
System.err.println("[panel] 接按钮失败: " + error);
}
}
/** 按钮上的文字(TextButton 自己的 getText;不取 Actor.getName)。 */
static String buttonLabel(Object button) {
for (Class<?> type = button.getClass(); type != null
&& !type.getName().equals(Obf.ACTOR); type = type.getSuperclass()) {
for (Method method : type.getDeclaredMethods()) {
if (method.getParameterTypes().length != 0) continue;
if (method.getReturnType() != String.class) continue;
// 跳过 Object/Actor 那一批(toString 会返回 "TextButton: Refresh" 这种调试串)
if (method.getName().equals("toString")) continue;
if (method.getName().equals("hashCode")) continue;
if (method.getName().equals("equals")) continue;
if (method.getName().equals("getName")) continue;
try {
method.setAccessible(true);
Object value = method.invoke(button);
if (value instanceof String) return (String) value;
} catch (Throwable ignored) {
// 换下一个
}
}
}
return "?";
}
private static void startRouter() {
if (routerStarted) return;
synchronized (ObjectPanel.class) {
if (routerStarted) return;
routerStarted = true;
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
List<Object[]> snapshot;
synchronized (ROUTES) {
snapshot = new ArrayList<Object[]>(ROUTES);
}
for (Object[] route : snapshot) {
if (!Boolean.TRUE.equals(((Method) route[2]).invoke(route[0]))) continue;
Method resetQuiet = (Method) route[3];
if (resetQuiet != null) {
resetQuiet.invoke(route[0], Boolean.FALSE, Boolean.FALSE);
}
try {
((Runnable) route[1]).run();
} catch (Throwable error) {
System.err.println("[panel] 动作失败: " + error);
error.printStackTrace(System.err);
}
}
Thread.sleep(80);
} catch (InterruptedException ignored) {
return;
} catch (Throwable error) {
System.err.println("[panel] 轮询异常: " + error);
return;
}
}
}
}, "spineview-crud-router");
thread.setDaemon(true);
thread.start();
}
// ==================== 反射小工具(名字被混淆,一律"名字 + 参数 + 返回类型"三判) ====================
/**
* 验证用探针:按顺序"点"面板上的按钮,然后把每一步的结果打出来。
* 点击 = 调 `setChecked(true, true)`(与真人点击效果一致:路由器看到 isChecked 就执行动作)。
*/
public static String probePanel(ClassLoader loader) {
StringBuilder out = new StringBuilder();
out.append("buttons: ").append(buttonLabels()).append('\n');
press(0, out); // Refresh
out.append("after Refresh: ").append(rows(loader)).append('\n');
// 列表是 Refresh 之后才建出来的,找到 "bone bone3" 那一行并按它
press(panelButton("bone bone3"), "row bone bone3", out);
out.append("after select: ").append(rows(loader)).append('\n');
press(2, out); // New Slot(会弹输入框,看面板能不能自己填+点 OK)
sleepQuiet(3000);
out.append("after New Slot: ").append(rows(loader)).append('\n');
press(3, out); // New BBox
sleepQuiet(3000);
out.append("after New BBox: ").append(rows(loader)).append('\n');
return out.toString();
}
private static void press(String label, StringBuilder out) {
press(panelButton(label), label, out);
}
private static void press(int index, StringBuilder out) {
press(buttonAt(index), "#" + index, out);
}
private static void press(Object button, String label, StringBuilder out) {
if (button == null) {
out.append("press ").append(label).append(" -> button not found\n");
return;
}
try {
Method setChecked = null;
for (Method method : button.getClass().getMethods()) {
if (!method.getName().equals(Obf.BUTTON_SET_CHECKED_FIRE)) continue;
Class<?>[] types = method.getParameterTypes();
if (types.length != 2 || types[0] != boolean.class || types[1] != boolean.class) continue;
setChecked = method;
break;
}
if (setChecked == null) {
out.append("press ").append(label).append(" -> no setChecked(boolean,boolean)\n");
return;
}
setChecked.setAccessible(true);
setChecked.invoke(button, Boolean.TRUE, Boolean.TRUE);
out.append("press ").append(label).append(" ok\n");
sleepQuiet(600);
} catch (Throwable error) {
out.append("press ").append(label).append(" failed: ").append(error).append('\n');
}
}
private static void sleepQuiet(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private static String buttonLabels() {
StringBuilder out = new StringBuilder();
synchronized (BUTTONS) {
for (String label : BUTTONS.keySet()) {
if (out.length() > 0) out.append(", ");
out.append('"').append(label).append('"');
}
}
return out.toString();
}
private static String rows(ClassLoader loader) {
StringBuilder out = new StringBuilder();
try {
for (Object item : SceneObjects.listAllNodes(loader)) {
out.append(SceneObjects.kind(item)).append(' ').append(SceneObjects.nameOf(item))
.append(" | ");
}
} catch (Throwable ignored) {
// 换下面那条路
}
return out.toString();
}
static Object call(Object target, String name, Class<?>[] parameterTypes, Class<?> returnType,
Object... arguments) throws Exception {
for (Class<?> type = target.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Method method : type.getMethods()) {
if (!method.getName().equals(name)) continue;
if (Modifier.isStatic(method.getModifiers())) continue;
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
method.setAccessible(true);
return method.invoke(target, arguments);
}
}
throw new NoSuchMethodException(target.getClass().getName() + "." + name
+ java.util.Arrays.toString(parameterTypes));
}
static void layoutCell(Object cell, String... names) {
for (String name : names) {
try {
Method method = cell.getClass().getMethod(name);
method.setAccessible(true);
method.invoke(cell);
} catch (Throwable error) {
System.err.println("[panel] 单元格布局 " + name + " 失败: " + error);
}
}
}
}
@@ -0,0 +1,904 @@
package com.spineview;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
/**
* 场景对象的"增 / 删 / 改"。
*
* 两条路(都在 `编辑器交互与数据API.md` 里有字节码出处):
*
* 1. **走编辑器菜单命令**(`New Bone` / `Delete` / `Rename` …):
* 路径 = 先 `VZ.D(对象)` 把选择设成目标 → 跑命令。
* 编辑器自己会改属性面板、插树节点、弹确认框、进撤销栈。**增**走这条。
* 2. **直接改模型 + 自己包撤销**:`un.D()` … `对象.L()`/`对象.D(名字,true)` … `un.B()`,
* 再 `VZ.D(骨架,true,true)` 重建树节点。**删/改**走这条(不进确认框,但带撤销)。
*
* 模型对象(载荷名):`GP` 骨架、`Gp` 骨骼、`GU` 插槽、`GT`/`GO` 附件,都 extends `Gj`。
* `L()V` = 把自己从模型里摘掉;`D(String,boolean)V` = 改名(第二参 = 是否同步子对象名字)。
*/
public final class SceneActions {
/** 标记"当前线程就是编辑器渲染线程",避免 `OG.B` 二次投递(它内部会断言不在渲染线程)。 */
private static final ThreadLocal<Boolean> ON_EDITOR_THREAD = new ThreadLocal<Boolean>();
private SceneActions() {
}
/**
* 把一段动作切到**编辑器的渲染线程**上执行并等待。
*
* 为什么必须切:编辑器在改模型前会调 `OG.I()`(`javap -c OG`):
* ```java
* if (!Editor.S && Editor.D != null && Editor.D != Thread.currentThread())
* throw new RuntimeException(); // "不能在非渲染线程动模型"
* ```
* 而 `OG.B(Runnable)V` / `OG.D(Runnable)V` 就是编辑器自己的"投递到渲染线程"接口
* (`B` 用 `CountDownLatch` 等它跑完,`D` 不等;内部走 `cOm4.D:LnUl;.D(Runnable)`)。
*
* 面板按钮本身是 libGDX 事件回调,天然就在渲染线程上;这个方法主要给后台线程/探针用。
*/
public static void onEditorThread(ClassLoader loader, Runnable body) {
if (Boolean.TRUE.equals(ON_EDITOR_THREAD.get())) {
body.run();
return;
}
final Runnable wrapped = new Runnable() {
@Override
public void run() {
ON_EDITOR_THREAD.set(Boolean.TRUE);
try {
body.run();
} finally {
ON_EDITOR_THREAD.remove();
}
}
};
try {
Class<?> og = Class.forName("OG", true, loader);
for (Method method : og.getDeclaredMethods()) {
if (!method.getName().equals("B")) continue;
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length != 1 || parameters[0] != Runnable.class) continue;
method.setAccessible(true);
method.invoke(null, wrapped);
return;
}
System.err.println("[crud] OG.B(Runnable) 没找到,直接在当前线程执行");
} catch (Throwable error) {
System.err.println("[crud] 切渲染线程失败,直接在当前线程执行: " + error);
}
wrapped.run();
}
/** 撤销栈(载荷类 un):`D()` 记一次编辑开始,`B()` 提交。 */
private static Class<?> undoManager(ClassLoader loader) throws Exception {
return Class.forName("un", true, loader);
}
/** 调 `un.D()` / `un.B()` 这类无参静态方法(名字被混淆过,按"名字 + 空参数 + void"取)。 */
private static void undoCall(Class<?> undoClass, String name) throws Exception {
// ⚠️ 不能 getDeclaredMethod(name):`un` 上有 4 个同名无参方法(返回 Z/J/I/V),会抛 NoSuchMethod。
for (Method method : undoClass.getDeclaredMethods()) {
if (!method.getName().equals(name)) continue;
if (!hasNoArgs(method)) continue;
method.setAccessible(true);
method.invoke(null);
return;
}
throw new NoSuchMethodException("un." + name + "():V");
}
private static boolean hasNoArgs(Method method) {
return method.getParameterTypes().length == 0 && method.getReturnType() == void.class
&& Modifier.isStatic(method.getModifiers());
}
/** 选中某个场景对象(编辑器层级树里高亮它,并切换它的属性面板)。 */
public static boolean select(ClassLoader loader, Object sceneObject) {
if (sceneObject == null) return false;
final Object target = sceneObject;
final ClassLoader classLoader = loader;
final boolean[] result = new boolean[1];
// 选中会触发 editor 的事件链路(改属性面板等),同样必须在渲染线程上做。
onEditorThread(loader, new Runnable() {
@Override
public void run() {
try {
result[0] = SceneObjects.select(classLoader, target);
} catch (Throwable error) {
System.err.println("[crud] 选中失败: " + error);
}
}
});
return result[0];
}
/**
* 新建:先把选择设成父对象,再跑编辑器的 `New <type>` 命令。
*
* `type` 用编辑器自己的类型名:`Bone` / `Slot` / `Skin Placeholder` / `Bounding Box` /
* `Path` / `Clipping` / `IK Constraint` / `Transform Constraint` / `Path Constraint`。
* 命令只在"当前选择是合法父级"时生效(例如选骨骼才能 New Bone / New Slot),否则编辑器什么都不做。
*/
public static boolean create(ClassLoader loader, Object parent, String type) {
final Object target = parent;
final String typeName = type;
final ClassLoader classLoader = loader;
final boolean[] result = new boolean[1];
onEditorThread(loader, new Runnable() {
@Override
public void run() {
result[0] = createNow(classLoader, target, typeName);
}
});
// ⚠️ 输入名字的对话框是**下一帧**才建出来的,只能在渲染线程之外等它出现
// (在渲染线程里 sleep 会卡住帧,对话框永远不出现)。
// `New Bone` 是编辑器自动编号、不弹框;其余 New X 都会弹,所以:
// 没给名字 → 自动生成一个不重名的;给了名字 → 用它;然后自动点 OK。
if (!"Bone".equals(typeName)) {
String pending = PENDING_NAME.get();
if (pending == null || pending.length() == 0) {
pending = autoName(loader, typeName);
}
LAST_NAME.set(pending);
fillDialogWhenReady(loader, pending);
PENDING_NAME.remove();
}
return result[0];
}
private static boolean createNow(ClassLoader loader, Object parent, String type) {
if (parent == null) {
System.err.println("[crud] 新建失败:没有选中的父对象");
return false;
}
if (!select(loader, parent)) {
System.err.println("[crud] 新建失败:选不中父对象 " + SceneObjects.nameOf(parent));
return false;
}
String command = type.startsWith("New ") ? type : "New " + type;
System.err.println("[crud] 新建: " + command + "(父级 = " + SceneObjects.nameOf(parent) + ")");
// 有些 New X 会弹"输入名字"对话框(New Slot / New Bounding Box / New Path / …),
// 光执行命令不会建对象,对话框由 create() 在本线程轮询后填名字 + 点 OK(见 fillDialogWhenReady)。
return Commands.run(loader, command);
}
/** 面板/探针给下一次"会弹对话框的新建"准备好的名字。 */
private static final ThreadLocal<String> PENDING_NAME = new ThreadLocal<String>();
/** 这一次新建实际用的名字(面板用来显示)。 */
private static final ThreadLocal<String> LAST_NAME = new ThreadLocal<String>();
public static void nextName(String name) {
PENDING_NAME.set(name);
}
public static String lastName() {
return LAST_NAME.get();
}
/**
* 自动生成一个**不重名**的名字:`Slot` → `slot` / `slot_2` / `slot_3` …
* 判断重名时比的是"同一类对象"的现有名字(骨骼/插槽全局唯一,附件同名也能查出来)。
*/
static String autoName(ClassLoader loader, String type) {
String base = type.toLowerCase().replace(' ', '_');
java.util.Set<String> used = new java.util.HashSet<String>();
try {
for (Object item : SceneObjects.listAllNodes(loader)) {
used.add(SceneObjects.nameOf(item));
}
} catch (Throwable ignored) {
// 读不到就用 1
}
if (!used.contains(base)) return base;
for (int i = 2; i < 10000; i++) {
String candidate = base + "_" + i;
if (!used.contains(candidate)) return candidate;
}
return base + "_" + System.nanoTime();
}
// ==================== 输入对话框自动化 ====================
//
// `New Slot` / `New Bounding Box` / `New Path` / `New Clipping` / `New Skin Placeholder` /
// `New IK Constraint` / `New Event` / `New Animation` 都会弹一个"输入名字"的窗口
// (标题如 "New Slot",正文 "Enter a name to create a new slot:",按钮 OK / Cancel)。
// 命令本身只负责弹窗,名字与 OK 必须有人点。这里把这一步自动化:
// Stage(OG.D:Ldt;) → 找最后一个 TextField(对话框里的输入框)
// → 找最后一个文字含 "OK" 的 TextButton → 填名字 + 点它。
/** 轮询等对话框出现(最多约 3 秒),出现后填名字 + 点 OK。 */
static boolean fillDialogWhenReady(ClassLoader loader, String name) {
for (int i = 0; i < 30; i++) {
if (fillDialog(loader, name)) {
System.err.println("[crud] 已自动填写对话框并点 OK: " + name);
sleep(300);
return true;
}
sleep(100);
}
System.err.println("[crud] 没等到输入对话框: " + name);
return false;
}
static boolean fillDialog(ClassLoader loader, String name) {
try {
Object stage = SceneObjects.global(loader, "dt");
if (stage == null) return false;
// 不顺着"children 数组"找了(编辑器的弹窗并不一定在 Stage 根的孩子里)。
// 改成:从 Stage 出发按字段广度遍历,找"**有 TextField 字段 + 有 L(Object)V 回调**"的对象
// —— 载荷里所有这类输入对话框(New Slot / New Bounding Box / New Event / …)都长这样:
// zY(New Slot 对话框)的 OK 回调就是 `L(Object)`,体里只做
// `String name = TextField.getText(...); …建对象…`。
Object dialog = findDialog(stage);
if (dialog == null) return false;
Object textField = SceneObjects.fieldOfType(dialog, "com.badlogic.gdx.scenes.scene2d.ui.TextField");
if (textField == null) return false;
setText(textField, name);
Method ok = findMethod(dialog.getClass(), "L", new Class<?>[] {Object.class}, void.class);
if (ok == null) return false;
ok.setAccessible(true);
ok.invoke(dialog, Boolean.TRUE);
return true;
} catch (Throwable error) {
System.err.println("[crud] 对话框自动化失败: " + error);
return false;
}
}
/** 广度遍历 Stage 上的对象图,找"带 TextField 且有 L(Object)V"的对话框。 */
private static Object findDialog(Object stage) {
java.util.Set<Object> seen = java.util.Collections.newSetFromMap(
new java.util.IdentityHashMap<Object, Boolean>());
java.util.ArrayDeque<Object> queue = new java.util.ArrayDeque<Object>();
queue.add(stage);
int visited = 0;
while (!queue.isEmpty() && visited++ < 40000) {
Object current = queue.poll();
if (current == null || !seen.add(current)) continue;
if (isDialog(current)) return current;
if (current.getClass().isArray()) {
if (!current.getClass().getComponentType().isPrimitive()) {
for (Object item : (Object[]) current) queue.add(item);
}
continue;
}
String typeName = current.getClass().getName();
if (typeName.startsWith("java.") || typeName.startsWith("javax.")) continue;
for (Class<?> type = current.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
if (field.getType().isPrimitive()) continue;
try {
field.setAccessible(true);
Object value = field.get(current);
if (value != null) queue.add(value);
} catch (Throwable ignored) {
// 跳过
}
}
}
}
return null;
}
private static boolean isDialog(Object candidate) {
if (SceneObjects.fieldOfType(candidate, "com.badlogic.gdx.scenes.scene2d.ui.TextField") == null) {
return false;
}
return findMethod(candidate.getClass(), "L", new Class<?>[] {Object.class}, void.class) != null;
}
/** 深度优先遍历 actor 树,记住**最后一个** TextField 和最后一个文字含 OK 的按钮。 */
private static void walkActors(Object actor, Object[] found, int depth) {
if (actor == null || depth > 24) return;
String type = actor.getClass().getName();
if (type.equals("com.badlogic.gdx.scenes.scene2d.ui.TextField")) found[0] = actor;
if (type.startsWith("com.badlogic.gdx.scenes.scene2d.ui.TextButton")) {
String label = ObjectPanel.buttonLabel(actor);
if (label != null && label.toUpperCase().contains("OK")) found[1] = actor;
}
for (Object child : children(actor)) walkActors(child, found, depth + 1);
}
/** Group 的子节点(`do` 上的 Object[] 字段 + 一个 int size)。 */
private static Object[] children(Object actor) {
for (Class<?> type = actor.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
try {
field.setAccessible(true);
Object value = field.get(actor);
Object[] items = SceneObjects.arrayItems(value);
if (items.length > 0) return items;
} catch (Throwable ignored) {
// 跳过
}
}
if (type.getName().equals(Obf.GROUP)) break;
}
return new Object[0];
}
private static void setText(Object textField, String text) throws Exception {
Method setter = findMethod(textField.getClass(), Obf.LABEL_SET_TEXT, new Class<?>[] {String.class}, void.class);
if (setter == null) {
setter = findMethod(textField.getClass(), Obf.LABEL_SET_TEXT, new Class<?>[] {CharSequence.class}, void.class);
}
if (setter == null) throw new NoSuchMethodException("TextField.setText");
setter.setAccessible(true);
setter.invoke(textField, text);
}
private static void pressButton(Object button) throws Exception {
Method setChecked = findMethod(button.getClass(), Obf.BUTTON_SET_CHECKED_FIRE,
new Class<?>[] {boolean.class, boolean.class}, void.class);
if (setChecked == null) {
setChecked = findMethod(button.getClass(), Obf.BUTTON_SET_CHECKED,
new Class<?>[] {boolean.class}, void.class);
setChecked.setAccessible(true);
setChecked.invoke(button, Boolean.TRUE);
return;
}
setChecked.setAccessible(true);
setChecked.invoke(button, Boolean.TRUE, Boolean.TRUE);
}
/**
* 删除:`un.D()` → `对象.L()` → `un.B()` → 重建树。
* 返回 true 表示调用链没抛异常(不代表编辑器真的删掉了,删掉与否看树)。
*/
public static boolean delete(ClassLoader loader, Object sceneObject) {
final Object target = sceneObject;
final ClassLoader classLoader = loader;
final boolean[] result = new boolean[1];
onEditorThread(loader, new Runnable() {
@Override
public void run() {
result[0] = deleteNow(classLoader, target);
}
});
return result[0];
}
private static boolean deleteNow(ClassLoader loader, Object sceneObject) {
if (sceneObject == null) return false;
try {
Method remove = findMethod(sceneObject.getClass(), "L", new Class<?>[0], void.class);
if (remove == null) {
System.err.println("[crud] 删除失败:" + sceneObject.getClass().getName() + " 上没有 L()V");
return false;
}
Object skeleton = skeletonOf(sceneObject);
Class<?> undoClass = undoManager(loader);
undoCall(undoClass, "D");
remove.setAccessible(true);
remove.invoke(sceneObject);
undoCall(undoClass, "B");
refresh(loader, skeleton);
System.err.println("[crud] 已删除 " + SceneObjects.kind(sceneObject) + " "
+ SceneObjects.nameOf(sceneObject));
return true;
} catch (Throwable error) {
System.err.println("[crud] 删除失败: " + error);
error.printStackTrace(System.err);
return false;
}
}
/** 改名:`un.D()` → `对象.D(新名字, true)` → `un.B()` → 重建树。 */
public static boolean rename(ClassLoader loader, Object sceneObject, String newName) {
final Object target = sceneObject;
final String name = newName;
final ClassLoader classLoader = loader;
final boolean[] result = new boolean[1];
onEditorThread(loader, new Runnable() {
@Override
public void run() {
result[0] = renameNow(classLoader, target, name);
}
});
return result[0];
}
private static boolean renameNow(ClassLoader loader, Object sceneObject, String newName) {
if (sceneObject == null || newName == null || newName.length() == 0) return false;
try {
Method setName = findMethod(sceneObject.getClass(), "D",
new Class<?>[] {String.class, boolean.class}, void.class);
if (setName == null) {
System.err.println("[crud] 改名失败:" + sceneObject.getClass().getName()
+ " 上没有 D(String,boolean)V");
return false;
}
Object skeleton = skeletonOf(sceneObject);
Class<?> undoClass = undoManager(loader);
undoCall(undoClass, "D");
setName.setAccessible(true);
setName.invoke(sceneObject, newName, Boolean.TRUE);
undoCall(undoClass, "B");
refresh(loader, skeleton);
System.err.println("[crud] 已改名 -> " + newName);
return true;
} catch (Throwable error) {
System.err.println("[crud] 改名失败: " + error);
error.printStackTrace(System.err);
return false;
}
}
/**
* 重建某个骨架在层级树里的节点(编辑器删除流程里用的就是这个:
* `VZ.D(LGP;ZZ)V`,见 `vq_0.java:54`)。
*/
public static boolean refresh(ClassLoader loader, Object skeleton) {
if (skeleton == null) return false;
try {
Object treeView = SceneObjects.global(loader, SceneObjects.TREE_VIEW);
Class<?> skeletonClass = Class.forName("GP", true, loader);
Method rebuild = null;
for (Method method : treeView.getClass().getDeclaredMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 3 || types[0] != skeletonClass) continue;
if (types[1] != boolean.class || types[2] != boolean.class) continue;
rebuild = method;
break;
}
if (rebuild == null) {
System.err.println("[crud] 刷新失败:VZ 上没有 D(GP,boolean,boolean)V");
return false;
}
System.err.println("[crud] 刷新用方法 " + rebuild.getDeclaringClass().getName() + "."
+ rebuild.getName() + java.util.Arrays.toString(rebuild.getParameterTypes())
+ " static=" + Modifier.isStatic(rebuild.getModifiers()));
rebuild.setAccessible(true);
// ⚠️ 参数就 3 个(GP, boolean, boolean):editor 不是参数,别多传。
rebuild.invoke(treeView, skeleton, Boolean.TRUE, Boolean.TRUE);
return true;
} catch (Throwable error) {
System.err.println("[crud] 刷新失败: " + error);
return false;
}
}
/** 找到所属骨架:骨骼是 `Gp.D:LGP;`,骨架自己就是 `GP`。 */
public static Object skeletonOf(Object sceneObject) {
if (sceneObject == null) return null;
if (sceneObject.getClass().getName().equals("GP")) return sceneObject;
for (Class<?> type = sceneObject.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (!field.getType().getName().equals("GP")) continue;
try {
field.setAccessible(true);
Object value = field.get(sceneObject);
if (value != null) return value;
} catch (Throwable ignored) {
// 跳过
}
}
}
return null;
}
/** 当前选中对象的类型名(给面板显示用)。 */
public static String selectedKind(ClassLoader loader) {
try {
return SceneObjects.kind(SceneObjects.selected(loader));
} catch (Throwable error) {
return "?";
}
}
/** 当前显示在 Tree 视图里的"属性面板控件"类名(诊断用)。 */
public static String propertyPanelClass(ClassLoader loader) {
try {
Object treeView = SceneObjects.global(loader, SceneObjects.TREE_VIEW);
Object panel = SceneObjects.fieldOfType(treeView, "VY");
return panel == null ? "null" : panel.getClass().getName();
} catch (Throwable error) {
return "err:" + error;
}
}
/** 按"名字 + 参数 + 返回类型"找实例方法(载荷里同名方法成堆,必须全判)。 */
static Method findMethod(Class<?> owner, String name, Class<?>[] parameters,
Class<?> returnType) {
for (Class<?> type = owner; type != null && type != Object.class;
type = type.getSuperclass()) {
for (Method method : type.getDeclaredMethods()) {
if (!method.getName().equals(name)) continue;
if (!java.util.Arrays.equals(method.getParameterTypes(), parameters)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
return method;
}
}
return null;
}
/** 诊断:把树里每个对象的类型 + 属性面板类名打出来。 */
static String describePanels(ClassLoader loader) {
StringBuilder out = new StringBuilder();
try {
List<Object> all = SceneObjects.listAllNodes(loader);
for (Object item : all) {
out.append(" ").append(SceneObjects.kind(item)).append(' ')
.append(SceneObjects.nameOf(item));
// 选中动作必须在编辑器渲染线程上做(否则属性面板不刷新,见 onEditorThread)。
final Object target = item;
final String[] info = new String[1];
onEditorThread(loader, new Runnable() {
@Override
public void run() {
boolean ok = select(loader, target);
Object selected;
try {
selected = SceneObjects.selected(loader);
} catch (Throwable error) {
selected = error;
}
info[0] = " select=" + ok + " treeSelected="
+ (selected == null ? "null" : SceneObjects.nameOf(selected))
+ " panel=" + propertyPanelClass(loader);
}
});
out.append(info[0] == null ? " (no info)" : info[0]);
out.append('\n');
}
} catch (Throwable error) {
out.append("describePanels failed: ").append(error).append('\n');
}
return out.toString();
}
/**
* 探针:真机跑一遍"选中 → 新建 → 改名 → 删除",每步都把对象列表打出来。
* 只用于验证,不参与面板逻辑。
*/
/** 探针:走编辑器自己的 `Select - Bones`,用来确认"选中"到底体现在哪。 */
public static String probeSelect(final ClassLoader loader) {
final StringBuilder out = new StringBuilder();
onEditorThread(loader, new Runnable() {
@Override
public void run() {
out.append("before: treeSelected=").append(describeSelection(loader)).append('\n');
Commands.run(loader, "Select - Bones");
out.append("after Select - Bones: ").append(describeSelection(loader)).append('\n');
Object bone = null;
try {
bone = findByName(loader, "bone3");
} catch (Throwable error) {
out.append("findByName failed: ").append(error).append('\n');
}
out.append("select(\"bone3\") = ").append(select(loader, bone)).append('\n');
out.append("after select: ").append(describeSelection(loader)).append('\n');
}
});
return out.toString();
}
private static String describeSelection(ClassLoader loader) {
StringBuilder out = new StringBuilder();
try {
Object selected = SceneObjects.selected(loader);
out.append("treeSelected=").append(selected == null ? "null" : SceneObjects.nameOf(selected));
Object treeView = SceneObjects.global(loader, SceneObjects.TREE_VIEW);
Object selection = SceneObjects.fieldNamed(treeView, "D", "fC");
out.append(" selectionItems=").append(selection == null ? "noSelection" : String.valueOf(
selectionCount(selection)));
out.append(" lastSelected=").append(selection == null ? "-" : String.valueOf(
lastSelected(selection)));
out.append(" panel=").append(propertyPanelClass(loader));
} catch (Throwable error) {
out.append(" describeSelection failed: ").append(error);
}
return out.toString();
}
private static int selectionCount(Object selection) {
try {
Method items = findMethod(selection.getClass(), "D", new Class<?>[0],
Class.forName("fI", true, selection.getClass().getClassLoader()));
if (items == null) return -1;
items.setAccessible(true);
Object array = items.invoke(selection);
Object[] values = SceneObjects.arrayItems(array);
return values.length;
} catch (Throwable error) {
return -2;
}
}
private static String lastSelected(Object selection) {
try {
Method last = findMethod(selection.getClass(), "D", new Class<?>[0], Object.class);
if (last == null) return "?";
last.setAccessible(true);
Object value = last.invoke(selection);
if (value == null) return "null";
Object object = SceneObjects.nodeObject(value);
return object == null ? String.valueOf(value) : SceneObjects.nameOf(object);
} catch (Throwable error) {
return "err:" + error;
}
}
public static String probeCrud(ClassLoader loader) {
StringBuilder out = new StringBuilder();
try {
out.append("--- before ---\n").append(listNames(loader));
Object parent = findByName(loader, "bone");
out.append("parent = ").append(parent == null ? "null" : SceneObjects.kind(parent)
+ " " + SceneObjects.nameOf(parent)).append('\n');
if (parent == null) return out.toString();
select(loader, parent);
out.append("parent panel = ").append(propertyPanelClass(loader)).append('\n');
out.append("--- create New Bone under ").append(SceneObjects.nameOf(parent)).append(" ---\n");
boolean created = create(loader, parent, "Bone");
sleep(400);
out.append("created=").append(created).append('\n').append(listNames(loader));
Object fresh = findByName(loader, "bone3");
out.append("fresh = ").append(fresh == null ? "null" : SceneObjects.nameOf(fresh))
.append('\n');
if (fresh != null) {
out.append("--- rename -> panel_test ---\n");
boolean renamed = rename(loader, fresh, "panel_test");
sleep(400);
out.append("renamed=").append(renamed).append('\n').append(listNames(loader));
Object renamedObject = findByName(loader, "panel_test");
if (renamedObject != null) {
out.append("--- delete panel_test ---\n");
boolean deleted = delete(loader, renamedObject);
sleep(400);
out.append("deleted=").append(deleted).append('\n').append(listNames(loader));
}
}
} catch (Throwable error) {
out.append("probeCrud failed: ").append(error).append('\n');
error.printStackTrace(System.err);
}
return out.toString();
}
private static String listNames(ClassLoader loader) throws Exception {
StringBuilder out = new StringBuilder();
for (Object item : SceneObjects.listAllNodes(loader)) {
out.append(" ").append(SceneObjects.kind(item)).append(' ')
.append(SceneObjects.nameOf(item)).append('\n');
}
return out.toString();
}
/** 按名字在树里找一个场景对象。 */
static Object findByName(ClassLoader loader, String name) throws Exception {
for (Object item : SceneObjects.listAllNodes(loader)) {
if (name.equals(SceneObjects.nameOf(item))) return item;
}
return null;
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
// ==================== 综合矩阵测试:骨骼 / 插槽 / 附件 的增删改查 + 同名规则 ====================
/**
* 一次跑完三类对象的增删改查与同名规则,把每一步的**事实**打出来(不做断言)。
*
* 三类对象的模型 API 不一样,这正是要测的点:
* - 骨骼:`GP.D(String)LGp;`(按名字查,全局唯一)
* - 插槽:`GP.D(String)LGU;`(按名字查,全局唯一)
* - 附件:`GP.D(LGU;String)LGn;`(**先按插槽、再按名字**查 —— 附件名只在自己插槽内唯一)
*/
public static String probeMatrix(final ClassLoader loader) {
final StringBuilder out = new StringBuilder();
// ⚠️ 不要整段包 onEditorThread:编辑器有些创建是"投递到下一帧"再做的,
// 而 onEditorThread 会一直占着渲染线程等我们返回,帧就跑不起来、对象永远建不出来。
// 所以:每一步各自切渲染线程(select/create/delete 内部已经切了),sleep 留在本线程。
{
try {
out.append("== initial ==\n").append(names(loader));
// ---- 骨骼 ----
out.append("\n== bone ==\n");
Object bone = findByName(loader, "bone");
Object created = createAndReport(loader, bone, "Bone", out, "New Bone");
Object created2 = createAndReport(loader, bone, "Bone", out, "New Bone#2");
out.append(" names like bone*: ").append(countNames(loader, "bone", "bone")).append('\n');
if (created2 != null) {
// 重名:把第二个新建的骨骼改成第一个的名字
boolean renamed = rename(loader, created2, SceneObjects.nameOf(created));
sleep(200);
out.append(" rename(").append(SceneObjects.nameOf(created2)).append(" -> ")
.append(SceneObjects.nameOf(created)).append(") = ").append(renamed)
.append("; 现在叫 ").append(SceneObjects.nameOf(created2))
.append("; 同名骨骼数=").append(countNames(loader, "bone",
SceneObjects.nameOf(created))).append('\n');
}
// 按名字查(模型 API)
Object skeleton = findByName(loader, "skeleton");
out.append(" getBone(\"").append(SceneObjects.nameOf(created)).append("\") = ")
.append(describe(lookup(skeleton, "Gp", SceneObjects.nameOf(created)))).append('\n');
out.append(" getBone(\"no_such_bone\") = ")
.append(describe(lookup(skeleton, "Gp", "no_such_bone"))).append('\n');
// 删除
if (created != null) {
boolean ok = delete(loader, created);
sleep(200);
out.append(" delete(").append(SceneObjects.nameOf(created)).append(") = ").append(ok)
.append("; 还在吗=").append(findByName(loader, SceneObjects.nameOf(created)) != null)
.append('\n');
}
// ---- 插槽 ----
out.append("\n== slot ==\n");
Object slot = createAndReport(loader, bone, "Slot", out, "New Slot");
Object slot2 = createAndReport(loader, bone, "Slot", out, "New Slot#2");
if (slot != null) {
out.append(" getSlot(\"").append(SceneObjects.nameOf(slot)).append("\") = ")
.append(describe(lookup(skeleton, "GU", SceneObjects.nameOf(slot)))).append('\n');
}
if (slot2 != null && slot != null) {
boolean renamed = rename(loader, slot2, SceneObjects.nameOf(slot));
sleep(200);
out.append(" rename(slot2 -> \"").append(SceneObjects.nameOf(slot)).append("\") = ")
.append(renamed).append("; 现在叫 ").append(SceneObjects.nameOf(slot2))
.append("; 同名插槽数=").append(countNames(loader, "slot",
SceneObjects.nameOf(slot))).append('\n');
}
// ---- 附件(挂在插槽下)----
out.append("\n== attachment ==\n");
if (slot != null) {
out.append(" select slot ").append(SceneObjects.nameOf(slot)).append(" = ")
.append(select(loader, slot)).append('\n');
sleep(200);
Object att1 = createAndReport(loader, slot, "Bounding Box", out, "New Bounding Box");
Object att2 = createAndReport(loader, slot, "Path", out, "New Path");
if (att1 != null && att2 != null) {
out.append(" attachment 类: ").append(att1.getClass().getName()).append(" / ")
.append(att2.getClass().getName()).append('\n');
boolean renamed = rename(loader, att2, SceneObjects.nameOf(att1));
sleep(200);
out.append(" rename(att2 -> \"").append(SceneObjects.nameOf(att1))
.append("\") = ").append(renamed).append("; att2 现在叫 ")
.append(SceneObjects.nameOf(att2)).append('\n');
out.append(" getAttachment(slot,\"").append(SceneObjects.nameOf(att1))
.append("\") = ")
.append(describe(lookupAttachment(skeleton, slot, SceneObjects.nameOf(att1))))
.append('\n');
out.append(" delete(att1) = ").append(delete(loader, att1)).append('\n');
sleep(200);
}
}
// ---- 跨类型同名 ----
out.append("\n== cross-type duplicate names ==\n");
if (slot != null && bone != null) {
boolean renamed = rename(loader, slot, SceneObjects.nameOf(bone));
sleep(200);
out.append(" rename(slot -> bone 的名字 \"").append(SceneObjects.nameOf(bone))
.append("\") = ").append(renamed).append("; slot 现在叫 ")
.append(SceneObjects.nameOf(slot)).append('\n');
}
out.append("\n== final ==\n").append(names(loader));
} catch (Throwable error) {
out.append("probeMatrix failed: ").append(error).append('\n');
error.printStackTrace(System.err);
}
}
return out.toString();
}
/** 建一个对象并把"新建前后的差集"报出来。 */
private static Object createAndReport(ClassLoader loader, Object parent, String type,
StringBuilder out, String label) {
try {
List<Object> before = SceneObjects.listAllNodes(loader);
// 会弹名字对话框的那几种,先把名字准备好(bone 不用,编辑器自己编号)。
if (!type.equals("Bone")) {
nextName(type.toLowerCase().replace(' ', '_') + "_probe"
+ (System.nanoTime() % 1000));
}
boolean ok = create(loader, parent, type);
sleep(300);
List<Object> after = SceneObjects.listAllNodes(loader);
Object fresh = null;
for (Object item : after) {
boolean seen = false;
for (Object old : before) {
if (old == item) {
seen = true;
break;
}
}
if (!seen) {
fresh = item;
break;
}
}
out.append(" ").append(label).append("(parent=").append(SceneObjects.nameOf(parent))
.append(") -> ok=").append(ok).append(" new=")
.append(fresh == null ? "null" : fresh.getClass().getName() + " \""
+ SceneObjects.nameOf(fresh) + "\"")
.append('\n');
return fresh;
} catch (Throwable error) {
out.append(" ").append(label).append(" failed: ").append(error).append('\n');
return null;
}
}
/** 模型层的按名字查:骨骼 `GP.D(String)LGp;` / 插槽 `GP.D(String)LGU;`。 */
static Object lookup(Object skeleton, String typeName, String name) {
if (skeleton == null) return null;
for (Method method : skeleton.getClass().getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || types[0] != String.class) continue;
if (!method.getReturnType().getName().equals(typeName)) continue;
try {
method.setAccessible(true);
return method.invoke(skeleton, name);
} catch (Throwable ignored) {
// 换下一个
}
}
return null;
}
/** 附件的按名字查要连插槽一起给:`GP.D(LGU;Ljava/lang/String;)LGn;`。 */
static Object lookupAttachment(Object skeleton, Object slot, String name) {
if (skeleton == null || slot == null) return null;
for (Method method : skeleton.getClass().getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 2 || types[0] != slot.getClass()) continue;
if (types[1] != String.class) continue;
try {
method.setAccessible(true);
return method.invoke(skeleton, slot, name);
} catch (Throwable ignored) {
// 换下一个
}
}
return null;
}
private static String describe(Object object) {
return object == null ? "null" : object.getClass().getName() + " \""
+ SceneObjects.nameOf(object) + "\"";
}
private static String names(ClassLoader loader) throws Exception {
StringBuilder out = new StringBuilder();
for (Object item : SceneObjects.listAllNodes(loader)) {
String kind = SceneObjects.kind(item);
if (kind.equals("skeleton") || kind.equals("bone") || kind.equals("slot")
|| kind.equals("attachment")) {
out.append(" ").append(kind).append(" \"").append(SceneObjects.nameOf(item))
.append("\"\n");
}
}
return out.toString();
}
private static int countNames(ClassLoader loader, String kind, String name) throws Exception {
int count = 0;
for (Object item : SceneObjects.listAllNodes(loader)) {
if (!SceneObjects.kind(item).equals(kind)) continue;
if (SceneObjects.nameOf(item).equals(name)) count++;
}
return count;
}
}
+95 -4
View File
@@ -114,24 +114,115 @@ public final class SceneObjects {
Object treeView = global(loader, TREE_VIEW);
if (treeView == null || sceneObject == null) return false;
Class<?> nodeClass = Class.forName(NODE, true, loader);
Object node = null;
for (Method method : treeView.getClass().getDeclaredMethods()) {
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length != 1 || parameters[0] != Object.class) continue;
if (method.getReturnType() != nodeClass) continue;
// 返回类型是 Uo(Tree.Node 的子类),所以只能判"能容纳 eM"
if (!nodeClass.isAssignableFrom(method.getReturnType())) continue;
method.setAccessible(true);
return method.invoke(treeView, sceneObject) != null;
node = method.invoke(treeView, sceneObject);
break;
}
if (node == null) return false;
// ⚠️ `VZ.D(Object)` 内部调的是 `fC.L(node)`(不是"设成选中")。
// 真正"设成选中"的是 `VZ.D(LUo;Z)V`(内部 `fC.B(node)` = Selection.set + 展开 + 刷新)。
for (Method method : treeView.getClass().getDeclaredMethods()) {
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length != 2) continue;
if (!nodeClass.isAssignableFrom(parameters[0])) continue;
if (parameters[1] != boolean.class) continue;
method.setAccessible(true);
method.invoke(treeView, node, Boolean.TRUE);
return true;
}
return false;
}
/** 场景对象的中文类型不在需求里(面板一律英文),这里给英文缩写归类。 */
/** 按类型找一个实例字段(Tree 视图上的 `D:LVY` 就是"当前属性面板")。 */
static Object fieldOfType(Object target, String typeName) {
if (target == null) return null;
for (Class<?> type = target.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (!field.getType().getName().equals(typeName)) continue;
try {
field.setAccessible(true);
return field.get(target);
} catch (Throwable ignored) {
// 跳过
}
}
}
return null;
}
/** 按"字段名 + 类型"取实例字段(`VZ` 上一堆同名字段,两个都要判)。 */
static Object fieldNamed(Object target, String name, String typeName) {
if (target == null) return null;
for (Class<?> type = target.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (!field.getName().equals(name)) continue;
if (!field.getType().getName().equals(typeName)) continue;
try {
field.setAccessible(true);
return field.get(target);
} catch (Throwable ignored) {
// 跳过
}
}
}
return null;
}
/**
* 树里所有节点对象(**扁平**,按树的先序)。
* 面板的对象列表就是它;要看层级用 {@link #describe}。
*/
public static List<Object> listAllNodes(ClassLoader loader) throws Exception {
Object tree = global(loader, TREE);
if (tree == null) throw new IllegalStateException("拿不到 Tree 控件(OG 上没有 Uq 字段)");
Class<?> nodeClass = Class.forName(NODE, true, loader);
Class<?> arrayClass = Class.forName(ARRAY, true, loader);
List<Object> roots = new ArrayList<Object>();
for (Class<?> type = tree.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (field.getType() != arrayClass) continue;
try {
field.setAccessible(true);
Object value = field.get(tree);
if (value == null) continue;
for (Object item : arrayItems(value)) {
if (item != null && nodeClass.isInstance(item)) roots.add(item);
}
} catch (Throwable ignored) {
// 跳过
}
}
}
List<Object> out = new ArrayList<Object>();
Set<Object> seen = java.util.Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
for (Object root : roots) collectNode(root, out, seen);
return out;
}
private static void collectNode(Object node, List<Object> out, Set<Object> seen) {
if (node == null || !seen.add(node)) return;
Object object = nodeObject(node);
if (object != null) out.add(object);
for (Object child : nodeChildren(node)) collectNode(child, out, seen);
}
/** 场景对象的类型归类(面板上显示这个英文名)。 */
public static String kind(Object sceneObject) {
if (sceneObject == null) return "null";
String name = sceneObject.getClass().getName();
if (name.equals("GP")) return "skeleton";
if (name.equals("Gp")) return "bone";
if (name.equals("GU")) return "slot";
if (name.equals("GT")) return "attachment";
if (name.equals("GT") || name.equals("GO")) return "attachment";
return name;
}
+2 -39
View File
@@ -197,45 +197,8 @@ public final class ViewInstaller {
* 少判一个条件就会调到别的方法上。名字出处见 {@link Obf}。
*/
private static Object buildPanel(ClassLoader loader, Object skin) throws Exception {
Class<?> skinClass = Class.forName(SKIN, true, loader);
Class<?> tableClass = Class.forName(Obf.TABLE, true, loader);
Class<?> cellClass = Class.forName(Obf.CELL, true, loader);
Class<?> actorClass = Class.forName(Obf.ACTOR, true, loader);
Class<?> buttonClass = Class.forName(TEXT_BUTTON, true, loader);
Class<?> labelClass = Class.forName(LABEL, true, loader);
Object table = tableClass.getConstructor().newInstance();
Object button = buttonClass.getConstructor(String.class, skinClass)
.newInstance("Read Scene Objects", skin);
Object label = labelClass.getConstructor(CharSequence.class, skinClass)
.newInstance("Click the button to list scene objects.", skin);
// 读出来的东西会很长,包进 ScrollPane 才能滚动查看。
Object scrollBody = label;
try {
Class<?> scrollClass = Class.forName(SCROLL_PANE, true, loader);
scrollBody = scrollClass.getConstructor(actorClass, skinClass).newInstance(label, skin);
System.err.println("[view] 文字区已包进 ScrollPane");
} catch (Throwable error) {
System.err.println("[view] ScrollPane 建不出来,直接用 Label: " + error);
}
// 让面板铺满停靠框(WidgetGroup.setFillParent)。
// 注意别调成 setDebug(boolean) —— 名字都是单字母,调错只会给单元格描上彩色边框。
call(table, Obf.TABLE_SET_FILL_PARENT, new Class<?>[] {boolean.class}, void.class,
Boolean.TRUE);
// 第一行:按钮,靠左。
Object buttonCell = call(table, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, button);
layoutCell(buttonCell, Obf.CELL_LEFT);
// 换行:不调 row() 的话按钮和文字会并排挤在同一行。
call(table, Obf.TABLE_ROW, new Class<?>[0], cellClass);
// 第二行:文字区,占满剩余空间并撑开单元格。
Object textCell = call(table, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, scrollBody);
layoutCell(textCell, Obf.CELL_EXPAND, Obf.CELL_FILL, Obf.CELL_TOP);
System.err.println("[view] 面板已按两行布局:按钮 / 可滚动文字区");
Scene2dPanel.wireButton(button, label, loader);
return table;
// 面板内容(增删改查)在 ObjectPanel 里,这里只负责把它建出来。
return ObjectPanel.build(loader, skin);
}
/**