@forgeax-extension/character-3d 0.1.2
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/SOURCE_PROVENANCE.md +8 -0
- package/dist/assets/index-CIx3aXo1.js +4336 -0
- package/dist/assets/index-CIx3aXo1.js.map +1 -0
- package/dist/assets/index-CmhOHLEB.css +1 -0
- package/dist/hdr/README.md +28 -0
- package/dist/hdr/presets.json +8 -0
- package/dist/index.html +13 -0
- package/forgeax-extension.json +428 -0
- package/marketplace-card.json +56 -0
- package/package.json +59 -0
- package/packages/external-asset-meta/package.json +24 -0
- package/packages/external-asset-meta/src/cook.test.ts +211 -0
- package/packages/external-asset-meta/src/cook.ts +188 -0
- package/packages/external-asset-meta/src/draco3dgltf.d.ts +11 -0
- package/packages/external-asset-meta/src/index.ts +18 -0
- package/packages/external-asset-meta/src/normalize.test.ts +36 -0
- package/packages/external-asset-meta/src/normalize.ts +101 -0
- package/packages/external-asset-meta/src/types.ts +82 -0
- package/schemas/adopt-playable-character.args.json +26 -0
- package/schemas/adopt-playable-character.returns.json +31 -0
- package/schemas/apply-motion.args.json +47 -0
- package/schemas/apply-motion.returns.json +23 -0
- package/schemas/auto-rig.args.json +35 -0
- package/schemas/auto-rig.returns.json +23 -0
- package/schemas/delete-asset.args.json +18 -0
- package/schemas/delete-asset.returns.json +18 -0
- package/schemas/engine-import-status.args.json +18 -0
- package/schemas/engine-import-status.returns.json +50 -0
- package/schemas/export-playable-character.args.json +21 -0
- package/schemas/export-playable-character.returns.json +47 -0
- package/schemas/gen3d-asset-manifest.json +281 -0
- package/schemas/generate-meshy-text-mock.args.json +42 -0
- package/schemas/generate-meshy-text-mock.returns.json +301 -0
- package/schemas/get-credentials.args.json +7 -0
- package/schemas/get-credentials.returns.json +45 -0
- package/schemas/get-playable-profile.args.json +11 -0
- package/schemas/get-playable-profile.returns.json +188 -0
- package/schemas/image-to-3d.args.json +47 -0
- package/schemas/image-to-3d.returns.json +306 -0
- package/schemas/import-to-engine.args.json +18 -0
- package/schemas/import-to-engine.returns.json +35 -0
- package/schemas/list-assets.args.json +22 -0
- package/schemas/list-assets.returns.json +296 -0
- package/schemas/list-motions.args.json +29 -0
- package/schemas/list-motions.returns.json +41 -0
- package/schemas/pose-standardization.args.json +28 -0
- package/schemas/pose-standardization.returns.json +29 -0
- package/schemas/provider-status.args.json +7 -0
- package/schemas/provider-status.returns.json +70 -0
- package/schemas/refine-mesh.args.json +33 -0
- package/schemas/refine-mesh.returns.json +306 -0
- package/schemas/rename-asset.args.json +22 -0
- package/schemas/rename-asset.returns.json +14 -0
- package/schemas/retopo-lowpoly.args.json +34 -0
- package/schemas/retopo-lowpoly.returns.json +24 -0
- package/schemas/score-quality.args.json +36 -0
- package/schemas/score-quality.returns.json +12 -0
- package/schemas/set-credentials.args.json +32 -0
- package/schemas/set-credentials.returns.json +45 -0
- package/schemas/set-playable-motion-mapping.args.json +25 -0
- package/schemas/set-playable-motion-mapping.returns.json +33 -0
- package/schemas/set-playable-profile.args.json +32 -0
- package/schemas/set-playable-profile.returns.json +67 -0
- package/schemas/text-to-3d.args.json +51 -0
- package/schemas/text-to-3d.returns.json +306 -0
- package/schemas/upload-image.args.json +19 -0
- package/schemas/upload-image.returns.json +21 -0
- package/schemas/upload-video.args.json +14 -0
- package/schemas/upload-video.returns.json +14 -0
- package/schemas/views-to-3d.args.json +58 -0
- package/schemas/views-to-3d.returns.json +306 -0
- package/server/adopt-playable-character.ts +291 -0
- package/server/asset-storage.ts +136 -0
- package/server/asset-upload.ts +123 -0
- package/server/audit.ts +58 -0
- package/server/cache.ts +75 -0
- package/server/cos-uploader.ts +141 -0
- package/server/credentials-store.ts +186 -0
- package/server/engine-import.ts +238 -0
- package/server/env.ts +287 -0
- package/server/export-playable-character.ts +364 -0
- package/server/generate.ts +89 -0
- package/server/merge-playable-character.ts +259 -0
- package/server/motion-catalog.ts +137 -0
- package/server/per-game-store.ts +1016 -0
- package/server/providers/gateway-client.ts +171 -0
- package/server/providers/gateway-data.ts +182 -0
- package/server/providers/gateway-models.ts +49 -0
- package/server/providers/hunyuan-rest.ts +317 -0
- package/server/providers/hunyuan-workflow.ts +225 -0
- package/server/providers/meshy-direct-client.ts +223 -0
- package/server/providers/meshy.ts +548 -0
- package/server/providers/rodin-gateway.ts +193 -0
- package/server/providers/rodin.ts +253 -0
- package/server/providers/visvise.ts +329 -0
- package/server/rate-guard.ts +29 -0
- package/server/tool-handlers.ts +2193 -0
- package/shared/catalog.ts +307 -0
- package/shared/manifest.ts +378 -0
- package/shared/meshy-actions.ts +690 -0
- package/shared/playable-preview-url.test.ts +14 -0
- package/shared/playable-preview-url.ts +13 -0
- package/shared/playable-profile.test.ts +127 -0
- package/shared/playable-profile.ts +171 -0
- package/shared/provider-params.test.ts +172 -0
- package/shared/provider-params.ts +389 -0
- package/shared/quality/heuristics.test.ts +84 -0
- package/shared/quality/heuristics.ts +100 -0
- package/shared/rodin-image.test.ts +51 -0
- package/shared/rodin-image.ts +125 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "gen3d:views-to-3d returns",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"ok": {
|
|
7
|
+
"const": true
|
|
8
|
+
},
|
|
9
|
+
"cacheKey": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"cacheHit": {
|
|
13
|
+
"type": "boolean"
|
|
14
|
+
},
|
|
15
|
+
"usedMock": {
|
|
16
|
+
"type": "boolean",
|
|
17
|
+
"description": "True when the real provider was not configured and a deterministic mock was used (quota-safe fallback)."
|
|
18
|
+
},
|
|
19
|
+
"manifest": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"manifestVersion": {
|
|
23
|
+
"const": 1
|
|
24
|
+
},
|
|
25
|
+
"assetId": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"kind": {
|
|
29
|
+
"enum": [
|
|
30
|
+
"mesh",
|
|
31
|
+
"animation"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"provider": {
|
|
35
|
+
"enum": [
|
|
36
|
+
"meshy",
|
|
37
|
+
"hunyuan_workflow",
|
|
38
|
+
"hunyuan_rest",
|
|
39
|
+
"rodin",
|
|
40
|
+
"visvise"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"providerMode": {
|
|
44
|
+
"enum": [
|
|
45
|
+
"mock",
|
|
46
|
+
"real"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"mode": {
|
|
50
|
+
"enum": [
|
|
51
|
+
"text",
|
|
52
|
+
"image",
|
|
53
|
+
"views",
|
|
54
|
+
"refine"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"sourceJobId": {
|
|
58
|
+
"type": [
|
|
59
|
+
"string",
|
|
60
|
+
"null"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"sourceInputAssetIds": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"prompt": {
|
|
70
|
+
"type": [
|
|
71
|
+
"string",
|
|
72
|
+
"null"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"files": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"fileId": {
|
|
81
|
+
"type": "string"
|
|
82
|
+
},
|
|
83
|
+
"role": {
|
|
84
|
+
"enum": [
|
|
85
|
+
"source_mesh",
|
|
86
|
+
"rigged_model",
|
|
87
|
+
"preview_image",
|
|
88
|
+
"texture",
|
|
89
|
+
"animation_clip",
|
|
90
|
+
"animated_model"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"format": {
|
|
94
|
+
"enum": [
|
|
95
|
+
"glb",
|
|
96
|
+
"fbx",
|
|
97
|
+
"obj",
|
|
98
|
+
"mtl",
|
|
99
|
+
"usdz",
|
|
100
|
+
"stl",
|
|
101
|
+
"png",
|
|
102
|
+
"jpg",
|
|
103
|
+
"mp4"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"storageKey": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
109
|
+
"bytes": {
|
|
110
|
+
"type": "integer",
|
|
111
|
+
"minimum": 0
|
|
112
|
+
},
|
|
113
|
+
"sha256": {
|
|
114
|
+
"type": "string"
|
|
115
|
+
},
|
|
116
|
+
"localUrl": {
|
|
117
|
+
"type": [
|
|
118
|
+
"string",
|
|
119
|
+
"null"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"hasSkeleton": {
|
|
123
|
+
"type": "boolean"
|
|
124
|
+
},
|
|
125
|
+
"skeletonProfile": {
|
|
126
|
+
"enum": [
|
|
127
|
+
"humanoid",
|
|
128
|
+
"unknown"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"animationInputReady": {
|
|
132
|
+
"type": "boolean"
|
|
133
|
+
},
|
|
134
|
+
"motionType": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"minimum": 9,
|
|
137
|
+
"maximum": 16,
|
|
138
|
+
"description": "Present only on role=animated_model files: the motion_retarget v1 motion this clip is (9 跨步 / 10 摔倒 / 11 跳跃 / 12 踢腿 / 13 挥击 / 14 步行 / 15 跑步 / 16 跳舞). Structural so multiple motions coexist per asset. Absent for non-animated roles."
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"required": [
|
|
142
|
+
"fileId",
|
|
143
|
+
"role",
|
|
144
|
+
"format",
|
|
145
|
+
"storageKey",
|
|
146
|
+
"bytes",
|
|
147
|
+
"sha256",
|
|
148
|
+
"localUrl",
|
|
149
|
+
"hasSkeleton",
|
|
150
|
+
"skeletonProfile",
|
|
151
|
+
"animationInputReady"
|
|
152
|
+
],
|
|
153
|
+
"additionalProperties": false
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"readiness": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"properties": {
|
|
159
|
+
"hasSourceMesh": {
|
|
160
|
+
"type": "boolean"
|
|
161
|
+
},
|
|
162
|
+
"rigged": {
|
|
163
|
+
"type": "boolean"
|
|
164
|
+
},
|
|
165
|
+
"animated": {
|
|
166
|
+
"type": "boolean"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"required": [
|
|
170
|
+
"hasSourceMesh",
|
|
171
|
+
"rigged",
|
|
172
|
+
"animated"
|
|
173
|
+
],
|
|
174
|
+
"additionalProperties": false
|
|
175
|
+
},
|
|
176
|
+
"quality": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"geometry": {
|
|
180
|
+
"type": [
|
|
181
|
+
"number",
|
|
182
|
+
"null"
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"topology": {
|
|
186
|
+
"type": [
|
|
187
|
+
"number",
|
|
188
|
+
"null"
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
"texture": {
|
|
192
|
+
"type": [
|
|
193
|
+
"number",
|
|
194
|
+
"null"
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
"pbr": {
|
|
198
|
+
"type": [
|
|
199
|
+
"number",
|
|
200
|
+
"null"
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
"prompt_fidelity": {
|
|
204
|
+
"type": [
|
|
205
|
+
"number",
|
|
206
|
+
"null"
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
"total": {
|
|
210
|
+
"type": [
|
|
211
|
+
"number",
|
|
212
|
+
"null"
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"required": [
|
|
217
|
+
"geometry",
|
|
218
|
+
"topology",
|
|
219
|
+
"texture",
|
|
220
|
+
"pbr",
|
|
221
|
+
"prompt_fidelity",
|
|
222
|
+
"total"
|
|
223
|
+
],
|
|
224
|
+
"additionalProperties": false
|
|
225
|
+
},
|
|
226
|
+
"createdAt": {
|
|
227
|
+
"type": "string"
|
|
228
|
+
},
|
|
229
|
+
"updatedAt": {
|
|
230
|
+
"type": "string"
|
|
231
|
+
},
|
|
232
|
+
"meshyTaskRefs": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"description": "Meshy preview vs result task ids. Manual refine uses previewTaskId; rig uses resultTaskId.",
|
|
235
|
+
"properties": {
|
|
236
|
+
"previewTaskId": {
|
|
237
|
+
"type": [
|
|
238
|
+
"string",
|
|
239
|
+
"null"
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
"resultTaskId": {
|
|
243
|
+
"type": [
|
|
244
|
+
"string",
|
|
245
|
+
"null"
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"required": [
|
|
250
|
+
"previewTaskId",
|
|
251
|
+
"resultTaskId"
|
|
252
|
+
],
|
|
253
|
+
"additionalProperties": false
|
|
254
|
+
},
|
|
255
|
+
"visviseRefs": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"description": "VISVISE COS URLs from generate/rig. Downstream rig/motion must reuse these; do not re-host on ForgeaX COS first.",
|
|
258
|
+
"properties": {
|
|
259
|
+
"meshUrl": {
|
|
260
|
+
"type": [
|
|
261
|
+
"string",
|
|
262
|
+
"null"
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
"rigUrl": {
|
|
266
|
+
"type": [
|
|
267
|
+
"string",
|
|
268
|
+
"null"
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"required": [
|
|
273
|
+
"meshUrl",
|
|
274
|
+
"rigUrl"
|
|
275
|
+
],
|
|
276
|
+
"additionalProperties": false
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"required": [
|
|
280
|
+
"manifestVersion",
|
|
281
|
+
"assetId",
|
|
282
|
+
"kind",
|
|
283
|
+
"provider",
|
|
284
|
+
"providerMode",
|
|
285
|
+
"mode",
|
|
286
|
+
"sourceJobId",
|
|
287
|
+
"sourceInputAssetIds",
|
|
288
|
+
"prompt",
|
|
289
|
+
"files",
|
|
290
|
+
"readiness",
|
|
291
|
+
"quality",
|
|
292
|
+
"createdAt",
|
|
293
|
+
"updatedAt"
|
|
294
|
+
],
|
|
295
|
+
"additionalProperties": false
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"required": [
|
|
299
|
+
"ok",
|
|
300
|
+
"cacheKey",
|
|
301
|
+
"cacheHit",
|
|
302
|
+
"usedMock",
|
|
303
|
+
"manifest"
|
|
304
|
+
],
|
|
305
|
+
"additionalProperties": false
|
|
306
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// ADOPT1 — take over an existing hand-made merged.glb + engine meta that is not
|
|
2
|
+
// yet bound to this gen3d source asset (PLAN §4.4 / §5.3).
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import type { ExternalAssetMeta } from '@forgeax-extension/external-asset-meta';
|
|
6
|
+
import {
|
|
7
|
+
effectiveSlots,
|
|
8
|
+
gameProfileFromPreset,
|
|
9
|
+
type GameMotionProfile,
|
|
10
|
+
type RootMotionStrategy,
|
|
11
|
+
} from '../shared/playable-profile';
|
|
12
|
+
import type { PlayableDeliverySnapshot } from '../shared/manifest';
|
|
13
|
+
import type { PerGameAssetStore } from './per-game-store';
|
|
14
|
+
import {
|
|
15
|
+
mappingFingerprint,
|
|
16
|
+
type PlayableClipDelivery,
|
|
17
|
+
type PlayableDeliveryJson,
|
|
18
|
+
} from './export-playable-character';
|
|
19
|
+
import { playableDeliveryLocalUrl } from '../shared/playable-preview-url';
|
|
20
|
+
|
|
21
|
+
export interface AdoptClipInfo {
|
|
22
|
+
name: string;
|
|
23
|
+
guid: string;
|
|
24
|
+
sourceIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AdoptCandidate {
|
|
28
|
+
modelPath: string;
|
|
29
|
+
metaPath: string;
|
|
30
|
+
playablePath: string;
|
|
31
|
+
localUrl: string;
|
|
32
|
+
clips: AdoptClipInfo[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type AdoptPlayableResult =
|
|
36
|
+
| {
|
|
37
|
+
ok: true;
|
|
38
|
+
modelPath: string;
|
|
39
|
+
playablePath: string;
|
|
40
|
+
localUrl: string;
|
|
41
|
+
reusedGuidCount: number;
|
|
42
|
+
clipCount: number;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
ok: false;
|
|
47
|
+
code: string;
|
|
48
|
+
message: string;
|
|
49
|
+
retryable: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface AdoptSlotMapping {
|
|
53
|
+
slotId: string;
|
|
54
|
+
/** Prefer matching by clip name (animation-clip.name in engine meta). */
|
|
55
|
+
clipName?: string | null;
|
|
56
|
+
/** Fallback: engine meta sourceIndex. */
|
|
57
|
+
sourceIndex?: number | null;
|
|
58
|
+
guid?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function exists(path: string): Promise<boolean> {
|
|
62
|
+
try {
|
|
63
|
+
await access(path);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stemFromAssetPath(assetPath: string): string {
|
|
71
|
+
const base = assetPath.split('/').pop() ?? 'character.glb';
|
|
72
|
+
return base.replace(/\.glb$/i, '') || 'character';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function localUrlFor(slug: string, modelRel: string): string {
|
|
76
|
+
return playableDeliveryLocalUrl(slug, modelRel);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function inspectAdoptCandidate(
|
|
80
|
+
store: PerGameAssetStore,
|
|
81
|
+
slug: string,
|
|
82
|
+
assetPath: string,
|
|
83
|
+
): Promise<AdoptCandidate | null> {
|
|
84
|
+
const state = await store.getCharacterPlayableState(slug, assetPath);
|
|
85
|
+
if (state?.delivery) return null;
|
|
86
|
+
|
|
87
|
+
const stem = stemFromAssetPath(assetPath);
|
|
88
|
+
const modelRel = `assets/characters/${stem}-merged.glb`;
|
|
89
|
+
const metaRel = `${modelRel}.meta.json`;
|
|
90
|
+
const playableRel = `${modelRel}.playable.json`;
|
|
91
|
+
const glbAbs = store.resolveGameRelPath(slug, modelRel);
|
|
92
|
+
const metaAbs = store.resolveGameRelPath(slug, metaRel);
|
|
93
|
+
if (!(await exists(glbAbs)) || !(await exists(metaAbs))) return null;
|
|
94
|
+
|
|
95
|
+
let meta: ExternalAssetMeta;
|
|
96
|
+
try {
|
|
97
|
+
meta = JSON.parse(await readFile(metaAbs, 'utf8')) as ExternalAssetMeta;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (meta.kind !== 'external-asset-package') return null;
|
|
102
|
+
|
|
103
|
+
const clips = meta.subAssets
|
|
104
|
+
.filter((s) => s.kind === 'animation-clip')
|
|
105
|
+
.map((s) => ({
|
|
106
|
+
name: s.name?.trim() || `clip_${s.sourceIndex}`,
|
|
107
|
+
guid: s.guid,
|
|
108
|
+
sourceIndex: s.sourceIndex,
|
|
109
|
+
}))
|
|
110
|
+
.sort((a, b) => a.sourceIndex - b.sourceIndex);
|
|
111
|
+
if (clips.length === 0) return null;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
modelPath: modelRel,
|
|
115
|
+
metaPath: metaRel,
|
|
116
|
+
playablePath: playableRel,
|
|
117
|
+
localUrl: localUrlFor(slug, modelRel),
|
|
118
|
+
clips,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveClip(
|
|
123
|
+
clips: AdoptClipInfo[],
|
|
124
|
+
mapping: AdoptSlotMapping,
|
|
125
|
+
): AdoptClipInfo | null {
|
|
126
|
+
if (mapping.guid) {
|
|
127
|
+
const byGuid = clips.find((c) => c.guid === mapping.guid);
|
|
128
|
+
if (byGuid) return byGuid;
|
|
129
|
+
}
|
|
130
|
+
if (mapping.clipName) {
|
|
131
|
+
const want = mapping.clipName.toLowerCase();
|
|
132
|
+
const byName = clips.find((c) => c.name.toLowerCase() === want);
|
|
133
|
+
if (byName) return byName;
|
|
134
|
+
}
|
|
135
|
+
if (typeof mapping.sourceIndex === 'number') {
|
|
136
|
+
return clips.find((c) => c.sourceIndex === mapping.sourceIndex) ?? null;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function adoptPlayableCharacter(
|
|
142
|
+
store: PerGameAssetStore,
|
|
143
|
+
args: {
|
|
144
|
+
slug: string;
|
|
145
|
+
assetPath: string;
|
|
146
|
+
slotMappings: AdoptSlotMapping[];
|
|
147
|
+
confirmed?: boolean;
|
|
148
|
+
},
|
|
149
|
+
): Promise<AdoptPlayableResult> {
|
|
150
|
+
const { slug, assetPath } = args;
|
|
151
|
+
if (!args.confirmed) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
code: 'not_confirmed',
|
|
155
|
+
message: 'Confirm slot → clip mappings before adopting (ADOPT1).',
|
|
156
|
+
retryable: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const manifest = await store.getAsset(slug, assetPath);
|
|
161
|
+
if (!manifest) {
|
|
162
|
+
return { ok: false, code: 'asset_not_found', message: 'Character asset not found.', retryable: false };
|
|
163
|
+
}
|
|
164
|
+
if (manifest.assetSlot !== 'characters') {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
code: 'not_a_character',
|
|
168
|
+
message: 'Adopt Playable Character is only for characters (ROLE1).',
|
|
169
|
+
retryable: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const candidate = await inspectAdoptCandidate(store, slug, assetPath);
|
|
174
|
+
if (!candidate) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
code: 'nothing_to_adopt',
|
|
178
|
+
message:
|
|
179
|
+
'No orphan merged.glb + engine meta found (or this source already has a delivery snapshot).',
|
|
180
|
+
retryable: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const gameProfileStored = await store.getGameMotionProfile(slug);
|
|
185
|
+
const gameProfile: GameMotionProfile =
|
|
186
|
+
gameProfileStored ?? gameProfileFromPreset('basic-character-v1', new Date(0).toISOString());
|
|
187
|
+
const state = await store.getCharacterPlayableState(slug, assetPath);
|
|
188
|
+
const override = state?.override ?? null;
|
|
189
|
+
const slots = effectiveSlots(gameProfile, override);
|
|
190
|
+
const required = slots.filter((s) => s.required);
|
|
191
|
+
if (required.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
code: 'no_required_slots',
|
|
195
|
+
message: 'Effective profile has no required slots — set a playable profile first.',
|
|
196
|
+
retryable: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const mappingBySlot = new Map(args.slotMappings.map((m) => [m.slotId, m]));
|
|
201
|
+
const clips: PlayableDeliveryJson['clips'] = {};
|
|
202
|
+
const nextRegistry: Record<string, string> = {};
|
|
203
|
+
let reusedGuidCount = 0;
|
|
204
|
+
const missing: string[] = [];
|
|
205
|
+
|
|
206
|
+
for (const slot of slots) {
|
|
207
|
+
const m = mappingBySlot.get(slot.slotId);
|
|
208
|
+
if (!m) {
|
|
209
|
+
if (slot.required) missing.push(slot.slotId);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const clip = resolveClip(candidate.clips, m);
|
|
213
|
+
if (!clip) {
|
|
214
|
+
if (slot.required) missing.push(slot.slotId);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
nextRegistry[slot.slotId] = clip.guid;
|
|
218
|
+
reusedGuidCount += 1;
|
|
219
|
+
const deliveryClip: PlayableClipDelivery = {
|
|
220
|
+
guid: clip.guid,
|
|
221
|
+
sourceIndex: clip.sourceIndex,
|
|
222
|
+
loop: slot.playbackMode === 'loop',
|
|
223
|
+
speed: slot.speed,
|
|
224
|
+
rootMotion: slot.rootMotion as RootMotionStrategy,
|
|
225
|
+
};
|
|
226
|
+
clips[slot.slotId] = deliveryClip;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (missing.length > 0) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
code: 'missing_required_slots',
|
|
233
|
+
message: `Required slots not mapped to existing clips: ${missing.join(', ')}`,
|
|
234
|
+
retryable: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const profileId = override?.basedOnProfileId ?? gameProfile.profileId;
|
|
239
|
+
const profileVersion = override?.basedOnProfileVersion ?? gameProfile.profileVersion;
|
|
240
|
+
const playable: PlayableDeliveryJson = {
|
|
241
|
+
schemaVersion: 1,
|
|
242
|
+
kind: 'playable-character-delivery',
|
|
243
|
+
sourceAssetPath: assetPath,
|
|
244
|
+
modelPath: candidate.modelPath,
|
|
245
|
+
profileId,
|
|
246
|
+
profileVersion,
|
|
247
|
+
sceneGuid:
|
|
248
|
+
(
|
|
249
|
+
JSON.parse(
|
|
250
|
+
await readFile(store.resolveGameRelPath(slug, candidate.metaPath), 'utf8'),
|
|
251
|
+
) as ExternalAssetMeta
|
|
252
|
+
).subAssets.find((s) => s.kind === 'scene')?.guid ?? '',
|
|
253
|
+
clips,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const playableAbs = store.resolveGameRelPath(slug, candidate.playablePath);
|
|
257
|
+
await mkdir(dirname(playableAbs), { recursive: true });
|
|
258
|
+
await writeFile(playableAbs, `${JSON.stringify(playable, null, 2)}\n`, 'utf8');
|
|
259
|
+
|
|
260
|
+
const exportSlotIds = Object.keys(clips);
|
|
261
|
+
// Adopted delivery has no source motion mapping yet — fingerprint uses null
|
|
262
|
+
// motion refs so one-click stays off until the user confirms a real mapping.
|
|
263
|
+
const fp = mappingFingerprint(
|
|
264
|
+
slots,
|
|
265
|
+
slots.map((s) => ({ slotId: s.slotId, motionRefKey: null })),
|
|
266
|
+
profileId,
|
|
267
|
+
profileVersion,
|
|
268
|
+
);
|
|
269
|
+
const snapshot: PlayableDeliverySnapshot = {
|
|
270
|
+
modelPath: candidate.modelPath,
|
|
271
|
+
playablePath: candidate.playablePath,
|
|
272
|
+
profileId,
|
|
273
|
+
profileVersion,
|
|
274
|
+
clipSlotIds: exportSlotIds,
|
|
275
|
+
slotGuidRegistry: nextRegistry,
|
|
276
|
+
mappingFingerprint: fp,
|
|
277
|
+
exportedAt: new Date().toISOString(),
|
|
278
|
+
};
|
|
279
|
+
await store.updatePlayableDeliverySnapshot(slug, assetPath, snapshot);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
modelPath: candidate.modelPath,
|
|
284
|
+
playablePath: candidate.playablePath,
|
|
285
|
+
localUrl: candidate.localUrl,
|
|
286
|
+
reusedGuidCount,
|
|
287
|
+
clipCount: exportSlotIds.length,
|
|
288
|
+
message:
|
|
289
|
+
'Adopted existing playable character into Edit asset catalog. Gameplay code was not changed.',
|
|
290
|
+
};
|
|
291
|
+
}
|