modelstage 0.0.0-stage → 0.5.28
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.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +137 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +63 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +654 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +396 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/shared/type-labels.mjs +31 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +524 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +117 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +92 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +39 -0
- package/src/web/roster/type-filter.js +97 -0
- package/src/web/roster/view.js +761 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// plugins/unity-live2d-bundle/lib/unityfs.js —— UnityFS 容器解析
|
|
3
|
+
//
|
|
4
|
+
// 职责:把 UnityFS(Unity 5+ AssetBundle)容器解开,得到
|
|
5
|
+
// ① 头信息(版本/引擎版本/归档标志)
|
|
6
|
+
// ② blocksInfo:数据块表 + **节点表**(每个节点 = 一个内嵌文件,如 CAB-xxx 与 .resS)
|
|
7
|
+
// ③ 数据区起点(节点数据在整份文件里的绝对偏移)与拼接后的数据字节
|
|
8
|
+
//
|
|
9
|
+
// 结构(UnityFS,v6+ 的 read_fs 路径):
|
|
10
|
+
// signature "UnityFS\0" | version u32 | playerVer cstring | engineVer cstring
|
|
11
|
+
// size i64 | blocksInfoCompressedSize u32 | blocksInfoUncompressedSize u32 | dataflags u32
|
|
12
|
+
// [2019.4.15+ 或 version>=7:对齐到 16 字节] ← 未压缩包里会看到一段填充
|
|
13
|
+
// blocksInfo(长度 = blocksInfoCompressedSize,可能被压缩):
|
|
14
|
+
// 16B hash | i32 blockCount | blockCount×{ u32 uncompressedSize, u32 compressedSize, u16 flags }
|
|
15
|
+
// i32 nodeCount | nodeCount×{ i64 offset, i64 size, u32 flags, cstring path }
|
|
16
|
+
// 若 dataflags 带 BlockInfoNeedPaddingAtStart:再对齐到 16 字节
|
|
17
|
+
// blocks:blockCount 段数据(各自按 flags 压缩),拼接后即所有节点的数据源
|
|
18
|
+
//
|
|
19
|
+
// 压缩分支:flags 低 6 位是压缩类型(0=none,1=LZMA,2=LZ4,3=LZ4HC,4=LZHAM)。
|
|
20
|
+
// 本插件实现 none 与 LZ4;其余(LZMA/LZHAM)**抛可读错误**而不是猜,
|
|
21
|
+
// 避免解出错数据后被后面几层误当成"模型损坏"。
|
|
22
|
+
//
|
|
23
|
+
// 挂载:globalThis.__ULB__.unityfs
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
const root = globalThis; // 原 IIFE 的 root 引用(脚本级裸块内保持同名)
|
|
30
|
+
const NS = (root.__ULB__ = root.__ULB__ || {});
|
|
31
|
+
const B = NS.bytes;
|
|
32
|
+
|
|
33
|
+
// 归档标志位(ArchiveFlags,Unity 2020.3 及以后;名字沿用 Unity 源码里的 kArchive*)
|
|
34
|
+
const ARCHIVE = {
|
|
35
|
+
CompressionTypeMask: 0x3f,
|
|
36
|
+
BlocksAndDirectoryInfoCombined: 0x40,
|
|
37
|
+
BlocksInfoAtTheEnd: 0x80,
|
|
38
|
+
OldWebPluginCompatibility: 0x100,
|
|
39
|
+
BlockInfoNeedPaddingAtStart: 0x200,
|
|
40
|
+
UsesAssetBundleEncryption: 0x1400,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 关于 BlocksAndDirectoryInfoCombined(0x40):**不参与解析判断**。
|
|
44
|
+
//
|
|
45
|
+
// 它名义上表示「blocks 与 directoryInfo 合并为一段」,但权威实现(AssetStudio 的
|
|
46
|
+
// BundleFile.cs → ReadBlocksInfoAndDirectory)的判断链里**从不读这一位**,
|
|
47
|
+
// 实际只按 0x80(BlocksInfoAtTheEnd)区分两种布局。详见 doc/参考/README.md 结论一。
|
|
48
|
+
//
|
|
49
|
+
// 不能拿「0x40 置位」当拒绝/不支持的依据:实测正常包 dataflags = 0x240 本身就含 0x40
|
|
50
|
+
// (0x240 & 0x40 = 0x40),按它拒绝会把**全部正常包一起拒掉**。
|
|
51
|
+
// 名义语义与实现语义不一致时,以实现的判断链为准。
|
|
52
|
+
|
|
53
|
+
const COMPRESSION = { NONE: 0, LZMA: 1, LZ4: 2, LZ4HC: 3, LZHAM: 4 };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* LZ4 块解压(LZ4 block format,不含帧头)。
|
|
57
|
+
*
|
|
58
|
+
* 为什么自己实现:容器允许 LZ4 压缩的数据块,浏览器端没有内置 LZ4。
|
|
59
|
+
* 与 UnityPy 的 lz4.block.decompress 对齐:入参是裸块,输出固定为 expectedSize。
|
|
60
|
+
*/
|
|
61
|
+
function lz4Decompress(src, expectedSize, target, targetOffset = 0) {
|
|
62
|
+
// target:若调用方已预分配好整段缓冲(多块拼接场景),直接写到它的对应区间,
|
|
63
|
+
// 省掉"每块各分配一份 + 最后 concat 再复制一份"的第二份内存。
|
|
64
|
+
// 安全性:LZ4 block 的匹配段只引用**同一块内**已解出的字节,不跨块 ⇒ 写到大缓冲的任意 offset 都成立。
|
|
65
|
+
const dst = target ? target.subarray(targetOffset, targetOffset + expectedSize) : new Uint8Array(expectedSize);
|
|
66
|
+
let si = 0;
|
|
67
|
+
let di = 0;
|
|
68
|
+
while (si < src.length) {
|
|
69
|
+
const token = src[si++];
|
|
70
|
+
// ① 字面量段
|
|
71
|
+
let litLen = token >>> 4;
|
|
72
|
+
if (litLen === 15) {
|
|
73
|
+
let b;
|
|
74
|
+
do { b = src[si++]; litLen += b; } while (b === 255);
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < litLen; i++) dst[di++] = src[si++];
|
|
77
|
+
if (si >= src.length) break; // 末段只有字面量
|
|
78
|
+
// ② 匹配段:2 字节小端偏移,长度 4 + 扩展
|
|
79
|
+
const offset = src[si] | (src[si + 1] << 8);
|
|
80
|
+
si += 2;
|
|
81
|
+
if (offset === 0) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', 'LZ4 数据损坏:匹配偏移为 0');
|
|
82
|
+
let matchLen = token & 15;
|
|
83
|
+
if (matchLen === 15) {
|
|
84
|
+
let b;
|
|
85
|
+
do { b = src[si++]; matchLen += b; } while (b === 255);
|
|
86
|
+
}
|
|
87
|
+
matchLen += 4;
|
|
88
|
+
const start = di - offset;
|
|
89
|
+
if (start < 0) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', 'LZ4 数据损坏:匹配位置越界');
|
|
90
|
+
for (let i = 0; i < matchLen; i++) dst[di++] = dst[start + i];
|
|
91
|
+
}
|
|
92
|
+
if (di !== expectedSize) {
|
|
93
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `LZ4 解压长度不符:得到 ${di},期望 ${expectedSize}`);
|
|
94
|
+
}
|
|
95
|
+
return dst;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* LZMA 块解压。
|
|
100
|
+
*
|
|
101
|
+
* **格式取证(从真实包读出,不是按文档推测)**:UnityFS 的 LZMA 块是
|
|
102
|
+
* `[5 字节 props][raw LZMA1 流]` —— **没有** .lzma(alone) 那 8 字节未压缩长度。
|
|
103
|
+
* props 5 字节 = 1 字节 lc/lp/pb + 4 字节 dictSize(LE),实测全部样本恒为 `5d 00 00 08 00`
|
|
104
|
+
* (lc=3 / lp=0 / pb=2 / dictSize=512KB,即 LZMA 标准默认值)。
|
|
105
|
+
*
|
|
106
|
+
* 做法:补上那 8 字节长度(用容器给的 uncompressedSize,小端)拼成标准 .lzma(alone),
|
|
107
|
+
* 再交给 vendored 的 `LZMA.decompress`。这样不必用 raw API —— 任何支持 alone 的解码器都能接。
|
|
108
|
+
*
|
|
109
|
+
* **为什么必须自己校验长度**:实测把 uncompressedSize 传错(误用 blocksInfo 的 91 而非本块的
|
|
110
|
+
* 8677760)时,解码器**不报错**,静默解出 91 字节就返回。不校验会让半截数据流到下游,
|
|
111
|
+
* 表现成"模型损坏",极难定位。
|
|
112
|
+
*
|
|
113
|
+
* 调用方注意:blocksInfo 传**头的 usz**,block 数据传**该块自己的 uncompressedSize**(两者不同)。
|
|
114
|
+
* @param {Uint8Array} src 原始压缩字节(含开头 5 字节 props)
|
|
115
|
+
* @param {number} uncompressedSize 本段声明解压后大小
|
|
116
|
+
* @param {Uint8Array} [target] 预分配目标缓冲(多块拼接场景)
|
|
117
|
+
* @param {number} [targetOffset]
|
|
118
|
+
* @returns {Uint8Array} 解压后的字节
|
|
119
|
+
*/
|
|
120
|
+
function lzmaDecompress(src, uncompressedSize, target, targetOffset = 0) {
|
|
121
|
+
// 解码器由 lib/lzma-d-min.js 挂到全局(classic script,靠 plugin.json 的 files[] 顺序加载)
|
|
122
|
+
const LZMA = root.LZMA;
|
|
123
|
+
if (!LZMA || typeof LZMA.decompress !== 'function') {
|
|
124
|
+
throw NS.errors.fail('ULB_ERR_LZMA_UNAVAILABLE',
|
|
125
|
+
'LZMA 解码器未就绪:lib/lzma-d-min.js 需在 plugin.json 的 files[] 中排在 lib/unityfs.js 之前');
|
|
126
|
+
}
|
|
127
|
+
if (!src || src.length < 6) {
|
|
128
|
+
throw NS.errors.fail('ULB_ERR_LZMA_FAIL',
|
|
129
|
+
`LZMA 块数据过短:${src ? src.length : 0} 字节(至少需 5 字节 props + 压缩数据)`);
|
|
130
|
+
}
|
|
131
|
+
// 拼 13 字节 alone 头:5 props + 8 字节未压缩长度(小端)
|
|
132
|
+
const alone = new Uint8Array(13 + src.length - 5);
|
|
133
|
+
alone.set(src.subarray(0, 5), 0);
|
|
134
|
+
new DataView(alone.buffer, alone.byteOffset).setBigUint64(5, BigInt(uncompressedSize), true);
|
|
135
|
+
alone.set(src.subarray(5), 13);
|
|
136
|
+
|
|
137
|
+
let out;
|
|
138
|
+
try {
|
|
139
|
+
out = LZMA.decompress(alone);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
throw NS.errors.fail('ULB_ERR_LZMA_FAIL', `LZMA 解压失败:${(e && e.message) || e}`);
|
|
142
|
+
}
|
|
143
|
+
// 上游返回普通 Array(且跨 realm)⇒ 统一归一成 Uint8Array,调用方只处理一种类型
|
|
144
|
+
const bytes = out instanceof Uint8Array ? out : Uint8Array.from(out || []);
|
|
145
|
+
if (bytes.length !== uncompressedSize) {
|
|
146
|
+
throw NS.errors.fail('ULB_ERR_LZMA_FAIL',
|
|
147
|
+
`LZMA 解压长度不符:声明 ${uncompressedSize},实际 ${bytes.length}(解码器不校验长度,必须由本层兜住)`);
|
|
148
|
+
}
|
|
149
|
+
if (target) {
|
|
150
|
+
target.set(bytes, targetOffset);
|
|
151
|
+
return target.subarray(targetOffset, targetOffset + bytes.length);
|
|
152
|
+
}
|
|
153
|
+
return bytes;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 按压缩类型解压一段数据(flags 低 6 位) */
|
|
157
|
+
function decompress(bytes, uncompressedSize, flags, where, target, targetOffset = 0) {
|
|
158
|
+
const type = flags & ARCHIVE.CompressionTypeMask;
|
|
159
|
+
if (type === COMPRESSION.NONE) {
|
|
160
|
+
if (bytes.length !== uncompressedSize) {
|
|
161
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `${where || '数据块'} 声明未压缩但长度不符:实际 ${bytes.length},声明 ${uncompressedSize}`);
|
|
162
|
+
}
|
|
163
|
+
// 未压缩块没有"解压"这步,只能拷进目标区间(语义与拼接一致,不再多留一份视图)
|
|
164
|
+
if (target) { target.set(bytes, targetOffset); return target.subarray(targetOffset, targetOffset + bytes.length); }
|
|
165
|
+
return bytes;
|
|
166
|
+
}
|
|
167
|
+
if (type === COMPRESSION.LZ4 || type === COMPRESSION.LZ4HC) {
|
|
168
|
+
return lz4Decompress(bytes, uncompressedSize, target, targetOffset);
|
|
169
|
+
}
|
|
170
|
+
// LZMA 按**每块自己的 flags** 判(Unity 允许 blocksInfo 与 block 用不同压缩:
|
|
171
|
+
// 实测 57 个包 blocksInfo 是 LZ4HC/LZ4/none 而 block 是 LZMA)⇒ 不能按整包类型一刀切
|
|
172
|
+
if (type === COMPRESSION.LZMA) {
|
|
173
|
+
return lzmaDecompress(bytes, uncompressedSize, target, targetOffset);
|
|
174
|
+
}
|
|
175
|
+
const label = type === COMPRESSION.LZHAM ? 'LZHAM' : `未知(${type})`;
|
|
176
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `${where || '数据块'} 使用 ${label} 压缩,本插件尚未支持该分支(已支持 none / LZ4 / LZMA)`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 解析 UnityFS 容器。
|
|
181
|
+
* @param {Uint8Array} u8 整个文件字节
|
|
182
|
+
* @returns {{
|
|
183
|
+
* signature:string, version:number, playerVersion:string, engineVersion:string,
|
|
184
|
+
* dataflags:number, blocksInfoAtEnd:boolean, usesEncryption:boolean,
|
|
185
|
+
* blocks:{uncompressedSize:number,compressedSize:number,flags:number}[],
|
|
186
|
+
* nodes:{path:string,offset:number,size:number,flags:number}[],
|
|
187
|
+
* blocksInfoBytes:number, dataStart:number, data:Uint8Array
|
|
188
|
+
* }}
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* 只读容器头 + blocksInfo 表(**不解压任何数据块**)。
|
|
192
|
+
*
|
|
193
|
+
* 为什么单拎出来:准入判断只需要"解压后总共多大",而解压数据块是全流程最贵的一步
|
|
194
|
+
* (LZ4 包 19MB→42MB 实测 150ms+)。有了它就能在一份块表上求和,P90 < 5ms。
|
|
195
|
+
* @param {Uint8Array} u8
|
|
196
|
+
* @returns {object} parse 返回里的"头部字段"(不含 data / blockSpans / totalSize)
|
|
197
|
+
*/
|
|
198
|
+
function readHeader(u8) {
|
|
199
|
+
// UnityFS 的容器头与 blocksInfo 表都是**大端**(与内嵌 SerializedFile 的小端不同)
|
|
200
|
+
const r = new B.Reader(u8, { endian: '>' });
|
|
201
|
+
const signature = r.cstring('signature');
|
|
202
|
+
if (signature !== 'UnityFS') throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `不是 UnityFS 容器(signature="${signature}")`);
|
|
203
|
+
const version = r.u32();
|
|
204
|
+
const playerVersion = r.cstring('playerVersion');
|
|
205
|
+
const engineVersion = r.cstring('engineVersion');
|
|
206
|
+
|
|
207
|
+
// 逻辑长度:**标准包 == 文件长度**;带尾随数据的包(实测 NIKKE 在 UnityFS 内容后追加 16 字节 trailer)
|
|
208
|
+
// 则小于文件长度。定位 blocksInfo 必须用它,不能用文件长度(见下方 blocksInfoAtEnd 分支)。
|
|
209
|
+
const bundleSize = r.i64('bundleSize');
|
|
210
|
+
const blocksInfoCompressedSize = r.u32();
|
|
211
|
+
const blocksInfoUncompressedSize = r.u32();
|
|
212
|
+
const dataflags = r.u32();
|
|
213
|
+
|
|
214
|
+
if (dataflags & ARCHIVE.UsesAssetBundleEncryption) {
|
|
215
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', '该资源包使用了 AssetBundle 加密(UsesAssetBundleEncryption),本插件无法读取');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// version>=7(或 2019.4.15+)时,blocksInfo 之前要对齐到 16 字节
|
|
219
|
+
if (version >= 7) r.align(16);
|
|
220
|
+
|
|
221
|
+
const blocksInfoAtEnd = (dataflags & ARCHIVE.BlocksInfoAtTheEnd) !== 0;
|
|
222
|
+
const start = r.pos;
|
|
223
|
+
let blocksInfoRaw;
|
|
224
|
+
if (blocksInfoAtEnd) {
|
|
225
|
+
// 从「逻辑末尾」往前取 blocksInfo,用 bundleSize 而不是文件长度 r.length:
|
|
226
|
+
// 标准包两者相等(行为不变);但实测 NIKKE 包在 UnityFS 内容之后又追加了 16 字节 trailer
|
|
227
|
+
// (三样本一致),此时 r.length 会把 trailer 算进来 ⇒ 起点后移 16 字节 ⇒ LZ4 解出垃圾,
|
|
228
|
+
// 报「LZ4 数据损坏:匹配偏移为 0」,与真实原因相距极远。
|
|
229
|
+
// 边界:bundleSize 不合理(非正数 / 超出文件长度)时回退 r.length,
|
|
230
|
+
// 避免坏包让 seek 落到非法位置(那种情况的报错会更难读)。
|
|
231
|
+
const logicalEnd = (bundleSize > 0 && bundleSize <= r.length) ? bundleSize : r.length;
|
|
232
|
+
r.seek(logicalEnd - blocksInfoCompressedSize);
|
|
233
|
+
blocksInfoRaw = r.bytes(blocksInfoCompressedSize, 'blocksInfo');
|
|
234
|
+
r.seek(start);
|
|
235
|
+
} else {
|
|
236
|
+
blocksInfoRaw = r.bytes(blocksInfoCompressedSize, 'blocksInfo');
|
|
237
|
+
}
|
|
238
|
+
const blocksInfoBytes = decompress(blocksInfoRaw, blocksInfoUncompressedSize, dataflags & ARCHIVE.CompressionTypeMask, 'blocksInfo');
|
|
239
|
+
|
|
240
|
+
const bi = new B.Reader(blocksInfoBytes, { baseOffset: start, endian: '>' });
|
|
241
|
+
const hash = bi.bytes(16, 'blocksInfo.hash');
|
|
242
|
+
const blockCount = bi.i32();
|
|
243
|
+
const blocks = [];
|
|
244
|
+
for (let i = 0; i < blockCount; i++) {
|
|
245
|
+
blocks.push({
|
|
246
|
+
uncompressedSize: bi.u32(),
|
|
247
|
+
compressedSize: bi.u32(),
|
|
248
|
+
flags: bi.u16(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const nodeCount = bi.i32();
|
|
252
|
+
const nodes = [];
|
|
253
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
254
|
+
nodes.push({
|
|
255
|
+
offset: bi.i64('node.offset'),
|
|
256
|
+
size: bi.i64('node.size'),
|
|
257
|
+
flags: bi.u32(),
|
|
258
|
+
path: bi.cstring('node.path'),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const blocksInfoContentEnd = bi.absOffset; // 表**内容**结束位置(不含表尾填充)
|
|
262
|
+
// 数据区起点(依据 AssetStudio BundleFile.cs ReadBlocksInfoAndDirectory,d158e86):
|
|
263
|
+
// · blocksInfo **内联在头部**(未置 0x80)时,该段按**声明长度** blocksInfoCompressedSize
|
|
264
|
+
// 占位——未压缩包的声明长度可能比表内容大,差值即表尾对齐填充
|
|
265
|
+
// (实测样本 160 - 153 = 7 字节),故需把游标推到 start + compressedSize 才是数据起点。
|
|
266
|
+
// 别把「表内容结束」当成「数据开始」:那会整体错位 7 字节,moc3 的 sha256 立刻不对。
|
|
267
|
+
// · blocksInfo **在文件尾**(置 0x80)时,数据块紧接 header 之后开始;
|
|
268
|
+
// 上面读 blocksInfo 前已保存 position、读后已 seek 回来,此处**不能再加 declaredEnd**,
|
|
269
|
+
// 否则会把数据起点整体推后 compressedSize 字节 → 数据块错位 → LZ4 解出垃圾
|
|
270
|
+
// (报"LZ4 数据损坏:匹配位置越界",与真实原因相距极远,极难排查)。
|
|
271
|
+
// 实测踩坑:live2d/aijier_3(Unity 2018.4 / version 6 / LZ4 / flags 0xC2)正是此情形,
|
|
272
|
+
// 19 个样本里唯一一个 blocksInfo 在尾的包,旧代码一直没覆盖到。
|
|
273
|
+
if (!blocksInfoAtEnd) {
|
|
274
|
+
const declaredEnd = start + blocksInfoCompressedSize;
|
|
275
|
+
if (declaredEnd > r.pos) r.seek(declaredEnd);
|
|
276
|
+
}
|
|
277
|
+
// ── 内联 blocksInfo 与数据区之间的**对齐填充**(实测 3 个 version 6 / Unity 5.x.x 包)──
|
|
278
|
+
//
|
|
279
|
+
// 实测(aidang_2 / banrenma_2 / xianghe_2,三者结构完全同型):
|
|
280
|
+
// header 50 字节 + blocksInfo 91 字节 = 141,但数据块实际从 **144** 开始
|
|
281
|
+
// (141 后跟 3 字节填充,144 正好 16 字节对齐)。
|
|
282
|
+
// 这三个包的 node.offset 都写着 **3** —— 恰好等于填充量,
|
|
283
|
+
// 说明节点偏移是以「blocksInfo 末尾」为基准记录的,而不是对齐后的数据起点。
|
|
284
|
+
// 验证(算术闭合):144 + node.size == 文件长度
|
|
285
|
+
// aidang_2 144 + 18666148 = 18666292 ✓
|
|
286
|
+
// banrenma_2 144 + 6115708 = 6115852 ✓
|
|
287
|
+
// xianghe_2 144 + 11740648 = 11740792 ✓
|
|
288
|
+
// 不处理会怎样:数据区少取 3 字节(末 3 字节被切掉),而节点又按 3 起算 ⇒
|
|
289
|
+
// 报「节点 CAB-xxx 越界:offset=3 size=18666148 数据区=18666148」,与真实原因相距极远。
|
|
290
|
+
//
|
|
291
|
+
// 判据用**数据自证**而不是猜标志位:这三包的 dataflags = 0x40,并没有置 0x200
|
|
292
|
+
// (BlockInfoNeedPaddingAtStart),所以不能靠标志位判断填充是否存在。
|
|
293
|
+
// 这里用「文件里剩下的字节 - 各块声明压缩大小之和」当填充量:
|
|
294
|
+
// 正常包该差值为 0(行为完全不变);这三个包为 3。
|
|
295
|
+
const logicalEndForPad = (bundleSize > 0 && bundleSize <= r.length) ? bundleSize : r.length;
|
|
296
|
+
let dataPadding = 0;
|
|
297
|
+
if (!blocksInfoAtEnd) {
|
|
298
|
+
const declaredData = blocks.reduce((a, b) => a + b.compressedSize, 0);
|
|
299
|
+
const pad = logicalEndForPad - r.pos - declaredData;
|
|
300
|
+
// 判据必须**两条同时成立**,否则会把"文件尾 trailer"误当成"头部填充":
|
|
301
|
+
// ① 0 < pad < 16(对齐填充的量级);
|
|
302
|
+
// ② **最小节点偏移 == pad** —— 这是决定性判据:
|
|
303
|
+
// 头部填充时,节点偏移以 blocksInfo 末尾为基准,首个节点正好落在填充之后(offset==pad);
|
|
304
|
+
// 尾部 trailer 时,节点偏移本来就以数据起点为基准(首个节点 offset==0),
|
|
305
|
+
// 差值全在文件末尾。实测:不收紧这条会误判 26 个包、把节点偏移减成负数。
|
|
306
|
+
const minNodeOffset = nodes.length ? Math.min(...nodes.map((n) => n.offset)) : 0;
|
|
307
|
+
if (pad > 0 && pad < 16 && minNodeOffset === pad) {
|
|
308
|
+
dataPadding = pad;
|
|
309
|
+
// 节点偏移统一减掉填充 ⇒ 全部改为「相对数据起点」,与下面解压出的缓冲同一基准
|
|
310
|
+
for (const n of nodes) n.offset -= pad;
|
|
311
|
+
r.seek(r.pos + pad);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (dataflags & ARCHIVE.BlockInfoNeedPaddingAtStart) r.align(16);
|
|
315
|
+
const dataStart = r.pos;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
signature, version, playerVersion, engineVersion, dataflags, blocksInfoAtEnd,
|
|
319
|
+
usesEncryption: false, blocks, nodes, blocksInfoContentEnd, dataPadding,
|
|
320
|
+
blocksInfoBytes: blocksInfoContentEnd - start, dataStart, blocksInfoHash: B.toHex(hash),
|
|
321
|
+
reader: r, // 游标停在 dataStart,parse 解压数据块时要用;对外返回前剔除
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 解析 UnityFS 容器(**含数据块解压**)。头部与块表复用 readHeader。
|
|
327
|
+
* @param {Uint8Array} u8 整个文件字节
|
|
328
|
+
*/
|
|
329
|
+
function parse(u8) {
|
|
330
|
+
const h = readHeader(u8);
|
|
331
|
+
const { blocks, dataStart, reader: r } = h;
|
|
332
|
+
|
|
333
|
+
// 数据块:**预分配整段**,逐块直接解压到对应 offset。
|
|
334
|
+
// 为什么不再用「每块各解一份 + concat」:那会让峰值 = 解压总量 ×2
|
|
335
|
+
// (实测 19MB 包:解压 42.1MB + concat 复制 42.1MB = 84.2MB),而块只有一个时又白留一份。
|
|
336
|
+
// 节点的 offset/size 相对这份整段缓冲,语义与拼接完全相同。
|
|
337
|
+
const totalUncompressed = blocks.reduce((a, b) => a + b.uncompressedSize, 0);
|
|
338
|
+
const data = new Uint8Array(totalUncompressed);
|
|
339
|
+
const blockSpans = [];
|
|
340
|
+
let cursor = 0;
|
|
341
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
342
|
+
const b = blocks[i];
|
|
343
|
+
const raw = r.bytes(b.compressedSize, `block[${i}]`);
|
|
344
|
+
decompress(raw, b.uncompressedSize, b.flags, `block[${i}]`, data, cursor);
|
|
345
|
+
blockSpans.push({ start: cursor, end: cursor + b.uncompressedSize });
|
|
346
|
+
cursor += b.uncompressedSize;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { reader, ...head } = h; // reader 是内部游标,不对外暴露
|
|
350
|
+
return { ...head, blockSpans, data, totalSize: u8.length };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 轻量摘要:只读头 + blocksInfo,**不解压数据块**。
|
|
355
|
+
* 供准入判断与插件声明内存用(见 docs/设计/解包内存预估.md)。
|
|
356
|
+
* @param {Uint8Array} u8
|
|
357
|
+
* @returns {{decompressedBytes:number, compressedBytes:number, blockCount:number, cabBytes:number, resSBytes:number}}
|
|
358
|
+
*/
|
|
359
|
+
function blocksSummary(u8) {
|
|
360
|
+
const h = readHeader(u8);
|
|
361
|
+
let decompressedBytes = 0;
|
|
362
|
+
let compressedBytes = 0;
|
|
363
|
+
for (const b of h.blocks) { decompressedBytes += b.uncompressedSize; compressedBytes += b.compressedSize; }
|
|
364
|
+
// serialized file 节点的判据是**结构形式**:CAB- + 32 位十六进制、无后缀。
|
|
365
|
+
// 旧写法 `CAB- && !.resS` 漏掉了 `.resource`(魔数 FSB5 = FMOD 音频银行)——
|
|
366
|
+
// 把它当 serialized file 解会读到 2^60 量级的垃圾 fileSize。
|
|
367
|
+
const cab = h.nodes.find((n) => /^CAB-[0-9a-f]{32}$/i.test(n.path));
|
|
368
|
+
const resS = h.nodes.find((n) => /\.resS$/i.test(n.path));
|
|
369
|
+
// 压缩类型:每块的 flags 低 6 位(Unity 的 CompressionType)。
|
|
370
|
+
// 为什么要在这里给出来:内存峰值**按压缩类型不同**(LZMA 实测是 LZ4 的 ~1.8 倍,见 index.js 的表),
|
|
371
|
+
// 而块表本来就已经解析好了 —— 从这里取是"顺手带上",不需要为它再扫一遍包。
|
|
372
|
+
const types = [];
|
|
373
|
+
for (const b of h.blocks) {
|
|
374
|
+
const t = b.flags & 0x3f;
|
|
375
|
+
if (!types.includes(t)) types.push(t);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
decompressedBytes, compressedBytes, blockCount: h.blocks.length,
|
|
379
|
+
cabBytes: cab ? cab.size : 0, resSBytes: resS ? resS.size : 0,
|
|
380
|
+
// 全部块只会有一种压缩类型(Unity 打包即如此);万一是混合,保留全部,由调用方取最保守的
|
|
381
|
+
compressionTypes: types,
|
|
382
|
+
compression: types.length === 1 ? types[0] : (types.length ? -1 : 0),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function nodeBytes(parsed, path) {
|
|
387
|
+
const node = parsed.nodes.find((n) => n.path === path);
|
|
388
|
+
if (!node) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `容器内没有节点: ${path}`);
|
|
389
|
+
if (node.offset + node.size > parsed.data.length) {
|
|
390
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `节点 ${path} 越界:offset=${node.offset} size=${node.size} 数据区=${parsed.data.length}`);
|
|
391
|
+
}
|
|
392
|
+
return parsed.data.subarray(node.offset, node.offset + node.size);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
NS.unityfs = { parse, readHeader, blocksSummary, nodeBytes, lz4Decompress, ARCHIVE, COMPRESSION };
|
|
396
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "unity-live2d-bundle",
|
|
3
|
+
"name": "Unity Live2D 资源包",
|
|
4
|
+
"provides": [
|
|
5
|
+
"asset-provider"
|
|
6
|
+
],
|
|
7
|
+
"label": "Live2D包",
|
|
8
|
+
"accent": "#6d28d9",
|
|
9
|
+
"matchPriority": 10,
|
|
10
|
+
"version": 1,
|
|
11
|
+
"runtime": "node",
|
|
12
|
+
"entry": "index.js",
|
|
13
|
+
"fileKinds": [
|
|
14
|
+
"bundle"
|
|
15
|
+
],
|
|
16
|
+
"produces": {
|
|
17
|
+
"kind": "live2d",
|
|
18
|
+
"entry": "resolve(file)",
|
|
19
|
+
"schema": "docs/契约/插件.md",
|
|
20
|
+
"fields": [
|
|
21
|
+
"prefix",
|
|
22
|
+
"files",
|
|
23
|
+
"entry"
|
|
24
|
+
],
|
|
25
|
+
"note": "resolve(file) 按资产提供者契约 §1.2 返回 { prefix, files, entry }:prefix 形如 vfs://unity-live2d-bundle/<rootId>/<相对目录>/,files 为 relPath→Blob|string 映射,entry 是 files 内的渲染入口键",
|
|
26
|
+
"memory": {
|
|
27
|
+
"via": "estimateMemory(file)",
|
|
28
|
+
"fields": [
|
|
29
|
+
"decompressedBytes",
|
|
30
|
+
"rgbaBytes",
|
|
31
|
+
"peakBytes"
|
|
32
|
+
],
|
|
33
|
+
"supported": [
|
|
34
|
+
"decompressedBytes",
|
|
35
|
+
"peakBytes"
|
|
36
|
+
],
|
|
37
|
+
"note": "returns { decompressedBytes, peakBytes }: decompressedBytes comes from the container block table (no inflate); peakBytes comes from this plugin's own segmented model (measured max x1.15 per bucket). rgbaBytes is backfilled into data/unpacked/<fp>/probe.json after a real resolve"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"errorCodes": [
|
|
41
|
+
"ULB_ERR_BAD_ARGS",
|
|
42
|
+
"ULB_ERR_NO_FILE",
|
|
43
|
+
"ULB_ERR_IO",
|
|
44
|
+
"ULB_ERR_NOT_UNITYFS",
|
|
45
|
+
"ULB_ERR_NO_CAB",
|
|
46
|
+
"ULB_ERR_CONTAINER_PARSE",
|
|
47
|
+
"ULB_ERR_LZMA_FAIL",
|
|
48
|
+
"ULB_ERR_LZMA_UNAVAILABLE",
|
|
49
|
+
"ULB_ERR_EXTRACT",
|
|
50
|
+
"ULB_ERR_TEX_FORMAT_UNSUPPORTED",
|
|
51
|
+
"ULB_ERR_TEX_DATA_SHORT",
|
|
52
|
+
"ULB_ERR_TEX_DECODE",
|
|
53
|
+
"ULB_ERR_CURVE_BINDING_MISMATCH",
|
|
54
|
+
"ULB_ERR_STREAMED_CLIP_INVALID",
|
|
55
|
+
"ULB_ERR_MODEL_BUILD",
|
|
56
|
+
"ULB_ERR_NO_HOST",
|
|
57
|
+
"ULB_ERR_EXPORT_NO_SPACE",
|
|
58
|
+
"ULB_ERR_EXPORT_INTERRUPTED"
|
|
59
|
+
],
|
|
60
|
+
"files": [
|
|
61
|
+
"lib/errors.js",
|
|
62
|
+
"lib/bytes.js",
|
|
63
|
+
"lib/common-strings.js",
|
|
64
|
+
"lib/lzma-d-min.js",
|
|
65
|
+
"lib/unityfs.js",
|
|
66
|
+
"lib/serialized.js",
|
|
67
|
+
"lib/typetree.js",
|
|
68
|
+
"lib/extract.js",
|
|
69
|
+
"lib/etc2.js",
|
|
70
|
+
"lib/dxt.js",
|
|
71
|
+
"lib/texdecode.js",
|
|
72
|
+
"lib/bundle-probe.js",
|
|
73
|
+
"lib/png.js",
|
|
74
|
+
"lib/anim-to-motion.js",
|
|
75
|
+
"lib/model3.js",
|
|
76
|
+
"index.js"
|
|
77
|
+
],
|
|
78
|
+
"assets": [
|
|
79
|
+
"profiles/unityfs-live2d.json"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "unityfs-live2d",
|
|
3
|
+
"name": "UnityFS Live2D 容器",
|
|
4
|
+
"comment": "UnityFS 打包的 Live2D 资源包适配数据(同族容器通用,不绑定单一游戏)。只放**数据性差异**,代码里不写游戏名。注意只有这些字段会被消费:motion.loopPatterns(循环判定)、motion.fadeInTime/fadeOutTime(动作淡入淡出)、motion.groups(动作分组)、hitAreas(点击热区)、model3(覆盖/补充 model3 顶层);comment/notes/label 仅作说明,不参与判定。",
|
|
5
|
+
"label": "Live2D包",
|
|
6
|
+
"notes": [
|
|
7
|
+
"单文件无扩展名、UnityFS 容器、Unity 2020.3 打包",
|
|
8
|
+
"包内 moc3 在 MonoBehaviour 的字节数组里(魔数 MOC3 + 前置 u32 长度)",
|
|
9
|
+
"physics3 以 TextAsset 形式存在,名字形如 <资源名>.physics3",
|
|
10
|
+
"纹理为 ETC2_RGBA8,数据在 .resS 或对象内联,Unity 存储方向自下而上",
|
|
11
|
+
"循环规则依据实测:18 个可解析包 / 282 个动作实例 / 34 个去重名。去重名集合为 complete、effect、home、idle、idle1~7、login、mail、main_1~4、mission、mission_complete、touch_body、touch_drag2、touch_head、touch_idle1~10、touch_special、wedding(样本取自碧蓝航线,规则本身与游戏无关)",
|
|
12
|
+
"touch_idle1~10 是触摸待机族,必须循环;它们不以 idle 开头(旧规则 ^idle 抓不到),故补 _idle 覆盖这 10 个名字",
|
|
13
|
+
"motion.groups 故意留空:分组粒度=动作名本身(34 个名字 → 34 组)。若要按 idle/touch/main 归组,在这里加 { group, patterns } 规则,但会改变动作列表的分组粒度"
|
|
14
|
+
],
|
|
15
|
+
"motion": {
|
|
16
|
+
"loopPatterns": ["^idle", "_idle", "^main_", "^complete$", "^mission$", "^home$"],
|
|
17
|
+
"fadeInTime": 0.5,
|
|
18
|
+
"fadeOutTime": 0.5,
|
|
19
|
+
"groups": []
|
|
20
|
+
},
|
|
21
|
+
"hitAreas": [
|
|
22
|
+
{ "Id": "TouchBody", "Motion": "touch_body" },
|
|
23
|
+
{ "Id": "TouchHead", "Motion": "touch_head" },
|
|
24
|
+
{ "Id": "TouchSpecial", "Motion": "touch_special" }
|
|
25
|
+
]
|
|
26
|
+
}
|