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,233 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// plugins/unity-live2d-bundle/lib/serialized.js —— SerializedFile 解析(对象表)
|
|
3
|
+
//
|
|
4
|
+
// 职责:把 UnityFS 里的 CAB-<hash> 节点(一个 SerializedFile)解析成**对象表**:
|
|
5
|
+
// 每个对象 = { pathID, classID, className, typeIndex, dataOffset, byteSize },
|
|
6
|
+
// 加上文件级信息(Unity 版本、目标平台、类型表、脚本类型、外部引用、容器清单)。
|
|
7
|
+
// 派生能力:按类名统计(本样本 MonoBehaviour 2167 / AnimationClip 34 / Texture2D 2 …)、
|
|
8
|
+
// 按类取对象切片(moc3 在 MonoBehaviour 里、physics3 在 TextAsset 里、纹理在 Texture2D 里)。
|
|
9
|
+
//
|
|
10
|
+
// 字节序(同一个文件里会变,必须逐段确认):
|
|
11
|
+
// · 头部前 4 个 u32 与 v22 的追加字段(metadataSize/fileSize/dataOffset/unknown)
|
|
12
|
+
// 跟在**容器**后面,用容器端的字节序(UnityFS 是大端)——这一点最容易踩:
|
|
13
|
+
// 直接用文件自己的端序去读这 4 个 u32,版本号会变成一个天文数字。
|
|
14
|
+
// · 头部里的 endian 标志字节(0=小端/1=大端)之后的元数据段,才是文件自己的端序。
|
|
15
|
+
//
|
|
16
|
+
// 版本覆盖:本实现覆盖 v22(Unity 2020.3,本样本)用到的字段;更老/更新的版本按
|
|
17
|
+
// 同样的分支条件处理,遇到没实现的字段(v23 的类型树 hash + 长度前缀等)抛可读错误。
|
|
18
|
+
//
|
|
19
|
+
// 挂载:globalThis.__ULB__.serialized
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
const root = globalThis; // 原 IIFE 的 root 引用(脚本级裸块内保持同名)
|
|
26
|
+
const NS = (root.__ULB__ = root.__ULB__ || {});
|
|
27
|
+
const B = NS.bytes;
|
|
28
|
+
|
|
29
|
+
// Unity 的类 ID → 可读类名(只列本插件会用到/会遇到的;其余用 "ClassID <n>" 表示)
|
|
30
|
+
const CLASS_NAMES = {
|
|
31
|
+
1: 'GameObject', 4: 'Transform', 21: 'Material', 23: 'MeshRenderer', 28: 'Texture2D',
|
|
32
|
+
33: 'MeshFilter', 43: 'Mesh', 48: 'Shader', 49: 'TextAsset', 65: 'BoxCollider',
|
|
33
|
+
74: 'AnimationClip', 82: 'AudioSource', 83: 'AudioClip', 91: 'AnimatorController',
|
|
34
|
+
95: 'Animator', 104: 'RenderSettings', 111: 'Animation', 114: 'MonoBehaviour',
|
|
35
|
+
115: 'MonoScript', 128: 'Font', 142: 'AssetBundle', 213: 'Sprite', 222: 'CanvasRenderer',
|
|
36
|
+
223: 'Canvas', 224: 'RectTransform', 225: 'CanvasGroup',
|
|
37
|
+
};
|
|
38
|
+
const CLASS_IDS = { MonoBehaviour: 114, Texture2D: 28, TextAsset: 49, AssetBundle: 142, AnimationClip: 74 };
|
|
39
|
+
|
|
40
|
+
const className = (id) => CLASS_NAMES[id] || `ClassID ${id}`;
|
|
41
|
+
|
|
42
|
+
/** 读一个 SerializedType(含类型树,类型树只记偏移/长度,不在此处展开字段) */
|
|
43
|
+
function readType(r, opts) {
|
|
44
|
+
const version = opts.version;
|
|
45
|
+
const isRefType = !!opts.isRefType;
|
|
46
|
+
const t = { classID: r.i32(), scriptTypeIndex: -1, scriptID: null, oldTypeHash: null, typeTree: null };
|
|
47
|
+
if (version >= 16) t.isStripped = r.bool();
|
|
48
|
+
if (version >= 17) t.scriptTypeIndex = r.i16();
|
|
49
|
+
if (version >= 13) {
|
|
50
|
+
if ((isRefType && t.scriptTypeIndex >= 0) || (version < 16 && t.classID < 0) || (version >= 16 && t.classID === 114)) {
|
|
51
|
+
t.scriptID = r.bytes(16, 'scriptID');
|
|
52
|
+
}
|
|
53
|
+
t.oldTypeHash = r.bytes(16, 'oldTypeHash');
|
|
54
|
+
}
|
|
55
|
+
if (opts.enableTypeTree) {
|
|
56
|
+
let treeSizeHint = null;
|
|
57
|
+
if (version >= 23) {
|
|
58
|
+
t.typeTreeContentHash = r.bytes(16, 'typeTreeContentHash');
|
|
59
|
+
treeSizeHint = r.i32();
|
|
60
|
+
t.typeTreeSerializedSize = treeSizeHint;
|
|
61
|
+
}
|
|
62
|
+
// v12+/v10 的类型树是**内联 blob**:i32 节点数 + i32 字符串池长度 + 节点数组 + 字符串池。
|
|
63
|
+
// 节点记录(v19+)32 字节:h(m_Version) B(m_Level) B(m_TypeFlags) I(m_TypeStrOffset)
|
|
64
|
+
// I(m_NameStrOffset) i(m_ByteSize) i(m_Index) i(m_MetaFlag) Q(m_RefTypeHash)
|
|
65
|
+
// —— 前面 8 个字段 24 字节,v19 起多 8 字节的 m_RefTypeHash,合计 32。
|
|
66
|
+
// (这也是本文件的坑之一:把节点记录当 28 字节算,整段类型表就会错位。)
|
|
67
|
+
if (version >= 12 || version === 10) {
|
|
68
|
+
const start = r.pos;
|
|
69
|
+
const nodeCount = r.i32();
|
|
70
|
+
const stringBufferSize = r.i32();
|
|
71
|
+
if (nodeCount < 0 || stringBufferSize < 0) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `类型树长度非法:节点数=${nodeCount} 字符串池=${stringBufferSize}`);
|
|
72
|
+
const nodeStructSize = version >= 19 ? 32 : 24;
|
|
73
|
+
r.skip(nodeCount * nodeStructSize + stringBufferSize);
|
|
74
|
+
t.typeTree = { nodeCount, stringBufferSize, offset: start, size: r.pos - start };
|
|
75
|
+
} else {
|
|
76
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `尚未支持 v${version} 的非内联类型树(v12 之前的旧格式)`);
|
|
77
|
+
}
|
|
78
|
+
if (version >= 21) {
|
|
79
|
+
if (isRefType) {
|
|
80
|
+
t.m_ClassName = r.cstring('m_ClassName');
|
|
81
|
+
t.m_NameSpace = r.cstring('m_NameSpace');
|
|
82
|
+
t.m_AssemblyName = r.cstring('m_AssemblyName');
|
|
83
|
+
} else {
|
|
84
|
+
const n = r.i32();
|
|
85
|
+
if (n < 0 || n > 128) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `type_dependencies 长度非法:${n}`);
|
|
86
|
+
const deps = [];
|
|
87
|
+
for (let i = 0; i < n; i++) deps.push(r.i32());
|
|
88
|
+
t.typeDependencies = deps;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
t.className = className(t.classID);
|
|
93
|
+
return t;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 解析 SerializedFile。
|
|
98
|
+
* @param {Uint8Array} u8 CAB 节点字节
|
|
99
|
+
* @returns {object} { version, unityVersion, platform, enableTypeTree, types, objects, scripts, externals, dataOffset, container }
|
|
100
|
+
*/
|
|
101
|
+
function parse(u8) {
|
|
102
|
+
// 头部前 4 个 u32 用**容器端**字节序(大端)——它们只是"预览",真实值在 v22 段重读
|
|
103
|
+
const r = new B.Reader(u8, { endian: '>' });
|
|
104
|
+
const preview = { metadataSize: r.u32(), fileSize: r.u32(), version: r.u32(), dataOffset: r.u32() };
|
|
105
|
+
const fieldOrder = preview.version;
|
|
106
|
+
const endianFlag = r.u8();
|
|
107
|
+
r.skip(3); // reserved
|
|
108
|
+
let metadataSize = preview.metadataSize;
|
|
109
|
+
let fileSize = preview.fileSize;
|
|
110
|
+
let dataOffset = preview.dataOffset;
|
|
111
|
+
let unknown = 0;
|
|
112
|
+
if (fieldOrder >= 22) {
|
|
113
|
+
metadataSize = r.u32();
|
|
114
|
+
fileSize = r.i64('fileSize');
|
|
115
|
+
dataOffset = r.i64('dataOffset');
|
|
116
|
+
unknown = r.i64('unknown');
|
|
117
|
+
}
|
|
118
|
+
if (fieldOrder >= 9 && endianFlag > 1) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `字节序标志非法:${endianFlag}`);
|
|
119
|
+
// 之后的元数据段用文件自己的字节序
|
|
120
|
+
r.le = endianFlag === 0;
|
|
121
|
+
|
|
122
|
+
const version = fieldOrder;
|
|
123
|
+
let unityVersion = '';
|
|
124
|
+
let platform = 0;
|
|
125
|
+
let enableTypeTree = true;
|
|
126
|
+
if (version >= 7) unityVersion = r.cstring('unityVersion');
|
|
127
|
+
if (version >= 8) platform = r.i32();
|
|
128
|
+
if (version >= 13) enableTypeTree = r.bool();
|
|
129
|
+
|
|
130
|
+
const typeCount = r.i32();
|
|
131
|
+
if (typeCount < 0 || typeCount > 4096) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `类型数非法:${typeCount}`);
|
|
132
|
+
const opt = { version, enableTypeTree, isRefType: false };
|
|
133
|
+
const types = [];
|
|
134
|
+
for (let i = 0; i < typeCount; i++) types.push(readType(r, opt));
|
|
135
|
+
|
|
136
|
+
let bigIdEnabled = 0;
|
|
137
|
+
if (version >= 7 && version < 14) bigIdEnabled = r.i32();
|
|
138
|
+
|
|
139
|
+
const objectCount = r.i32();
|
|
140
|
+
if (objectCount < 0 || objectCount > 1e6) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `对象数非法:${objectCount}`);
|
|
141
|
+
const objects = [];
|
|
142
|
+
const byPathID = new Map();
|
|
143
|
+
for (let i = 0; i < objectCount; i++) {
|
|
144
|
+
// pathID 是 int64,**实测会超过 2^53**(本样本有 2.8e17 这样的值),
|
|
145
|
+
// 所以用 BigInt 读、以十进制字符串对外(JSON 化安全),同时保留 BigInt 供内部比较。
|
|
146
|
+
let pathIDBig;
|
|
147
|
+
if (bigIdEnabled) pathIDBig = r.i64b('pathID');
|
|
148
|
+
else if (version < 14) pathIDBig = BigInt(r.i32());
|
|
149
|
+
else { r.align(4); pathIDBig = r.i64b('pathID'); }
|
|
150
|
+
let byteStart = version >= 22 ? r.i64('byteStart') : r.u32();
|
|
151
|
+
const byteSize = r.u32();
|
|
152
|
+
const typeIndex = r.i32();
|
|
153
|
+
const type = types[typeIndex];
|
|
154
|
+
if (!type) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `对象 ${pathIDBig} 的 typeIndex 越界:${typeIndex}(类型数 ${types.length})`);
|
|
155
|
+
byteStart += dataOffset;
|
|
156
|
+
if (byteStart + byteSize > u8.length) {
|
|
157
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `对象 ${pathIDBig} 越界:offset=${byteStart} size=${byteSize} 文件=${u8.length}`);
|
|
158
|
+
}
|
|
159
|
+
const obj = {
|
|
160
|
+
pathID: pathIDBig.toString(), pathIDBig, typeIndex, classID: type.classID, className: type.className,
|
|
161
|
+
dataOffset: byteStart, byteSize,
|
|
162
|
+
};
|
|
163
|
+
objects.push(obj);
|
|
164
|
+
byPathID.set(obj.pathID, obj);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let scripts = [];
|
|
168
|
+
if (version >= 11) {
|
|
169
|
+
const scriptCount = r.i32();
|
|
170
|
+
if (scriptCount < 0 || scriptCount > 1e5) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `脚本类型数非法:${scriptCount}`);
|
|
171
|
+
for (let i = 0; i < scriptCount; i++) {
|
|
172
|
+
const localSerializedFileIndex = r.i32();
|
|
173
|
+
let localIdentifierInFile;
|
|
174
|
+
if (version < 14) localIdentifierInFile = String(r.i32());
|
|
175
|
+
else { r.align(4); localIdentifierInFile = r.i64b('localIdentifierInFile').toString(); }
|
|
176
|
+
scripts.push({ localSerializedFileIndex, localIdentifierInFile });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const externalCount = r.i32();
|
|
181
|
+
if (externalCount < 0 || externalCount > 1e5) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `外部引用数非法:${externalCount}`);
|
|
182
|
+
const externals = [];
|
|
183
|
+
for (let i = 0; i < externalCount; i++) {
|
|
184
|
+
const e = {};
|
|
185
|
+
if (version >= 6) e.tempEmpty = r.cstring('tempEmpty');
|
|
186
|
+
if (version >= 5) { e.guid = B.toHex(r.bytes(16, 'guid')); e.type = r.i32(); }
|
|
187
|
+
e.path = r.cstring('externalPath');
|
|
188
|
+
externals.push(e);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let refTypes = [];
|
|
192
|
+
if (version >= 20) {
|
|
193
|
+
const refCount = r.i32();
|
|
194
|
+
if (refCount < 0 || refCount > 1e5) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `引用类型数非法:${refCount}`);
|
|
195
|
+
const refOpt = { version, enableTypeTree, isRefType: true };
|
|
196
|
+
for (let i = 0; i < refCount; i++) refTypes.push(readType(r, refOpt));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let userInformation = '';
|
|
200
|
+
if (version >= 5) userInformation = r.cstring('userInformation');
|
|
201
|
+
|
|
202
|
+
// 强自检:元数据段必须落在数据区之前(偏移算错时这里会先炸,而不是等到取值阶段)
|
|
203
|
+
if (dataOffset && r.pos > dataOffset) {
|
|
204
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `元数据段越过了数据区:读到的位置=${r.pos} 但 dataOffset=${dataOffset}(字段布局可能不匹配该版本)`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
version, unityVersion, platform, enableTypeTree, metadataSize, fileSize, dataOffset, unknown,
|
|
209
|
+
bigIdEnabled, le: endianFlag === 0, types, objects, byPathID, scripts, externals, refTypes, userInformation,
|
|
210
|
+
metaEnd: r.pos,
|
|
211
|
+
bytes: u8,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 按类统计对象数(M4 的验收口径) */
|
|
216
|
+
function typeStats(sf) {
|
|
217
|
+
const stats = {};
|
|
218
|
+
for (const o of sf.objects) stats[o.className] = (stats[o.className] || 0) + 1;
|
|
219
|
+
return stats;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 取某个类 ID 的全部对象(按文件内出现顺序) */
|
|
223
|
+
function byClass(sf, classID) {
|
|
224
|
+
return sf.objects.filter((o) => o.classID === classID);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 取某个对象的字节切片 */
|
|
228
|
+
function objectBytes(sf, obj) {
|
|
229
|
+
return sf.bytes.subarray(obj.dataOffset, obj.dataOffset + obj.byteSize);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
NS.serialized = { parse, typeStats, byClass, objectBytes, className, CLASS_NAMES, CLASS_IDS };
|
|
233
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// plugins/unity-live2d-bundle/lib/texdecode.js —— 纹理格式分发
|
|
3
|
+
//
|
|
4
|
+
// 职责:读 Texture2D 的 m_TextureFormat,分发给对应解码器,返回 RGBA 字节。
|
|
5
|
+
// · 已实现:ETC2_RGBA8(47) / ETC2_RGB(45) / ETC1_RGB4(34) / 未压缩 RGBA32/RGB24/Alpha8 等直通格式
|
|
6
|
+
// · 未实现:ASTC / PVRTC / BCn 等 —— **抛可读错误**并把格式名与格式号带上,
|
|
7
|
+
// 绝不返回空白图或猜出来的图(否则上层会把"解码失败"当成"资源损坏")。
|
|
8
|
+
//
|
|
9
|
+
// 纹理尺寸与数据量的自检:按格式的块大小算出期望数据量,与给的字节数比对;
|
|
10
|
+
// 不匹配直接报错——这类错通常意味着字段读取错位,早报比晚报好。
|
|
11
|
+
//
|
|
12
|
+
// 挂载:globalThis.__ULB__.texdecode
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
const root = globalThis; // 原 IIFE 的 root 引用(脚本级裸块内保持同名)
|
|
19
|
+
const NS = (root.__ULB__ = root.__ULB__ || {});
|
|
20
|
+
|
|
21
|
+
// Unity TextureFormat → 名称 + 每块字节数 + 块边长(4 或 0 表示未压缩的逐像素格式)
|
|
22
|
+
const FORMATS = {
|
|
23
|
+
1: { name: 'Alpha8', block: 1, size: 1 },
|
|
24
|
+
3: { name: 'RGB24', block: 3, size: 1 },
|
|
25
|
+
4: { name: 'RGBA32', block: 4, size: 1 },
|
|
26
|
+
5: { name: 'ARGB32', block: 4, size: 1 },
|
|
27
|
+
7: { name: 'RGB565', block: 2, size: 1 },
|
|
28
|
+
34: { name: 'ETC_RGB4', compress: 'etc1', block: 8, size: 4 },
|
|
29
|
+
45: { name: 'ETC2_RGB', compress: 'etc2', block: 8, size: 4 },
|
|
30
|
+
47: { name: 'ETC2_RGBA8', compress: 'etc2a8', block: 16, size: 4 },
|
|
31
|
+
48: { name: 'ETC2_RGBA1', compress: 'etc2a1', block: 16, size: 4 },
|
|
32
|
+
// BC/DXT 家族(PC/主机资源包常用;块式压缩,解码实现见 lib/dxt.js)
|
|
33
|
+
10: { name: 'DXT1', compress: 'bc1', block: 8, size: 4 },
|
|
34
|
+
12: { name: 'DXT5', compress: 'bc3', block: 16, size: 4 },
|
|
35
|
+
26: { name: 'BC4', compress: 'bc4', block: 8, size: 4 },
|
|
36
|
+
27: { name: 'BC5', compress: 'bc5', block: 16, size: 4 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function formatName(fmt) {
|
|
40
|
+
const f = FORMATS[fmt];
|
|
41
|
+
return f ? f.name : `未知格式 ${fmt}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 按格式算期望的压缩/原始数据量 */
|
|
45
|
+
function expectedSize(fmt, width, height) {
|
|
46
|
+
const f = FORMATS[fmt];
|
|
47
|
+
if (!f) return null;
|
|
48
|
+
if (f.size === 1) return width * height * f.block;
|
|
49
|
+
const bw = Math.ceil(width / 4);
|
|
50
|
+
const bh = Math.ceil(height / 4);
|
|
51
|
+
return bw * bh * f.block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** unpack ARGB32 → RGBA */
|
|
55
|
+
function argbToRgba(data) {
|
|
56
|
+
const out = new Uint8Array(data.length);
|
|
57
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
58
|
+
out[i] = data[i + 1];
|
|
59
|
+
out[i + 1] = data[i + 2];
|
|
60
|
+
out[i + 2] = data[i + 3];
|
|
61
|
+
out[i + 3] = data[i];
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** RGB24(3) → RGBA:每 3 字节一像素,alpha 补 255 */
|
|
67
|
+
function rgb24ToRgba(data, width, height, need) {
|
|
68
|
+
const out = new Uint8Array(width * height * 4);
|
|
69
|
+
for (let i = 0, p = 0; i < need; i += 3, p += 4) {
|
|
70
|
+
out[p] = data[i]; out[p + 1] = data[i + 1]; out[p + 2] = data[i + 2]; out[p + 3] = 255;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Alpha8(1) → RGBA:单通道进 alpha,RGB 填白 */
|
|
76
|
+
function alpha8ToRgba(data, width, height, need) {
|
|
77
|
+
const out = new Uint8Array(width * height * 4);
|
|
78
|
+
for (let i = 0, p = 0; i < need; i++, p += 4) {
|
|
79
|
+
out[p] = 255; out[p + 1] = 255; out[p + 2] = 255; out[p + 3] = data[i];
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** RGB565(7) → RGBA:5/6/5 位展开到 8 位(低位复制高位,保持满量程) */
|
|
85
|
+
function rgb565ToRgba(data, width, height, need) {
|
|
86
|
+
const out = new Uint8Array(width * height * 4);
|
|
87
|
+
for (let i = 0, p = 0; i < need; i += 2, p += 4) {
|
|
88
|
+
const v = data[i] | (data[i + 1] << 8);
|
|
89
|
+
const r = (v >> 11) & 0x1f, g = (v >> 5) & 0x3f, b = v & 0x1f;
|
|
90
|
+
out[p] = (r << 3) | (r >> 2);
|
|
91
|
+
out[p + 1] = (g << 2) | (g >> 4);
|
|
92
|
+
out[p + 2] = (b << 3) | (b >> 2);
|
|
93
|
+
out[p + 3] = 255;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 垂直翻转 RGBA。
|
|
100
|
+
*
|
|
101
|
+
* 为什么所有格式都要翻:Unity 的纹理数据按 **GL 习惯自下而上**存放(第 0 行是图像底部),
|
|
102
|
+
* 而页面/PNG 期望自上而下。实测证据:按存储顺序解码后与权威参考 PNG 比对,
|
|
103
|
+
* 不做翻转时 2048² 的采样差异 461,728/466,489,翻转后差异为 0。
|
|
104
|
+
* 所以解码的输出统一为"显示方向",调用方拿到的就是能直接显示/编码的朝向。
|
|
105
|
+
*/
|
|
106
|
+
function flipY(rgba, width, height) {
|
|
107
|
+
const out = new Uint8Array(rgba.length);
|
|
108
|
+
const stride = width * 4;
|
|
109
|
+
for (let y = 0; y < height; y++) {
|
|
110
|
+
out.set(rgba.subarray((height - 1 - y) * stride, (height - y) * stride), y * stride);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 格式号 → 解码器。
|
|
117
|
+
*
|
|
118
|
+
* **等价性前提**:原来是"先按 compress 字段判 7 个分支、再按 fmt 号判 5 个",
|
|
119
|
+
* 这里改成按 fmt 号查表 —— 两者等价的唯一前提是"一个 fmt 只对应一条解码路径"。
|
|
120
|
+
* 实测成立:FORMATS 里每个 compress 值都只挂着一个 fmt
|
|
121
|
+
* (bc1→10 / bc3→12 / bc4→26 / bc5→27 / etc1→34 / etc2→45 / etc2a8→47 / etc2a1→48)。
|
|
122
|
+
* 所以不存在"两个 fmt 共享 compress 却需要不同处理"的情况。
|
|
123
|
+
*
|
|
124
|
+
* 解码器签名统一 `(data, width, height, need) => Uint8Array`,返回**未翻转**的 RGBA;
|
|
125
|
+
* 翻转由 decode 末尾统一做(与原来的 finish() 等价)。
|
|
126
|
+
*
|
|
127
|
+
* **buffer 共享约定**:未压缩格式走 slice(拷贝一份),压缩格式直接透传原始 data。
|
|
128
|
+
* 调用方**不得依赖**返回的 rgba 与入参共享底层 buffer(有的格式共享、有的不共享,
|
|
129
|
+
* 原地修改它可能改到源数据、也可能改不到)。
|
|
130
|
+
*/
|
|
131
|
+
const DECODERS = {
|
|
132
|
+
// 压缩格式:解码器自身产出 RGBA
|
|
133
|
+
34: (d, w, h) => NS.etc2.decodeEtc1Rgb(d, w, h),
|
|
134
|
+
45: (d, w, h) => NS.etc2.decodeEtc2Rgb(d, w, h),
|
|
135
|
+
47: (d, w, h) => NS.etc2.decodeEtc2Rgba8(d, w, h),
|
|
136
|
+
// 48 ETC2_RGBA1:ETC2 RGB 块(8 字节)+ 1bit alpha 平面(8 字节)。块布局与位序见
|
|
137
|
+
// lib/etc2.js 的 decodeEtc2Rgba1 注释。此格式长期"格式表里有、解码器没登记",
|
|
138
|
+
// 导致该格式的包整包报 ULB_ERR_TEX_DECODE(现场:蔚蓝档案部分 spinebackground 包)。
|
|
139
|
+
48: (d, w, h) => NS.etc2.decodeEtc2Rgba1(d, w, h),
|
|
140
|
+
10: (d, w, h) => NS.dxt.decodeBc1(d, w, h),
|
|
141
|
+
12: (d, w, h) => NS.dxt.decodeBc3(d, w, h),
|
|
142
|
+
26: (d, w, h) => NS.dxt.decodeBc4(d, w, h),
|
|
143
|
+
27: (d, w, h) => NS.dxt.decodeBc5(d, w, h),
|
|
144
|
+
// 未压缩格式:切片后按通道展开
|
|
145
|
+
4: (d, w, h, need) => d.slice(0, need),
|
|
146
|
+
5: (d, w, h, need) => argbToRgba(d.slice(0, need)),
|
|
147
|
+
3: (d, w, h, need) => rgb24ToRgba(d, w, h, need),
|
|
148
|
+
1: (d, w, h, need) => alpha8ToRgba(d, w, h, need),
|
|
149
|
+
7: (d, w, h, need) => rgb565ToRgba(d, w, h, need),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 解码一张纹理的压缩数据。
|
|
154
|
+
* @param {{format:number, width:number, height:number, name?:string}} meta
|
|
155
|
+
* @param {Uint8Array} data 压缩/原始数据(取自 .resS 或对象内联)
|
|
156
|
+
* @param {{flipY?:boolean}} [opts] flipY 默认 true(Unity 存储方向);给 false 拿原始存储方向
|
|
157
|
+
* @returns {Uint8Array} RGBA(显示方向)
|
|
158
|
+
*/
|
|
159
|
+
function decode(meta, data, opts) {
|
|
160
|
+
const fmt = meta.format;
|
|
161
|
+
const f = FORMATS[fmt];
|
|
162
|
+
if (!f) throw NS.errors.fail('ULB_ERR_TEX_FORMAT_UNSUPPORTED', `暂不支持纹理格式 ${fmt}(${meta.name || ''})——已支持:ETC2_RGBA8(47)、ETC2_RGBA1(48)、ETC2_RGB(45)、ETC_RGB4(34)、DXT1(10)、DXT5(12)、BC4(26)、BC5(27)、RGBA32(4)、ARGB32(5)、RGB24(3)、Alpha8(1)、RGB565(7);暂未实现:ASTC(50 系列)、PVRTC(30/31)、BC6H(24)、BC7(25)`);
|
|
163
|
+
const need = expectedSize(fmt, meta.width, meta.height);
|
|
164
|
+
if (data.length < need) throw NS.errors.fail('ULB_ERR_TEX_DATA_SHORT', `纹理 ${meta.name || ''}(${f.name} ${meta.width}x${meta.height})数据不足:需要 ${need} 字节,实际 ${data.length}`);
|
|
165
|
+
// 表里有格式、但没有登记解码器(如 ASTC 50 系列)⇒ 继续走这条兜底
|
|
166
|
+
const decoder = DECODERS[fmt];
|
|
167
|
+
if (!decoder) throw NS.errors.fail('ULB_ERR_TEX_DECODE', `纹理格式 ${f.name} 的分发分支缺失`);
|
|
168
|
+
const rgba = decoder(data, meta.width, meta.height, need);
|
|
169
|
+
// 统一翻转:原实现用 finish() 包住每个 return,等价于在这里一次做完
|
|
170
|
+
return !opts || opts.flipY !== false ? flipY(rgba, meta.width, meta.height) : rgba;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 某个 Unity TextureFormat 能否**真正解码**。
|
|
175
|
+
*
|
|
176
|
+
* 为什么需要它:FORMATS 是"格式字典"(名称 / 尺寸 / block 布局),**不代表能解码** ——
|
|
177
|
+
* 例如 ASTC(50 系列) 在 FORMATS 里可能有条目、但 DECODERS 没登记,decode() 会抛
|
|
178
|
+
* ULB_ERR_TEX_DECODE。若外部(预检 / 面板提示)拿 `!!FORMATS[fmt]` 当"支持",
|
|
179
|
+
* 就会先报"支持"、真正解包时才炸 —— 同一件事两套判据。
|
|
180
|
+
* ⇒ 能力判定只认这张表:**这是"能不能解码"的唯一真相**。
|
|
181
|
+
*
|
|
182
|
+
* @param {number} fmt
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
function canDecode(fmt) {
|
|
186
|
+
return typeof DECODERS[fmt] === 'function';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
NS.texdecode = { decode, canDecode, formatName, expectedSize, flipY, FORMATS };
|
|
190
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// plugins/unity-live2d-bundle/lib/typetree.js —— 类型树解析 + 按字段名取值
|
|
3
|
+
//
|
|
4
|
+
// 为什么需要它:SerializedFile 里的对象是"裸字节",字段边界只存在于**类型树**里。
|
|
5
|
+
// 有了类型树,才能按字段名取到 m_Width / m_TextureFormat / m_StreamData / m_Script / 曲线数组……
|
|
6
|
+
// 没有它就只能靠魔数/长度前缀猜(本插件把它作为兜底,而不是主路径)。
|
|
7
|
+
//
|
|
8
|
+
// 类型树 blob(v12+/v10,内联在 SerializedType 里):
|
|
9
|
+
// i32 节点数 | i32 字符串池长度 | 节点记录×N | 字符串池
|
|
10
|
+
// 节点记录(v19+,32 字节):
|
|
11
|
+
// u16 m_Version | u8 m_Level | u8 m_TypeFlags | u32 m_TypeStrOffset | u32 m_NameStrOffset
|
|
12
|
+
// i32 m_ByteSize | i32 m_Index | i32 m_MetaFlag | u64 m_RefTypeHash
|
|
13
|
+
// 字符串引用:最高位为 0 → 指向字符串池的偏移(读到 NUL 结束);
|
|
14
|
+
// 最高位为 1 → Unity 内置公共字符串表的下标(本插件遇到时抛可读错误,
|
|
15
|
+
// 不猜名字,避免把字段名张冠李戴)。
|
|
16
|
+
//
|
|
17
|
+
// 取值规则(与 Unity 的序列化规则一致,两处最容易错):
|
|
18
|
+
// · 每个节点自己的 m_MetaFlag 带 0x4000(kAlignBytes)时,读完该节点要 4 字节对齐;
|
|
19
|
+
// · 变长数组是「i32 长度 + 元素」,且当**容器节点**或 Array 子节点带对齐标志时,读完数组也要对齐。
|
|
20
|
+
//
|
|
21
|
+
// 挂载:globalThis.__ULB__.typetree
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
const root = globalThis; // 原 IIFE 的 root 引用(脚本级裸块内保持同名)
|
|
28
|
+
const NS = (root.__ULB__ = root.__ULB__ || {});
|
|
29
|
+
const B = NS.bytes;
|
|
30
|
+
|
|
31
|
+
const ALIGN_BYTES = 0x4000;
|
|
32
|
+
|
|
33
|
+
const isAligned = (metaFlag) => ((metaFlag || 0) & ALIGN_BYTES) !== 0;
|
|
34
|
+
|
|
35
|
+
/** 解析类型树 blob → 根节点(含 children 树) */
|
|
36
|
+
function parseBlob(blob, version) {
|
|
37
|
+
const r = new B.Reader(blob);
|
|
38
|
+
if (version >= 23) {
|
|
39
|
+
const magic = B.latin1(r.bytes(4), 0, 4);
|
|
40
|
+
if (magic !== 'mhtt') throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `类型树 blob 魔数不是 mhtt:${magic}`);
|
|
41
|
+
const fmt = r.i32();
|
|
42
|
+
if (fmt !== version) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `类型树格式版本不一致:${fmt} vs ${version}`);
|
|
43
|
+
}
|
|
44
|
+
const nodeCount = r.i32();
|
|
45
|
+
const stringBufferSize = r.i32();
|
|
46
|
+
if (nodeCount <= 0 || stringBufferSize < 0) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `类型树长度非法:节点数=${nodeCount} 字符串池=${stringBufferSize}`);
|
|
47
|
+
const recordSize = version >= 19 ? 32 : 24;
|
|
48
|
+
|
|
49
|
+
const raw = [];
|
|
50
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
51
|
+
const start = r.pos;
|
|
52
|
+
const rec = {
|
|
53
|
+
m_Version: r.u16(),
|
|
54
|
+
m_Level: r.u8(),
|
|
55
|
+
m_TypeFlags: r.u8(),
|
|
56
|
+
m_TypeStrOffset: r.u32(),
|
|
57
|
+
m_NameStrOffset: r.u32(),
|
|
58
|
+
m_ByteSize: r.i32(),
|
|
59
|
+
m_Index: r.i32(),
|
|
60
|
+
m_MetaFlag: r.i32(),
|
|
61
|
+
};
|
|
62
|
+
if (version >= 19) rec.m_RefTypeHash = r.i64('m_RefTypeHash');
|
|
63
|
+
if (r.pos - start !== recordSize) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', '类型树节点记录长度与预期不符');
|
|
64
|
+
raw.push(rec);
|
|
65
|
+
}
|
|
66
|
+
const stringBuffer = r.bytes(stringBufferSize, '类型树字符串池');
|
|
67
|
+
|
|
68
|
+
// 字符串池就是一个连续字节区,用 NUL 分隔;按偏移直接解出
|
|
69
|
+
const readPoolString = (offset) => {
|
|
70
|
+
let end = offset;
|
|
71
|
+
while (end < stringBuffer.length && stringBuffer[end] !== 0) end++;
|
|
72
|
+
return new TextDecoder('utf-8').decode(stringBuffer.subarray(offset, end));
|
|
73
|
+
};
|
|
74
|
+
const resolveString = (value, kind) => {
|
|
75
|
+
const isOffset = (value & 0x80000000) === 0;
|
|
76
|
+
if (isOffset) return readPoolString(value);
|
|
77
|
+
const idx = value & 0x7fffffff;
|
|
78
|
+
const hit = NS.commonStrings && NS.commonStrings[idx];
|
|
79
|
+
if (hit === undefined) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `公共字符串表里没有下标 ${idx}(${kind}),无法确定字段名`);
|
|
80
|
+
return hit;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 用层级栈把平铺的节点还原成树(与 Unity 的类型树遍历顺序一致)
|
|
84
|
+
const fakeRoot = { name: '', type: '', level: -1, metaFlag: 0, byteSize: 0, children: [] };
|
|
85
|
+
const stack = [fakeRoot];
|
|
86
|
+
let parent = fakeRoot;
|
|
87
|
+
let prev = fakeRoot;
|
|
88
|
+
for (const rec of raw) {
|
|
89
|
+
const node = {
|
|
90
|
+
version: rec.m_Version,
|
|
91
|
+
level: rec.m_Level,
|
|
92
|
+
flags: rec.m_TypeFlags,
|
|
93
|
+
byteSize: rec.m_ByteSize,
|
|
94
|
+
index: rec.m_Index,
|
|
95
|
+
metaFlag: rec.m_MetaFlag,
|
|
96
|
+
type: resolveString(rec.m_TypeStrOffset, 'type'),
|
|
97
|
+
name: resolveString(rec.m_NameStrOffset, 'name'),
|
|
98
|
+
children: [],
|
|
99
|
+
};
|
|
100
|
+
if (node.level > prev.level) { stack.push(parent); parent = prev; }
|
|
101
|
+
else if (node.level < prev.level) { while (node.level <= parent.level) parent = stack.pop(); }
|
|
102
|
+
parent.children.push(node);
|
|
103
|
+
prev = node;
|
|
104
|
+
}
|
|
105
|
+
if (!fakeRoot.children.length) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', '类型树为空');
|
|
106
|
+
return fakeRoot.children[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 64 位字段:能精确表示就返回 Number(好用好 JSON 化),超出安全范围才保留 BigInt。
|
|
110
|
+
// 为什么不直接抛错:对象里本来就有大整数(如 PPtr 的 pathID、哈希),抛错会让整棵树读不出来。
|
|
111
|
+
const readLong = (r) => {
|
|
112
|
+
const v = r.i64b('SInt64');
|
|
113
|
+
const n = Number(v);
|
|
114
|
+
return Math.abs(n) <= Number.MAX_SAFE_INTEGER ? n : v;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// 基本类型的读取函数(与 Unity 的类型名一一对应)
|
|
118
|
+
const PRIMITIVE = {
|
|
119
|
+
SInt8: (r) => r.i8(), UInt8: (r) => r.u8(), char: (r) => r.u8(),
|
|
120
|
+
short: (r) => r.i16(), SInt16: (r) => r.i16(), 'unsigned short': (r) => r.u16(), UInt16: (r) => r.u16(),
|
|
121
|
+
int: (r) => r.i32(), SInt32: (r) => r.i32(), 'unsigned int': (r) => r.u32(), UInt32: (r) => r.u32(),
|
|
122
|
+
'Type*': (r) => r.u32(),
|
|
123
|
+
'long long': readLong, SInt64: readLong,
|
|
124
|
+
'unsigned long long': readLong, UInt64: readLong, FileSize: readLong,
|
|
125
|
+
float: (r) => r.f32(), double: (r) => r.f64(), bool: (r) => r.bool(),
|
|
126
|
+
string: (r) => r.string('string'),
|
|
127
|
+
// TypelessData:i32 长度 + 原始字节(moc3 这类"字节数组"字段就是它)
|
|
128
|
+
TypelessData: (r) => { const n = r.i32(); return r.bytes(n, 'TypelessData'); },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* string 的「原始字节」读取:返回 { text, bytes },而不是只给 text。
|
|
133
|
+
*
|
|
134
|
+
* 为什么需要:Unity 的 `string` 在序列化层是「i32 长度 + 原始字节」,**Unity 自己不校验 UTF-8** ——
|
|
135
|
+
* TextAsset 允许承载二进制(spine 的 `.skel` 就是二进制,实测 651994 字节)。
|
|
136
|
+
* 而 `bytes.string()` 一律 UTF-8 解码 ⇒ 非法序列被替换成 U+FFFD,**信息永久丢失且不可逆**
|
|
137
|
+
* (实测某 .skel:641818 字符里 165995 个 U+FFFD,再编码回去长度都不对了)。
|
|
138
|
+
* 所以需要二进制字段时必须走这条,不能事后从 text 反推。
|
|
139
|
+
* 保留 text 是为了同一字段两种用途都能覆盖(`.atlas` 要文本、`.skel` 要字节)。
|
|
140
|
+
* @param {object} r Reader
|
|
141
|
+
*/
|
|
142
|
+
function readRawString(r) {
|
|
143
|
+
const len = r.i32();
|
|
144
|
+
if (len === 0) { r.align(4); return { text: '', bytes: new Uint8Array(0) }; }
|
|
145
|
+
if (len < 0 || len > r.remaining) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `字符串长度非法(string.raw):${len}`);
|
|
146
|
+
const raw = r.bytes(len, 'string.raw');
|
|
147
|
+
r.align(4);
|
|
148
|
+
// text 做成**惰性**:调用方常常只要 bytes(.skel 二进制,实测 651994 字节)或只要 text
|
|
149
|
+
// (m_Name 几十字节)。不访问就不解码 —— 探测阶段逐字段全量解码会明显拖慢 canHandle。
|
|
150
|
+
// 行为不变:访问 .text 得到的字符串与立即解码完全相同。
|
|
151
|
+
let decoded = null;
|
|
152
|
+
return {
|
|
153
|
+
bytes: raw,
|
|
154
|
+
get text() {
|
|
155
|
+
if (decoded === null) decoded = new TextDecoder('utf-8').decode(raw);
|
|
156
|
+
return decoded;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 按类型树递归取值。
|
|
163
|
+
* @param {object} node 类型树节点
|
|
164
|
+
* @param {object} r Reader
|
|
165
|
+
* @param {boolean} rawStrings true 时 `string` 返回 `{text,bytes}` 而非 `text`(见 readRawString)
|
|
166
|
+
*/
|
|
167
|
+
function readValue(node, r, rawStrings) {
|
|
168
|
+
let align = isAligned(node.metaFlag);
|
|
169
|
+
let value;
|
|
170
|
+
const primitive = (rawStrings && node.type === 'string') ? readRawString : PRIMITIVE[node.type];
|
|
171
|
+
if (primitive) {
|
|
172
|
+
value = primitive(r);
|
|
173
|
+
} else if (node.type === 'pair') {
|
|
174
|
+
value = [readValue(node.children[0], r, rawStrings), readValue(node.children[1], r, rawStrings)];
|
|
175
|
+
} else if (node.children.length && node.children[0].type === 'Array') {
|
|
176
|
+
// 变长数组/容器:先读 i32 长度,再读元素
|
|
177
|
+
const arrayNode = node.children[0];
|
|
178
|
+
if (isAligned(arrayNode.metaFlag)) align = true;
|
|
179
|
+
const size = r.i32();
|
|
180
|
+
if (size < 0 || size > 1e7) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `数组长度非法:${size}(字段 ${node.name})`);
|
|
181
|
+
const subtype = arrayNode.children[1];
|
|
182
|
+
value = [];
|
|
183
|
+
for (let i = 0; i < size; i++) value.push(readValue(subtype, r, rawStrings));
|
|
184
|
+
} else if (node.children.length) {
|
|
185
|
+
// 结构体/类:按子节点顺序取值
|
|
186
|
+
value = {};
|
|
187
|
+
for (const child of node.children) {
|
|
188
|
+
if (child.type === 'ManagedReferencesRegistry') throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', '尚未支持 ManagedReferencesRegistry(Unity 2020.1+ 的引用注册表)');
|
|
189
|
+
value[child.name] = readValue(child, r, rawStrings);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `未知类型,无法取值:type=${node.type} name=${node.name}`);
|
|
193
|
+
}
|
|
194
|
+
if (align) r.align(4);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 按类型树读取一个对象的全部字段。
|
|
200
|
+
* @param {object} sf serialized.parse() 的结果
|
|
201
|
+
* @param {object} obj serialized 的对象条目
|
|
202
|
+
* @param {{rawStrings?: boolean}} [opts] rawStrings=true 时 `string` 字段返回 `{text,bytes}`(需要二进制时用)
|
|
203
|
+
* @returns {object} 字段名 → 值(结构体为嵌套对象,数组为数组)
|
|
204
|
+
*/
|
|
205
|
+
function readObject(sf, obj, opts) {
|
|
206
|
+
const type = sf.types[obj.typeIndex];
|
|
207
|
+
if (!type || !type.typeTree) throw NS.errors.fail('ULB_ERR_CONTAINER_PARSE', `对象 ${obj.pathID} 的类型没有类型树`);
|
|
208
|
+
const blob = sf.bytes.subarray(type.typeTree.offset, type.typeTree.offset + type.typeTree.size);
|
|
209
|
+
const root = parseBlob(blob, sf.version);
|
|
210
|
+
const r = new B.Reader(sf.bytes, { pos: obj.dataOffset, endian: sf.le === false ? '>' : '<' });
|
|
211
|
+
return readValue(root, r, !!(opts && opts.rawStrings));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
NS.typetree = { parseBlob, readValue, readObject, readRawString, isAligned, ALIGN_BYTES };
|
|
215
|
+
}
|