From be0ed2352b2ccf3c03663d3d28a12d9256d83743 Mon Sep 17 00:00:00 2001 From: tianmo Date: Fri, 16 Oct 2026 16:35:23 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E6=94=AF=E6=8C=81=E7=83=AD?= =?UTF-8?q?=E6=9B=B4=EF=BC=9A=E8=A7=86=E5=9B=BE=E9=87=8C=E6=8C=82=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E5=AE=B9=E5=99=A8=EF=BC=88=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E7=B1=BB=E5=8A=A0=E8=BD=BD=E5=99=A8=E5=BB=BA=E7=9A=84=20Table?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E9=9D=A2=E6=9D=BF=E5=86=85=E5=AE=B9=E6=94=BE?= =?UTF-8?q?=E5=9C=A8=E5=AE=B9=E5=99=A8=E9=87=8C=EF=BC=9B=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E8=BD=AE=E8=AF=A2=20build/classes/com/spinev?= =?UTF-8?q?iew/ObjectPanel.class=20=E7=9A=84=20mtime=EF=BC=8C=E4=B8=80?= =?UTF-8?q?=E5=8F=98=E5=B0=B1=E7=94=A8=E6=96=B0=E7=9A=84=E5=AD=90=E7=B1=BB?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=99=A8=E9=87=8D=E5=BB=BA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=8D=A2=E8=BF=9B=E5=AE=B9=E5=99=A8=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E6=94=B9=E5=AE=8C=E4=BB=A3=E7=A0=81=E5=8F=AA=E9=9C=80=20build.?= =?UTF-8?q?sh=EF=BC=8C=E7=BC=96=E8=BE=91=E5=99=A8=E4=B8=8D=E7=94=A8?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E3=80=81=E5=9C=BA=E6=99=AF=E4=B8=8E=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=8A=B6=E6=80=81=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/com/spineview/ViewInstaller.java | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/viewpanel/src/com/spineview/ViewInstaller.java b/viewpanel/src/com/spineview/ViewInstaller.java index d8a3d85..b75b283 100644 --- a/viewpanel/src/com/spineview/ViewInstaller.java +++ b/viewpanel/src/com/spineview/ViewInstaller.java @@ -70,11 +70,19 @@ public final class ViewInstaller { System.err.println("[view] 找不到 Skin"); return false; } + // 视图里挂的是**稳定容器**(由编辑器类加载器建),面板内容放在它里面: + // 这样热更时只要换容器里的内容,编辑器不用重启、场景与选择状态都保留。 + Class tableClass = Class.forName("et", true, loader); + Object holder = tableClass.getConstructor().newInstance(); + callStatic(tableClass, Obf.TABLE_SET_FILL_PARENT, new Class[] {boolean.class}, holder, + Boolean.TRUE); Object panel = buildPanel(loader, skin); + addChild(tableClass, holder, panel); + startHotReload(loader, skin, holder); for (Field field : viewEnum.getDeclaredFields()) { if (!field.getType().getName().equals(ACTOR)) continue; field.setAccessible(true); - field.set(view, panel); + field.set(view, holder); System.err.println("[view] 已把 Spine View 面板挂到视图 " + view); showView(loader, view); return true; @@ -113,6 +121,91 @@ public final class ViewInstaller { } } + /** 调一个"静态/实例"都行的方法:这里用来调 Table.setFillParent(实例方法)。 */ + private static void callStatic(Class owner, String name, Class[] parameterTypes, + Object receiver, Object... arguments) throws Exception { + for (Method method : owner.getMethods()) { + if (!method.getName().equals(name)) continue; + if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue; + method.setAccessible(true); + method.invoke(receiver, arguments); + return; + } + throw new NoSuchMethodException(owner.getName() + "." + name); + } + + /** 往容器里加 Actor(Table.add(Actor))。 */ + private static void addChild(Class tableClass, Object table, Object actor) throws Exception { + for (Method method : tableClass.getMethods()) { + Class[] types = method.getParameterTypes(); + if (types.length != 1) continue; + if (!types[0].isAssignableFrom(actor.getClass())) continue; + if (method.getReturnType() == void.class) continue; + method.setAccessible(true); + method.invoke(table, actor); + return; + } + throw new NoSuchMethodException("找不到 add(Actor)"); + } + + /** 清空容器(Table.clearChildren())。 */ + private static void clearChildren(Class tableClass, Object table) throws Exception { + for (Method method : tableClass.getMethods()) { + if (!method.getName().equals(Obf.TABLE_CLEAR_CHILDREN)) continue; + if (method.getParameterTypes().length != 0) continue; + method.setAccessible(true); + method.invoke(table); + return; + } + throw new NoSuchMethodException("找不到 clearChildren()"); + } + + /** + * 热更:轮询 `viewpanel/build/classes/com/spineview/ObjectPanel.class` 的修改时间, + * 一变就用**新的子类加载器**(parent = 编辑器加载器)重建面板内容,换进稳定容器里。 + * 这样改完代码只要 `./viewpanel/build.sh`,编辑器不用重启。 + */ + private static void startHotReload(final ClassLoader loader, final Object skin, + final Object holder) { + final String classesDir = System.getProperty("spine.classes"); + if (classesDir == null) { + System.err.println("[view] 没有 -Dspine.classes,热更关闭"); + return; + } + final java.io.File marker = new java.io.File(classesDir, "com/spineview/ObjectPanel.class"); + final long[] lastSeen = new long[] {marker.lastModified()}; + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + Thread.sleep(1000); + long stamp = marker.lastModified(); + if (stamp == lastSeen[0] || stamp == 0) continue; + lastSeen[0] = stamp; + Thread.sleep(400); // 等编译落盘 + java.net.URL classesUrl = new java.io.File(classesDir).toURI().toURL(); + java.net.URLClassLoader child = new java.net.URLClassLoader( + new java.net.URL[] {classesUrl}, loader); + Class panelClass = Class.forName("com.spineview.ObjectPanel", true, child); + Object fresh = panelClass.getMethod("build", ClassLoader.class, Object.class) + .invoke(null, child, skin); + Class tableClass = Class.forName("et", true, loader); + clearChildren(tableClass, holder); + addChild(tableClass, holder, fresh); + System.err.println("[view] 热更完成:面板已用新的类加载器重建(未重启编辑器)"); + } catch (InterruptedException ignored) { + return; + } catch (Throwable error) { + System.err.println("[view] 热更失败: " + error); + } + } + } + }, "spineview-hot-reload"); + thread.setDaemon(true); + thread.start(); + } + /** Skin 挂在 OG 的静态字段上(字节码里 OG 有 LSkin;D)。 */ private static Object findSkin(ClassLoader loader) throws Exception { Class holder = Class.forName("OG", true, loader);