@linxin666/dsh-pet 0.2.2 → 0.2.3
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/README.i18n.yaml +2 -2
- package/README.md +66 -3
- package/README.zh.md +66 -3
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/lib/client.js +438 -21
- package/lib/client.js.map +1 -1
- package/lib/index.js +752 -36
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -1
- package/lib/types/client/PetSettingsCard.d.ts +10 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +22 -1
- package/lib/types/client/PetSprite.d.ts +7 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +17 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +10 -0
- package/lib/types/client/phase-stream.d.ts +22 -0
- package/lib/types/client/phase-stream.d.ts.map +1 -0
- package/lib/types/client/phase-stream.js +28 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
- package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/runtime.js +63 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
- package/lib/types/client/renderers/live2d.d.ts +52 -0
- package/lib/types/client/renderers/live2d.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d.js +180 -0
- package/lib/types/client/renderers/registry.d.ts +32 -0
- package/lib/types/client/renderers/registry.d.ts.map +1 -0
- package/lib/types/client/renderers/registry.js +49 -0
- package/lib/types/contracts/renderer.d.ts +50 -0
- package/lib/types/contracts/renderer.d.ts.map +1 -0
- package/lib/types/contracts/renderer.js +15 -0
- package/lib/types/manifest-v2.d.ts +129 -0
- package/lib/types/manifest-v2.d.ts.map +1 -0
- package/lib/types/manifest-v2.js +387 -0
- package/lib/types/model3.d.ts +26 -0
- package/lib/types/model3.d.ts.map +1 -0
- package/lib/types/model3.js +96 -0
- package/lib/types/registry.d.ts +57 -3
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +208 -33
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +183 -12
- package/lib/types/service.d.ts +5 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +4 -0
- package/package.json +3 -1
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +39 -0
- package/src/client/PetSprite.test.tsx +11 -0
- package/src/client/PetSprite.tsx +24 -9
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +10 -0
- package/src/client/phase-stream.test.ts +76 -0
- package/src/client/phase-stream.ts +39 -0
- package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
- package/src/client/renderers/PetRendererSwitch.tsx +48 -0
- package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
- package/src/client/renderers/live2d/runtime.test.ts +61 -0
- package/src/client/renderers/live2d/runtime.ts +119 -0
- package/src/client/renderers/live2d/vendor-entry.ts +14 -0
- package/src/client/renderers/live2d.test.ts +242 -0
- package/src/client/renderers/live2d.ts +218 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -0
- package/src/manifest-v2.test.ts +251 -0
- package/src/manifest-v2.ts +414 -0
- package/src/model3.test.ts +79 -0
- package/src/model3.ts +91 -0
- package/src/registry.test.ts +215 -2
- package/src/registry.ts +246 -34
- package/src/routes.ts +202 -13
- package/src/service.ts +6 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet manifest v2 — parsing, fail-closed validation, and the v1 compat read
|
|
3
|
+
* (issue #623, milestone M1/M2). This module is the single entry point every
|
|
4
|
+
* manifest parse flows through: manifests without 'petManifestVersion' are
|
|
5
|
+
* compat-read as v1 sprite2d pets (with a migration hint diagnostic), v2
|
|
6
|
+
* manifests are validated fail-closed (unknown top-level keys, unknown
|
|
7
|
+
* renderer kinds, missing conditional blocks and unsafe paths all reject the
|
|
8
|
+
* manifest with human-readable diagnostics).
|
|
9
|
+
*
|
|
10
|
+
* Discipline split: STRUCTURE is fail-closed (types, key sets, paths,
|
|
11
|
+
* required fields); CONTENT stays warn-and-drop (a bad sequence entry drops
|
|
12
|
+
* the entry, never the pet) — matching the registry's long-standing
|
|
13
|
+
* never-throw philosophy. Deep normalization of sequences/remarks stays with
|
|
14
|
+
* the registry's existing normalizers; this module only gates structure.
|
|
15
|
+
*
|
|
16
|
+
* The JSON Schema twin lives at contracts/pet-manifest-v2.schema.json for
|
|
17
|
+
* documentation, the CLI, and external tooling; the hand-rolled validator
|
|
18
|
+
* here is authoritative (the repository ships no schema-validator runtime).
|
|
19
|
+
*
|
|
20
|
+
* This file is imported directly by scripts/ (dsh-pet-migrate-v2) under
|
|
21
|
+
* node's strip-only TypeScript mode: keep it erasable-syntax-only (no
|
|
22
|
+
* parameter properties, enums, or namespaces).
|
|
23
|
+
* @module @linxin666/dsh-pet/manifest-v2
|
|
24
|
+
*/
|
|
25
|
+
import type { ActivityPhase, PetAnimation } from './state.ts';
|
|
26
|
+
/** Schema version this module validates. */
|
|
27
|
+
export declare const PET_MANIFEST_V2: 2;
|
|
28
|
+
/** Renderer kinds the pet center knows how to dispatch (M1 §2). */
|
|
29
|
+
export declare const PET_RENDERER_KINDS: readonly ['sprite2d', 'live2d'];
|
|
30
|
+
export type PetRendererKind = (typeof PET_RENDERER_KINDS)[number];
|
|
31
|
+
/** The seven ActivityPhase semantics (pet-center owned; M1 §1). */
|
|
32
|
+
export declare const PET_ACTIVITY_PHASES: readonly ActivityPhase[];
|
|
33
|
+
/** sprite2d renderer block (v2 nested shape of the v1 atlas contract). */
|
|
34
|
+
export interface PetManifestSprite2d {
|
|
35
|
+
/** Atlas path relative to the manifest directory (safe segments only). */
|
|
36
|
+
spritesheetPath: string;
|
|
37
|
+
/** Atlas cell size in px; defaults resolved by the registry. */
|
|
38
|
+
cell?: {
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
};
|
|
42
|
+
/** Columns per row; defaults to 8. */
|
|
43
|
+
columns?: number;
|
|
44
|
+
/** Total atlas rows (9 classic; 11 for v2 look-row atlases). */
|
|
45
|
+
atlasRows?: number;
|
|
46
|
+
/** Per-row used frame counts. */
|
|
47
|
+
frames?: number[];
|
|
48
|
+
/** Per-track rhythm overrides (durations/loop/fallback), v1 shape. */
|
|
49
|
+
tracks?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** live2d renderer block (Cubism Core is always user-supplied; M1 §0). */
|
|
52
|
+
export interface PetManifestLive2d {
|
|
53
|
+
/** Path of the .model3.json relative to the manifest directory. */
|
|
54
|
+
model: string;
|
|
55
|
+
/** Model scale, (0, 10]; defaults to 1. */
|
|
56
|
+
scale?: number;
|
|
57
|
+
/** Model offset in canvas space. */
|
|
58
|
+
translate?: {
|
|
59
|
+
x?: number;
|
|
60
|
+
y?: number;
|
|
61
|
+
};
|
|
62
|
+
/** ActivityPhase -> motion group name; idle is required, unmapped phases fall back to idle. */
|
|
63
|
+
motions: Partial<Record<ActivityPhase, string>> & {
|
|
64
|
+
idle: string;
|
|
65
|
+
};
|
|
66
|
+
/** Optional ActivityPhase -> expression name layered over the motion. */
|
|
67
|
+
expressions?: Partial<Record<ActivityPhase, string>>;
|
|
68
|
+
/** Hit area names triggering pet.interact; defaults to every model HitArea. */
|
|
69
|
+
hitAreas?: string[];
|
|
70
|
+
/** Whisper-paced lip sync (post-M3). */
|
|
71
|
+
lipSync?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** Normalized v2 manifest the registry consumes. */
|
|
74
|
+
export interface PetManifestV2 {
|
|
75
|
+
petManifestVersion: typeof PET_MANIFEST_V2;
|
|
76
|
+
id: string;
|
|
77
|
+
displayName: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
version?: string;
|
|
80
|
+
author?: string;
|
|
81
|
+
/** Required by v2; v1 compat reads may lack it (warning, not rejection). */
|
|
82
|
+
license?: string;
|
|
83
|
+
homepage?: string;
|
|
84
|
+
renderer: PetRendererKind;
|
|
85
|
+
sprite2d?: PetManifestSprite2d;
|
|
86
|
+
live2d?: PetManifestLive2d;
|
|
87
|
+
sequences?: Partial<Record<ActivityPhase, PetAnimation[]>>;
|
|
88
|
+
/** Pass-through for the registry's remarks normalizer (v1 shape). */
|
|
89
|
+
remarks?: unknown;
|
|
90
|
+
}
|
|
91
|
+
/** One structured diagnostic emitted while parsing a manifest. */
|
|
92
|
+
export interface PetManifestDiagnostic {
|
|
93
|
+
level: 'error' | 'warning';
|
|
94
|
+
message: string;
|
|
95
|
+
}
|
|
96
|
+
/** Parse outcome: a usable manifest plus diagnostics, or rejection. */
|
|
97
|
+
export type PetManifestParse = {
|
|
98
|
+
ok: true;
|
|
99
|
+
manifest: PetManifestV2;
|
|
100
|
+
migrated: 'v1-compat' | undefined;
|
|
101
|
+
diagnostics: PetManifestDiagnostic[];
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
diagnostics: PetManifestDiagnostic[];
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Field allow-lists mirroring contracts/pet-manifest-v2.schema.json. Exported
|
|
108
|
+
* so the drift test can lock the schema file and this validator together;
|
|
109
|
+
* the CLI reuses parsePetManifest instead of these.
|
|
110
|
+
*/
|
|
111
|
+
export declare const KNOWN_TOP_LEVEL: Set<string>;
|
|
112
|
+
/** sprite2d block field allow-list (drift-locked to the schema file). */
|
|
113
|
+
export declare const KNOWN_SPRITE2D: Set<string>;
|
|
114
|
+
/** live2d block field allow-list (drift-locked to the schema file). */
|
|
115
|
+
export declare const KNOWN_LIVE2D: Set<string>;
|
|
116
|
+
/**
|
|
117
|
+
* Validate a manifest-relative asset path: no absolute paths, no backslashes,
|
|
118
|
+
* no traversal, plain safe segments only. Returns the normalized path.
|
|
119
|
+
*/
|
|
120
|
+
export declare function safeManifestPath(raw: unknown): string | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Parse one pet manifest: v1 (no petManifestVersion) is compat-read as a
|
|
123
|
+
* sprite2d pet with a migration hint; v2 is validated fail-closed. The parse
|
|
124
|
+
* never throws — every failure comes back as structured diagnostics.
|
|
125
|
+
* @param raw - the parsed pet.json value.
|
|
126
|
+
* @param sourceLabel - human-readable origin for diagnostics (dir or file).
|
|
127
|
+
*/
|
|
128
|
+
export declare function parsePetManifest(raw: unknown, sourceLabel: string): PetManifestParse;
|
|
129
|
+
//# sourceMappingURL=manifest-v2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-v2.d.ts","sourceRoot":"","sources":["../../src/manifest-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE7D,4CAA4C;AAC5C,eAAO,MAAM,eAAe,EAAG,CAAU,CAAA;AAEzC,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,YAAI,UAAU,EAAE,QAAQ,CAAU,CAAA;AACjE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEjE,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,SAAS,aAAa,EAEvD,CAAA;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAA;IACvB,gEAAgE;IAChE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,SAAS,CAAC,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAA;IACpD,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,wCAAwC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,oDAAoD;AACpD,MAAM,WAAW,aAAa;IAC5B,kBAAkB,EAAE,OAAO,eAAe,CAAA;IAC1C,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,eAAe,CAAA;IACzB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC,CAAA;IAC1D,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,WAAW,GAAG,SAAS,CAAC;IAAC,WAAW,EAAE,qBAAqB,EAAE,CAAA;CAAE,GAC9G;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,WAAW,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAAA;AAKvD;;;;GAIG;AACH,eAAO,MAAM,eAAe,aAG1B,CAAA;AACF,yEAAyE;AACzE,eAAO,MAAM,cAAc,aAAmF,CAAA;AAC9G,uEAAuE;AACvE,eAAO,MAAM,YAAY,aAA4F,CAAA;AAmBrH;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQjE;AAiPD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAepF"}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet manifest v2 — parsing, fail-closed validation, and the v1 compat read
|
|
3
|
+
* (issue #623, milestone M1/M2). This module is the single entry point every
|
|
4
|
+
* manifest parse flows through: manifests without 'petManifestVersion' are
|
|
5
|
+
* compat-read as v1 sprite2d pets (with a migration hint diagnostic), v2
|
|
6
|
+
* manifests are validated fail-closed (unknown top-level keys, unknown
|
|
7
|
+
* renderer kinds, missing conditional blocks and unsafe paths all reject the
|
|
8
|
+
* manifest with human-readable diagnostics).
|
|
9
|
+
*
|
|
10
|
+
* Discipline split: STRUCTURE is fail-closed (types, key sets, paths,
|
|
11
|
+
* required fields); CONTENT stays warn-and-drop (a bad sequence entry drops
|
|
12
|
+
* the entry, never the pet) — matching the registry's long-standing
|
|
13
|
+
* never-throw philosophy. Deep normalization of sequences/remarks stays with
|
|
14
|
+
* the registry's existing normalizers; this module only gates structure.
|
|
15
|
+
*
|
|
16
|
+
* The JSON Schema twin lives at contracts/pet-manifest-v2.schema.json for
|
|
17
|
+
* documentation, the CLI, and external tooling; the hand-rolled validator
|
|
18
|
+
* here is authoritative (the repository ships no schema-validator runtime).
|
|
19
|
+
*
|
|
20
|
+
* This file is imported directly by scripts/ (dsh-pet-migrate-v2) under
|
|
21
|
+
* node's strip-only TypeScript mode: keep it erasable-syntax-only (no
|
|
22
|
+
* parameter properties, enums, or namespaces).
|
|
23
|
+
* @module @linxin666/dsh-pet/manifest-v2
|
|
24
|
+
*/
|
|
25
|
+
import { isAbsolute } from 'node:path';
|
|
26
|
+
/** Schema version this module validates. */
|
|
27
|
+
export const PET_MANIFEST_V2 = 2;
|
|
28
|
+
/** Renderer kinds the pet center knows how to dispatch (M1 §2). */
|
|
29
|
+
export const PET_RENDERER_KINDS = ['sprite2d', 'live2d'];
|
|
30
|
+
/** The seven ActivityPhase semantics (pet-center owned; M1 §1). */
|
|
31
|
+
export const PET_ACTIVITY_PHASES = [
|
|
32
|
+
'idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed',
|
|
33
|
+
];
|
|
34
|
+
const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
35
|
+
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
36
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
37
|
+
/**
|
|
38
|
+
* Field allow-lists mirroring contracts/pet-manifest-v2.schema.json. Exported
|
|
39
|
+
* so the drift test can lock the schema file and this validator together;
|
|
40
|
+
* the CLI reuses parsePetManifest instead of these.
|
|
41
|
+
*/
|
|
42
|
+
export const KNOWN_TOP_LEVEL = new Set([
|
|
43
|
+
'$schema', 'petManifestVersion', 'id', 'displayName', 'description', 'version',
|
|
44
|
+
'author', 'license', 'homepage', 'renderer', 'sprite2d', 'live2d', 'sequences', 'remarks',
|
|
45
|
+
]);
|
|
46
|
+
/** sprite2d block field allow-list (drift-locked to the schema file). */
|
|
47
|
+
export const KNOWN_SPRITE2D = new Set(['spritesheetPath', 'cell', 'columns', 'atlasRows', 'frames', 'tracks']);
|
|
48
|
+
/** live2d block field allow-list (drift-locked to the schema file). */
|
|
49
|
+
export const KNOWN_LIVE2D = new Set(['model', 'scale', 'translate', 'motions', 'expressions', 'hitAreas', 'lipSync']);
|
|
50
|
+
class Diagnostics {
|
|
51
|
+
list = [];
|
|
52
|
+
source;
|
|
53
|
+
constructor(source) { this.source = source; }
|
|
54
|
+
error(message) { this.list.push({ level: 'error', message: this.source + ': ' + message }); }
|
|
55
|
+
warn(message) { this.list.push({ level: 'warning', message: this.source + ': ' + message }); }
|
|
56
|
+
get hasErrors() { return this.list.some(d => d.level === 'error'); }
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function unknownKeys(source, known) {
|
|
62
|
+
return Object.keys(source).filter(key => !known.has(key));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validate a manifest-relative asset path: no absolute paths, no backslashes,
|
|
66
|
+
* no traversal, plain safe segments only. Returns the normalized path.
|
|
67
|
+
*/
|
|
68
|
+
export function safeManifestPath(raw) {
|
|
69
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
70
|
+
return undefined;
|
|
71
|
+
const value = raw.trim();
|
|
72
|
+
if (isAbsolute(value) || value.includes('\\') || /^[a-z][a-z0-9+.-]*:/i.test(value))
|
|
73
|
+
return undefined;
|
|
74
|
+
const segments = value.split('/').filter(segment => segment !== '');
|
|
75
|
+
if (segments.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (segments.some(segment => segment === '.' || segment === '..' || !PATH_SEGMENT_PATTERN.test(segment)))
|
|
78
|
+
return undefined;
|
|
79
|
+
return segments.join('/');
|
|
80
|
+
}
|
|
81
|
+
function parseStringBlock(record, key, diag, required) {
|
|
82
|
+
const value = record[key];
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
if (required)
|
|
85
|
+
diag.error('missing required field ' + JSON.stringify(key));
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
89
|
+
diag.error('field ' + JSON.stringify(key) + ' must be a non-empty string');
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return value.trim();
|
|
93
|
+
}
|
|
94
|
+
/** Validate the phase-keyed string map shape shared by motions/expressions. */
|
|
95
|
+
function parsePhaseStringMap(raw, field, diag) {
|
|
96
|
+
if (raw === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!isRecord(raw)) {
|
|
99
|
+
diag.error('field ' + JSON.stringify(field) + ' must be an object keyed by activity phase');
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [phase, value] of Object.entries(raw)) {
|
|
104
|
+
if (!PET_ACTIVITY_PHASES.includes(phase)) {
|
|
105
|
+
diag.error(field + ': unknown activity phase ' + JSON.stringify(phase));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
109
|
+
diag.error(field + '.' + phase + ' must be a non-empty string');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
result[phase] = value.trim();
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/** Structural gate for sequences: content stays warn-and-drop (registry's job). */
|
|
117
|
+
function parseSequences(raw, diag) {
|
|
118
|
+
if (raw === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (!isRecord(raw)) {
|
|
121
|
+
diag.warn('sequences must be an object keyed by activity phase; ignoring');
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const sequences = {};
|
|
125
|
+
for (const [phase, value] of Object.entries(raw)) {
|
|
126
|
+
if (!PET_ACTIVITY_PHASES.includes(phase)) {
|
|
127
|
+
diag.warn('sequences: unknown activity phase ' + JSON.stringify(phase) + '; entry dropped');
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(value) || value.length < 5 || value.some(item => typeof item !== 'string')) {
|
|
131
|
+
diag.warn('sequences.' + phase + ' must be an array of at least 5 animation names; entry dropped');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
sequences[phase] = value;
|
|
135
|
+
}
|
|
136
|
+
return Object.keys(sequences).length === 0 ? undefined : sequences;
|
|
137
|
+
}
|
|
138
|
+
function parseSprite2dBlock(raw, diag) {
|
|
139
|
+
if (!isRecord(raw)) {
|
|
140
|
+
diag.error('renderer sprite2d requires a "sprite2d" block object');
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const extra = unknownKeys(raw, KNOWN_SPRITE2D);
|
|
144
|
+
if (extra.length > 0)
|
|
145
|
+
diag.error('sprite2d: unknown field(s) ' + extra.map(k => JSON.stringify(k)).join(', '));
|
|
146
|
+
const spritesheetPath = safeManifestPath(raw.spritesheetPath);
|
|
147
|
+
if (spritesheetPath === undefined) {
|
|
148
|
+
diag.error('sprite2d.spritesheetPath must be a safe manifest-relative path');
|
|
149
|
+
}
|
|
150
|
+
const block = { spritesheetPath: spritesheetPath ?? '' };
|
|
151
|
+
if (raw.cell !== undefined) {
|
|
152
|
+
if (!isRecord(raw.cell))
|
|
153
|
+
diag.error('sprite2d.cell must be an object { width?, height? }');
|
|
154
|
+
else
|
|
155
|
+
block.cell = raw.cell;
|
|
156
|
+
}
|
|
157
|
+
if (raw.columns !== undefined) {
|
|
158
|
+
if (typeof raw.columns !== 'number' || !Number.isInteger(raw.columns) || raw.columns < 1)
|
|
159
|
+
diag.error('sprite2d.columns must be a positive integer');
|
|
160
|
+
else
|
|
161
|
+
block.columns = raw.columns;
|
|
162
|
+
}
|
|
163
|
+
if (raw.atlasRows !== undefined) {
|
|
164
|
+
if (typeof raw.atlasRows !== 'number' || !Number.isInteger(raw.atlasRows) || raw.atlasRows < 1)
|
|
165
|
+
diag.error('sprite2d.atlasRows must be a positive integer');
|
|
166
|
+
else
|
|
167
|
+
block.atlasRows = raw.atlasRows;
|
|
168
|
+
}
|
|
169
|
+
if (raw.frames !== undefined) {
|
|
170
|
+
if (!Array.isArray(raw.frames) || raw.frames.some(v => typeof v !== 'number' || !Number.isInteger(v) || v < 0)) {
|
|
171
|
+
diag.error('sprite2d.frames must be an array of non-negative integers');
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
block.frames = raw.frames;
|
|
175
|
+
}
|
|
176
|
+
if (raw.tracks !== undefined) {
|
|
177
|
+
if (!isRecord(raw.tracks))
|
|
178
|
+
diag.error('sprite2d.tracks must be an object keyed by animation');
|
|
179
|
+
else
|
|
180
|
+
block.tracks = raw.tracks;
|
|
181
|
+
}
|
|
182
|
+
return diag.hasErrors ? undefined : block;
|
|
183
|
+
}
|
|
184
|
+
function parseLive2dBlock(raw, diag) {
|
|
185
|
+
if (!isRecord(raw)) {
|
|
186
|
+
diag.error('renderer live2d requires a "live2d" block object');
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const extra = unknownKeys(raw, KNOWN_LIVE2D);
|
|
190
|
+
if (extra.length > 0)
|
|
191
|
+
diag.error('live2d: unknown field(s) ' + extra.map(k => JSON.stringify(k)).join(', '));
|
|
192
|
+
const model = safeManifestPath(raw.model);
|
|
193
|
+
if (model === undefined) {
|
|
194
|
+
diag.error('live2d.model must be a safe manifest-relative path to a .model3.json');
|
|
195
|
+
}
|
|
196
|
+
else if (!model.endsWith('.model3.json')) {
|
|
197
|
+
diag.error('live2d.model must point at a .model3.json file');
|
|
198
|
+
}
|
|
199
|
+
const motions = parsePhaseStringMap(raw.motions, 'live2d.motions', diag);
|
|
200
|
+
if (raw.motions === undefined)
|
|
201
|
+
diag.error('live2d.motions is required (at least an "idle" group)');
|
|
202
|
+
else if (motions !== undefined && motions.idle === undefined)
|
|
203
|
+
diag.error('live2d.motions.idle is required (unmapped phases fall back to it)');
|
|
204
|
+
const block = {
|
|
205
|
+
model: model ?? '',
|
|
206
|
+
motions: (motions ?? { idle: '' }),
|
|
207
|
+
};
|
|
208
|
+
if (raw.scale !== undefined) {
|
|
209
|
+
if (typeof raw.scale !== 'number' || !Number.isFinite(raw.scale) || raw.scale <= 0 || raw.scale > 10) {
|
|
210
|
+
diag.error('live2d.scale must be a number in (0, 10]');
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
block.scale = raw.scale;
|
|
214
|
+
}
|
|
215
|
+
if (raw.translate !== undefined) {
|
|
216
|
+
if (!isRecord(raw.translate)
|
|
217
|
+
|| (raw.translate.x !== undefined && typeof raw.translate.x !== 'number')
|
|
218
|
+
|| (raw.translate.y !== undefined && typeof raw.translate.y !== 'number')) {
|
|
219
|
+
diag.error('live2d.translate must be an object { x?: number, y?: number }');
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
block.translate = raw.translate;
|
|
223
|
+
}
|
|
224
|
+
const expressions = parsePhaseStringMap(raw.expressions, 'live2d.expressions', diag);
|
|
225
|
+
if (expressions !== undefined)
|
|
226
|
+
block.expressions = expressions;
|
|
227
|
+
if (raw.hitAreas !== undefined) {
|
|
228
|
+
if (!Array.isArray(raw.hitAreas) || raw.hitAreas.some(v => typeof v !== 'string' || v.trim() === '')) {
|
|
229
|
+
diag.error('live2d.hitAreas must be an array of non-empty strings');
|
|
230
|
+
}
|
|
231
|
+
else
|
|
232
|
+
block.hitAreas = raw.hitAreas;
|
|
233
|
+
}
|
|
234
|
+
if (raw.lipSync !== undefined) {
|
|
235
|
+
if (typeof raw.lipSync !== 'boolean')
|
|
236
|
+
diag.error('live2d.lipSync must be a boolean');
|
|
237
|
+
else
|
|
238
|
+
block.lipSync = raw.lipSync;
|
|
239
|
+
}
|
|
240
|
+
return diag.hasErrors ? undefined : block;
|
|
241
|
+
}
|
|
242
|
+
/** v1 compat read: map the legacy flat manifest onto the v2 sprite2d shape. */
|
|
243
|
+
function compatV1(source, diag) {
|
|
244
|
+
const id = parseStringBlock(source, 'id', diag, true);
|
|
245
|
+
if (id !== undefined && !PET_ID_PATTERN.test(id)) {
|
|
246
|
+
diag.error('id ' + JSON.stringify(id) + ' is not a lowercase kebab id');
|
|
247
|
+
}
|
|
248
|
+
const displayName = typeof source.displayName === 'string' && source.displayName.trim() !== ''
|
|
249
|
+
? source.displayName.trim()
|
|
250
|
+
: id;
|
|
251
|
+
const spritesheetRaw = source.spritesheetPath === undefined ? 'spritesheet.webp' : source.spritesheetPath;
|
|
252
|
+
const spritesheetPath = safeManifestPath(spritesheetRaw);
|
|
253
|
+
if (spritesheetPath === undefined) {
|
|
254
|
+
diag.error('spritesheetPath ' + JSON.stringify(String(source.spritesheetPath)) + ' is not a safe relative path');
|
|
255
|
+
}
|
|
256
|
+
if (source.license === undefined) {
|
|
257
|
+
diag.warn('v1 compat read: no license field; run scripts/dsh-pet-migrate-v2 to migrate this pet');
|
|
258
|
+
}
|
|
259
|
+
const sprite2d = { spritesheetPath: spritesheetPath ?? 'spritesheet.webp' };
|
|
260
|
+
if (isRecord(source.cell))
|
|
261
|
+
sprite2d.cell = source.cell;
|
|
262
|
+
if (typeof source.columns === 'number')
|
|
263
|
+
sprite2d.columns = source.columns;
|
|
264
|
+
if (Array.isArray(source.frames))
|
|
265
|
+
sprite2d.frames = source.frames;
|
|
266
|
+
if (isRecord(source.tracks))
|
|
267
|
+
sprite2d.tracks = source.tracks;
|
|
268
|
+
// v2 spritesheet atlases (spriteVersionNumber 2) carry 11 rows: 9 + 2 look rows.
|
|
269
|
+
if (source.spriteVersionNumber === 2)
|
|
270
|
+
sprite2d.atlasRows = 11;
|
|
271
|
+
const manifest = {
|
|
272
|
+
petManifestVersion: PET_MANIFEST_V2,
|
|
273
|
+
id: id ?? '',
|
|
274
|
+
displayName: displayName ?? '',
|
|
275
|
+
renderer: 'sprite2d',
|
|
276
|
+
sprite2d,
|
|
277
|
+
};
|
|
278
|
+
if (typeof source.description === 'string' && source.description.trim() !== '')
|
|
279
|
+
manifest.description = source.description.trim();
|
|
280
|
+
if (typeof source.license === 'string' && source.license.trim() !== '')
|
|
281
|
+
manifest.license = source.license.trim();
|
|
282
|
+
const sequences = parseSequences(source.sequences, diag);
|
|
283
|
+
if (sequences !== undefined)
|
|
284
|
+
manifest.sequences = sequences;
|
|
285
|
+
if (source.remarks !== undefined)
|
|
286
|
+
manifest.remarks = source.remarks;
|
|
287
|
+
return diag.hasErrors ? undefined : manifest;
|
|
288
|
+
}
|
|
289
|
+
/** Strict v2 validation (fail-closed on structure). */
|
|
290
|
+
function parseV2(source, diag) {
|
|
291
|
+
const extra = unknownKeys(source, KNOWN_TOP_LEVEL);
|
|
292
|
+
if (extra.length > 0)
|
|
293
|
+
diag.error('unknown top-level field(s) ' + extra.map(k => JSON.stringify(k)).join(', '));
|
|
294
|
+
if (source.petManifestVersion !== PET_MANIFEST_V2) {
|
|
295
|
+
diag.error('petManifestVersion must be 2 (got ' + JSON.stringify(source.petManifestVersion) + ')');
|
|
296
|
+
}
|
|
297
|
+
const id = parseStringBlock(source, 'id', diag, true);
|
|
298
|
+
if (id !== undefined && (!PET_ID_PATTERN.test(id) || id.length > 64)) {
|
|
299
|
+
diag.error('id ' + JSON.stringify(id) + ' must be a lowercase kebab id of at most 64 chars');
|
|
300
|
+
}
|
|
301
|
+
const displayName = parseStringBlock(source, 'displayName', diag, true);
|
|
302
|
+
const license = parseStringBlock(source, 'license', diag, true);
|
|
303
|
+
const rendererRaw = source.renderer === undefined ? 'sprite2d' : source.renderer;
|
|
304
|
+
if (!PET_RENDERER_KINDS.includes(rendererRaw)) {
|
|
305
|
+
diag.error('unknown renderer ' + JSON.stringify(rendererRaw) + '; expected one of ' + PET_RENDERER_KINDS.join(', '));
|
|
306
|
+
}
|
|
307
|
+
const renderer = rendererRaw;
|
|
308
|
+
const manifest = {
|
|
309
|
+
petManifestVersion: PET_MANIFEST_V2,
|
|
310
|
+
id: id ?? '',
|
|
311
|
+
displayName: displayName ?? '',
|
|
312
|
+
renderer,
|
|
313
|
+
};
|
|
314
|
+
if (license !== undefined)
|
|
315
|
+
manifest.license = license;
|
|
316
|
+
if (source.description !== undefined) {
|
|
317
|
+
if (typeof source.description !== 'string' || source.description.length > 500)
|
|
318
|
+
diag.error('description must be a string of at most 500 chars');
|
|
319
|
+
else
|
|
320
|
+
manifest.description = source.description;
|
|
321
|
+
}
|
|
322
|
+
if (source.version !== undefined) {
|
|
323
|
+
if (typeof source.version !== 'string' || !SEMVER_PATTERN.test(source.version))
|
|
324
|
+
diag.error('version must be a semver string (x.y.z)');
|
|
325
|
+
else
|
|
326
|
+
manifest.version = source.version;
|
|
327
|
+
}
|
|
328
|
+
if (source.author !== undefined) {
|
|
329
|
+
if (typeof source.author !== 'string' || source.author.length > 128)
|
|
330
|
+
diag.error('author must be a string of at most 128 chars');
|
|
331
|
+
else
|
|
332
|
+
manifest.author = source.author;
|
|
333
|
+
}
|
|
334
|
+
if (source.homepage !== undefined) {
|
|
335
|
+
if (typeof source.homepage !== 'string')
|
|
336
|
+
diag.error('homepage must be a string URL');
|
|
337
|
+
else
|
|
338
|
+
manifest.homepage = source.homepage;
|
|
339
|
+
}
|
|
340
|
+
if (renderer === 'sprite2d') {
|
|
341
|
+
const block = parseSprite2dBlock(source.sprite2d, diag);
|
|
342
|
+
if (block !== undefined)
|
|
343
|
+
manifest.sprite2d = block;
|
|
344
|
+
if (source.live2d !== undefined)
|
|
345
|
+
diag.error('renderer sprite2d must not declare a live2d block');
|
|
346
|
+
}
|
|
347
|
+
else if (renderer === 'live2d') {
|
|
348
|
+
const block = parseLive2dBlock(source.live2d, diag);
|
|
349
|
+
if (block !== undefined)
|
|
350
|
+
manifest.live2d = block;
|
|
351
|
+
if (source.sprite2d !== undefined)
|
|
352
|
+
diag.error('renderer live2d must not declare a sprite2d block');
|
|
353
|
+
}
|
|
354
|
+
const sequences = parseSequences(source.sequences, diag);
|
|
355
|
+
if (sequences !== undefined)
|
|
356
|
+
manifest.sequences = sequences;
|
|
357
|
+
if (source.remarks !== undefined && !isRecord(source.remarks))
|
|
358
|
+
diag.error('remarks must be an object of remark pools');
|
|
359
|
+
else if (source.remarks !== undefined)
|
|
360
|
+
manifest.remarks = source.remarks;
|
|
361
|
+
return diag.hasErrors ? undefined : manifest;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Parse one pet manifest: v1 (no petManifestVersion) is compat-read as a
|
|
365
|
+
* sprite2d pet with a migration hint; v2 is validated fail-closed. The parse
|
|
366
|
+
* never throws — every failure comes back as structured diagnostics.
|
|
367
|
+
* @param raw - the parsed pet.json value.
|
|
368
|
+
* @param sourceLabel - human-readable origin for diagnostics (dir or file).
|
|
369
|
+
*/
|
|
370
|
+
export function parsePetManifest(raw, sourceLabel) {
|
|
371
|
+
const diag = new Diagnostics(sourceLabel);
|
|
372
|
+
if (!isRecord(raw)) {
|
|
373
|
+
diag.error('manifest is not an object');
|
|
374
|
+
return { ok: false, diagnostics: diag.list };
|
|
375
|
+
}
|
|
376
|
+
if (raw.petManifestVersion === undefined) {
|
|
377
|
+
const manifest = compatV1(raw, diag);
|
|
378
|
+
if (manifest === undefined)
|
|
379
|
+
return { ok: false, diagnostics: diag.list };
|
|
380
|
+
diag.warn('v1 compat read: manifest treated as renderer "sprite2d"; run scripts/dsh-pet-migrate-v2 to migrate');
|
|
381
|
+
return { ok: true, manifest, migrated: 'v1-compat', diagnostics: diag.list };
|
|
382
|
+
}
|
|
383
|
+
const manifest = parseV2(raw, diag);
|
|
384
|
+
if (manifest === undefined)
|
|
385
|
+
return { ok: false, diagnostics: diag.list };
|
|
386
|
+
return { ok: true, manifest, migrated: undefined, diagnostics: diag.list };
|
|
387
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live2D .model3.json reference closure — the set of files a model declares
|
|
3
|
+
* (pet-center M2, issue #623). The host asset route only ever serves a pet's
|
|
4
|
+
* declared manifest, its declared primary assets, and this closure; the CLI
|
|
5
|
+
* validator reuses the same extractor so an install-time check proves the
|
|
6
|
+
* serving set is complete.
|
|
7
|
+
*
|
|
8
|
+
* Cubism file family: Moc (.moc3), Textures (images), Physics (.physics3.json),
|
|
9
|
+
* Pose (.pose3.json), DisplayInfo (.cdi3.json), Expressions[].File
|
|
10
|
+
* (.exp3.json), Motions.<group>[].File (.motion3.json), UserData
|
|
11
|
+
* (.userdata3.json). Every reference must be a safe manifest-relative path
|
|
12
|
+
* (safeManifestPath); unsafe entries make the model unloadable.
|
|
13
|
+
*
|
|
14
|
+
* Erasable-syntax-only: scripts/ import this under node strip-only mode.
|
|
15
|
+
* @module @linxin666/dsh-pet/model3
|
|
16
|
+
*/
|
|
17
|
+
/** Collect the safe relative paths one model3.json references. */
|
|
18
|
+
export declare function collectModel3References(model3: unknown): {
|
|
19
|
+
references: string[];
|
|
20
|
+
errors: string[];
|
|
21
|
+
};
|
|
22
|
+
/** The motion group names a model3.json declares (for CLI diagnostics). */
|
|
23
|
+
export declare function model3MotionGroups(model3: unknown): string[];
|
|
24
|
+
/** The hit area names a model3.json declares (top-level HitAreas). */
|
|
25
|
+
export declare function model3HitAreas(model3: unknown): string[];
|
|
26
|
+
//# sourceMappingURL=model3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model3.d.ts","sourceRoot":"","sources":["../../src/model3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,kEAAkE;AAClE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAiDnG;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAO5D;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAQxD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live2D .model3.json reference closure — the set of files a model declares
|
|
3
|
+
* (pet-center M2, issue #623). The host asset route only ever serves a pet's
|
|
4
|
+
* declared manifest, its declared primary assets, and this closure; the CLI
|
|
5
|
+
* validator reuses the same extractor so an install-time check proves the
|
|
6
|
+
* serving set is complete.
|
|
7
|
+
*
|
|
8
|
+
* Cubism file family: Moc (.moc3), Textures (images), Physics (.physics3.json),
|
|
9
|
+
* Pose (.pose3.json), DisplayInfo (.cdi3.json), Expressions[].File
|
|
10
|
+
* (.exp3.json), Motions.<group>[].File (.motion3.json), UserData
|
|
11
|
+
* (.userdata3.json). Every reference must be a safe manifest-relative path
|
|
12
|
+
* (safeManifestPath); unsafe entries make the model unloadable.
|
|
13
|
+
*
|
|
14
|
+
* Erasable-syntax-only: scripts/ import this under node strip-only mode.
|
|
15
|
+
* @module @linxin666/dsh-pet/model3
|
|
16
|
+
*/
|
|
17
|
+
import { safeManifestPath } from './manifest-v2.js';
|
|
18
|
+
/** Collect the safe relative paths one model3.json references. */
|
|
19
|
+
export function collectModel3References(model3) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
if (typeof model3 !== 'object' || model3 === null) {
|
|
22
|
+
return { references: [], errors: ['model3.json is not an object'] };
|
|
23
|
+
}
|
|
24
|
+
const fileReferences = model3.FileReferences;
|
|
25
|
+
if (typeof fileReferences !== 'object' || fileReferences === null) {
|
|
26
|
+
return { references: [], errors: ['model3.json has no FileReferences'] };
|
|
27
|
+
}
|
|
28
|
+
const refs = fileReferences;
|
|
29
|
+
const collected = new Set();
|
|
30
|
+
const push = (raw, field) => {
|
|
31
|
+
const safe = safeManifestPath(raw);
|
|
32
|
+
if (safe === undefined) {
|
|
33
|
+
errors.push(field + ' is not a safe relative path: ' + JSON.stringify(String(raw)));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
collected.add(safe);
|
|
37
|
+
};
|
|
38
|
+
if (refs.Moc !== undefined)
|
|
39
|
+
push(refs.Moc, 'FileReferences.Moc');
|
|
40
|
+
if (Array.isArray(refs.Textures)) {
|
|
41
|
+
refs.Textures.forEach((texture, index) => push(texture, 'FileReferences.Textures[' + index + ']'));
|
|
42
|
+
}
|
|
43
|
+
for (const scalar of ['Physics', 'Pose', 'DisplayInfo', 'UserData']) {
|
|
44
|
+
if (refs[scalar] !== undefined)
|
|
45
|
+
push(refs[scalar], 'FileReferences.' + scalar);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(refs.Expressions)) {
|
|
48
|
+
refs.Expressions.forEach((expression, index) => {
|
|
49
|
+
const file = typeof expression === 'object' && expression !== null
|
|
50
|
+
? expression.File
|
|
51
|
+
: undefined;
|
|
52
|
+
if (file !== undefined)
|
|
53
|
+
push(file, 'FileReferences.Expressions[' + index + '].File');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (typeof refs.Motions === 'object' && refs.Motions !== null) {
|
|
57
|
+
for (const [group, motions] of Object.entries(refs.Motions)) {
|
|
58
|
+
if (!Array.isArray(motions)) {
|
|
59
|
+
errors.push('FileReferences.Motions.' + group + ' is not an array');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
motions.forEach((motion, index) => {
|
|
63
|
+
const file = typeof motion === 'object' && motion !== null
|
|
64
|
+
? motion.File
|
|
65
|
+
: undefined;
|
|
66
|
+
if (file !== undefined)
|
|
67
|
+
push(file, 'FileReferences.Motions.' + group + '[' + index + '].File');
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { references: [...collected].sort(), errors };
|
|
72
|
+
}
|
|
73
|
+
/** The motion group names a model3.json declares (for CLI diagnostics). */
|
|
74
|
+
export function model3MotionGroups(model3) {
|
|
75
|
+
if (typeof model3 !== 'object' || model3 === null)
|
|
76
|
+
return [];
|
|
77
|
+
const fileReferences = model3.FileReferences;
|
|
78
|
+
if (typeof fileReferences !== 'object' || fileReferences === null)
|
|
79
|
+
return [];
|
|
80
|
+
const motions = fileReferences.Motions;
|
|
81
|
+
if (typeof motions !== 'object' || motions === null)
|
|
82
|
+
return [];
|
|
83
|
+
return Object.keys(motions).sort();
|
|
84
|
+
}
|
|
85
|
+
/** The hit area names a model3.json declares (top-level HitAreas). */
|
|
86
|
+
export function model3HitAreas(model3) {
|
|
87
|
+
if (typeof model3 !== 'object' || model3 === null)
|
|
88
|
+
return [];
|
|
89
|
+
const hitAreas = model3.HitAreas;
|
|
90
|
+
if (!Array.isArray(hitAreas))
|
|
91
|
+
return [];
|
|
92
|
+
return hitAreas
|
|
93
|
+
.map(area => typeof area === 'object' && area !== null ? area.Name : undefined)
|
|
94
|
+
.filter((name) => typeof name === 'string')
|
|
95
|
+
.sort();
|
|
96
|
+
}
|