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);