Compare commits
2
Commits
64361e389e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6580611012 | ||
|
|
8df990a058 |
No files matched your search
@@ -339,6 +339,83 @@ com.spineview.SceneObjects listAllNodes (Ljava/lang/ClassLoader;)Ljava/util/List
|
|||||||
com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete
|
com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete
|
||||||
# ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它
|
# ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 【动画模型】(2026-10-17 实测:新建 Gl / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线均跑通并读回验证)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ---- 骨架上的动画表与 fps ----
|
||||||
|
GP I LfI animations
|
||||||
|
# ^ 骨架的动画表(fI,元素是 Gl)。新建动画 = new Gl() → 设 D:LGP、D:String → Gl.D() 注册 → animations.add(gl)
|
||||||
|
GP D LFT; projectSettings
|
||||||
|
FT D F fps
|
||||||
|
# ^ 本项目 fps=30(编辑器时间轴刻度直接用帧:关键帧时间写 0 和 30,不要换算成秒)
|
||||||
|
|
||||||
|
# ---- Gl = 动画 ----
|
||||||
|
Gl D LGP; skeleton
|
||||||
|
Gl D Ljava/lang/String; name
|
||||||
|
Gl D LfI timelineGroups
|
||||||
|
# ^ 时间轴组表(FH)。清空动画:直接换一个新的空 fI(removeRange 两个重载实测都不缩容)
|
||||||
|
Gl D ()V register
|
||||||
|
# ^ 把动画注册进骨架的动画列表模型(编辑器 New Animation 命令里就是这么做的)
|
||||||
|
Gl D (LGp;)LFI; rotateTimeline
|
||||||
|
Gl B (LGp;)LFI; translateTimeline
|
||||||
|
Gl L (LGp;)LFI; scaleTimeline
|
||||||
|
Gl I (LGp;)LFI; shearTimeline
|
||||||
|
# ^ 骨骼四条轴(取或建):运行时类分别是 ih / io / ii / ij
|
||||||
|
Gl D (LGp;LGU;)LFI; slotColorTimeline
|
||||||
|
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
|
||||||
|
# ^ 插槽两条轴:hU / ip
|
||||||
|
Gl D ()LFI; drawOrderTimeline
|
||||||
|
# ^ 骨架级绘制顺序轴(hZ)
|
||||||
|
|
||||||
|
# ---- FH = 时间轴组(按对象分组)----
|
||||||
|
FH D LGl; animation
|
||||||
|
FH D LGp; bone
|
||||||
|
FH D LfI timelines
|
||||||
|
|
||||||
|
# ---- FI = 时间轴 ----
|
||||||
|
FI D Ljava/lang/Class; runtimeTimelineClass
|
||||||
|
# ^ 运行时轴类:ih=Rotate(旋转) io=Translate(位移) ii=Scale(缩放) ij=Shear(斜切)
|
||||||
|
# hU=SlotColor(插槽颜色) ip=SlotAttachment(附件切换) hZ=DrawOrder(绘制顺序)
|
||||||
|
FI D LfI keys
|
||||||
|
FI D (F)LFN; findKey
|
||||||
|
# ^ 按时间精确找关键帧;找不到返回 null
|
||||||
|
FI D (LFN;Z)V insertKey
|
||||||
|
# ^ 插入关键帧(第二参=是否记撤销)
|
||||||
|
FI L ()V invalidateRuntime
|
||||||
|
# ^ 标脏运行时轴(改完关键帧调它,让编辑器重建 il)
|
||||||
|
FI D (FI;LFH;)V copyFrom
|
||||||
|
|
||||||
|
# ---- FN = 关键帧(数值字段以动画 JSON 导出器为准)----
|
||||||
|
FN D F time
|
||||||
|
FN D LFV; curve
|
||||||
|
# ^ 曲线类型;FV 枚举见下
|
||||||
|
FN B F curveC2x
|
||||||
|
FN L F curveC2y
|
||||||
|
FN I F curveC3x
|
||||||
|
FN S F curveC3y
|
||||||
|
# ^ 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1)
|
||||||
|
FN D (F)V setAngle
|
||||||
|
FN D ()F getAngle
|
||||||
|
# ^ 旋转轴的 angle(**不是** FN.D():ga!ga 是 deform 的数值数组)
|
||||||
|
FN B (F)V setX
|
||||||
|
FN B ()F getX
|
||||||
|
FN L (F)V setY
|
||||||
|
FN L ()F getY
|
||||||
|
# ^ 位移/缩放/斜切轴的 x / y
|
||||||
|
FN D Lcom/badlogic/gdx/graphics/Color; color
|
||||||
|
FN D LGn; attachment
|
||||||
|
# ^ 插槽颜色 / 挂哪个附件(null = 隐藏)
|
||||||
|
FN D LOr; valueSchema
|
||||||
|
FN D ()Lga; values
|
||||||
|
# ^ deform 用的数值数组(旋转/位移/缩放/斜切**不要**写这里)
|
||||||
|
|
||||||
|
# ---- FV = 曲线枚举(名字来自 FV.<clinit>)----
|
||||||
|
FV D LFV; linear
|
||||||
|
FV B LFV; curve
|
||||||
|
FV L LFV; stepped
|
||||||
|
# ^ 依次是 linear / curve(贝塞尔) / stepped;"自动"= curve + 一组控制值(如 0.25,0.25,0.75,0.75)
|
||||||
|
|
||||||
# ---- WidgetGroup 的尺寸读取(本次用来把对话框撑到合适大小)----
|
# ---- WidgetGroup 的尺寸读取(本次用来把对话框撑到合适大小)----
|
||||||
eX LineNumberTable ()F getMinWidth
|
eX LineNumberTable ()F getMinWidth
|
||||||
eX String ()F getMinHeight
|
eX String ()F getMinHeight
|
||||||
|
|||||||
|
Can't render this file because it contains an unexpected character in line 41 and column 97.
|
@@ -1,5 +1,34 @@
|
|||||||
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
|
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
|
||||||
# owner name desc newName
|
# owner name desc newName
|
||||||
|
FH D LGl; animation
|
||||||
|
FH D LGp; bone
|
||||||
|
FH D LfI timelines
|
||||||
|
FI D (F)LFN; findKey
|
||||||
|
FI D (FI;LFH;)V copyFrom
|
||||||
|
FI D (LFN;Z)V insertKey
|
||||||
|
FI D LfI keys
|
||||||
|
FI D Ljava/lang/Class; runtimeTimelineClass
|
||||||
|
FI L ()V invalidateRuntime
|
||||||
|
FN B ()F getX
|
||||||
|
FN B (F)V setX
|
||||||
|
FN B F curveC2x
|
||||||
|
FN D ()F getAngle
|
||||||
|
FN D ()Lga; values
|
||||||
|
FN D (F)V setAngle
|
||||||
|
FN D F time
|
||||||
|
FN D LFV; curve
|
||||||
|
FN D LGn; attachment
|
||||||
|
FN D LOr; valueSchema
|
||||||
|
FN D Lcom/badlogic/gdx/graphics/Color; color
|
||||||
|
FN I F curveC3x
|
||||||
|
FN L ()F getY
|
||||||
|
FN L (F)V setY
|
||||||
|
FN L F curveC2y
|
||||||
|
FN S F curveC3y
|
||||||
|
FT D F fps
|
||||||
|
FV B LFV; curve
|
||||||
|
FV D LFV; linear
|
||||||
|
FV L LFV; stepped
|
||||||
GD D F position
|
GD D F position
|
||||||
GD D LGU; target
|
GD D LGU; target
|
||||||
GD D Ljava/lang/String; name
|
GD D Ljava/lang/String; name
|
||||||
@@ -25,6 +54,8 @@ GO L F scaleX
|
|||||||
GO S F rotation
|
GO S F rotation
|
||||||
GO Z F height
|
GO Z F height
|
||||||
GP D (LGp;)V addBone
|
GP D (LGp;)V addBone
|
||||||
|
GP D LFT; projectSettings
|
||||||
|
GP I LfI animations
|
||||||
GP L (LGn;ZZ)V removeAttachment
|
GP L (LGn;ZZ)V removeAttachment
|
||||||
GP L LfI attachments
|
GP L LfI attachments
|
||||||
GP Z LfI slots
|
GP Z LfI slots
|
||||||
@@ -51,6 +82,17 @@ GX SourceFile F rotation
|
|||||||
GX StackMapTable F scaleX
|
GX StackMapTable F scaleX
|
||||||
GX Z F scaleMix
|
GX Z F scaleMix
|
||||||
GX init F scaleY
|
GX init F scaleY
|
||||||
|
Gl B (LGp;)LFI; translateTimeline
|
||||||
|
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
|
||||||
|
Gl D ()LFI; drawOrderTimeline
|
||||||
|
Gl D ()V register
|
||||||
|
Gl D (LGp;)LFI; rotateTimeline
|
||||||
|
Gl D (LGp;LGU;)LFI; slotColorTimeline
|
||||||
|
Gl D LGP; skeleton
|
||||||
|
Gl D LfI timelineGroups
|
||||||
|
Gl D Ljava/lang/String; name
|
||||||
|
Gl I (LGp;)LFI; shearTimeline
|
||||||
|
Gl L (LGp;)LFI; scaleTimeline
|
||||||
Gn D LHi; placeholderRef
|
Gn D LHi; placeholderRef
|
||||||
Gn D Ljava/lang/String; name
|
Gn D Ljava/lang/String; name
|
||||||
Gn I ()Ljava/lang/String; type
|
Gn I ()Ljava/lang/String; type
|
||||||
|
|||||||
|
+3
-1
@@ -20,7 +20,9 @@ mkdir -p build/classes
|
|||||||
|
|
||||||
echo "== 编译 Java 侧(javac $("$JAVA_HOME/bin/javac" -version 2>&1))"
|
echo "== 编译 Java 侧(javac $("$JAVA_HOME/bin/javac" -version 2>&1))"
|
||||||
find src -name '*.class' -delete 2>/dev/null || true
|
find src -name '*.class' -delete 2>/dev/null || true
|
||||||
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -d build/classes $(find src -name '*.java')
|
# 面板里有一处用了 ASM(NewFifteen 现场生成编辑器对话框子类),编译期也要能看到 asm.jar
|
||||||
|
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -cp "../tools/asm/asm.jar" \
|
||||||
|
-d build/classes $(find src -name '*.java')
|
||||||
|
|
||||||
# 保名类清单:这套运行时把 libGDX 基类混淆了,但 ui/* 和 editor/* 保名。
|
# 保名类清单:这套运行时把 libGDX 基类混淆了,但 ui/* 和 editor/* 保名。
|
||||||
# 旧版本的明文载荷里能直接列出这些类名,用它们做静态字段扫描的候选。
|
# 旧版本的明文载荷里能直接列出这些类名,用它们做静态字段扫描的候选。
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
package com.spineview;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画面板工具(正式按钮,重启后仍在):
|
||||||
|
* - "Animate Random Props":随机骨骼/插槽/附件,把所有能 K 帧的属性做成 0f/30f 两个关键帧(值变化明显)
|
||||||
|
* - "Curve Test 6 Modes":6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别用 linear / curve 平滑 /
|
||||||
|
* stepped / ease-in / ease-out / 对称自动 六种曲线
|
||||||
|
*
|
||||||
|
* 动画模型(全部来自字节码,见 混淆名对照表.md 第十一节):
|
||||||
|
* GP.I:fI = 动画表;Gl = 动画(D:LGP 骨架、D:String 名字、D:fI 时间轴组)
|
||||||
|
* Gl.D(Gp)=旋转、Gl.B(Gp)=位移、Gl.L(Gp)=缩放、Gl.I(Gp)=斜切
|
||||||
|
* Gl.D(Gp,GU)=插槽颜色、Gl.B(Gp,GU)=附件切换
|
||||||
|
* FH = 组(D:Gl、D:Gp、D:fI 时间轴);FI = 时间轴(D:Class 运行时类、D:fI 关键帧、L() 重建)
|
||||||
|
* FN = 关键帧(D:F 时间、D:FV 曲线、B/L/I/S 贝塞尔控制值;
|
||||||
|
* D(float)=angle、B(float)=x、L(float)=y、D:Color 颜色、D:Gn 附件)
|
||||||
|
* FV = 曲线枚举(D=linear、B=curve、L=stepped)
|
||||||
|
*/
|
||||||
|
final class AnimationTools {
|
||||||
|
private static boolean installed;
|
||||||
|
|
||||||
|
private AnimationTools() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 由 ObjectPanel.build 调用:加两个按钮并接上点击。 */
|
||||||
|
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
|
||||||
|
Class<?> tableClass, final Class<?> cellClass, final Class<?> actorClass) {
|
||||||
|
if (installed) return;
|
||||||
|
try {
|
||||||
|
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Animate Random Props",
|
||||||
|
new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
final ClassLoader classLoader = loader;
|
||||||
|
Thread thread = new Thread(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { animateRandom(classLoader); } catch (Throwable error) {
|
||||||
|
System.err.println("[animtool] 随机动画失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "spineview-animate-random");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Curve Test 6 Modes",
|
||||||
|
new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
final ClassLoader classLoader = loader;
|
||||||
|
Thread thread = new Thread(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { curveTest(classLoader); } catch (Throwable error) {
|
||||||
|
System.err.println("[animtool] 曲线测试失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "spineview-curve-test");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
installed = true;
|
||||||
|
System.err.println("[animtool] 两个动画按钮已加入");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[animtool] 加按钮失败: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addButton(ClassLoader loader, Object skin, Object buttonsTable,
|
||||||
|
Class<?> cellClass, Class<?> actorClass, String text, Runnable action) throws Exception {
|
||||||
|
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
|
||||||
|
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||||
|
final Object button = textButton.getConstructor(String.class, skinClass).newInstance(text, skin);
|
||||||
|
Object cell = find(buttonsTable.getClass(), "D", new Class<?>[] {actorClass}, cellClass)
|
||||||
|
.invoke(buttonsTable, button);
|
||||||
|
find(cellClass, "C", new Class<?>[0], cellClass).invoke(cell); // Cell.left()
|
||||||
|
find(buttonsTable.getClass(), "I", new Class<?>[0], cellClass).invoke(buttonsTable);
|
||||||
|
ObjectPanel.wire(loader, button, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 按钮 1:随机对象的所有可 K 属性 ====================
|
||||||
|
|
||||||
|
static void animateRandom(ClassLoader loader) throws Exception {
|
||||||
|
final ClassLoader L = loader;
|
||||||
|
final Class<?> gp = Class.forName("GP", true, L);
|
||||||
|
final Class<?> boneC = Class.forName("Gp", true, L);
|
||||||
|
final Class<?> slotC = Class.forName("GU", true, L);
|
||||||
|
final Class<?> attC = Class.forName("Gn", true, L);
|
||||||
|
final Class<?> glC = Class.forName("Gl", true, L);
|
||||||
|
final Class<?> fiC = Class.forName("FI", true, L);
|
||||||
|
final Class<?> fnC = Class.forName("FN", true, L);
|
||||||
|
final Class<?> colorC = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||||
|
SceneActions.onEditorThread(L, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
Object skeleton = null;
|
||||||
|
List<Object> bones = new ArrayList<Object>(), slots = new ArrayList<Object>(),
|
||||||
|
atts = new ArrayList<Object>();
|
||||||
|
for (Object o : SceneObjects.listAllNodes(L)) {
|
||||||
|
if (o == null) continue;
|
||||||
|
if (skeleton == null && gp.isInstance(o)) skeleton = o;
|
||||||
|
if (boneC.isInstance(o)) bones.add(o);
|
||||||
|
if (slotC.isInstance(o)) slots.add(o);
|
||||||
|
if (attC.isInstance(o)) atts.add(o);
|
||||||
|
}
|
||||||
|
if (skeleton == null || bones.isEmpty() || slots.isEmpty() || atts.isEmpty()) {
|
||||||
|
System.err.println("[animtool] 场景对象不够");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Random random = new Random();
|
||||||
|
Object bone = bones.get(random.nextInt(bones.size()));
|
||||||
|
Object slot = slots.get(random.nextInt(slots.size()));
|
||||||
|
Object slotBone = fieldOfType(slotC, slot, boneC);
|
||||||
|
List<Object> own = new ArrayList<Object>();
|
||||||
|
for (Object a : atts) if (fieldOfType(attC, a, slotC) == slot) own.add(a);
|
||||||
|
Object attachment = own.isEmpty() ? atts.get(random.nextInt(atts.size()))
|
||||||
|
: own.get(random.nextInt(own.size()));
|
||||||
|
System.err.println("[animtool] 随机选中:骨骼=" + SceneObjects.nameOf(bone)
|
||||||
|
+ " 插槽=" + SceneObjects.nameOf(slot) + " 附件=" + SceneObjects.nameOf(attachment));
|
||||||
|
|
||||||
|
Object anim = animation(L, skeleton, "ani_api_test");
|
||||||
|
set(anim, "D", Class.forName("fI", true, L),
|
||||||
|
Class.forName("fI", true, L).getConstructor().newInstance());
|
||||||
|
Object rotate = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
|
||||||
|
scalarPair(L, rotate, fiC, fnC, 0f, "D", 0f);
|
||||||
|
scalarPair(L, rotate, fiC, fnC, 30f, "D", 90f);
|
||||||
|
Object translate = find(glC, "B", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
|
||||||
|
xy(L, translate, fiC, fnC, 0f, 0f, 0f);
|
||||||
|
xy(L, translate, fiC, fnC, 30f, 120f, -60f);
|
||||||
|
Object scale = find(glC, "L", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
|
||||||
|
xy(L, scale, fiC, fnC, 0f, 1f, 1f);
|
||||||
|
xy(L, scale, fiC, fnC, 30f, 1.6f, 0.6f);
|
||||||
|
Object shear = find(glC, "I", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
|
||||||
|
xy(L, shear, fiC, fnC, 0f, 0f, 0f);
|
||||||
|
xy(L, shear, fiC, fnC, 30f, 25f, 15f);
|
||||||
|
Object color = find(glC, "D", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
|
||||||
|
color(L, color, fiC, fnC, colorC, 0f, 1f, 1f, 1f, 1f);
|
||||||
|
color(L, color, fiC, fnC, colorC, 30f, 1f, 0.2f, 0.2f, 0.25f);
|
||||||
|
Object att = find(glC, "B", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
|
||||||
|
attachment(L, att, fiC, fnC, attC, 0f, attachment);
|
||||||
|
attachment(L, att, fiC, fnC, attC, 30f, null);
|
||||||
|
refresh(L, skeleton);
|
||||||
|
System.err.println("[animtool] ani_api_test 已重建:6 条时间轴(0f/30f)");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[animtool] 随机动画失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 按钮 2:六种曲线 ====================
|
||||||
|
|
||||||
|
static void curveTest(ClassLoader loader) throws Exception {
|
||||||
|
final ClassLoader L = loader;
|
||||||
|
final Class<?> gp = Class.forName("GP", true, L);
|
||||||
|
final Class<?> boneC = Class.forName("Gp", true, L);
|
||||||
|
final Class<?> glC = Class.forName("Gl", true, L);
|
||||||
|
final Class<?> fiC = Class.forName("FI", true, L);
|
||||||
|
final Class<?> fnC = Class.forName("FN", true, L);
|
||||||
|
final Class<?> fvC = Class.forName("FV", true, L);
|
||||||
|
SceneActions.onEditorThread(L, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
Object skeleton = null;
|
||||||
|
Map<String, Object> bones = new LinkedHashMap<String, Object>();
|
||||||
|
for (Object o : SceneObjects.listAllNodes(L)) {
|
||||||
|
if (o == null) continue;
|
||||||
|
if (skeleton == null && gp.isInstance(o)) skeleton = o;
|
||||||
|
if (boneC.isInstance(o)) bones.put(SceneObjects.nameOf(o), o);
|
||||||
|
}
|
||||||
|
if (skeleton == null) return;
|
||||||
|
String[] order = {"root", "bone", "bone2", "bone3", "bone5", "lizi"};
|
||||||
|
String[] labels = {"linear", "curve smooth", "stepped", "ease-in", "ease-out", "symmetric auto"};
|
||||||
|
Object linear = fvC.getField("D").get(null);
|
||||||
|
Object curve = fvC.getField("B").get(null);
|
||||||
|
Object stepped = fvC.getField("L").get(null);
|
||||||
|
Object[] modes = {linear, curve, stepped, curve, curve, curve};
|
||||||
|
float[][] controls = {{0, 0, 0, 0}, {0.25f, 0f, 0.75f, 1f}, {0, 0, 0, 0},
|
||||||
|
{0.42f, 0f, 1f, 1f}, {0f, 0f, 0.58f, 1f}, {0.25f, 0.25f, 0.75f, 0.75f}};
|
||||||
|
Object anim = animation(L, skeleton, "curve_test");
|
||||||
|
set(anim, "D", Class.forName("fI", true, L),
|
||||||
|
Class.forName("fI", true, L).getConstructor().newInstance());
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < order.length; i++) {
|
||||||
|
Object bone = bones.get(order[i]);
|
||||||
|
if (bone == null) continue;
|
||||||
|
Object tl = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
|
||||||
|
Object k0 = key(L, tl, fiC, fnC, 0f);
|
||||||
|
Object k1 = key(L, tl, fiC, fnC, 30f);
|
||||||
|
setFloatMethod(fnC, k0, "D", 0f); // angle 0
|
||||||
|
setFloatMethod(fnC, k1, "D", 45f); // angle 45
|
||||||
|
set(k0, "D", fvC, modes[i]);
|
||||||
|
set(k0, "B", float.class, Float.valueOf(controls[i][0]));
|
||||||
|
set(k0, "L", float.class, Float.valueOf(controls[i][1]));
|
||||||
|
set(k0, "I", float.class, Float.valueOf(controls[i][2]));
|
||||||
|
set(k0, "S", float.class, Float.valueOf(controls[i][3]));
|
||||||
|
rebuild(fiC, tl);
|
||||||
|
System.err.println("[animtool] curve_test " + order[i] + " = " + labels[i]);
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
refresh(L, skeleton);
|
||||||
|
System.err.println("[animtool] curve_test 已重建:共 " + made + " 条旋转轴(0f/30f,0°→45°)");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[animtool] 曲线测试失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 时间轴/关键帧 ====================
|
||||||
|
|
||||||
|
/** 取或建动画(并按编辑器自己的 Gl.D() 注册)。 */
|
||||||
|
private static Object animation(ClassLoader L, Object skeleton, String name) throws Exception {
|
||||||
|
Class<?> gp = Class.forName("GP", true, L);
|
||||||
|
Class<?> glC = Class.forName("Gl", true, L);
|
||||||
|
Class<?> fI = Class.forName("fI", true, L);
|
||||||
|
Field animsF = gp.getDeclaredField("I");
|
||||||
|
animsF.setAccessible(true);
|
||||||
|
Object anims = animsF.get(skeleton);
|
||||||
|
for (int i = 0; i < size(fI, anims); i++) {
|
||||||
|
Object a = get(fI, anims, i);
|
||||||
|
if (a != null && name.equals(fieldOfType(glC, a, String.class))) return a;
|
||||||
|
}
|
||||||
|
Object anim = glC.getConstructor().newInstance();
|
||||||
|
set(anim, "D", gp, skeleton);
|
||||||
|
set(anim, "D", String.class, name);
|
||||||
|
for (Method m : glC.getDeclaredMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 0
|
||||||
|
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(anim); }
|
||||||
|
}
|
||||||
|
add(fI, anims, anim);
|
||||||
|
System.err.println("[animtool] 新建动画 " + name);
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object key(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, float time)
|
||||||
|
throws Exception {
|
||||||
|
Method get = find(fiC, "D", new Class<?>[] {float.class}, fnC);
|
||||||
|
Object k = get.invoke(tl, Float.valueOf(time));
|
||||||
|
if (k != null) return k;
|
||||||
|
k = fnC.getConstructor(fiC).newInstance(tl);
|
||||||
|
set(k, "D", float.class, Float.valueOf(time));
|
||||||
|
find(fiC, "D", new Class<?>[] {fnC, boolean.class}, void.class)
|
||||||
|
.invoke(tl, k, Boolean.FALSE);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scalarPair(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
|
||||||
|
float time, String setter, float value) throws Exception {
|
||||||
|
Object k = key(L, tl, fiC, fnC, time);
|
||||||
|
setFloatMethod(fnC, k, setter, value);
|
||||||
|
rebuild(fiC, tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void xy(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
|
||||||
|
float time, float x, float y) throws Exception {
|
||||||
|
Object k = key(L, tl, fiC, fnC, time);
|
||||||
|
setFloatMethod(fnC, k, "B", x);
|
||||||
|
setFloatMethod(fnC, k, "L", y);
|
||||||
|
rebuild(fiC, tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void color(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, Class<?> colorC,
|
||||||
|
float time, float r, float g, float b, float a) throws Exception {
|
||||||
|
Object k = key(L, tl, fiC, fnC, time);
|
||||||
|
Object color = fieldOfType(fnC, k, colorC);
|
||||||
|
if (color == null) color = colorC.getConstructor().newInstance();
|
||||||
|
colorC.getField("r").setFloat(color, r);
|
||||||
|
colorC.getField("g").setFloat(color, g);
|
||||||
|
colorC.getField("b").setFloat(color, b);
|
||||||
|
colorC.getField("a").setFloat(color, a);
|
||||||
|
set(k, "D", colorC, color);
|
||||||
|
rebuild(fiC, tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void attachment(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
|
||||||
|
Class<?> attC, float time, Object attachment) throws Exception {
|
||||||
|
Object k = key(L, tl, fiC, fnC, time);
|
||||||
|
set(k, "D", attC, attachment);
|
||||||
|
rebuild(fiC, tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rebuild(Class<?> fiC, Object tl) throws Exception {
|
||||||
|
find(fiC, "L", new Class<?>[0], void.class).invoke(tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void refresh(ClassLoader L, Object skeleton) throws Exception {
|
||||||
|
Class<?> gp = Class.forName("GP", true, L);
|
||||||
|
Class<?> vz = Class.forName("VZ", true, L);
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
Object tree = staticField(og, vz);
|
||||||
|
Method refresh = find(vz, "D", new Class<?>[] {gp, boolean.class, boolean.class}, void.class);
|
||||||
|
if (refresh != null) refresh.invoke(tree, skeleton, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 反射工具 ====================
|
||||||
|
|
||||||
|
private static void setFloatMethod(Class<?> owner, Object target, String name, float value)
|
||||||
|
throws Exception {
|
||||||
|
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
for (Method m : c.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
Class<?>[] types = m.getParameterTypes();
|
||||||
|
if (types.length != 1 || types[0] != float.class) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
m.invoke(target, Float.valueOf(value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchMethodException("float " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object fieldOfType(Class<?> owner, Object target, Class<?> type) {
|
||||||
|
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
for (Field f : c.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(target);
|
||||||
|
if (v != null) return v;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object staticField(Class<?> owner, Class<?> type) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void set(Object target, String name, Class<?> type, Object value) throws Exception {
|
||||||
|
for (Class<?> c = target.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
for (Field f : c.getDeclaredFields()) {
|
||||||
|
if (!f.getName().equals(name) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(target, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchFieldException(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
for (Method m : c.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(owner.getName() + "." + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void add(Class<?> arrayClass, Object array, Object item) throws Exception {
|
||||||
|
find(arrayClass, "D", new Class<?>[] {Object.class}, void.class).invoke(array, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int size(Class<?> arrayClass, Object array) {
|
||||||
|
if (array == null) return 0;
|
||||||
|
for (Class<?> c = arrayClass; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||||
|
for (Field f : c.getDeclaredFields()) {
|
||||||
|
if (f.getType() != int.class) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object get(Class<?> arrayClass, Object array, int index) {
|
||||||
|
try {
|
||||||
|
return find(arrayClass, "D", new Class<?>[] {int.class}, Object.class)
|
||||||
|
.invoke(array, Integer.valueOf(index));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,682 @@
|
|||||||
|
package com.spineview;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* “New 15 Bone+Slot+Att” 按钮(正式版,重启后仍在)。
|
||||||
|
*
|
||||||
|
* 流程:点一下 → 弹编辑器自带的名称输入面板 → 用输入的名字匹配骨架里的附件
|
||||||
|
* → 名字不对/找不到就结束 → 否则在**当前选中骨骼**下建 15 根骨骼,
|
||||||
|
* 每根 1 个插槽 + 1 个附件(该名字的附件副本)。
|
||||||
|
*
|
||||||
|
* 参数要求(全部按已确认的字段语义写,见 混淆名对照表.md):
|
||||||
|
* 骨骼:x/y 各 0~500 随机;rotation=0;scaleX=scaleY=1;shearX=shearY=0
|
||||||
|
* 插槽:颜色 alpha 0.5~1 随机;blend = normal / additive 随机
|
||||||
|
* 附件(挂在插槽上):x=y=rotation=0;scaleX=scaleY=1
|
||||||
|
*
|
||||||
|
* 关键 API(都在 tools/deobf/anchors.tsv 里,附字节码依据):
|
||||||
|
* UH.D(GU,Gp,Z,fI)GU addSlot ⚠️ 会连模板插槽的附件一起复制
|
||||||
|
* UH.D(Gn,GU,Z,Z,fI)Gn addAttachmentCopy(内部 Gn.D(GP) 复制模板附件)
|
||||||
|
* GP.D(Gp)V addBone
|
||||||
|
* OG.D(String,fI,GU)String uniqueName
|
||||||
|
* VZ.D(GP,Z,Z)V refresh
|
||||||
|
* un.D()/un.B() undoBegin/undoCommit
|
||||||
|
* Gn.L()V delete
|
||||||
|
* dt.D(dl)V Stage.addActor
|
||||||
|
* xl.D(dt) / xl.L(Object) / xl.L(String) 编辑器名称面板:显示 / OK-Cancel 回调 / 拿到名字(抽象)
|
||||||
|
*/
|
||||||
|
final class NewFifteen {
|
||||||
|
private static final int COUNT = 15;
|
||||||
|
private static final String BUTTON_TEXT = "New 15 Bone+Slot+Att";
|
||||||
|
|
||||||
|
/** 生成的对话框子类(ASM 现生成:xl 是抽象类,且必须在载荷类加载器那边实现)。 */
|
||||||
|
private static Class<?> dialogClass;
|
||||||
|
private static ClassLoader dialogLoader;
|
||||||
|
private static boolean installed;
|
||||||
|
|
||||||
|
private NewFifteen() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 由 ObjectPanel.build 调用:往按钮表格里加一行按钮并接上点击。 */
|
||||||
|
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
|
||||||
|
Class<?> tableClass, Class<?> cellClass, Class<?> actorClass) {
|
||||||
|
if (installed) return;
|
||||||
|
try {
|
||||||
|
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
|
||||||
|
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||||
|
final Object button = textButton.getConstructor(String.class, skinClass)
|
||||||
|
.newInstance(BUTTON_TEXT, skin);
|
||||||
|
Object cell = call(buttonsTable, "D", new Class<?>[] {actorClass}, cellClass, button);
|
||||||
|
call(cell, "C", new Class<?>[0], cellClass); // Cell.left()
|
||||||
|
call(buttonsTable, "I", new Class<?>[0], cellClass); // Table.row()
|
||||||
|
ObjectPanel.wire(loader, button, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
// 面板路由线程里:只负责开线程走流程,别把它堵住(要等用户输入)
|
||||||
|
Thread thread = new Thread(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
promptThenCreate(loader);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] 流程失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "spineview-new15");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
installed = true;
|
||||||
|
System.err.println("[new15] 按钮已加入: " + BUTTON_TEXT);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] 加按钮失败: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 弹面板 → 等名字 → 建对象 ====================
|
||||||
|
|
||||||
|
private static void promptThenCreate(final ClassLoader loader) throws Exception {
|
||||||
|
final Object[] holder = new Object[1];
|
||||||
|
SceneActions.onEditorThread(loader, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
holder[0] = showPrompt(loader);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] 弹面板失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Object dialog = holder[0];
|
||||||
|
if (dialog == null) {
|
||||||
|
System.err.println("[new15] 面板没建出来");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = waitForName(loader, dialog, 300000L);
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
System.err.println("[new15] 没拿到名字(取消/超时)→ 结束");
|
||||||
|
removeQuietly(dialog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String finalName = name.trim();
|
||||||
|
System.err.println("[new15] 输入的名字 = " + finalName);
|
||||||
|
removeQuietly(dialog);
|
||||||
|
SceneActions.onEditorThread(loader, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
create(loader, finalName);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] 建对象失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在渲染线程里:建对话框 → 挂进 Stage → 按 pref 撑开 → 居中(都在同一帧里完成,不会先闪别处)。 */
|
||||||
|
private static Object showPrompt(ClassLoader loader) throws Exception {
|
||||||
|
Class<?> xl = Class.forName("xl", true, loader);
|
||||||
|
Class<?> stageClass = Class.forName("dt", true, loader);
|
||||||
|
Object stage = global(loader, stageClass);
|
||||||
|
if (stage == null) throw new IllegalStateException("拿不到 Stage");
|
||||||
|
Class<?> dialog = dialogClass(loader);
|
||||||
|
Object dialogObject = dialog.getConstructor(String.class, String.class)
|
||||||
|
.newInstance("New Bones + Slots + Attachment",
|
||||||
|
"Enter an existing attachment name in this skeleton:");
|
||||||
|
// ① 挂进 Stage(实测 xl.D(dt) 不会挂;Stage.addActor 用"扫描 getMethods"那个才生效)
|
||||||
|
addActor(stage, dialogObject);
|
||||||
|
// ② 走编辑器自己的显示流程(把键盘焦点给名字框)
|
||||||
|
Method show = null;
|
||||||
|
for (Method method : xl.getMethods()) {
|
||||||
|
Class<?>[] types = method.getParameterTypes();
|
||||||
|
if (types.length == 1 && types[0] == stageClass) show = method;
|
||||||
|
}
|
||||||
|
if (show != null) {
|
||||||
|
try {
|
||||||
|
show.setAccessible(true);
|
||||||
|
show.invoke(dialogObject, stage);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] xl.D(dt) 失败: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ③ 尺寸:默认 150x150 会把正文/按钮裁掉 → 用 pref(下限 360x170)
|
||||||
|
float prefW = floatOf(dialogObject, "StackMapTable"); // getPrefWidth
|
||||||
|
float prefH = floatOf(dialogObject, "init"); // getPrefHeight
|
||||||
|
float minW = floatOf(dialogObject, "LineNumberTable"); // getMinWidth
|
||||||
|
float minH = floatOf(dialogObject, "String"); // getMinHeight
|
||||||
|
float width = Math.max(Math.max(prefW, minW), 360f);
|
||||||
|
float height = Math.max(Math.max(prefH, minH), 170f);
|
||||||
|
setFloatField(loader, dialogObject, "L", width); // Actor.width
|
||||||
|
setFloatField(loader, dialogObject, "I", height); // Actor.height
|
||||||
|
invalidate(loader, dialogObject);
|
||||||
|
// ④ 居中:setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心"
|
||||||
|
center(stage, dialogObject);
|
||||||
|
return dialogObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轮询用户操作:OK 时 NAME 有值;Cancel 时 DONE=true 且 NAME 为空;窗口被移除时 stage 字段变 null。 */
|
||||||
|
private static String waitForName(ClassLoader loader, Object dialog, long timeoutMs)
|
||||||
|
throws Exception {
|
||||||
|
Field nameField = dialog.getClass().getField("NAME");
|
||||||
|
Field doneField = dialog.getClass().getField("DONE");
|
||||||
|
Field stageField = fieldOfType(Class.forName("dl", true, loader),
|
||||||
|
Class.forName("dt", true, loader));
|
||||||
|
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
Object name = nameField.get(dialog);
|
||||||
|
if (name != null) return (String) name;
|
||||||
|
if (Boolean.TRUE.equals(doneField.get(dialog))) return null;
|
||||||
|
if (stageField != null && stageField.get(dialog) == null) return null;
|
||||||
|
Thread.sleep(120L);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 建 15 组对象 ====================
|
||||||
|
|
||||||
|
private static void create(ClassLoader loader, String name) throws Exception {
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> attClass = Class.forName("Gn", true, loader);
|
||||||
|
Class<?> hiClass = Class.forName("Hi", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> blendClass = Class.forName("iz", true, loader);
|
||||||
|
Class<?> uhClass = Class.forName("UH", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> undoClass = Class.forName("un", true, loader);
|
||||||
|
Class<?> treeClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Object selected = SceneObjects.selected(loader);
|
||||||
|
if (selected == null || !boneClass.isInstance(selected)) {
|
||||||
|
System.err.println("[new15] 当前选中的不是骨骼 → 结束");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object skeleton = firstFieldOfType(boneClass, selected, skeletonClass);
|
||||||
|
if (skeleton == null) {
|
||||||
|
System.err.println("[new15] 拿不到骨架 → 结束");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object bones = arrayOfType(skeletonClass, skeleton, arrayClass, boneClass);
|
||||||
|
Object slots = arrayOfType(skeletonClass, skeleton, arrayClass, slotClass);
|
||||||
|
Object attachments = arrayOfType(skeletonClass, skeleton, arrayClass, attClass);
|
||||||
|
if (bones == null || slots == null || attachments == null) {
|
||||||
|
System.err.println("[new15] 骨架的骨骼/插槽/附件表没找齐 → 结束");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 按名字找附件(优先非 Hi 占位)
|
||||||
|
Object template = null;
|
||||||
|
for (int i = 0; i < size(arrayClass, attachments); i++) {
|
||||||
|
Object candidate = get(arrayClass, attachments, i);
|
||||||
|
if (candidate == null || !attClass.isInstance(candidate)) continue;
|
||||||
|
if (!name.equals(SceneObjects.nameOf(candidate))) continue;
|
||||||
|
if (!hiClass.isInstance(candidate)) {
|
||||||
|
template = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (template == null) template = candidate;
|
||||||
|
}
|
||||||
|
if (template == null) {
|
||||||
|
System.err.println("[new15] 骨架里没有名为 \"" + name + "\" 的附件 → 结束");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object templateSlot = size(arrayClass, slots) > 0 ? get(arrayClass, slots, 0) : null;
|
||||||
|
|
||||||
|
Object uh = staticField(og, uhClass);
|
||||||
|
if (uh == null) {
|
||||||
|
System.err.println("[new15] 拿不到编辑器模型助手 UH → 结束");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Method addBone = find(skeletonClass, "D", new Class<?>[] {boneClass}, void.class);
|
||||||
|
Method addSlot = find(uhClass, "D", new Class<?>[] {slotClass, boneClass, boolean.class,
|
||||||
|
arrayClass}, slotClass);
|
||||||
|
Method addAttachment = find(uhClass, "D", new Class<?>[] {attClass, slotClass, boolean.class,
|
||||||
|
boolean.class, arrayClass}, attClass);
|
||||||
|
Method nameGen = find(og, "D", new Class<?>[] {String.class, arrayClass, slotClass}, String.class);
|
||||||
|
Method refresh = find(treeClass, "D", new Class<?>[] {skeletonClass, boolean.class,
|
||||||
|
boolean.class}, void.class);
|
||||||
|
Method deleteAttachment = find(attClass, "L", new Class<?>[0], void.class);
|
||||||
|
Method undoBegin = noArgVoid(undoClass, "D");
|
||||||
|
Method undoCommit = noArgVoid(undoClass, "B");
|
||||||
|
if (addBone == null || addSlot == null || addAttachment == null || nameGen == null
|
||||||
|
|| undoBegin == null || undoCommit == null) {
|
||||||
|
System.err.println("[new15] 编辑器 API 没找齐,停止");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object blendNormal = blendClass.getField("D").get(null);
|
||||||
|
Object blendAdditive = blendClass.getField("B").get(null);
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Object empty = arrayClass.getConstructor().newInstance();
|
||||||
|
Random random = new Random();
|
||||||
|
int made = 0;
|
||||||
|
|
||||||
|
undoBegin.setAccessible(true);
|
||||||
|
undoBegin.invoke(null);
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
// ---- 骨骼:x/y 0~500 随机,其余按 0/1 ----
|
||||||
|
Object bone = boneCtor.newInstance(selected, skeleton, selected);
|
||||||
|
set(bone, "D", String.class, nameGen.invoke(null, "bone", bones, null));
|
||||||
|
setFloatField(loader, bone, "Z", random.nextFloat() * 500f); // x
|
||||||
|
setFloatField(loader, bone, "V", random.nextFloat() * 500f); // y
|
||||||
|
setFloatField(loader, bone, "F", 0f); // rotation
|
||||||
|
setFloatField(loader, bone, "Code", 1f); // scaleX
|
||||||
|
setFloatField(loader, bone, "SourceFile", 1f); // scaleY
|
||||||
|
setFloatField(loader, bone, "StackMapTable", 0f); // shearX
|
||||||
|
setFloatField(loader, bone, "LineNumberTable", 0f); // shearY
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
addBone.invoke(skeleton, bone);
|
||||||
|
|
||||||
|
// ---- 插槽:alpha 0.5~1 随机;blend normal/additive 随机 ----
|
||||||
|
Object out = arrayClass.getConstructor().newInstance();
|
||||||
|
addSlot.setAccessible(true);
|
||||||
|
Object slot = addSlot.invoke(uh, templateSlot, bone, Boolean.FALSE, out);
|
||||||
|
if (slot == null) continue;
|
||||||
|
// ADD_SLOT 会把模板插槽整个复制,连它挂着的附件一起复制 → 记下来待会删
|
||||||
|
Object inherited = readSlotAttachment(slotClass, attClass, slot);
|
||||||
|
float alpha = 0.5f + random.nextFloat() * 0.5f;
|
||||||
|
Object color = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 1f, 1f, alpha);
|
||||||
|
set(slot, "B", colorClass, color); // setup 颜色
|
||||||
|
set(slot, "D", colorClass, color); // animate 颜色
|
||||||
|
set(slot, "D", blendClass, random.nextBoolean() ? blendNormal : blendAdditive);
|
||||||
|
|
||||||
|
// ---- 附件:复制"输入名字"的附件,变换 0/0/0/1/1 ----
|
||||||
|
addAttachment.setAccessible(true);
|
||||||
|
Object attachment = addAttachment.invoke(uh, template, slot, Boolean.FALSE, Boolean.FALSE, empty);
|
||||||
|
if (attachment == null) continue;
|
||||||
|
setFloatField(loader, attachment, "D", 0f); // x
|
||||||
|
setFloatField(loader, attachment, "B", 0f); // y
|
||||||
|
setFloatField(loader, attachment, "S", 0f); // rotation
|
||||||
|
setFloatField(loader, attachment, "L", 1f); // scaleX
|
||||||
|
setFloatField(loader, attachment, "I", 1f); // scaleY
|
||||||
|
if (!name.equals(SceneObjects.nameOf(attachment))) {
|
||||||
|
set(attachment, "D", String.class, name); // 编辑器若自动改名,强制回输入的名字
|
||||||
|
}
|
||||||
|
// 插槽的当前附件 = 我们这份(两个字段都写,setup/animate 都覆盖)
|
||||||
|
set(slot, "B", attClass, attachment);
|
||||||
|
set(slot, "D", attClass, attachment);
|
||||||
|
// 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件")
|
||||||
|
if (inherited != null && inherited != attachment && deleteAttachment != null) {
|
||||||
|
deleteAttachment.setAccessible(true);
|
||||||
|
deleteAttachment.invoke(inherited);
|
||||||
|
}
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
undoCommit.setAccessible(true);
|
||||||
|
undoCommit.invoke(null);
|
||||||
|
if (refresh != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(staticField(og, treeClass), skeleton, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
System.err.println("[new15] 完成:在 " + SceneObjects.nameOf(selected) + " 下建了 " + made
|
||||||
|
+ " 根骨骼(每根 1 插槽 1 附件 \"" + name + "\")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 对话框子类(ASM) ====================
|
||||||
|
|
||||||
|
private static synchronized Class<?> dialogClass(final ClassLoader loader) throws Exception {
|
||||||
|
if (dialogClass != null) return dialogClass;
|
||||||
|
final String internal = "com/spineview/NewFifteenPrompt";
|
||||||
|
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||||
|
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null);
|
||||||
|
writer.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd();
|
||||||
|
writer.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd();
|
||||||
|
// 构造:super(title, body, (Ge) null)
|
||||||
|
MethodVisitor ctor = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)V", null, null);
|
||||||
|
ctor.visitCode();
|
||||||
|
ctor.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
|
ctor.visitVarInsn(Opcodes.ALOAD, 1);
|
||||||
|
ctor.visitVarInsn(Opcodes.ALOAD, 2);
|
||||||
|
ctor.visitInsn(Opcodes.ACONST_NULL);
|
||||||
|
ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "<init>",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;LGe;)V", false);
|
||||||
|
ctor.visitInsn(Opcodes.RETURN);
|
||||||
|
ctor.visitMaxs(0, 0);
|
||||||
|
ctor.visitEnd();
|
||||||
|
// 抽象回调:OK 时拿到名字 → 记 NAME,并关窗
|
||||||
|
MethodVisitor name = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V",
|
||||||
|
null, null);
|
||||||
|
name.visitCode();
|
||||||
|
name.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
|
name.visitVarInsn(Opcodes.ALOAD, 1);
|
||||||
|
name.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;");
|
||||||
|
name.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
|
name.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false); // remove()
|
||||||
|
name.visitInsn(Opcodes.POP);
|
||||||
|
name.visitInsn(Opcodes.RETURN);
|
||||||
|
name.visitMaxs(0, 0);
|
||||||
|
name.visitEnd();
|
||||||
|
// OK/Cancel 总回调:不管哪个都置 DONE
|
||||||
|
MethodVisitor done = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V",
|
||||||
|
null, null);
|
||||||
|
done.visitCode();
|
||||||
|
done.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
|
done.visitVarInsn(Opcodes.ALOAD, 1);
|
||||||
|
done.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false);
|
||||||
|
done.visitVarInsn(Opcodes.ALOAD, 0);
|
||||||
|
done.visitInsn(Opcodes.ICONST_1);
|
||||||
|
done.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z");
|
||||||
|
done.visitInsn(Opcodes.RETURN);
|
||||||
|
done.visitMaxs(0, 0);
|
||||||
|
done.visitEnd();
|
||||||
|
writer.visitEnd();
|
||||||
|
final byte[] bytes = writer.toByteArray();
|
||||||
|
dialogLoader = new ClassLoader(loader) {
|
||||||
|
@Override protected Class<?> loadClass(String name, boolean resolve)
|
||||||
|
throws ClassNotFoundException {
|
||||||
|
if (name.equals("com.spineview.NewFifteenPrompt")) {
|
||||||
|
Class<?> found = findLoadedClass(name);
|
||||||
|
if (found == null) found = defineClass(name, bytes, 0, bytes.length);
|
||||||
|
if (resolve) resolveClass(found);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dialogClass = Class.forName("com.spineview.NewFifteenPrompt", true, dialogLoader);
|
||||||
|
return dialogClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 小工具 ====================
|
||||||
|
|
||||||
|
private static Object global(ClassLoader loader, Class<?> type) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
for (Field field : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object value = field.get(null);
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 读不到就返回 null
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 挂进 Stage:用"扫描 getMethods 里 1 个 Actor 参数 + void"的那个(getMethod 那个实测不生效)。 */
|
||||||
|
private static void addActor(Object stage, Object actor) throws Exception {
|
||||||
|
ClassLoader loader = stage.getClass().getClassLoader();
|
||||||
|
Class<?> actorClass = Class.forName("dl", true, loader);
|
||||||
|
Method add = null;
|
||||||
|
for (Method method : stage.getClass().getMethods()) {
|
||||||
|
Class<?>[] types = method.getParameterTypes();
|
||||||
|
if (types.length != 1) continue;
|
||||||
|
if (!actorClass.isAssignableFrom(types[0])) continue;
|
||||||
|
if (method.getReturnType() != void.class) continue;
|
||||||
|
add = method;
|
||||||
|
}
|
||||||
|
if (add == null) throw new NoSuchMethodException("Stage.addActor");
|
||||||
|
add.setAccessible(true);
|
||||||
|
add.invoke(stage, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void center(Object stage, Object dialog) {
|
||||||
|
try {
|
||||||
|
float width = floatOf(dialog, "L"); // getWidth
|
||||||
|
float height = floatOf(dialog, "I"); // getHeight
|
||||||
|
float cx = floatOf(stage, "D") / 2f; // Stage.getWidth / 2
|
||||||
|
float cy = floatOf(stage, "B") / 2f; // Stage.getHeight / 2
|
||||||
|
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
|
||||||
|
Method setPosition = null;
|
||||||
|
for (Method method : actorClass.getMethods()) {
|
||||||
|
Class<?>[] types = method.getParameterTypes();
|
||||||
|
if (types.length == 3 && types[0] == float.class && types[1] == float.class
|
||||||
|
&& types[2] == int.class) setPosition = method;
|
||||||
|
}
|
||||||
|
if (setPosition != null) {
|
||||||
|
setPosition.setAccessible(true);
|
||||||
|
// align=0 → 以 (x,y) 为中心
|
||||||
|
setPosition.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0));
|
||||||
|
}
|
||||||
|
System.err.println("[new15] 弹窗 " + width + "x" + height + " 居中于 (" + cx + "," + cy + ")");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[new15] 居中失败: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invalidate(ClassLoader loader, Object dialog) {
|
||||||
|
try {
|
||||||
|
Class<?> widgetGroup = Class.forName("eX", true, loader);
|
||||||
|
for (Method method : widgetGroup.getMethods()) {
|
||||||
|
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
|
||||||
|
&& method.getReturnType() == void.class) {
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(dialog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 没 invalidate 也不致命
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeQuietly(Object dialog) {
|
||||||
|
try {
|
||||||
|
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
|
||||||
|
Method remove = null;
|
||||||
|
for (Method method : actorClass.getMethods()) {
|
||||||
|
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
|
||||||
|
&& method.getReturnType() == boolean.class) remove = method;
|
||||||
|
}
|
||||||
|
if (remove != null) {
|
||||||
|
remove.setAccessible(true);
|
||||||
|
remove.invoke(dialog);
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 已经关掉了
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object readSlotAttachment(Class<?> slotClass, Class<?> attClass, Object slot)
|
||||||
|
throws Exception {
|
||||||
|
for (Class<?> type = slotClass; type != null && type != Object.class; type = type.getSuperclass()) {
|
||||||
|
for (Field field : type.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != attClass) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object value = field.get(slot);
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field fieldOfType(Class<?> owner, Class<?> type) {
|
||||||
|
for (Class<?> current = owner; current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object firstFieldOfType(Class<?> owner, Object target, Class<?> type) {
|
||||||
|
for (Class<?> current = owner; current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object value = field.get(target);
|
||||||
|
if (value != null) return value;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object arrayOfType(Class<?> owner, Object target, Class<?> arrayClass,
|
||||||
|
Class<?> elementType) {
|
||||||
|
for (Class<?> current = owner; current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || !arrayClass.isAssignableFrom(field.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object array;
|
||||||
|
try {
|
||||||
|
array = field.get(target);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (array == null) continue;
|
||||||
|
for (int i = 0; i < size(arrayClass, array); i++) {
|
||||||
|
Object element = get(arrayClass, array, i);
|
||||||
|
if (element != null && elementType.isInstance(element)) return array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloatField(ClassLoader loader, Object target, String name, float value)
|
||||||
|
throws Exception {
|
||||||
|
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||||||
|
type = type.getSuperclass()) {
|
||||||
|
for (Field field : type.getDeclaredFields()) {
|
||||||
|
if (!field.getName().equals(name) || field.getType() != float.class) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.setFloat(target, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void set(Object target, String name, Class<?> type, Object value) throws Exception {
|
||||||
|
for (Class<?> current = target.getClass(); current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
if (!field.getName().equals(name) || field.getType() != type) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float floatOf(Object target, String name) {
|
||||||
|
try {
|
||||||
|
for (Method method : target.getClass().getMethods()) {
|
||||||
|
if (!method.getName().equals(name)) continue;
|
||||||
|
if (method.getParameterTypes().length != 0) continue;
|
||||||
|
if (method.getReturnType() != float.class) continue;
|
||||||
|
method.setAccessible(true);
|
||||||
|
return ((Float) method.invoke(target)).floatValue();
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 读不到
|
||||||
|
}
|
||||||
|
return -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object call(Object target, String name, Class<?>[] parameterTypes,
|
||||||
|
Class<?> returnType, Object... args) throws Exception {
|
||||||
|
for (Class<?> current = target.getClass(); current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Method method : current.getDeclaredMethods()) {
|
||||||
|
if (!method.getName().equals(name)) continue;
|
||||||
|
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||||||
|
if (returnType != null && method.getReturnType() != returnType) continue;
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(target, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchMethodException(target.getClass().getName() + "." + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object staticField(Class<?> owner, Class<?> type) {
|
||||||
|
for (Field field : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object value = field.get(null);
|
||||||
|
if (value != null) return value;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method find(Class<?> owner, String name, Class<?>[] parameterTypes,
|
||||||
|
Class<?> returnType) {
|
||||||
|
for (Class<?> current = owner; current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Method method : current.getDeclaredMethods()) {
|
||||||
|
if (!method.getName().equals(name)) continue;
|
||||||
|
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||||||
|
if (returnType != null && method.getReturnType() != returnType) continue;
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method noArgVoid(Class<?> owner, String name) {
|
||||||
|
for (Method method : owner.getDeclaredMethods()) {
|
||||||
|
if (!method.getName().equals(name)) continue;
|
||||||
|
if (method.getParameterTypes().length != 0) continue;
|
||||||
|
if (method.getReturnType() != void.class) continue;
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int size(Class<?> arrayClass, Object array) {
|
||||||
|
if (array == null) return 0;
|
||||||
|
for (Class<?> current = arrayClass; current != null && current != Object.class;
|
||||||
|
current = current.getSuperclass()) {
|
||||||
|
for (Field field : current.getDeclaredFields()) {
|
||||||
|
if (field.getType() != int.class) continue;
|
||||||
|
field.setAccessible(true);
|
||||||
|
try {
|
||||||
|
return field.getInt(array);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object get(Class<?> arrayClass, Object array, int index) {
|
||||||
|
for (Method method : arrayClass.getMethods()) {
|
||||||
|
if (!method.getName().equals("D")) continue;
|
||||||
|
Class<?>[] types = method.getParameterTypes();
|
||||||
|
if (types.length != 1 || types[0] != int.class) continue;
|
||||||
|
if (method.getReturnType() != Object.class) continue;
|
||||||
|
try {
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(array, Integer.valueOf(index));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,6 +136,10 @@ public final class ObjectPanel {
|
|||||||
// 每个按钮一行:上段是**一条整带**(不要左右两列),整体读起来才是"上/中/下"
|
// 每个按钮一行:上段是**一条整带**(不要左右两列),整体读起来才是"上/中/下"
|
||||||
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||||
}
|
}
|
||||||
|
// 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件)
|
||||||
|
NewFifteen.install(loader, skin, buttons, tableClass, cellClass, actorClass);
|
||||||
|
// 动画工具按钮:随机对象 6 属性动画 / 六种曲线测试
|
||||||
|
AnimationTools.install(loader, skin, buttons, tableClass, cellClass, actorClass);
|
||||||
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||||
Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
|
Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
|
||||||
layoutCell(nameCell, Obf.CELL_LEFT);
|
layoutCell(nameCell, Obf.CELL_LEFT);
|
||||||
|
|||||||
@@ -531,3 +531,62 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所
|
|||||||
| `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) |
|
| `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) |
|
||||||
| `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) |
|
| `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) |
|
||||||
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) |
|
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、动画模型(2026-10-17 实测:新建动画 / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线全部跑通)
|
||||||
|
|
||||||
|
这一节是"在动画面板下用编辑器自身 API 建 30 帧动画"时确认的整套模型。每条都真跑过并读回验证。
|
||||||
|
|
||||||
|
### 骨架 / 动画
|
||||||
|
|
||||||
|
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GP.I:LfI` | **animations(动画表)** | 元素是 `Gl`;新建动画后 `animations.add(gl)` |
|
||||||
|
| `GP.D:LFT;` → `FT.D:F` | **projectSettings.fps** | 本项目 fps=**30**。⚠️ 编辑器时间轴刻度直接用**帧**:关键帧时间写 0 和 30,不要换算成秒 |
|
||||||
|
| `Gl.D:LGP;` / `Gl.D:Ljava/lang/String;` | skeleton / name | |
|
||||||
|
| `Gl.D:LfI` | **timelineGroups(时间轴组表)** | 元素是 `FH`。清空动画最稳的做法:直接换一个**新的空 fI**(`removeRange` 的两个 `(int,int)` 重载实测都不缩容) |
|
||||||
|
| `Gl.D()V` | **register(注册到动画列表模型)** | 编辑器 New Animation 命令里就是这么做的 |
|
||||||
|
| `Gl.D(Gp)FI` / `Gl.B(Gp)FI` / `Gl.L(Gp)FI` / `Gl.I(Gp)FI` | **旋转 / 位移 / 缩放 / 斜切轴** | 运行时类依次 `ih` / `io` / `ii` / `ij` |
|
||||||
|
| `Gl.D(Gp,GU)FI` / `Gl.B(Gp,GU)FI` | **插槽颜色 / 附件切换轴** | 运行时类 `hU` / `ip` |
|
||||||
|
| `Gl.D()FI` | **绘制顺序轴** | 运行时类 `hZ` |
|
||||||
|
|
||||||
|
### 时间轴 / 关键帧
|
||||||
|
|
||||||
|
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `FH.D:Gl` / `FH.D:Gp` / `FH.D:LfI` | animation / bone / timelines | `FH` 是按"骨骼或插槽"分组的一层 |
|
||||||
|
| `FI.D:Ljava/lang/Class;` | **runtimeTimelineClass** | `ih`=Rotate、`io`=Translate、`ii`=Scale、`ij`=Shear、`hU`=SlotColor、`ip`=SlotAttachment、`hZ`=DrawOrder |
|
||||||
|
| `FI.D:LfI` | **keys(关键帧表)** | 元素是 `FN` |
|
||||||
|
| `FI.D(float)FN` | **findKey** | 按时间精确找关键帧(找不到返回 null) |
|
||||||
|
| `FI.D(FN,boolean)V` | **insertKey** | 插入关键帧(第二参=是否记撤销) |
|
||||||
|
| `FI.L()V` | **invalidateRuntime** | 标脏运行时轴(改完关键帧调它重建) |
|
||||||
|
| `FI.D(float,int)Z` | **addKeyFromPose** | 按当前姿态在指定时间建关键帧;⚠️ 时间轴上**必须已经有关键帧**,否则抛 `frameCount must be > 0` |
|
||||||
|
|
||||||
|
| `FN` 字段/方法 | 真名 | 依据 / 坑 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `D:F` | **time** | 关键帧时间(帧) |
|
||||||
|
| `D:FV` | **curve** | 曲线类型,见下 |
|
||||||
|
| `B/L/I/S:F` | **curveC2x / c2y / c3x / c3y** | 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1) |
|
||||||
|
| `D(float)` / `D()F` | **setAngle / getAngle** | 旋转轴的角度。⚠️ **不是** `FN.D():ga`! |
|
||||||
|
| `B(float)` / `B()F` | **setX / getX** | 位移/缩放/斜切的 x |
|
||||||
|
| `L(float)` / `L()F` | **setY / getY** | 位移/缩放/斜切的 y |
|
||||||
|
| `D:Color` | **color** | 插槽颜色关键帧 |
|
||||||
|
| `D:Gn` | **attachment** | 附件切换关键帧(null = 隐藏) |
|
||||||
|
| `D:Or` / `D():ga` | **valueSchema / values** | **deform** 用的数值数组(旋转/位移/缩放/斜切不要写这里——曾因此"帧有值但不生效") |
|
||||||
|
|
||||||
|
### 曲线枚举 `FV`(名字取自 `<clinit>`)
|
||||||
|
|
||||||
|
| 字段 | 真名 |
|
||||||
|
| --- | --- |
|
||||||
|
| `FV.D` | **linear(线性)** |
|
||||||
|
| `FV.B` | **curve(贝塞尔)** |
|
||||||
|
| `FV.L` | **stepped(阶跃)** |
|
||||||
|
|
||||||
|
"自动"不是单独的枚举值,而是 **curve + 一组控制值**(本次实测过 0.25,0.25,0.75,0.75 的对称自动;ease-in 用 0.42,0,1,1;ease-out 用 0,0,0.58,1)。
|
||||||
|
|
||||||
|
### 面板按钮(正式源码)
|
||||||
|
|
||||||
|
- `viewpanel/src/com/spineview/AnimationTools.java`
|
||||||
|
- **Animate Random Props**:随机骨骼/插槽/附件 → `ani_api_test`:旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)、插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
|
||||||
|
- **Curve Test 6 Modes**:`curve_test`:6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别 linear / curve 平滑 / stepped / ease-in / ease-out / 对称自动
|
||||||
Reference in new issue
Block a user