modelstage 0.0.0-stage → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +101 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +58 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +647 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +334 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/web/README.md +193 -0
  126. package/src/web/app/anim-list.js +222 -0
  127. package/src/web/app/background-solids.js +150 -0
  128. package/src/web/app/backgrounds.js +152 -0
  129. package/src/web/app/camera-log.js +28 -0
  130. package/src/web/app/context.js +72 -0
  131. package/src/web/app/dir-picker.js +242 -0
  132. package/src/web/app/entry-report.js +71 -0
  133. package/src/web/app/memory-bars.js +91 -0
  134. package/src/web/app/memory-budget.js +228 -0
  135. package/src/web/app/memory-guard.js +204 -0
  136. package/src/web/app/model-actions.js +682 -0
  137. package/src/web/app/renderer-setting.js +70 -0
  138. package/src/web/app/root-manager.js +254 -0
  139. package/src/web/app/status.js +138 -0
  140. package/src/web/app/topbar-actions.js +357 -0
  141. package/src/web/app/transport-actions.js +297 -0
  142. package/src/web/app/ui-state.js +559 -0
  143. package/src/web/app/url-params.js +171 -0
  144. package/src/web/app/view-settings.js +186 -0
  145. package/src/web/components.css +465 -0
  146. package/src/web/debug/diag-hud.js +95 -0
  147. package/src/web/debug/drag.js +369 -0
  148. package/src/web/debug/log.js +320 -0
  149. package/src/web/debug/model-cmp.js +292 -0
  150. package/src/web/debug/panel.css +474 -0
  151. package/src/web/debug/panel.js +712 -0
  152. package/src/web/debug/race-probe.js +82 -0
  153. package/src/web/debug/upload.js +130 -0
  154. package/src/web/engine/adapters/atlas.js +47 -0
  155. package/src/web/engine/adapters/index.js +22 -0
  156. package/src/web/engine/adapters/renderer.js +223 -0
  157. package/src/web/engine/adapters/skeleton.js +105 -0
  158. package/src/web/engine/adapters/texture.js +64 -0
  159. package/src/web/engine/animation.js +181 -0
  160. package/src/web/engine/appearance.js +136 -0
  161. package/src/web/engine/camera-adapter.js +212 -0
  162. package/src/web/engine/effect-chain.js +138 -0
  163. package/src/web/engine/engine-state.js +120 -0
  164. package/src/web/engine/filters.js +224 -0
  165. package/src/web/engine/fit.js +197 -0
  166. package/src/web/engine/frame-gate.js +33 -0
  167. package/src/web/engine/gl-driver.js +56 -0
  168. package/src/web/engine/gl.js +77 -0
  169. package/src/web/engine/index.js +20 -0
  170. package/src/web/engine/loader.js +55 -0
  171. package/src/web/engine/model-info.js +26 -0
  172. package/src/web/engine/pipeline.js +200 -0
  173. package/src/web/engine/player.js +427 -0
  174. package/src/web/engine/pma/blend.js +76 -0
  175. package/src/web/engine/pma/detect.js +118 -0
  176. package/src/web/engine/pma/index.js +8 -0
  177. package/src/web/engine/pma/resolve.js +112 -0
  178. package/src/web/engine/pma/upload.js +127 -0
  179. package/src/web/engine/pma.js +5 -0
  180. package/src/web/engine/render-frame.js +93 -0
  181. package/src/web/engine/runtime-capabilities.js +209 -0
  182. package/src/web/engine/runtime.js +37 -0
  183. package/src/web/engine/skins.js +165 -0
  184. package/src/web/engine/textures.js +174 -0
  185. package/src/web/index.html +115 -0
  186. package/src/web/infra/api.js +531 -0
  187. package/src/web/infra/diag.js +509 -0
  188. package/src/web/infra/document.js +269 -0
  189. package/src/web/infra/dom.js +31 -0
  190. package/src/web/infra/events.mjs +27 -0
  191. package/src/web/infra/log.js +153 -0
  192. package/src/web/infra/mem-stats.js +112 -0
  193. package/src/web/infra/plugin-host-registration.mjs +285 -0
  194. package/src/web/infra/plugin-host-report.mjs +32 -0
  195. package/src/web/infra/plugin-host-unload.mjs +79 -0
  196. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  197. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  198. package/src/web/infra/plugin-host.js +628 -0
  199. package/src/web/infra/ready.js +461 -0
  200. package/src/web/infra/schedule.js +51 -0
  201. package/src/web/infra/shortcuts.js +129 -0
  202. package/src/web/infra/slot-registry.js +402 -0
  203. package/src/web/infra/vfs.js +247 -0
  204. package/src/web/live2d/bridge.js +400 -0
  205. package/src/web/live2d/model.js +321 -0
  206. package/src/web/live2d/overlay.js +142 -0
  207. package/src/web/live2d/pixi-app.js +111 -0
  208. package/src/web/live2d/playback-probe.js +92 -0
  209. package/src/web/live2d/probe-geometry.js +329 -0
  210. package/src/web/live2d/probe.js +119 -0
  211. package/src/web/live2d/release.js +66 -0
  212. package/src/web/live2d/runtime-adapter.js +289 -0
  213. package/src/web/live2d/stage-ctx.js +152 -0
  214. package/src/web/live2d/stage.js +204 -0
  215. package/src/web/live2d/vfs-settings.js +174 -0
  216. package/src/web/live2d/view.js +89 -0
  217. package/src/web/main.js +629 -0
  218. package/src/web/panel/background-list.js +373 -0
  219. package/src/web/panel/bg-source.js +291 -0
  220. package/src/web/panel/color-picker.css +199 -0
  221. package/src/web/panel/color-picker.js +278 -0
  222. package/src/web/panel/dialog.css +116 -0
  223. package/src/web/panel/dialog.js +173 -0
  224. package/src/web/panel/dom.js +35 -0
  225. package/src/web/panel/icons.js +58 -0
  226. package/src/web/panel/memory-bar.js +195 -0
  227. package/src/web/panel/panel-settings.js +1263 -0
  228. package/src/web/panel/panel.css +1304 -0
  229. package/src/web/panel/render-choice.js +104 -0
  230. package/src/web/panel/schema.js +784 -0
  231. package/src/web/panel/settings-io.js +291 -0
  232. package/src/web/panel/shell-controls.js +274 -0
  233. package/src/web/panel/slider.js +210 -0
  234. package/src/web/panel/toolbar-schema.js +111 -0
  235. package/src/web/panel/toolbar.js +195 -0
  236. package/src/web/panel/transport-picker.js +375 -0
  237. package/src/web/panel/transport.js +321 -0
  238. package/src/web/panel/view.js +553 -0
  239. package/src/web/plugin/unpack-worker.js +185 -0
  240. package/src/web/render/camera/constants.js +60 -0
  241. package/src/web/render/camera/index.js +123 -0
  242. package/src/web/render/camera/spine.js +124 -0
  243. package/src/web/render/capture.js +30 -0
  244. package/src/web/render/interaction.js +527 -0
  245. package/src/web/render/live2d.js +627 -0
  246. package/src/web/render/playback.js +47 -0
  247. package/src/web/render/reset-view.js +29 -0
  248. package/src/web/render/spine.js +386 -0
  249. package/src/web/render/surface-switch.js +176 -0
  250. package/src/web/render/surface.js +571 -0
  251. package/src/web/render/surfaces.js +70 -0
  252. package/src/web/roster/filter.js +31 -0
  253. package/src/web/roster/list.js +276 -0
  254. package/src/web/roster/persist.js +52 -0
  255. package/src/web/roster/reveal.js +77 -0
  256. package/src/web/roster/rows.js +248 -0
  257. package/src/web/roster/snapshot.js +37 -0
  258. package/src/web/roster/view.js +670 -0
  259. package/src/web/spine/canvas-api-groups.js +267 -0
  260. package/src/web/spine/canvas-api.js +97 -0
  261. package/src/web/spine/canvas-document.js +80 -0
  262. package/src/web/spine/canvas-engine.js +193 -0
  263. package/src/web/spine/canvas-entry.js +28 -0
  264. package/src/web/spine/canvas-pixels.js +36 -0
  265. package/src/web/spine/canvas-runtime.js +127 -0
  266. package/src/web/spine/canvas-view.js +108 -0
  267. package/src/web/spine/canvas.js +7 -0
  268. package/src/web/spine/runtime-hint.js +73 -0
  269. package/src/web/spine/runtimes.js +212 -0
  270. package/src/web/style.css +493 -0
  271. package/tool/lib/bundle-probe-node.mjs +121 -0
  272. package/tool/lib/check-perms.mjs +114 -0
  273. package/tool/lib/contract-checks.mjs +582 -0
  274. package/tool/lib/cubism-core.mjs +122 -0
  275. package/tool/lib/export-bundle.mjs +328 -0
  276. package/tool/lib/paths.mjs +20 -0
  277. package/tool/lib/runtime-guides.mjs +65 -0
  278. package/tool/lib/runtime-package.mjs +403 -0
  279. package/tool/lib/spine-meta.mjs +227 -0
  280. package/tool/lib/spine-motions.mjs +104 -0
  281. package/tool/lib/spine-runtime.mjs +131 -0
  282. package/tool/lib/test-lock.mjs +178 -0
  283. package/tool/lib/test-report.mjs +133 -0
  284. package/tool/parse-jsonc.mjs +86 -0
  285. package/tool/resolve-live2d-runtimes.mjs +276 -0
  286. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ // resolve-live2d-runtimes.mjs — 自动定位 Live2D 运行时库(供服务端在运行时供给浏览器)
3
+ //
4
+ // 设计目的:
5
+ // 运行时库**不纳入本仓库**(Live2D Cubism Core 受 Live2D Inc. 专有许可约束,不在本项目 MIT 覆盖范围内),
6
+ // 因此需从仓库外部获取。为让「换机器 / 新克隆」也能用,这里做**多候选自动探测**,
7
+ // 与 tool/resolve-runtimes.mjs(Spine 运行时)保持同一套思路与优先级。
8
+ //
9
+ // 需要的文件:
10
+ // live2d.min.js Cubism 2 核心(.moc 模型必需,DestinyChild 资源即此格式)
11
+ // pixi.min.js PIXI 渲染器
12
+ // index.min.js pixi-live2d-display(同时支持 Cubism 2 与 4,Cubism 2 路径必需)
13
+ // live2dcubismcore.min.js Cubism 4 核心(.moc3 模型可选,没有也能跑 Cubism 2)
14
+ //
15
+ // 探测优先级(路径不写死,可由环境变量覆盖 —— 见 no-hardcode-config):
16
+ // 1. 环境变量 LIVE2D_RUNTIME_DIR 显式指定,最高优先
17
+ // 2. 项目内 src/live2d-runtimes/ 本地存在但已 gitignore(不入库)
18
+ // 3. 工作区级共享 ../live2d-runtimes/
19
+ // 4. 在「搜索根」下的各项目里按候选相对路径找——
20
+ // 同工作区的其它 Live2D 项目(spive2d / AzurLane-Live2D-Viewer / DestinyChildLive2D)
21
+ // 各自打包了这些库,直接复用即可,无需下载。
22
+ // 搜索根可用环境变量 LIVE2D_SEARCH_ROOTS(路径分隔符分隔)覆盖。
23
+ //
24
+ // 用法:
25
+ // import { resolveLive2DRuntimes } from './resolve-live2d-runtimes.mjs';
26
+ // const r = resolveLive2DRuntimes(); // { dir, files: { 文件名: 绝对路径 }, origin, missingOptional }
27
+
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+ // 缺库时的引导文案与下载工具同一份(含官网 URL、目标目录、"不代下"的说明),不各写一套
32
+ import { live2dGuide } from './lib/runtime-guides.mjs';
33
+
34
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
+ export const ROOT = path.resolve(__dirname, '..');
36
+
37
+ // 必需文件:前三个是渲染链本身;live2dcubismcore 虽然只有 Cubism 4 模型才真正用到,
38
+ // 但 pixi-live2d-display **初始化时就会检查它**,缺失则直接抛
39
+ // 「Could not find Cubism runtime. This plugin requires live2dcubismcore.js to be loaded.」
40
+ // (实测踩过:只加载 live2d.min.js + pixi + index 会导致整页渲染不可用),故列为必需。
41
+ const REQUIRED = ['live2d.min.js', 'pixi.min.js', 'index.min.js', 'live2dcubismcore.min.js'];
42
+ // 可选:Cubism 4 的框架层,缺了不影响 Cubism 2(天命之子即此格式)
43
+ const OPTIONAL = ['live2dcubismframework.min.js'];
44
+
45
+ // 候选相对路径:库通常放在这些位置(本工作区三个 Live2D 项目实测覆盖了前三种)
46
+ const CANDIDATE_SUBPATHS = [
47
+ 'lib',
48
+ 'static/lib',
49
+ 'js/lib',
50
+ 'src/lib',
51
+ '.', // 库直接放在项目根
52
+ // 项目自带的运行时按版本分目录(src/web/live2d-runtime/<版本>/),
53
+ // 多版本共存、由 pixiMajor 优选;新增版本只需拷一个子目录进来。
54
+ 'v6-6.5.3',
55
+ 'v8-8.19.0',
56
+ ];
57
+
58
+ // 读 pixi.min.js 的主版本号。
59
+ //
60
+ // 为什么要看它来优选运行时组合(**实测口径**):
61
+ // · v6 组合(PIXI 6.5.3 + 配套 index)能渲染 Cubism 2 与 Cubism 4 —— **当前默认选它**(更稳)。
62
+ // · v8 组合(PIXI 8.19.0 + 该 index.min.js)实测**也能跑 Cubism 4**(用 aijier_3 与 aderbote_3
63
+ // 双双出画、60 FPS),保留作对照版本,不默认选。
64
+ // (早期注释写过"PIXI 8 组合在 Cubism 4 上会卡住",那是已被实测推翻的旧结论,勿再引用。)
65
+ // 故仍按 PIXI 主版本优选,PIXI 6 优先。
66
+ //
67
+ // 版本号的三种实测写法(缺一种就会静默读成 null,优选规则再次悄悄失效):
68
+ // v6: `PixiJS 6.5.3 - ✰`(调试横幅)/ `pixi.js - v6.5.3`(头注释)
69
+ // v8: `Ai="8.19.0"`(压缩后的变量赋值,**不是** VERSION=…)
70
+ // 其它构建: `VERSION="x.y.z"`
71
+ function pixiMajor(dir) {
72
+ try {
73
+ const s = fs.readFileSync(path.join(dir, 'pixi.min.js'), 'utf8');
74
+ const patterns = [
75
+ /PixiJS\s+v?(\d+)\.\d+\.\d+/i,
76
+ /pixi\.js\s*-\s*v?(\d+)\.\d+\.\d+/i,
77
+ /VERSION\s*[:=]\s*["'](\d+)\.\d+\.\d+/i,
78
+ /[:=]\s*["'](\d+)\.\d+\.\d+["']/,
79
+ ];
80
+ for (const re of patterns) {
81
+ const m = s.match(re);
82
+ if (m) return Number(m[1]);
83
+ }
84
+ return null;
85
+ } catch { return null; }
86
+ }
87
+
88
+ /** 目录里是否具备必需文件(且非空)。 */
89
+ function hasRequired(dir) {
90
+ if (!dir || !fs.existsSync(dir)) return false;
91
+ return REQUIRED.every((n) => {
92
+ const f = path.join(dir, n);
93
+ try { return fs.statSync(f).isFile() && fs.statSync(f).size > 1024; } catch { return false; }
94
+ });
95
+ }
96
+
97
+ /** 在某个目录的候选子路径里找出具备必需文件的目录,返回其绝对路径或 null。 */
98
+ function findIn(dir) {
99
+ for (const sub of CANDIDATE_SUBPATHS) {
100
+ const d = sub === '.' ? dir : path.join(dir, sub);
101
+ if (hasRequired(d)) return d;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /** 收集搜索根:环境变量优先,否则默认「项目自带」+「工作区根」+「工作区/spine-viewers」+「数据根/下载落点」。 */
107
+ function searchRoots() {
108
+ const env = process.env.LIVE2D_SEARCH_ROOTS;
109
+ if (env) return env.split(path.delimiter).filter(Boolean);
110
+ return [
111
+ // 项目自带(与 src/spine-runtimes 并列):多版本子目录,由 pixiMajor 优选。
112
+ // 放第一位是硬要求 —— 外部目录随时可能被改动/删除,而"用哪套库"必须是项目自己能定的。
113
+ path.join(ROOT, 'src', 'live2d-runtimes'),
114
+ path.resolve(ROOT, '..'),
115
+ path.resolve(ROOT, '..', 'spine-viewers'),
116
+ // **末尾追加**:用户数据根下的下载/手工落点(<dataRoot>/runtimes/live2d/<ver>/)。
117
+ // 放最后 —— 上面三条的既有优先次序不变。
118
+ path.join(dataRootDir(), 'runtimes', 'live2d'),
119
+ ];
120
+ }
121
+
122
+ /**
123
+ * 数据根:与 web.sh / serve.mjs 同一口径(MODELSTAGE_DATA_ROOT 覆盖,缺省项目根)。
124
+ * @returns {string}
125
+ */
126
+ function dataRootDir() {
127
+ const env = (process.env.MODELSTAGE_DATA_ROOT || '').trim();
128
+ return env ? path.resolve(env) : ROOT;
129
+ }
130
+
131
+ /**
132
+ * 未注入 log 时的兜底实现(CLI 直接运行时用):
133
+ * 保留**多行**可读输出——CLI 是开发者主动在终端看的,可读性优先;
134
+ * 服务端则注入 createLogger 的实例,得到"单行 + 时间戳 + LEVEL + tag + 结构化 detail"。
135
+ * 两类消费者不共用一种格式,这是设计选择,不是妥协。
136
+ */
137
+ const cliLog = {
138
+ info: (msg, detail) => {
139
+ if (detail && Array.isArray(detail.candidates)) {
140
+ const list = detail.candidates
141
+ .map((c) => ` PIXI ${c.pixi || '?'} ${c.req}+${c.opt} 个文件 ${c.dir}`)
142
+ .join('\n');
143
+ console.log(`[live2d-runtimes] ${msg}:\n${list}`);
144
+ return;
145
+ }
146
+ console.log(`[live2d-runtimes] ${msg}${detail ? ' ' + JSON.stringify(detail) : ''}`);
147
+ },
148
+ warn: (msg, detail) => console.warn(`[live2d-runtimes] ${msg}`, detail || ''),
149
+ };
150
+
151
+ /**
152
+ * 主入口:按优先级探测 Live2D 运行时库。
153
+ *
154
+ * 选择策略:把**所有候选都收集起来**再按「完整度」择优,而不是"第一个命中就用"。
155
+ * 为什么:本工作区里多个项目各自打包了这些库,且版本并不一致——
156
+ * 实测 AzurLane-Live2D-Viewer 与 spive2d 的 live2d.min.js **不是同一份**(MD5 不同),
157
+ * 而 spive2d 的那份与 DestinyChildLive2D 自用的完全相同(即目标资源的同源核心)。
158
+ * 按「文件更全」择优能自然选中 spive2d 那套(它额外含 Cubism 4 框架,共 5 个文件,
159
+ * 而 AzurLane 只有 4 个)。显式指定的路径(环境变量 / 项目内 / 共享目录)依然最高优先。
160
+ *
161
+ * @returns {{dir: string, files: Object, origin: string, missingOptional: string[], candidates: Array}}
162
+ */
163
+ export function resolveLive2DRuntimes(options = {}) {
164
+ // 依赖反转:日志由调用方注入(tool 层不 import server 层的 logger)
165
+ const log = (options && options.log) || cliLog;
166
+ const tried = [];
167
+
168
+ // 1~3) 显式位置:命中即用(用户主动指定的优先,不做完整度比较)
169
+ const explicit = [
170
+ process.env.LIVE2D_RUNTIME_DIR
171
+ ? { dir: process.env.LIVE2D_RUNTIME_DIR, tag: '环境变量 LIVE2D_RUNTIME_DIR' } : null,
172
+ { dir: path.join(ROOT, 'src', 'live2d-runtimes'), tag: '项目内 src/live2d-runtimes/' },
173
+ { dir: path.resolve(ROOT, '..', 'live2d-runtimes'), tag: '工作区共享 ../live2d-runtimes/' },
174
+ ].filter(Boolean);
175
+ for (const c of explicit) {
176
+ if (hasRequired(c.dir)) return { ...pack(c.dir, c.tag), candidates: [] };
177
+ tried.push(c.tag);
178
+ }
179
+
180
+ // 4) 搜索根:收集全部候选
181
+ const candidates = [];
182
+ for (const base of searchRoots()) {
183
+ if (!fs.existsSync(base)) continue;
184
+ const self = findIn(base);
185
+ if (self) candidates.push({ dir: self, tag: `搜索根 ${base}` });
186
+ let subs = [];
187
+ try {
188
+ subs = fs.readdirSync(base, { withFileTypes: true })
189
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
190
+ .map((e) => path.join(base, e.name));
191
+ } catch { /* 读不到就跳过 */ }
192
+ for (const sub of subs) {
193
+ const hit = findIn(sub);
194
+ if (hit) candidates.push({ dir: hit, tag: `搜索根 ${base} 下的 ${path.basename(sub)}/` });
195
+ }
196
+ tried.push(`搜索根 ${base}`);
197
+ }
198
+
199
+ if (candidates.length) {
200
+ // 优选顺序(**实测依据**,见下方注释):
201
+ // ① PIXI 6 组合优先
202
+ // ② 文件更全(必需 → 可选)
203
+ // ③ 路径更短(稳定排序)
204
+ const scored = candidates.map((c) => {
205
+ const files = collectFiles(c.dir);
206
+ const req = REQUIRED.filter((n) => files[n]).length;
207
+ const opt = OPTIONAL.filter((n) => files[n]).length;
208
+ const pm = pixiMajor(c.dir);
209
+ // 读不出版本时必须告警:否则"PIXI 6 优先"会对这个候选**静默失效**,
210
+ // 结果就是"注释写着优先 6、实际选中 8"(这正是之前踩过的坑)。
211
+ if (pm === null) {
212
+ log.warn('该候选的 pixi.min.js 读不出主版本号,PIXI 6 优先规则对它不生效', { dir: c.dir });
213
+ }
214
+ // pixi6 = 1 表示是 PIXI 6 组合(优先)
215
+ return { ...c, files, req, opt, pixi: pm, pixi6: pm === 6 ? 1 : 0 };
216
+ }).sort((a, b) => (b.pixi6 - a.pixi6) || (b.req - a.req) || (b.opt - a.opt)
217
+ || a.dir.length - b.dir.length);
218
+
219
+ const best = scored[0];
220
+ // 候选清单结构化成一个数组塞进 detail:服务端得到单行可解析日志,
221
+ // CLI 兜底实现(cliLog)会把它还原成多行人类可读格式。
222
+ log.info('候选(按优选顺序)', {
223
+ candidates: scored.map((s) => ({ pixi: s.pixi, req: s.req, opt: s.opt, dir: s.dir })),
224
+ });
225
+ return {
226
+ ...pack(best.dir, best.tag),
227
+ candidates: scored.map((s) => ({ dir: s.dir, req: s.req, opt: s.opt, pixi: s.pixi })),
228
+ };
229
+ }
230
+
231
+ throw new Error(
232
+ '未找到 Live2D 运行时库(需要 live2d.min.js / pixi.min.js / index.min.js)。\n'
233
+ + '请设置环境变量 LIVE2D_RUNTIME_DIR 指向含这三个文件的目录,\n'
234
+ + '或用 LIVE2D_SEARCH_ROOTS 指定搜索根(多个用路径分隔符隔开)。\n'
235
+ + `已尝试:${tried.join('、')}\n`
236
+ // 引导必须是**服务端产出**:目标目录的绝对路径只有服务端知道(数据根),前端拼不出来。
237
+ + live2dGuide(dataRootDir())
238
+ );
239
+ }
240
+
241
+ /** 收集目录下存在(且非空)的必需 + 可选文件。 */
242
+ function collectFiles(dir) {
243
+ const files = {};
244
+ for (const n of [...REQUIRED, ...OPTIONAL]) {
245
+ const f = path.join(dir, n);
246
+ try {
247
+ if (fs.statSync(f).isFile() && fs.statSync(f).size > 1024) files[n] = f;
248
+ } catch { /* 不存在则跳过 */ }
249
+ }
250
+ return files;
251
+ }
252
+
253
+ /** 组装返回值:必需 + 可选文件的绝对路径。 */
254
+ function pack(dir, origin) {
255
+ const files = collectFiles(dir);
256
+ const missingOptional = OPTIONAL.filter((n) => !files[n]);
257
+ return { dir, files, origin, missingOptional };
258
+ }
259
+
260
+ // 直接运行时打印探测结果,便于排查
261
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
262
+ try {
263
+ const r = resolveLive2DRuntimes();
264
+ console.log('探测到 Live2D 运行时来源:', r.origin);
265
+ console.log('目录:', r.dir);
266
+ for (const [n, f] of Object.entries(r.files)) {
267
+ console.log(` ${n}: ${(fs.statSync(f).size / 1024).toFixed(0)} KB`);
268
+ }
269
+ if (r.missingOptional.length) {
270
+ console.log('缺少可选文件(仅影响 Cubism 4 .moc3 模型):', r.missingOptional.join(', '));
271
+ }
272
+ } catch (e) {
273
+ console.error('探测失败:', e.message);
274
+ process.exit(1);
275
+ }
276
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ // Spine 动作名 → 分组 / 循环 / 置信度 规则(能力层数据文件,可编辑)
3
+ // 依据:**全量**扫描 bd2(393 个 skel,成功 389)与 azurlane_spine(195 个,成功 194)的真实动作名归纳
4
+ // (spine_mod 下无 .skel);合计 1075 个去重动作名,其中 948 个被下列族覆盖,其余走 defaults。
5
+ // 5 个失败样本均无同名 .atlas(4 个 bd2 cutscene/illust + 1 个 azurlane_spine 的 jinshi)。
6
+ // 约定:
7
+ // · families 按声明顺序匹配,首个命中即用;overrides 优先于 families(精确名)。
8
+ // · 每条族的 hits 是「实测命中数」(对上述抽样样本的去重名字统计),由 tool 脚本回填校验。
9
+ // · 拿不准循环语义的族,一律 loop:false + confidence:"low"(宁可当一次性动作,也不误判成循环)。
10
+ // · 未命中任何族的名字走 defaults:group=动作名、loop=false、confidence:"low"。
11
+ "version": 1,
12
+ "comment": "Spine 动作分组/循环规则;新增游戏时在 families 里加族,并注明实测依据与命中数",
13
+ "defaults": {
14
+ "loop": false,
15
+ "confidence": "low"
16
+ },
17
+ "families": [
18
+ {
19
+ "id": "loop",
20
+ "group": "loop",
21
+ "patterns": ["^loop\\d*$", "^loop_"],
22
+ "loop": true,
23
+ "confidence": "high",
24
+ "hits": 26,
25
+ "note": "名字里显式带 loop(bd2 的 loop / loop_2 / loop2 / loop_2_button),语义明确,判为循环"
26
+ },
27
+ {
28
+ "id": "idle",
29
+ "group": "idle",
30
+ "patterns": ["^idle\\d*$", "_idle\\d*$"],
31
+ "loop": true,
32
+ "confidence": "high",
33
+ "hits": 24,
34
+ "note": "待机族(bd2 的 idle / idle1~3、azurlane_spine 的 idle);待机应当循环"
35
+ },
36
+ {
37
+ "id": "face",
38
+ "group": "face",
39
+ "patterns": ["^_face\\d+$", "^_face\\d+_talk$"],
40
+ "loop": false,
41
+ "confidence": "high",
42
+ "hits": 64,
43
+ "note": "bd2 的表情族(_face0~_face29 及 _faceN_talk),属于表情切换动作,一次性播放"
44
+ },
45
+ {
46
+ "id": "cut",
47
+ "group": "cut",
48
+ "patterns": ["(^|_)cut(\\d|_|$)"],
49
+ "loop": false,
50
+ "confidence": "high",
51
+ "hits": 148,
52
+ "note": "bd2 的 cut 演出族(cut_A/cut_B/cut_1~4/A_cut/B_cut_All/cut_A_BG 等),是过场演出,一次性"
53
+ },
54
+ {
55
+ "id": "mix",
56
+ "group": "mix",
57
+ "patterns": ["^mix\\d+_"],
58
+ "loop": false,
59
+ "confidence": "low",
60
+ "hits": 618,
61
+ "note": "bd2 的 mix 族(mix1_1_1 这类混合动作);命名族可识别,但循环语义无实测依据,保守按一次性、低置信"
62
+ },
63
+ {
64
+ "id": "motion",
65
+ "group": "motion",
66
+ "patterns": ["^motion\\d*(_\\d+)*$"],
67
+ "loop": false,
68
+ "confidence": "low",
69
+ "hits": 42,
70
+ "note": "通用 motion 命名(bd2 的 motion / motion1_19 / motion2_22);语义不明确,保守按一次性、低置信"
71
+ },
72
+ {
73
+ "id": "normal",
74
+ "group": "normal",
75
+ "patterns": ["^normal$"],
76
+ "loop": false,
77
+ "confidence": "low",
78
+ "hits": 1,
79
+ "note": "azurlane_spine 的 normal(默认立绘动作);是否循环无实测依据,保守按一次性、低置信"
80
+ },
81
+ {
82
+ "id": "variant",
83
+ "group": "variant",
84
+ "patterns": ["^\\d+$"],
85
+ "loop": false,
86
+ "confidence": "low",
87
+ "hits": 24,
88
+ "note": "azurlane_spine 的纯数字命名(1~11,多个立绘变体);语义不明,按变体组、一次性、低置信"
89
+ }
90
+ ],
91
+ "overrides": [
92
+ {
93
+ "name": "zhayan",
94
+ "group": "blink",
95
+ "loop": false,
96
+ "confidence": "high",
97
+ "hits": 1,
98
+ "note": "azurlane_spine 的 zhayan(眨眼);单次眨眼动作,一次性"
99
+ }
100
+ ]
101
+ }