@linxin666/dsh-pet 0.2.2 → 0.2.4
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 +129 -3
- package/README.zh.md +129 -3
- package/assets/decorations/whale/decoration.json +20 -0
- package/assets/decorations/whale/whale-frames.png +0 -0
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/contracts/pet-manifest-v2.schema.json +281 -0
- package/contracts/status-decoration-v1.schema.json +144 -0
- package/contracts/voice-pack-v1.schema.json +234 -0
- package/lib/client.js +581 -38
- package/lib/client.js.map +1 -1
- package/lib/index.js +1684 -59
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- package/lib/types/chatter.d.ts +61 -3
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +71 -12
- 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 +14 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +24 -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 +120 -12
- 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 +14 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +14 -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 +192 -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/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- 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/contracts/status-decoration.d.ts +85 -0
- package/lib/types/contracts/status-decoration.d.ts.map +1 -0
- package/lib/types/contracts/status-decoration.js +21 -0
- package/lib/types/decoration.d.ts +39 -0
- package/lib/types/decoration.d.ts.map +1 -0
- package/lib/types/decoration.js +210 -0
- package/lib/types/event-projection.d.ts +8 -3
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +9 -4
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -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 +112 -5
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +395 -38
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +312 -13
- package/lib/types/service.d.ts +38 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +46 -3
- package/lib/types/voice-pack.d.ts +98 -0
- package/lib/types/voice-pack.d.ts.map +1 -0
- package/lib/types/voice-pack.js +384 -0
- package/package.json +13 -10
- package/src/chatter.test.ts +89 -2
- package/src/chatter.ts +120 -14
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +57 -0
- package/src/client/PetSprite.test.tsx +265 -12
- package/src/client/PetSprite.tsx +165 -33
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +14 -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 +263 -0
- package/src/client/renderers/live2d.ts +231 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-form.ts +8 -6
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -0
- package/src/contracts/status-decoration.ts +78 -0
- package/src/decoration.test.ts +178 -0
- package/src/decoration.ts +220 -0
- package/src/event-projection.ts +10 -5
- package/src/index.ts +2 -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 +438 -2
- package/src/registry.ts +470 -38
- package/src/routes.ts +328 -14
- package/src/service.ts +65 -3
- package/src/voice-pack.test.ts +216 -0
- package/src/voice-pack.ts +413 -0
package/src/registry.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet registry — the multi-pet contract. One pet is a directory holding a
|
|
3
3
|
* 'pet.json' manifest plus an atlas image; nothing else is required, and no
|
|
4
|
-
* host or client code changes when a pet is added. The registry scans
|
|
4
|
+
* host or client code changes when a pet is added. The registry scans four
|
|
5
5
|
* sources, later sources overriding earlier ones on an id collision:
|
|
6
6
|
*
|
|
7
7
|
* 1. the package's own 'assets' subdirectories (built-in pets);
|
|
8
|
-
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets
|
|
9
|
-
*
|
|
8
|
+
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets,
|
|
9
|
+
* legacy source kept readable);
|
|
10
|
+
* 3. '$DSH_HOME/pets' subdirectories (the pet-center user directory);
|
|
11
|
+
* 4. 'PetConfig.pets' manifests composed by the embedding application
|
|
10
12
|
* (highest precedence).
|
|
11
13
|
*
|
|
14
|
+
* Manifests are parsed through manifest-v2 (pet-center M2, issue #623): v1
|
|
15
|
+
* manifests are compat-read as sprite2d, v2 manifests validate fail-closed,
|
|
16
|
+
* and structured diagnostics ride alongside the legacy warnings. Live2d
|
|
17
|
+
* entries (pet-center M3) list like any other pet: the entry carries the
|
|
18
|
+
* validated live2d block plus the model's reference closure (the servable
|
|
19
|
+
* set the asset route allows), and a model3.json that is unreadable or
|
|
20
|
+
* declares unsafe references rejects the entry with an error diagnostic.
|
|
21
|
+
*
|
|
12
22
|
* The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
|
|
13
23
|
* 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
|
|
14
24
|
* that only carry 'frames' keep working: geometry, per-row frame counts and
|
|
@@ -17,12 +27,18 @@
|
|
|
17
27
|
* @module @linxin666/dsh-pet/registry
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
30
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
21
31
|
import { homedir } from 'node:os'
|
|
22
32
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
23
33
|
import { fileURLToPath } from 'node:url'
|
|
24
34
|
import type { ActivityPhase, PetAnimation } from './state.ts'
|
|
25
35
|
import { normalizePetRemarks, type PetRemarks, type PetRemarksManifest } from './remarks.ts'
|
|
36
|
+
import { mergeVoicePacks, normalizeVoicePack, type PetPanelView, type VoicePack } from './voice-pack.ts'
|
|
37
|
+
import { parseDecorationManifest } from './decoration.ts'
|
|
38
|
+
import { PET_DECORATION_API_VERSION, type DecorationView } from './contracts/status-decoration.ts'
|
|
39
|
+
import { dshHome } from './dsh-home.ts'
|
|
40
|
+
import { parsePetManifest, type PetManifestLive2d, type PetManifestV2, type PetRendererKind } from './manifest-v2.ts'
|
|
41
|
+
import { collectModel3References } from './model3.ts'
|
|
26
42
|
|
|
27
43
|
/** Fixed row order of the 9-state animation contract. */
|
|
28
44
|
export const PET_ROW_ORDER: readonly PetAnimation[] = [
|
|
@@ -156,11 +172,33 @@ export interface PetTrackOverride {
|
|
|
156
172
|
fallback?: PetAnimation
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
/** The live2d renderer block as served to the browser half (pet-center M3). */
|
|
176
|
+
export interface PetLive2dDefinition {
|
|
177
|
+
/** Browser URL of the .model3.json (served by the host asset route). */
|
|
178
|
+
modelUrl: string
|
|
179
|
+
/** Manifest-relative model path (host route allow-list key). */
|
|
180
|
+
modelPath: string
|
|
181
|
+
/** Scale multiplier over the canvas auto-fit, (0, 10]. */
|
|
182
|
+
scale?: number
|
|
183
|
+
/** Model offset in canvas px from the center-bottom anchor. */
|
|
184
|
+
translate?: { x?: number; y?: number }
|
|
185
|
+
/** ActivityPhase -> motion group; unmapped phases fall back to idle. */
|
|
186
|
+
motions: Partial<Record<ActivityPhase, string>> & { idle: string }
|
|
187
|
+
/** Optional ActivityPhase -> expression name layered over the motion. */
|
|
188
|
+
expressions?: Partial<Record<ActivityPhase, string>>
|
|
189
|
+
/** Hit area names triggering the tap motion; defaults to every model HitArea. */
|
|
190
|
+
hitAreas?: string[]
|
|
191
|
+
}
|
|
192
|
+
|
|
159
193
|
/** A normalized pet as served to the browser half. */
|
|
160
194
|
export interface PetDefinition {
|
|
161
195
|
id: string
|
|
162
196
|
displayName: string
|
|
163
197
|
description: string
|
|
198
|
+
/** The renderer this entry mounts with (pet-center M2). */
|
|
199
|
+
renderer: PetRendererKind
|
|
200
|
+
/** Live2d render block; present exactly when renderer is 'live2d' (M3). */
|
|
201
|
+
live2d?: PetLive2dDefinition
|
|
164
202
|
/** Atlas cell size in px. */
|
|
165
203
|
cell: PetCell
|
|
166
204
|
/** Columns per row. */
|
|
@@ -177,6 +215,8 @@ export interface PetDefinition {
|
|
|
177
215
|
atlasUrl: string
|
|
178
216
|
/** Browser URL of the manifest (served by the host asset route). */
|
|
179
217
|
manifestUrl: string
|
|
218
|
+
/** Hover-panel chrome overrides (voice.json 'panel'; pet-center M4). */
|
|
219
|
+
panel?: PetPanelView
|
|
180
220
|
}
|
|
181
221
|
|
|
182
222
|
/** A resolved pet plus its host-side file location. */
|
|
@@ -185,17 +225,64 @@ export interface PetEntry extends PetDefinition {
|
|
|
185
225
|
dir: string
|
|
186
226
|
/** Atlas path relative to 'dir' (declared by the manifest). */
|
|
187
227
|
spritesheetPath: string
|
|
228
|
+
/**
|
|
229
|
+
* Manifest-relative files the asset route may serve beyond pet.json and
|
|
230
|
+
* 'previews/*' (pet-center M3): the sprite2d atlas, or the live2d model
|
|
231
|
+
* plus its model3.json reference closure.
|
|
232
|
+
*/
|
|
233
|
+
servable: readonly string[]
|
|
188
234
|
/** Normalized per-pet remark pools (manifest 'remarks'), when declared. */
|
|
189
235
|
remarks?: PetRemarks
|
|
236
|
+
/**
|
|
237
|
+
* Normalized per-pet voice pack (the directory's voice.json; pet-center
|
|
238
|
+
* M4). Host-side only — the browser half receives its 'panel' slice.
|
|
239
|
+
*/
|
|
240
|
+
voice?: VoicePack
|
|
190
241
|
}
|
|
191
242
|
|
|
192
243
|
/** Registry load result: resolved entries plus load warnings. */
|
|
193
244
|
export interface PetRegistry {
|
|
194
245
|
entries: PetEntry[]
|
|
195
246
|
warnings: string[]
|
|
247
|
+
/** Structured diagnostics from the manifest-v2 parse (superset detail of warnings). */
|
|
248
|
+
diagnostics: PetRegistryDiagnostic[]
|
|
196
249
|
byId(id: string): PetEntry | undefined
|
|
197
250
|
/** The pet an installation falls back to when the selection is unknown. */
|
|
198
251
|
defaultEntry(): PetEntry
|
|
252
|
+
/**
|
|
253
|
+
* The global voice override ('$DSH_HOME/pets/.voice.json'), when present —
|
|
254
|
+
* layers under every per-pet pack and over the built-in pools (M4, #677).
|
|
255
|
+
*/
|
|
256
|
+
globalVoice?: VoicePack
|
|
257
|
+
/**
|
|
258
|
+
* Status decorations (pet-center M5, #567): built-in 'assets/decorations'
|
|
259
|
+
* entries overridden by same-id user entries under
|
|
260
|
+
* '$DSH_HOME/pets/decorations'. Independent of the pet entries. Optional
|
|
261
|
+
* so prebuilt test registries without decorations keep compiling.
|
|
262
|
+
*/
|
|
263
|
+
decorations?: DecorationEntry[]
|
|
264
|
+
/** Look up one decoration by id. */
|
|
265
|
+
decorationById?(id: string): DecorationEntry | undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** One resolved status decoration plus its host-side file location. */
|
|
269
|
+
export interface DecorationEntry extends DecorationView {
|
|
270
|
+
/** Absolute directory holding the descriptor and strip. */
|
|
271
|
+
dir: string
|
|
272
|
+
/** Strip path relative to 'dir' (declared by the descriptor). */
|
|
273
|
+
entryPath: string
|
|
274
|
+
/** Descriptor-relative files the decoration asset route may serve. */
|
|
275
|
+
servable: readonly string[]
|
|
276
|
+
/** Asset license identifier (required by the descriptor). */
|
|
277
|
+
license: string
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** One structured registry diagnostic (manifest-v2 era). */
|
|
281
|
+
export interface PetRegistryDiagnostic {
|
|
282
|
+
level: 'error' | 'warning'
|
|
283
|
+
/** Where the diagnostic originates (directory or file). */
|
|
284
|
+
source: string
|
|
285
|
+
message: string
|
|
199
286
|
}
|
|
200
287
|
|
|
201
288
|
/** Registry sources. */
|
|
@@ -206,6 +293,8 @@ export interface PetRegistryOptions {
|
|
|
206
293
|
assetPrefix?: string
|
|
207
294
|
/** Custom pet directory (defaults to '${CODEX_HOME:-~/.codex}/pets'). */
|
|
208
295
|
petsDir?: string
|
|
296
|
+
/** Pet-center user directory (defaults to '$DSH_HOME/pets'; '' disables). */
|
|
297
|
+
dshPetsDir?: string
|
|
209
298
|
/** Extra manifest entries composed by the embedding application. */
|
|
210
299
|
extra?: readonly PetManifest[]
|
|
211
300
|
}
|
|
@@ -249,6 +338,47 @@ function normalizeSequences(
|
|
|
249
338
|
return Object.keys(sequences).length === 0 ? undefined : sequences
|
|
250
339
|
}
|
|
251
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Build the fully resolved animation tracks from the contract defaults plus
|
|
343
|
+
* optional per-track overrides. Shared by the sprite2d resolver and the
|
|
344
|
+
* live2d entry builder (which fills the sprite fields with contract
|
|
345
|
+
* defaults so the flat PetDefinition shape holds for every renderer).
|
|
346
|
+
*/
|
|
347
|
+
function buildTracks(
|
|
348
|
+
rows: readonly number[],
|
|
349
|
+
columns: number,
|
|
350
|
+
trackOverrides: Partial<Record<PetAnimation, PetTrackOverride>>,
|
|
351
|
+
warn: (message: string) => void,
|
|
352
|
+
): Record<PetAnimation, PetTrackDef> | undefined {
|
|
353
|
+
const tracks = {} as Record<PetAnimation, PetTrackDef>
|
|
354
|
+
for (const [row, animation] of PET_ROW_ORDER.entries()) {
|
|
355
|
+
const pattern = DEFAULT_TRACK_PATTERNS[animation]
|
|
356
|
+
const override = trackOverrides[animation]
|
|
357
|
+
const durations = Array.isArray(override?.durations) && override.durations.length > 0
|
|
358
|
+
? override.durations.filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
359
|
+
: pattern.durations
|
|
360
|
+
if (durations.length === 0) {
|
|
361
|
+
warn('track ' + animation + ' carries no usable durations')
|
|
362
|
+
return undefined
|
|
363
|
+
}
|
|
364
|
+
const frameCount = Math.max(1, Math.min(rows[row]!, columns))
|
|
365
|
+
const sized = durations.length >= frameCount
|
|
366
|
+
? durations.slice(0, frameCount)
|
|
367
|
+
: Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]!)
|
|
368
|
+
tracks[animation] = {
|
|
369
|
+
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
370
|
+
durations: sized,
|
|
371
|
+
loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
|
|
372
|
+
...(override?.fallback === undefined
|
|
373
|
+
? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
|
|
374
|
+
: PET_ROW_ORDER.includes(override.fallback)
|
|
375
|
+
? { fallback: override.fallback }
|
|
376
|
+
: pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return tracks
|
|
380
|
+
}
|
|
381
|
+
|
|
252
382
|
/**
|
|
253
383
|
* Normalize one parsed manifest into a renderable pet entry, or undefined
|
|
254
384
|
* (with a warning recorded) when the manifest violates the contract.
|
|
@@ -304,36 +434,14 @@ export function resolvePetManifest(
|
|
|
304
434
|
const remarks = normalizePetRemarks(source.remarks, message => warn('manifest ' + id + ': ' + message))
|
|
305
435
|
const sequences = normalizeSequences(source.sequences, id, warn)
|
|
306
436
|
const trackOverrides = (typeof source.tracks === 'object' && source.tracks !== null ? source.tracks : {}) as Partial<Record<PetAnimation, PetTrackOverride>>
|
|
307
|
-
const tracks =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const override = trackOverrides[animation]
|
|
311
|
-
const durations = Array.isArray(override?.durations) && override.durations.length > 0
|
|
312
|
-
? override.durations.filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
313
|
-
: pattern.durations
|
|
314
|
-
if (durations.length === 0) {
|
|
315
|
-
warn('manifest ' + id + ': track ' + animation + ' carries no usable durations')
|
|
316
|
-
return undefined
|
|
317
|
-
}
|
|
318
|
-
const frameCount = Math.max(1, Math.min(rows[row]!, columns))
|
|
319
|
-
const sized = durations.length >= frameCount
|
|
320
|
-
? durations.slice(0, frameCount)
|
|
321
|
-
: Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]!)
|
|
322
|
-
tracks[animation] = {
|
|
323
|
-
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
324
|
-
durations: sized,
|
|
325
|
-
loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
|
|
326
|
-
...(override?.fallback === undefined
|
|
327
|
-
? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
|
|
328
|
-
: PET_ROW_ORDER.includes(override.fallback)
|
|
329
|
-
? { fallback: override.fallback }
|
|
330
|
-
: pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
|
|
331
|
-
}
|
|
332
|
-
}
|
|
437
|
+
const tracks = buildTracks(rows, columns, trackOverrides, message => warn('manifest ' + id + ': ' + message))
|
|
438
|
+
if (tracks === undefined) return undefined
|
|
439
|
+
const sheet = spritesheetPath.join('/')
|
|
333
440
|
return {
|
|
334
441
|
id,
|
|
335
442
|
displayName,
|
|
336
443
|
description,
|
|
444
|
+
renderer: 'sprite2d' as const,
|
|
337
445
|
cell,
|
|
338
446
|
columns,
|
|
339
447
|
rows,
|
|
@@ -343,13 +451,121 @@ export function resolvePetManifest(
|
|
|
343
451
|
atlasUrl: assetUrl(assetPrefix, id, spritesheet),
|
|
344
452
|
manifestUrl: assetUrl(assetPrefix, id, 'pet.json'),
|
|
345
453
|
dir,
|
|
346
|
-
spritesheetPath:
|
|
454
|
+
spritesheetPath: sheet,
|
|
455
|
+
servable: [sheet],
|
|
456
|
+
...(remarks === undefined ? {} : { remarks }),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Adapt a validated v2 manifest's sprite2d block onto the legacy flat shape
|
|
462
|
+
* the established resolver consumes (pet-center M2 P2). The legacy resolver
|
|
463
|
+
* only expresses 9-row (default) and 11-row (spriteVersionNumber 2) atlases,
|
|
464
|
+
* so other atlasRows values are rejected here with a diagnostic.
|
|
465
|
+
*/
|
|
466
|
+
function flattenV2Sprite2d(manifest: PetManifestV2): Record<string, unknown> | undefined {
|
|
467
|
+
const block = manifest.sprite2d
|
|
468
|
+
if (block === undefined) return undefined
|
|
469
|
+
const legacy: Record<string, unknown> = {
|
|
470
|
+
id: manifest.id,
|
|
471
|
+
displayName: manifest.displayName,
|
|
472
|
+
spritesheetPath: block.spritesheetPath,
|
|
473
|
+
}
|
|
474
|
+
if (manifest.description !== undefined) legacy.description = manifest.description
|
|
475
|
+
if (block.cell !== undefined) legacy.cell = block.cell
|
|
476
|
+
if (block.columns !== undefined) legacy.columns = block.columns
|
|
477
|
+
if (block.frames !== undefined) legacy.frames = block.frames
|
|
478
|
+
if (block.tracks !== undefined) legacy.tracks = block.tracks
|
|
479
|
+
if (block.atlasRows !== undefined) {
|
|
480
|
+
if (block.atlasRows === 11) legacy.spriteVersionNumber = 2
|
|
481
|
+
else if (block.atlasRows !== DEFAULT_PET_ROW_COUNT) return undefined
|
|
482
|
+
}
|
|
483
|
+
if (manifest.sequences !== undefined) legacy.sequences = manifest.sequences
|
|
484
|
+
if (manifest.remarks !== undefined) legacy.remarks = manifest.remarks
|
|
485
|
+
return legacy
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a validated live2d manifest into a renderable entry (pet-center
|
|
490
|
+
* M3). The model3.json is read at scan time: its reference closure becomes
|
|
491
|
+
* the entry's servable set (the asset route's allow-list), and a model that
|
|
492
|
+
* is unreadable or declares unsafe references rejects the entry fail-closed
|
|
493
|
+
* with an error diagnostic. Closure files missing on disk warn but keep the
|
|
494
|
+
* entry listed — the client renderer's diagnostic card reports the broken
|
|
495
|
+
* render, matching the registry's never-throw philosophy (install-time
|
|
496
|
+
* strictness belongs to the CLI validator). The sprite fields carry contract
|
|
497
|
+
* defaults: the chrome sizes live2d pets off 'display.size', not the atlas.
|
|
498
|
+
*/
|
|
499
|
+
function resolveLive2dEntry(
|
|
500
|
+
manifest: PetManifestV2,
|
|
501
|
+
dir: string,
|
|
502
|
+
options: { assetPrefix?: string; warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] },
|
|
503
|
+
): PetEntry | undefined {
|
|
504
|
+
const assetPrefix = options.assetPrefix ?? '/pet'
|
|
505
|
+
const record = (level: 'error' | 'warning', message: string): void => {
|
|
506
|
+
options.diagnostics?.push({ level, source: dir, message })
|
|
507
|
+
options.warnings?.push(message)
|
|
508
|
+
}
|
|
509
|
+
const block = manifest.live2d as PetManifestLive2d | undefined
|
|
510
|
+
if (block === undefined) {
|
|
511
|
+
record('error', 'pet ' + manifest.id + ': renderer live2d requires a live2d block')
|
|
512
|
+
return undefined
|
|
513
|
+
}
|
|
514
|
+
let model3: unknown
|
|
515
|
+
try {
|
|
516
|
+
model3 = JSON.parse(readFileSync(join(dir, block.model), 'utf8'))
|
|
517
|
+
} catch (error) {
|
|
518
|
+
record('error', 'pet ' + manifest.id + ': live2d model ' + block.model + ' is not readable: '
|
|
519
|
+
+ (error instanceof Error ? error.message : String(error)))
|
|
520
|
+
return undefined
|
|
521
|
+
}
|
|
522
|
+
const { references, errors } = collectModel3References(model3)
|
|
523
|
+
if (errors.length > 0) {
|
|
524
|
+
for (const message of errors) {
|
|
525
|
+
record('error', 'pet ' + manifest.id + ': live2d model ' + block.model + ': ' + message)
|
|
526
|
+
}
|
|
527
|
+
return undefined
|
|
528
|
+
}
|
|
529
|
+
for (const reference of references) {
|
|
530
|
+
if (!existsSync(join(dir, reference))) {
|
|
531
|
+
record('warning', 'pet ' + manifest.id + ': live2d closure file missing: ' + reference)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const tracks = buildTracks(DEFAULT_FRAME_COUNTS, DEFAULT_PET_COLUMNS, {}, message => record('warning', 'pet ' + manifest.id + ': ' + message))
|
|
535
|
+
if (tracks === undefined) return undefined
|
|
536
|
+
const remarks = normalizePetRemarks(manifest.remarks, message => record('warning', 'pet ' + manifest.id + ': ' + message))
|
|
537
|
+
const modelUrl = assetUrl(assetPrefix, manifest.id, block.model)
|
|
538
|
+
const live2d: PetLive2dDefinition = {
|
|
539
|
+
modelUrl,
|
|
540
|
+
modelPath: block.model,
|
|
541
|
+
...(block.scale === undefined ? {} : { scale: block.scale }),
|
|
542
|
+
...(block.translate === undefined ? {} : { translate: block.translate }),
|
|
543
|
+
motions: block.motions,
|
|
544
|
+
...(block.expressions === undefined ? {} : { expressions: block.expressions }),
|
|
545
|
+
...(block.hitAreas === undefined ? {} : { hitAreas: block.hitAreas }),
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
id: manifest.id,
|
|
549
|
+
displayName: manifest.displayName,
|
|
550
|
+
description: manifest.description ?? '',
|
|
551
|
+
renderer: 'live2d' as const,
|
|
552
|
+
live2d,
|
|
553
|
+
cell: { ...DEFAULT_PET_CELL },
|
|
554
|
+
columns: DEFAULT_PET_COLUMNS,
|
|
555
|
+
rows: [...DEFAULT_FRAME_COUNTS],
|
|
556
|
+
atlasRows: DEFAULT_PET_ROW_COUNT,
|
|
557
|
+
tracks,
|
|
558
|
+
atlasUrl: modelUrl,
|
|
559
|
+
manifestUrl: assetUrl(assetPrefix, manifest.id, 'pet.json'),
|
|
560
|
+
dir,
|
|
561
|
+
spritesheetPath: block.model,
|
|
562
|
+
servable: [block.model, ...references],
|
|
347
563
|
...(remarks === undefined ? {} : { remarks }),
|
|
348
564
|
}
|
|
349
565
|
}
|
|
350
566
|
|
|
351
567
|
/** Scan one directory of pet folders; entries come back in name order. */
|
|
352
|
-
function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: string[] }): PetEntry[] {
|
|
568
|
+
function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] }): PetEntry[] {
|
|
353
569
|
if (!existsSync(dir)) return []
|
|
354
570
|
let names: string[] = []
|
|
355
571
|
try {
|
|
@@ -364,8 +580,30 @@ function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: str
|
|
|
364
580
|
if (!existsSync(manifestFile)) continue
|
|
365
581
|
const parsed = readPetJson(manifestFile, options.warnings)
|
|
366
582
|
if (parsed === undefined) continue
|
|
367
|
-
const
|
|
368
|
-
|
|
583
|
+
const entryDir = join(dir, name)
|
|
584
|
+
const verdict = parsePetManifest(parsed, entryDir)
|
|
585
|
+
for (const diagnostic of verdict.diagnostics) {
|
|
586
|
+
options.diagnostics?.push({ level: diagnostic.level, source: entryDir, message: diagnostic.message })
|
|
587
|
+
options.warnings?.push(diagnostic.message)
|
|
588
|
+
}
|
|
589
|
+
if (!verdict.ok) continue
|
|
590
|
+
let entry: PetEntry | undefined
|
|
591
|
+
if (verdict.manifest.renderer === 'live2d') {
|
|
592
|
+
entry = resolveLive2dEntry(verdict.manifest, entryDir, options)
|
|
593
|
+
} else {
|
|
594
|
+
const legacy = flattenV2Sprite2d(verdict.manifest)
|
|
595
|
+
if (legacy === undefined) {
|
|
596
|
+
const note = 'pet ' + verdict.manifest.id + ': sprite2d.atlasRows only supports 9 or 11 under the v1 compat resolver'
|
|
597
|
+
options.diagnostics?.push({ level: 'error', source: entryDir, message: note })
|
|
598
|
+
options.warnings?.push(note)
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
entry = resolvePetManifest(legacy, entryDir, options)
|
|
602
|
+
}
|
|
603
|
+
if (entry === undefined) continue
|
|
604
|
+
// Optional voice pack (voice.json) — pure content, warn-and-drop (M4).
|
|
605
|
+
const voice = loadVoicePackFile(join(entryDir, 'voice.json'), options)
|
|
606
|
+
entries.push({ ...entry, ...(voice === undefined ? {} : { voice }) })
|
|
369
607
|
}
|
|
370
608
|
return entries
|
|
371
609
|
}
|
|
@@ -380,6 +618,138 @@ function readPetJson(file: string, warnings: string[] | undefined): unknown {
|
|
|
380
618
|
}
|
|
381
619
|
}
|
|
382
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Scan-time read ceiling for user-authored JSON descriptors (voice.json,
|
|
623
|
+
* .voice.json, decoration.json): the registry reads these synchronously at
|
|
624
|
+
* plugin startup, and a pathological file — multi-GB, or a FIFO/device
|
|
625
|
+
* symlink — must not hang or exhaust the host before the warn-and-drop
|
|
626
|
+
* discipline can apply (review-spd follow-up, pet-center M4/M5).
|
|
627
|
+
*/
|
|
628
|
+
export const PET_SCAN_JSON_CAP = 64 * 1024
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Stat one scanned JSON descriptor with a regular-file + size guard, so a
|
|
632
|
+
* pathological user file is skipped with a warning instead of stalling or
|
|
633
|
+
* OOM-ing the host at startup. Returns the Stats, or undefined when the
|
|
634
|
+
* caller must skip the file (a warning was recorded).
|
|
635
|
+
*/
|
|
636
|
+
function guardedScannedJsonStat(
|
|
637
|
+
file: string,
|
|
638
|
+
options: { warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] },
|
|
639
|
+
what: string,
|
|
640
|
+
): ReturnType<typeof statSync> | undefined {
|
|
641
|
+
let st: ReturnType<typeof statSync>
|
|
642
|
+
try {
|
|
643
|
+
st = statSync(file)
|
|
644
|
+
} catch {
|
|
645
|
+
return undefined
|
|
646
|
+
}
|
|
647
|
+
const warn = (message: string): void => {
|
|
648
|
+
options.warnings?.push(file + ': ' + message)
|
|
649
|
+
options.diagnostics?.push({ level: 'warning', source: file, message: file + ': ' + message })
|
|
650
|
+
}
|
|
651
|
+
if (!st.isFile()) {
|
|
652
|
+
warn(what + ' is not a regular file; ignored')
|
|
653
|
+
return undefined
|
|
654
|
+
}
|
|
655
|
+
if (st.size > PET_SCAN_JSON_CAP) {
|
|
656
|
+
warn(what + ' exceeds the ' + PET_SCAN_JSON_CAP + '-byte scan ceiling; ignored')
|
|
657
|
+
return undefined
|
|
658
|
+
}
|
|
659
|
+
return st
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Load and normalize one optional voice.json (pet-center M4). A missing
|
|
664
|
+
* file is silent; a broken file warns and drops. The pack is pure content,
|
|
665
|
+
* so every issue stays a warning — a bad voice.json never rejects a pet.
|
|
666
|
+
*/
|
|
667
|
+
function loadVoicePackFile(
|
|
668
|
+
file: string,
|
|
669
|
+
options: { warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] },
|
|
670
|
+
): VoicePack | undefined {
|
|
671
|
+
if (!existsSync(file)) return undefined
|
|
672
|
+
if (guardedScannedJsonStat(file, options, 'voice pack') === undefined) return undefined
|
|
673
|
+
const warn = (message: string): void => {
|
|
674
|
+
options.warnings?.push(file + ': ' + message)
|
|
675
|
+
options.diagnostics?.push({ level: 'warning', source: file, message: file + ': ' + message })
|
|
676
|
+
}
|
|
677
|
+
let raw: unknown
|
|
678
|
+
try {
|
|
679
|
+
raw = JSON.parse(readFileSync(file, 'utf8'))
|
|
680
|
+
} catch (error) {
|
|
681
|
+
warn('voice pack is not valid JSON; ignored: ' + (error instanceof Error ? error.message : String(error)))
|
|
682
|
+
return undefined
|
|
683
|
+
}
|
|
684
|
+
return normalizeVoicePack(raw, warn)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Decoration asset URL prefix (served by the decoration route, M5). */
|
|
688
|
+
export const DECORATION_ASSET_PREFIX = '/api/pet/decoration'
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Scan one directory of decoration folders ('decoration.json' + strip).
|
|
692
|
+
* Later scans override earlier ones on id collision; a bad descriptor warns
|
|
693
|
+
* and skips — the never-throw philosophy holds for decorations too (M5).
|
|
694
|
+
*/
|
|
695
|
+
function scanDecorationDir(dir: string, options: { warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] }): DecorationEntry[] {
|
|
696
|
+
if (!existsSync(dir)) return []
|
|
697
|
+
let names: string[] = []
|
|
698
|
+
try {
|
|
699
|
+
names = readdirSync(dir).filter(name => !name.startsWith('.'))
|
|
700
|
+
} catch {
|
|
701
|
+
return []
|
|
702
|
+
}
|
|
703
|
+
names.sort()
|
|
704
|
+
const entries: DecorationEntry[] = []
|
|
705
|
+
for (const name of names) {
|
|
706
|
+
const entryDir = join(dir, name)
|
|
707
|
+
const manifestFile = join(entryDir, 'decoration.json')
|
|
708
|
+
if (!existsSync(manifestFile)) continue
|
|
709
|
+
if (guardedScannedJsonStat(manifestFile, options, 'decoration descriptor') === undefined) continue
|
|
710
|
+
let raw: unknown
|
|
711
|
+
try {
|
|
712
|
+
raw = JSON.parse(readFileSync(manifestFile, 'utf8'))
|
|
713
|
+
} catch (error) {
|
|
714
|
+
const message = 'skipping ' + manifestFile + ': ' + (error instanceof Error ? error.message : String(error))
|
|
715
|
+
options.warnings?.push(message)
|
|
716
|
+
options.diagnostics?.push({ level: 'error', source: entryDir, message })
|
|
717
|
+
continue
|
|
718
|
+
}
|
|
719
|
+
const verdict = parseDecorationManifest(raw, manifestFile)
|
|
720
|
+
for (const diagnostic of verdict.diagnostics) {
|
|
721
|
+
options.diagnostics?.push({ level: diagnostic.level, source: entryDir, message: diagnostic.message })
|
|
722
|
+
options.warnings?.push(diagnostic.message)
|
|
723
|
+
}
|
|
724
|
+
if (!verdict.ok) continue
|
|
725
|
+
const manifest = verdict.manifest
|
|
726
|
+
// A missing strip keeps the entry listed (mirroring the live2d closure
|
|
727
|
+
// discipline) but earns a diagnostic: the ornament will silently render
|
|
728
|
+
// nothing, and the warning names the file to fix.
|
|
729
|
+
if (!existsSync(join(entryDir, manifest.entry))) {
|
|
730
|
+
const message = 'decoration ' + manifest.id + ': strip file missing: ' + manifest.entry
|
|
731
|
+
options.warnings?.push(message)
|
|
732
|
+
options.diagnostics?.push({ level: 'warning', source: entryDir, message })
|
|
733
|
+
}
|
|
734
|
+
entries.push({
|
|
735
|
+
apiVersion: PET_DECORATION_API_VERSION,
|
|
736
|
+
id: manifest.id,
|
|
737
|
+
dir: entryDir,
|
|
738
|
+
entryPath: manifest.entry,
|
|
739
|
+
servable: ['decoration.json', manifest.entry],
|
|
740
|
+
license: manifest.license,
|
|
741
|
+
assetBase: DECORATION_ASSET_PREFIX + '/' + encodeURIComponent(manifest.id),
|
|
742
|
+
entryUrl: DECORATION_ASSET_PREFIX + '/' + encodeURIComponent(manifest.id) + '/' + manifest.entry,
|
|
743
|
+
cell: manifest.cell,
|
|
744
|
+
columns: manifest.columns,
|
|
745
|
+
durations: manifest.durations,
|
|
746
|
+
loop: manifest.loop,
|
|
747
|
+
phases: manifest.phases,
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
return entries
|
|
751
|
+
}
|
|
752
|
+
|
|
383
753
|
/**
|
|
384
754
|
* Load the pet registry: built-in 'assets/*' first, then the hatch-pet
|
|
385
755
|
* custom pets directory, then composed 'extra' manifests (each later source
|
|
@@ -389,10 +759,11 @@ function readPetJson(file: string, warnings: string[] | undefined): unknown {
|
|
|
389
759
|
export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
390
760
|
const { packageRoot, assetPrefix = '/pet' } = options
|
|
391
761
|
const warnings: string[] = []
|
|
762
|
+
const diagnostics: PetRegistryDiagnostic[] = []
|
|
392
763
|
const byId = new Map<string, PetEntry>()
|
|
393
764
|
const builtinIds = new Set<string>()
|
|
394
765
|
|
|
395
|
-
for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings })) {
|
|
766
|
+
for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings, diagnostics })) {
|
|
396
767
|
if (byId.has(entry.id)) {
|
|
397
768
|
warnings.push('duplicate built-in pet id ' + entry.id + '; the first one wins')
|
|
398
769
|
continue
|
|
@@ -403,12 +774,24 @@ export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
|
403
774
|
|
|
404
775
|
const petsDir = options.petsDir ?? codexPetsDir()
|
|
405
776
|
if (petsDir !== '') {
|
|
406
|
-
for (const entry of scanPetDir(petsDir, { assetPrefix, warnings })) {
|
|
777
|
+
for (const entry of scanPetDir(petsDir, { assetPrefix, warnings, diagnostics })) {
|
|
407
778
|
if (byId.has(entry.id)) warnings.push('custom pet ' + entry.id + ' overrides the built-in one')
|
|
408
779
|
byId.set(entry.id, entry)
|
|
409
780
|
}
|
|
410
781
|
}
|
|
411
782
|
|
|
783
|
+
// The pet-center user directory ranks above the legacy hatch-pet source.
|
|
784
|
+
const dshPetsDir = options.dshPetsDir ?? join(dshHome(), 'pets')
|
|
785
|
+
let globalVoice: VoicePack | undefined
|
|
786
|
+
if (dshPetsDir !== '') {
|
|
787
|
+
for (const entry of scanPetDir(dshPetsDir, { assetPrefix, warnings, diagnostics })) {
|
|
788
|
+
if (byId.has(entry.id)) warnings.push('user pet ' + entry.id + ' overrides an earlier registration')
|
|
789
|
+
byId.set(entry.id, entry)
|
|
790
|
+
}
|
|
791
|
+
// The global voice override layers under every per-pet pack (M4, #677).
|
|
792
|
+
globalVoice = loadVoicePackFile(join(dshPetsDir, '.voice.json'), { warnings, diagnostics })
|
|
793
|
+
}
|
|
794
|
+
|
|
412
795
|
for (const manifest of options.extra ?? []) {
|
|
413
796
|
const raw = manifest.spritesheetPath
|
|
414
797
|
const dir = raw === undefined || isAbsolute(raw)
|
|
@@ -427,21 +810,69 @@ export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
|
427
810
|
byId.set(entry.id, entry)
|
|
428
811
|
}
|
|
429
812
|
|
|
813
|
+
// Status decorations (pet-center M5, #567): built-in entries first,
|
|
814
|
+
// then same-id user entries under '$DSH_HOME/pets/decorations' override.
|
|
815
|
+
const decorationById = new Map<string, DecorationEntry>()
|
|
816
|
+
for (const entry of scanDecorationDir(join(packageRoot, 'assets', 'decorations'), { warnings, diagnostics })) {
|
|
817
|
+
decorationById.set(entry.id, entry)
|
|
818
|
+
}
|
|
819
|
+
if (dshPetsDir !== '') {
|
|
820
|
+
for (const entry of scanDecorationDir(join(dshPetsDir, 'decorations'), { warnings, diagnostics })) {
|
|
821
|
+
if (decorationById.has(entry.id)) {
|
|
822
|
+
warnings.push('user decoration ' + entry.id + ' overrides the built-in one')
|
|
823
|
+
}
|
|
824
|
+
decorationById.set(entry.id, entry)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
430
828
|
const entries = [...byId.values()]
|
|
829
|
+
const decorations = [...decorationById.values()]
|
|
431
830
|
return {
|
|
432
831
|
entries,
|
|
433
832
|
warnings,
|
|
833
|
+
diagnostics,
|
|
434
834
|
byId: (id: string) => byId.get(id),
|
|
435
835
|
defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0]!,
|
|
836
|
+
...(globalVoice === undefined ? {} : { globalVoice }),
|
|
837
|
+
decorations,
|
|
838
|
+
decorationById: (id: string) => decorationById.get(id),
|
|
436
839
|
}
|
|
437
840
|
}
|
|
438
841
|
|
|
439
|
-
/**
|
|
440
|
-
export
|
|
842
|
+
/** The built-in default decoration id (M5): the first reference ornament. */
|
|
843
|
+
export const DEFAULT_DECORATION_ID = 'whale'
|
|
844
|
+
|
|
845
|
+
/** Strip host-only fields, leaving the browser-visible decoration view. */
|
|
846
|
+
export function decorationView(entry: DecorationEntry): DecorationView {
|
|
847
|
+
return {
|
|
848
|
+
apiVersion: PET_DECORATION_API_VERSION,
|
|
849
|
+
id: entry.id,
|
|
850
|
+
assetBase: entry.assetBase,
|
|
851
|
+
entryUrl: entry.entryUrl,
|
|
852
|
+
cell: entry.cell,
|
|
853
|
+
columns: entry.columns,
|
|
854
|
+
durations: entry.durations,
|
|
855
|
+
loop: entry.loop,
|
|
856
|
+
phases: entry.phases,
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Strip host-only fields, leaving the client-visible definition. When the
|
|
862
|
+
* registry carries a global voice pack, its panel chrome layers under the
|
|
863
|
+
* entry's own pack (per-slot merge, pet > global), mirroring the voice-pool
|
|
864
|
+
* layering (pet-center M4, issue #677).
|
|
865
|
+
*/
|
|
866
|
+
export function petEntryView(entry: PetEntry, globalVoice?: VoicePack): PetDefinition {
|
|
867
|
+
const panel = globalVoice === undefined
|
|
868
|
+
? entry.voice?.panel
|
|
869
|
+
: mergeVoicePacks(globalVoice, entry.voice)?.panel
|
|
441
870
|
return {
|
|
442
871
|
id: entry.id,
|
|
443
872
|
displayName: entry.displayName,
|
|
444
873
|
description: entry.description,
|
|
874
|
+
renderer: entry.renderer,
|
|
875
|
+
...(entry.live2d === undefined ? {} : { live2d: entry.live2d }),
|
|
445
876
|
cell: entry.cell,
|
|
446
877
|
columns: entry.columns,
|
|
447
878
|
rows: entry.rows,
|
|
@@ -450,6 +881,7 @@ export function petEntryView(entry: PetEntry): PetDefinition {
|
|
|
450
881
|
...(entry.sequences === undefined ? {} : { sequences: entry.sequences }),
|
|
451
882
|
atlasUrl: entry.atlasUrl,
|
|
452
883
|
manifestUrl: entry.manifestUrl,
|
|
884
|
+
...(panel === undefined ? {} : { panel }),
|
|
453
885
|
}
|
|
454
886
|
}
|
|
455
887
|
|