@forgeax/engine-scene 0.1.28 → 0.1.29
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.md +50 -0
- package/dist/__tests__/keyed-scene.integration.test.d.ts +2 -0
- package/dist/__tests__/keyed-scene.integration.test.d.ts.map +1 -0
- package/dist/assets/scene-decoder.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +848 -229
- package/dist/index.mjs.map +1 -1
- package/dist/instances/binding.d.ts +7 -5
- package/dist/instances/binding.d.ts.map +1 -1
- package/dist/instances/externalization.d.ts +1 -1
- package/dist/instances/externalization.d.ts.map +1 -1
- package/dist/instances/keyed.d.ts +38 -0
- package/dist/instances/keyed.d.ts.map +1 -0
- package/dist/instances/legacy.d.ts +20 -0
- package/dist/instances/legacy.d.ts.map +1 -0
- package/dist/instances/runtime-types.d.ts +20 -0
- package/dist/instances/runtime-types.d.ts.map +1 -0
- package/dist/instances/scene-instances.d.ts +27 -37
- package/dist/instances/scene-instances.d.ts.map +1 -1
- package/dist/instances/state.d.ts +6 -1
- package/dist/instances/state.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/asset-owner.integration.test.ts +4 -7
- package/src/__tests__/flat-propagation.perf.test.ts +18 -17
- package/src/__tests__/keyed-scene.integration.test.ts +269 -0
- package/src/__tests__/scene-binding.integration.test.ts +141 -32
- package/src/__tests__/structural.test.ts +4 -1
- package/src/assets/scene-decoder.ts +123 -33
- package/src/collect-subtree.ts +2 -2
- package/src/index.ts +10 -1
- package/src/instances/binding.ts +26 -19
- package/src/instances/externalization.ts +115 -97
- package/src/instances/keyed.ts +505 -0
- package/src/instances/legacy.ts +108 -0
- package/src/instances/runtime-types.ts +21 -0
- package/src/instances/scene-instances.ts +220 -83
- package/src/instances/state.ts +6 -1
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
type Result,
|
|
9
9
|
type SceneAsset,
|
|
10
10
|
type SceneEntity,
|
|
11
|
-
type
|
|
11
|
+
type SceneInstanceOverride,
|
|
12
12
|
} from '@forgeax/engine-types';
|
|
13
|
+
import { normalizeLegacySceneAsset } from '../instances/legacy.js';
|
|
13
14
|
|
|
14
15
|
export const sceneAssetKind: AssetKind<SceneAsset, 'scene'> = {
|
|
15
16
|
kind: 'scene',
|
|
@@ -18,7 +19,7 @@ export const sceneAssetKind: AssetKind<SceneAsset, 'scene'> = {
|
|
|
18
19
|
function invalidScene(guid: string, reason: string): Result<SceneAsset, AssetLoadError> {
|
|
19
20
|
return err({
|
|
20
21
|
code: 'asset-package-invalid',
|
|
21
|
-
expected: 'a scene payload with
|
|
22
|
+
expected: 'a scene payload with keyed entities',
|
|
22
23
|
hint: 'recook the SceneAsset and publish its complete envelope',
|
|
23
24
|
detail: { guid, reason },
|
|
24
25
|
});
|
|
@@ -54,24 +55,16 @@ function resolveWireRef(
|
|
|
54
55
|
return { ok: true, value: guid };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function
|
|
58
|
-
|
|
58
|
+
function resolveInstanceSource(
|
|
59
|
+
source: unknown,
|
|
59
60
|
refs: readonly string[],
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
for (const mount of mounts) {
|
|
66
|
-
if (typeof mount.source !== 'number' || !Number.isInteger(mount.source)) {
|
|
67
|
-
resolved.push(mount);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const ref = resolveWireRef(refs, mount.source, `mount ${mount.localId} source`);
|
|
71
|
-
if (!ref.ok) return ref;
|
|
72
|
-
resolved.push({ ...mount, source: ref.value });
|
|
61
|
+
location: string,
|
|
62
|
+
): { readonly ok: true; readonly value: string } | { readonly ok: false; readonly reason: string } {
|
|
63
|
+
if (typeof source === 'string' && source.length > 0) return { ok: true, value: source };
|
|
64
|
+
if (typeof source !== 'number' || !Number.isInteger(source)) {
|
|
65
|
+
return { ok: false, reason: `${location} must be a GUID or refs index` };
|
|
73
66
|
}
|
|
74
|
-
return
|
|
67
|
+
return resolveWireRef(refs, source, location);
|
|
75
68
|
}
|
|
76
69
|
|
|
77
70
|
function resolveSkinGuids(
|
|
@@ -98,11 +91,46 @@ function resolveSkinGuids(
|
|
|
98
91
|
return { ok: true, value: resolved };
|
|
99
92
|
}
|
|
100
93
|
|
|
101
|
-
function resolveSceneWireRefs(
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
function resolveSceneWireRefs(
|
|
95
|
+
payload: { readonly entities: unknown; readonly skinGuids?: unknown },
|
|
96
|
+
refs: readonly string[],
|
|
97
|
+
): SceneWireRefResult {
|
|
98
|
+
const normalized = normalizeLegacySceneAsset({ kind: 'scene', entities: payload.entities });
|
|
99
|
+
const rawEntities = normalized.entities;
|
|
100
|
+
if (rawEntities === null || typeof rawEntities !== 'object' || Array.isArray(rawEntities)) {
|
|
101
|
+
return { ok: false, reason: 'entities must be a keyed object' };
|
|
102
|
+
}
|
|
103
|
+
const entities: Record<string, SceneEntity> = {};
|
|
104
|
+
for (const [key, rawEntity] of Object.entries(rawEntities as Record<string, unknown>)) {
|
|
105
|
+
const entity = rawEntity as
|
|
106
|
+
| {
|
|
107
|
+
readonly components?: unknown;
|
|
108
|
+
readonly instance?: {
|
|
109
|
+
readonly source?: unknown;
|
|
110
|
+
readonly overrides?: unknown;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
| undefined;
|
|
114
|
+
if (key.length === 0 || entity === undefined || typeof entity !== 'object') {
|
|
115
|
+
return { ok: false, reason: `entities[${JSON.stringify(key)}] is malformed` };
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
entity.components === null ||
|
|
119
|
+
typeof entity.components !== 'object' ||
|
|
120
|
+
Array.isArray(entity.components)
|
|
121
|
+
) {
|
|
122
|
+
return { ok: false, reason: `entities.${key}.components must be an object` };
|
|
123
|
+
}
|
|
104
124
|
const components: Record<string, Record<string, unknown>> = {};
|
|
105
|
-
for (const [componentName, rawFields] of Object.entries(
|
|
125
|
+
for (const [componentName, rawFields] of Object.entries(
|
|
126
|
+
entity.components as Record<string, unknown>,
|
|
127
|
+
)) {
|
|
128
|
+
if (rawFields === null || typeof rawFields !== 'object' || Array.isArray(rawFields)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: `entities.${key}.components.${componentName} must be an object`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
106
134
|
// Component schema lookup is World-local after the ECS core reduction.
|
|
107
135
|
// The runtime projection owns the World-local schema and converts
|
|
108
136
|
// authored GUID fields into World.sharedRefs handles. Keep this loader
|
|
@@ -110,17 +138,77 @@ function resolveSceneWireRefs(payload: SceneAsset, refs: readonly string[]): Sce
|
|
|
110
138
|
// component registry.
|
|
111
139
|
components[componentName] = { ...(rawFields as Record<string, unknown>) };
|
|
112
140
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
const instance = entity.instance;
|
|
142
|
+
let resolvedInstance: SceneEntity['instance'];
|
|
143
|
+
if (instance !== undefined) {
|
|
144
|
+
if (instance === null || typeof instance !== 'object') {
|
|
145
|
+
return { ok: false, reason: `entities.${key}.instance must be an object` };
|
|
146
|
+
}
|
|
147
|
+
const source = resolveInstanceSource(
|
|
148
|
+
instance.source,
|
|
149
|
+
refs,
|
|
150
|
+
`entities.${key}.instance.source`,
|
|
151
|
+
);
|
|
152
|
+
if (!source.ok) return source;
|
|
153
|
+
if (instance.overrides !== undefined && !Array.isArray(instance.overrides)) {
|
|
154
|
+
return { ok: false, reason: `entities.${key}.instance.overrides must be an array` };
|
|
155
|
+
}
|
|
156
|
+
let overrides: NonNullable<SceneEntity['instance']>['overrides'] | undefined;
|
|
157
|
+
if (instance.overrides === undefined) {
|
|
158
|
+
overrides = undefined;
|
|
159
|
+
} else {
|
|
160
|
+
const resolvedOverrides: SceneInstanceOverride[] = [];
|
|
161
|
+
for (const [index, rawOverride] of (instance.overrides as readonly unknown[]).entries()) {
|
|
162
|
+
if (
|
|
163
|
+
rawOverride === null ||
|
|
164
|
+
typeof rawOverride !== 'object' ||
|
|
165
|
+
Array.isArray(rawOverride) ||
|
|
166
|
+
!Array.isArray((rawOverride as { readonly target?: unknown }).target) ||
|
|
167
|
+
(rawOverride as { readonly target?: unknown[] }).target?.some(
|
|
168
|
+
(part) => typeof part !== 'string' || part.length === 0,
|
|
169
|
+
)
|
|
170
|
+
) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
reason: `entities.${key}.instance.overrides[${index}] is malformed`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const target = (rawOverride as { readonly target: readonly string[] }).target;
|
|
177
|
+
const rawComponents = (rawOverride as { readonly components?: unknown }).components;
|
|
178
|
+
if (
|
|
179
|
+
rawComponents === null ||
|
|
180
|
+
typeof rawComponents !== 'object' ||
|
|
181
|
+
Array.isArray(rawComponents)
|
|
182
|
+
) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
reason: `entities.${key}.instance.overrides[${index}].components is malformed`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
resolvedOverrides.push({
|
|
189
|
+
target: [...target] as [string, ...string[]],
|
|
190
|
+
components: rawComponents as SceneEntity['components'],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
overrides = resolvedOverrides;
|
|
194
|
+
}
|
|
195
|
+
resolvedInstance = {
|
|
196
|
+
source: source.value,
|
|
197
|
+
...(overrides === undefined ? {} : { overrides }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
entities[key] = {
|
|
116
201
|
components,
|
|
117
|
-
|
|
202
|
+
...(resolvedInstance === undefined ? {} : { instance: resolvedInstance }),
|
|
203
|
+
};
|
|
118
204
|
}
|
|
119
205
|
|
|
120
|
-
const mounts = resolveMounts(payload.mounts, refs);
|
|
121
|
-
if (!mounts.ok) return mounts;
|
|
122
206
|
const skinGuids = resolveSkinGuids(
|
|
123
|
-
payload.skinGuids
|
|
207
|
+
Array.isArray(payload.skinGuids)
|
|
208
|
+
? (payload.skinGuids as readonly (number | string)[])
|
|
209
|
+
: payload.skinGuids === undefined
|
|
210
|
+
? undefined
|
|
211
|
+
: ([] as readonly (number | string)[]),
|
|
124
212
|
refs,
|
|
125
213
|
);
|
|
126
214
|
if (!skinGuids.ok) return skinGuids;
|
|
@@ -129,9 +217,7 @@ function resolveSceneWireRefs(payload: SceneAsset, refs: readonly string[]): Sce
|
|
|
129
217
|
ok: true,
|
|
130
218
|
value: {
|
|
131
219
|
kind: 'scene',
|
|
132
|
-
...(payload.sourceKey === undefined ? {} : { sourceKey: payload.sourceKey }),
|
|
133
220
|
entities,
|
|
134
|
-
...(mounts.value === undefined ? {} : { mounts: mounts.value }),
|
|
135
221
|
...(skinGuids.value === undefined ? {} : { skinGuids: skinGuids.value }),
|
|
136
222
|
},
|
|
137
223
|
};
|
|
@@ -141,8 +227,12 @@ function resolveSceneWireRefs(payload: SceneAsset, refs: readonly string[]): Sce
|
|
|
141
227
|
export const sceneAssetDecoder: AssetDecoder<SceneAsset> = {
|
|
142
228
|
async decode({ envelope }): Promise<Result<SceneAsset, AssetLoadError>> {
|
|
143
229
|
const payload = envelope.payload;
|
|
144
|
-
if (
|
|
145
|
-
|
|
230
|
+
if (
|
|
231
|
+
payload.kind !== 'scene' ||
|
|
232
|
+
payload.entities === null ||
|
|
233
|
+
typeof payload.entities !== 'object'
|
|
234
|
+
) {
|
|
235
|
+
return invalidScene(envelope.guid, 'scene payload is missing keyed entities');
|
|
146
236
|
}
|
|
147
237
|
const resolved = resolveSceneWireRefs(payload, envelope.refs);
|
|
148
238
|
if (!resolved.ok) return invalidScene(envelope.guid, resolved.reason);
|
package/src/collect-subtree.ts
CHANGED
|
@@ -14,8 +14,8 @@ export function collectSubtree(
|
|
|
14
14
|
if (visited.has(spawnRoot as number)) return visited;
|
|
15
15
|
const queue: number[] = [spawnRoot as number];
|
|
16
16
|
visited.add(spawnRoot as number);
|
|
17
|
-
|
|
18
|
-
const current = queue
|
|
17
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
18
|
+
const current = queue[cursor] as number;
|
|
19
19
|
const children = world.get(current as EntityHandle, Children);
|
|
20
20
|
if (!children.ok) continue;
|
|
21
21
|
const entities = children.value.entities as ArrayLike<number>;
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,8 @@ export {
|
|
|
17
17
|
type SceneBindingError,
|
|
18
18
|
type SceneEntityRef,
|
|
19
19
|
sceneEntity,
|
|
20
|
-
|
|
20
|
+
sceneEntityAddressKey,
|
|
21
|
+
validateSceneEntityKeys,
|
|
21
22
|
} from './instances/binding';
|
|
22
23
|
export { SCENE_COLLECT_PROFILE, type SceneCollectProfile } from './instances/collect-profile';
|
|
23
24
|
export {
|
|
@@ -26,6 +27,14 @@ export {
|
|
|
26
27
|
type SceneComponentSchemaResolver,
|
|
27
28
|
type SceneExternalizationError,
|
|
28
29
|
} from './instances/externalization';
|
|
30
|
+
export {
|
|
31
|
+
type CompiledSceneAsset,
|
|
32
|
+
type CompiledSceneEntity,
|
|
33
|
+
type CompiledSceneResult,
|
|
34
|
+
compileKeyedSceneAsset,
|
|
35
|
+
type KeyedSceneCompileContext,
|
|
36
|
+
} from './instances/keyed';
|
|
37
|
+
export type { MountOverride, SceneInstanceMount } from './instances/runtime-types';
|
|
29
38
|
export {
|
|
30
39
|
type SceneAssetResolver,
|
|
31
40
|
type SceneInstanceStatePayload,
|
package/src/instances/binding.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
2
|
-
import type { SceneEntityRef } from '@forgeax/engine-types';
|
|
2
|
+
import type { SceneEntityAddress, SceneEntityRef } from '@forgeax/engine-types';
|
|
3
3
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
4
4
|
|
|
5
5
|
export type { SceneEntityRef } from '@forgeax/engine-types';
|
|
@@ -8,19 +8,19 @@ export type SceneBindingError = {
|
|
|
8
8
|
readonly code: 'scene-binding-missing' | 'scene-binding-wrong-instance';
|
|
9
9
|
readonly expected: string;
|
|
10
10
|
readonly hint: string;
|
|
11
|
-
readonly detail: { readonly sceneSourceKey: string; readonly
|
|
11
|
+
readonly detail: { readonly sceneSourceKey: string; readonly address: SceneEntityAddress };
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type SceneBindingDeclarationError = {
|
|
15
15
|
readonly code: 'scene-binding-duplicate' | 'scene-binding-source-missing';
|
|
16
16
|
readonly expected: string;
|
|
17
17
|
readonly hint: string;
|
|
18
|
-
readonly detail: { readonly sceneSourceKey?: string; readonly
|
|
18
|
+
readonly detail: { readonly sceneSourceKey?: string; readonly address?: SceneEntityAddress };
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export function
|
|
21
|
+
export function validateSceneEntityKeys(
|
|
22
22
|
sceneSourceKey: string,
|
|
23
|
-
|
|
23
|
+
entityKeys: readonly string[],
|
|
24
24
|
): Result<readonly string[], SceneBindingDeclarationError> {
|
|
25
25
|
if (sceneSourceKey.length === 0) {
|
|
26
26
|
return err({
|
|
@@ -31,22 +31,29 @@ export function validateSceneBindings(
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
const seen = new Set<string>();
|
|
34
|
-
for (const
|
|
35
|
-
if (
|
|
34
|
+
for (const entityKey of entityKeys) {
|
|
35
|
+
if (entityKey.length === 0 || seen.has(entityKey)) {
|
|
36
36
|
return err({
|
|
37
37
|
code: 'scene-binding-duplicate',
|
|
38
|
-
expected: 'unique non-empty
|
|
39
|
-
hint: 'rename the duplicate
|
|
40
|
-
detail: { sceneSourceKey,
|
|
38
|
+
expected: 'unique non-empty entity keys within one scene',
|
|
39
|
+
hint: 'rename the duplicate entity key in the scene producer',
|
|
40
|
+
detail: { sceneSourceKey, address: entityKey },
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
seen.add(
|
|
43
|
+
seen.add(entityKey);
|
|
44
44
|
}
|
|
45
|
-
return ok([...
|
|
45
|
+
return ok([...entityKeys]);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function sceneEntity(sceneSourceKey: string,
|
|
49
|
-
return { sceneSourceKey,
|
|
48
|
+
export function sceneEntity(sceneSourceKey: string, address: SceneEntityAddress): SceneEntityRef {
|
|
49
|
+
return { sceneSourceKey, address };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stable map key shared by direct and nested SceneEntityRef addresses. */
|
|
53
|
+
export function sceneEntityAddressKey(address: SceneEntityAddress): string {
|
|
54
|
+
if (typeof address === 'string') return `s:${JSON.stringify(address)}`;
|
|
55
|
+
if (address.length === 1) return `s:${JSON.stringify(address[0] ?? '')}`;
|
|
56
|
+
return `a:${JSON.stringify(address)}`;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export function resolveSceneEntity(
|
|
@@ -61,16 +68,16 @@ export function resolveSceneEntity(
|
|
|
61
68
|
code: 'scene-binding-wrong-instance',
|
|
62
69
|
expected: `scene instance ${ref.sceneSourceKey}`,
|
|
63
70
|
hint: 'resolve the SceneEntityRef against its owning SceneInstance',
|
|
64
|
-
detail: { sceneSourceKey: ref.sceneSourceKey,
|
|
71
|
+
detail: { sceneSourceKey: ref.sceneSourceKey, address: ref.address },
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
|
-
const value = instance.bindings.get(ref.
|
|
74
|
+
const value = instance.bindings.get(sceneEntityAddressKey(ref.address));
|
|
68
75
|
if (value === undefined) {
|
|
69
76
|
return err({
|
|
70
77
|
code: 'scene-binding-missing',
|
|
71
|
-
expected: '
|
|
72
|
-
hint: 'declare the
|
|
73
|
-
detail: { sceneSourceKey: ref.sceneSourceKey,
|
|
78
|
+
expected: 'entity key declared by the scene producer',
|
|
79
|
+
hint: 'declare the entity key in the scene producer before consuming it',
|
|
80
|
+
detail: { sceneSourceKey: ref.sceneSourceKey, address: ref.address },
|
|
74
81
|
});
|
|
75
82
|
}
|
|
76
83
|
return ok(value);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AssetRef,
|
|
1
|
+
import type { AssetRef, SceneAsset, SceneInstanceOverride } from '@forgeax/engine-types';
|
|
2
2
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
3
|
+
import { migrateLegacySceneComponentFields, normalizeLegacySceneAsset } from './legacy.js';
|
|
3
4
|
|
|
4
5
|
export type SceneComponentSchemaResolver = (
|
|
5
6
|
componentName: string,
|
|
@@ -21,123 +22,140 @@ function sharedKind(type: string | undefined): 'one' | 'many' | undefined {
|
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
interface RefContext {
|
|
26
|
+
readonly refs: AssetRef[];
|
|
27
|
+
readonly indexByGuid: Map<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function addRef(
|
|
31
|
+
context: RefContext,
|
|
32
|
+
guid: string,
|
|
33
|
+
sourceField: NonNullable<AssetRef['sourceField']>,
|
|
34
|
+
sceneEntityKey?: string,
|
|
35
|
+
): number {
|
|
36
|
+
const prior = context.indexByGuid.get(guid);
|
|
37
|
+
if (prior !== undefined) return prior;
|
|
38
|
+
const index = context.refs.length;
|
|
39
|
+
context.refs.push({
|
|
40
|
+
guid,
|
|
41
|
+
sourceField,
|
|
42
|
+
...(sceneEntityKey === undefined ? {} : { sceneEntityKey }),
|
|
43
|
+
} as AssetRef);
|
|
44
|
+
context.indexByGuid.set(guid, index);
|
|
45
|
+
return index;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function externalizeFields(
|
|
49
|
+
componentName: string,
|
|
50
|
+
source: Record<string, unknown>,
|
|
26
51
|
resolveSchema: SceneComponentSchemaResolver,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (kind === '
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
context: RefContext,
|
|
53
|
+
sceneEntityKey: string | undefined,
|
|
54
|
+
): Record<string, unknown> {
|
|
55
|
+
const schema = resolveSchema(componentName);
|
|
56
|
+
const fields: Record<string, unknown> = {};
|
|
57
|
+
for (const [fieldName, value] of Object.entries(
|
|
58
|
+
migrateLegacySceneComponentFields(componentName, source),
|
|
59
|
+
)) {
|
|
60
|
+
if (value === undefined) continue;
|
|
61
|
+
const kind = sharedKind(schema?.[fieldName]);
|
|
62
|
+
if (kind === 'one' && typeof value === 'string') {
|
|
63
|
+
fields[fieldName] = addRef(context, value, { componentName, fieldName }, sceneEntityKey);
|
|
64
|
+
} else if (kind === 'many' && Array.isArray(value)) {
|
|
65
|
+
fields[fieldName] = value.map((item, arrayIndex) =>
|
|
66
|
+
typeof item === 'string'
|
|
67
|
+
? addRef(context, item, { componentName, fieldName, arrayIndex }, sceneEntityKey)
|
|
68
|
+
: item,
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
fields[fieldName] = value;
|
|
42
72
|
}
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
}
|
|
74
|
+
return fields;
|
|
45
75
|
}
|
|
46
76
|
|
|
47
|
-
|
|
77
|
+
function externalizeOverride(
|
|
78
|
+
override: SceneInstanceOverride,
|
|
79
|
+
resolveSchema: SceneComponentSchemaResolver,
|
|
80
|
+
context: RefContext,
|
|
81
|
+
sceneEntityKey: string,
|
|
82
|
+
): SceneInstanceOverride {
|
|
83
|
+
const components: Record<string, Record<string, unknown>> = {};
|
|
84
|
+
for (const [componentName, rawFields] of Object.entries(override.components)) {
|
|
85
|
+
components[componentName] = externalizeFields(
|
|
86
|
+
componentName,
|
|
87
|
+
{ ...(rawFields as Record<string, unknown>) },
|
|
88
|
+
resolveSchema,
|
|
89
|
+
context,
|
|
90
|
+
sceneEntityKey,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
target: [...override.target],
|
|
95
|
+
components,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Project a keyed SceneAsset's shared asset fields into a payload plus refs. */
|
|
48
100
|
export function externalizeSceneAsset(
|
|
49
101
|
scene: SceneAsset,
|
|
50
102
|
resolveSchema: SceneComponentSchemaResolver,
|
|
51
103
|
): Result<ExternalizedSceneAsset, SceneExternalizationError> {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
sourceField: NonNullable<AssetRef['sourceField']>,
|
|
57
|
-
sceneEntityId?: number,
|
|
58
|
-
): number => {
|
|
59
|
-
const prior = indexByGuid.get(guid);
|
|
60
|
-
if (prior !== undefined) return prior;
|
|
61
|
-
const index = refs.length;
|
|
62
|
-
refs.push({ guid, sourceField, ...(sceneEntityId === undefined ? {} : { sceneEntityId }) });
|
|
63
|
-
indexByGuid.set(guid, index);
|
|
64
|
-
return index;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const entities = scene.entities.map((entity) => {
|
|
104
|
+
const normalized = normalizeLegacySceneAsset(scene);
|
|
105
|
+
const context: RefContext = { refs: [], indexByGuid: new Map() };
|
|
106
|
+
const entities: Record<string, Record<string, unknown>> = {};
|
|
107
|
+
for (const [key, entity] of Object.entries(normalized.entities)) {
|
|
68
108
|
const components: Record<string, Record<string, unknown>> = {};
|
|
69
|
-
for (const componentName of Object.
|
|
70
|
-
const
|
|
71
|
-
const source = entity.components[componentName] as Record<string, unknown> | undefined;
|
|
109
|
+
for (const [componentName, raw] of Object.entries(entity.components)) {
|
|
110
|
+
const source = raw as Record<string, unknown> | undefined;
|
|
72
111
|
if (source === undefined) continue;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else if (kind === 'many' && Array.isArray(value)) {
|
|
81
|
-
fields[fieldName] = value.map((item, arrayIndex) =>
|
|
82
|
-
typeof item === 'string'
|
|
83
|
-
? addRef(item, { componentName, fieldName, arrayIndex }, entity.localId as number)
|
|
84
|
-
: item,
|
|
85
|
-
);
|
|
86
|
-
} else {
|
|
87
|
-
fields[fieldName] = value;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (Object.keys(fields).length > 0 || Object.keys(schema ?? {}).length === 0) {
|
|
91
|
-
components[componentName] = fields;
|
|
92
|
-
}
|
|
112
|
+
components[componentName] = externalizeFields(
|
|
113
|
+
componentName,
|
|
114
|
+
source,
|
|
115
|
+
resolveSchema,
|
|
116
|
+
context,
|
|
117
|
+
key,
|
|
118
|
+
);
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
...(entity.bindingKey === undefined ? {} : { bindingKey: entity.bindingKey }),
|
|
120
|
+
const instance = entity.instance;
|
|
121
|
+
entities[key] = {
|
|
97
122
|
components,
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const mounts = scene.mounts?.map((mount) => {
|
|
102
|
-
const source =
|
|
103
|
-
typeof mount.source === 'string'
|
|
104
|
-
? addRef(
|
|
105
|
-
mount.source,
|
|
106
|
-
{ componentName: 'SceneInstance', fieldName: 'source' },
|
|
107
|
-
mount.localId as number,
|
|
108
|
-
)
|
|
109
|
-
: (mount.source as number);
|
|
110
|
-
for (const { field, guid } of (mount.overrides ?? []).flatMap((override) =>
|
|
111
|
-
overrideGuids(override, resolveSchema),
|
|
112
|
-
)) {
|
|
113
|
-
addRef(guid, { componentName: 'SceneInstance', fieldName: `overrides.${field}` });
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
localId: mount.localId as number,
|
|
117
|
-
source,
|
|
118
|
-
memberFirst: mount.memberFirst as number,
|
|
119
|
-
memberCount: mount.memberCount,
|
|
120
|
-
...(mount.parent === undefined ? {} : { parent: mount.parent as number }),
|
|
121
|
-
...(mount.publicationFence === undefined ? {} : { publicationFence: mount.publicationFence }),
|
|
122
|
-
...(mount.overrides === undefined
|
|
123
|
+
...(instance === undefined
|
|
123
124
|
? {}
|
|
124
|
-
: {
|
|
125
|
+
: {
|
|
126
|
+
instance: {
|
|
127
|
+
source: addRef(
|
|
128
|
+
context,
|
|
129
|
+
instance.source,
|
|
130
|
+
{ componentName: 'SceneInstance', fieldName: 'source' },
|
|
131
|
+
key,
|
|
132
|
+
),
|
|
133
|
+
...(instance.overrides === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: {
|
|
136
|
+
overrides: instance.overrides.map((override) =>
|
|
137
|
+
externalizeOverride(override, resolveSchema, context, key),
|
|
138
|
+
),
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
125
142
|
};
|
|
126
|
-
}
|
|
127
|
-
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [arrayIndex, guid] of (normalized.skinGuids ?? []).entries()) {
|
|
128
146
|
if (typeof guid !== 'string') return err({ field: 'skinGuids', value: guid });
|
|
129
|
-
addRef(guid, { componentName: '<scene>', fieldName: 'skinGuids', arrayIndex });
|
|
147
|
+
addRef(context, guid, { componentName: '<scene>', fieldName: 'skinGuids', arrayIndex });
|
|
130
148
|
}
|
|
131
149
|
return ok({
|
|
132
150
|
payload: {
|
|
133
151
|
kind: 'scene',
|
|
134
|
-
...(scene.sourceKey === undefined ? {} : { sourceKey: scene.sourceKey }),
|
|
135
152
|
entities,
|
|
136
|
-
...(
|
|
137
|
-
...(scene.skinGuids === undefined
|
|
153
|
+
...(normalized.skinGuids === undefined
|
|
138
154
|
? {}
|
|
139
|
-
: {
|
|
155
|
+
: {
|
|
156
|
+
skinGuids: normalized.skinGuids.map((guid) => context.indexByGuid.get(guid) as number),
|
|
157
|
+
}),
|
|
140
158
|
},
|
|
141
|
-
refs,
|
|
159
|
+
refs: context.refs,
|
|
142
160
|
});
|
|
143
161
|
}
|