@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
|
@@ -17,12 +17,11 @@ import { fillComponentDefaults, StaleEntityError } from '@forgeax/engine-ecs/pro
|
|
|
17
17
|
import type {
|
|
18
18
|
Handle,
|
|
19
19
|
LocalEntityId,
|
|
20
|
-
MountOverride,
|
|
21
20
|
PackErrorCode,
|
|
22
21
|
PackErrorDetail,
|
|
23
22
|
SceneAsset,
|
|
23
|
+
SceneEntityAddress,
|
|
24
24
|
SceneEntityRef,
|
|
25
|
-
SceneInstanceMount,
|
|
26
25
|
} from '@forgeax/engine-types';
|
|
27
26
|
import {
|
|
28
27
|
err,
|
|
@@ -33,7 +32,13 @@ import {
|
|
|
33
32
|
unwrapHandle,
|
|
34
33
|
} from '@forgeax/engine-types';
|
|
35
34
|
import { ComponentNotDefinedError } from '../errors';
|
|
36
|
-
import { resolveSceneEntity,
|
|
35
|
+
import { resolveSceneEntity, sceneEntityAddressKey } from './binding.js';
|
|
36
|
+
import {
|
|
37
|
+
type CompiledSceneAsset,
|
|
38
|
+
type CompiledSceneEntity,
|
|
39
|
+
compileKeyedSceneAsset,
|
|
40
|
+
} from './keyed.js';
|
|
41
|
+
import type { MountOverride, SceneInstanceMount } from './runtime-types.js';
|
|
37
42
|
import {
|
|
38
43
|
isPrimitiveScalarFieldType,
|
|
39
44
|
mountOverrideStateKey,
|
|
@@ -48,39 +53,32 @@ const entityIndex = (entity: EntityHandle): number => (entity as number) & 0x00f
|
|
|
48
53
|
const entityGeneration = (entity: EntityHandle): number => ((entity as number) >>> 24) & 0xff;
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* (an editor renames a field, an old `.pack.json` lags). `worldInstantiateScene`
|
|
56
|
-
* does NOT blank the whole scene over one such field (#478 lesson: a
|
|
57
|
-
* prod-silent strip re-introduced an invisible-entity class) and does NOT abort
|
|
58
|
-
* fatally. Instead it skips the unknown key (no write, no input mutation) and
|
|
59
|
-
* surfaces this record on the success value's `diagnostics[]` — observable in
|
|
60
|
-
* production (NOT NODE_ENV-gated), consumed by property access (no string parse):
|
|
56
|
+
* Legacy diagnostic shape retained on the scene-instantiation result for
|
|
57
|
+
* non-blocking runtime observations. SceneAsset schema violations are
|
|
58
|
+
* rejected by the keyed compiler before this result is produced; no authored
|
|
59
|
+
* unknown-field record is emitted by the current path.
|
|
61
60
|
*
|
|
62
61
|
* const r = worldInstantiateScene(world, handle);
|
|
63
62
|
* if (r.ok) for (const d of r.value.diagnostics)
|
|
64
|
-
* console.warn(
|
|
63
|
+
* console.warn('scene diagnostic', d);
|
|
65
64
|
*
|
|
66
|
-
* Direct `world.spawn` / `world.addComponent` / `Commands.spawn`
|
|
67
|
-
* with `SpawnDataUnknownFieldError
|
|
68
|
-
* is a programming error, not loader-fed data.
|
|
65
|
+
* Direct `world.spawn` / `world.addComponent` / `Commands.spawn` remain
|
|
66
|
+
* fail-fast with `SpawnDataUnknownFieldError`.
|
|
69
67
|
*/
|
|
70
68
|
export type SceneInstantiateDiagnostic = {
|
|
71
|
-
/** Component name
|
|
69
|
+
/** Component name associated with the observation. */
|
|
72
70
|
readonly component: string;
|
|
73
|
-
/**
|
|
71
|
+
/** Field associated with the observation. */
|
|
74
72
|
readonly field: string;
|
|
75
|
-
/** LocalEntityId
|
|
73
|
+
/** LocalEntityId within the owning SceneAsset, when applicable. */
|
|
76
74
|
readonly localId: number;
|
|
77
75
|
};
|
|
78
76
|
|
|
79
77
|
/**
|
|
80
78
|
* Success value of `worldInstantiateScene`. `root` is the synthetic scene-root
|
|
81
|
-
* EntityHandle (carries `SceneInstance`); `diagnostics`
|
|
82
|
-
*
|
|
83
|
-
*
|
|
79
|
+
* EntityHandle (carries `SceneInstance`); `diagnostics` contains only
|
|
80
|
+
* non-blocking runtime observations. Schema-invalid authored fields fail before
|
|
81
|
+
* an entity is created.
|
|
84
82
|
*/
|
|
85
83
|
export type SceneInstantiateOk = {
|
|
86
84
|
readonly root: EntityHandle;
|
|
@@ -137,11 +135,50 @@ export interface SceneMembersSpawn {
|
|
|
137
135
|
readonly mount: SceneInstanceMount;
|
|
138
136
|
readonly root: EntityHandle;
|
|
139
137
|
readonly mapping: Uint32Array;
|
|
138
|
+
readonly key?: string;
|
|
140
139
|
}[];
|
|
141
140
|
/** `entities.length + mounts + Σ memberCount`, captured at instantiate-time. */
|
|
142
141
|
readonly totalSlots: number;
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Populate the instance binding projection from the private numeric mapping.
|
|
146
|
+
* The authored key remains the only lookup identity: nested instance paths are
|
|
147
|
+
* represented as the same string/tuple key accepted by `SceneEntityRef`, while
|
|
148
|
+
* the numeric mapping stays local to the Scene owner.
|
|
149
|
+
*/
|
|
150
|
+
function collectSceneEntityBindings(
|
|
151
|
+
world: World,
|
|
152
|
+
root: EntityHandle,
|
|
153
|
+
prefix: readonly string[],
|
|
154
|
+
bindings: Map<string, EntityHandle>,
|
|
155
|
+
visited = new Set<number>(),
|
|
156
|
+
): void {
|
|
157
|
+
const rootRaw = root as unknown as number;
|
|
158
|
+
if (visited.has(rootRaw)) return;
|
|
159
|
+
visited.add(rootRaw);
|
|
160
|
+
const state = worldResolveSceneInstanceStatePayload(world, root);
|
|
161
|
+
if (!state.ok) return;
|
|
162
|
+
const sceneInstance = world.components.resolve('SceneInstance');
|
|
163
|
+
if (sceneInstance === undefined) return;
|
|
164
|
+
const component = world.get(root, sceneInstance);
|
|
165
|
+
if (!component.ok) return;
|
|
166
|
+
const mapping = (component.value as unknown as { mapping: ArrayLike<number> }).mapping;
|
|
167
|
+
for (const [slot, key] of state.value.keyByLocalId) {
|
|
168
|
+
const raw = mapping[slot];
|
|
169
|
+
if (raw === undefined || raw === ENTITY_NULL_RAW) continue;
|
|
170
|
+
const address: SceneEntityAddress =
|
|
171
|
+
prefix.length === 0 ? key : ([...prefix, key] as unknown as [string, ...string[]]);
|
|
172
|
+
bindings.set(sceneEntityAddressKey(address), raw as unknown as EntityHandle);
|
|
173
|
+
}
|
|
174
|
+
for (const childRoot of state.value.mountRoots) {
|
|
175
|
+
const childState = worldResolveSceneInstanceStatePayload(world, childRoot);
|
|
176
|
+
const childKey = childState.ok ? childState.value.instanceKey : undefined;
|
|
177
|
+
if (childKey === undefined) continue;
|
|
178
|
+
collectSceneEntityBindings(world, childRoot, [...prefix, childKey], bindings, visited);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
export type SceneAssetResolver = (
|
|
146
183
|
source: number | string,
|
|
147
184
|
parentHandle: Handle<'SceneAsset', 'shared'>,
|
|
@@ -173,8 +210,8 @@ export function worldGetSceneAssetResolver(world: World): SceneAssetResolver | n
|
|
|
173
210
|
* const r = worldInstantiateScene(world, handle);
|
|
174
211
|
* if (!r.ok) return r;
|
|
175
212
|
* const { root, diagnostics } = r.value;
|
|
176
|
-
* for (const d of diagnostics) //
|
|
177
|
-
* console.warn(
|
|
213
|
+
* for (const d of diagnostics) // non-blocking runtime observations
|
|
214
|
+
* console.warn('scene diagnostic', d);
|
|
178
215
|
* const inst = world.get(root, SceneInstance).value;
|
|
179
216
|
* const member = inst.mapping[0]; // first member entity
|
|
180
217
|
*/
|
|
@@ -182,13 +219,21 @@ export function worldInstantiateScene(
|
|
|
182
219
|
world: World,
|
|
183
220
|
handle: Handle<'SceneAsset', 'shared'>,
|
|
184
221
|
parent?: EntityHandle,
|
|
222
|
+
sceneSourceKey?: string,
|
|
185
223
|
): Result<SceneInstantiateOk, EcsError> {
|
|
186
224
|
const stack = new Set<number>();
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// this accumulator; only the public entry packages it onto the success value.
|
|
225
|
+
// Keep the existing result shape for non-blocking runtime observations. The
|
|
226
|
+
// keyed compiler rejects schema-invalid authoring data before spawning.
|
|
190
227
|
const diagnostics: SceneInstantiateDiagnostic[] = [];
|
|
191
|
-
const r = worldInstantiateSceneRec(
|
|
228
|
+
const r = worldInstantiateSceneRec(
|
|
229
|
+
world,
|
|
230
|
+
handle,
|
|
231
|
+
parent,
|
|
232
|
+
stack,
|
|
233
|
+
diagnostics,
|
|
234
|
+
undefined,
|
|
235
|
+
sceneSourceKey,
|
|
236
|
+
);
|
|
192
237
|
if (!r.ok) return r;
|
|
193
238
|
return ok({ root: r.value, diagnostics });
|
|
194
239
|
}
|
|
@@ -264,6 +309,8 @@ export function worldInstantiateSceneRec(
|
|
|
264
309
|
parent: EntityHandle | undefined,
|
|
265
310
|
stack: Set<number>,
|
|
266
311
|
diagnostics: SceneInstantiateDiagnostic[],
|
|
312
|
+
instanceKey?: string,
|
|
313
|
+
sceneSourceKey?: string,
|
|
267
314
|
): Result<EntityHandle, EcsError> {
|
|
268
315
|
const handleKey = unwrapHandle(handle);
|
|
269
316
|
if (stack.has(handleKey)) {
|
|
@@ -287,7 +334,16 @@ export function worldInstantiateSceneRec(
|
|
|
287
334
|
const asset = resolved.value;
|
|
288
335
|
stack.add(handleKey);
|
|
289
336
|
try {
|
|
290
|
-
return worldInstantiateSceneAsset(
|
|
337
|
+
return worldInstantiateSceneAsset(
|
|
338
|
+
world,
|
|
339
|
+
handle,
|
|
340
|
+
asset,
|
|
341
|
+
parent,
|
|
342
|
+
stack,
|
|
343
|
+
diagnostics,
|
|
344
|
+
instanceKey,
|
|
345
|
+
sceneSourceKey,
|
|
346
|
+
);
|
|
291
347
|
} finally {
|
|
292
348
|
stack.delete(handleKey);
|
|
293
349
|
}
|
|
@@ -320,9 +376,10 @@ export function worldResolveSceneAsset(
|
|
|
320
376
|
export function worldSpawnSceneMembers(
|
|
321
377
|
world: World,
|
|
322
378
|
handle: Handle<'SceneAsset', 'shared'>,
|
|
323
|
-
asset:
|
|
379
|
+
asset: CompiledSceneAsset,
|
|
324
380
|
stack: Set<number>,
|
|
325
381
|
diagnostics: SceneInstantiateDiagnostic[],
|
|
382
|
+
mountKeys?: ReadonlyMap<number, string>,
|
|
326
383
|
): Result<SceneMembersSpawn, EcsError> {
|
|
327
384
|
const sceneInstanceToken = world.components.resolve('SceneInstance');
|
|
328
385
|
if (sceneInstanceToken === undefined) {
|
|
@@ -335,15 +392,6 @@ export function worldSpawnSceneMembers(
|
|
|
335
392
|
|
|
336
393
|
const ownEntities = asset.entities;
|
|
337
394
|
const ownMounts = asset.mounts ?? [];
|
|
338
|
-
const bindingKeys = ownEntities.flatMap((entity) =>
|
|
339
|
-
entity.bindingKey === undefined ? [] : [entity.bindingKey],
|
|
340
|
-
);
|
|
341
|
-
if (bindingKeys.length > 0) {
|
|
342
|
-
const bindingCheck = validateSceneBindings(asset.sourceKey ?? '', bindingKeys);
|
|
343
|
-
if (!bindingCheck.ok) {
|
|
344
|
-
return err(bindingCheck.error as unknown as EcsError);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
395
|
const memberSum = ownMounts.reduce((s, m) => s + m.memberCount, 0);
|
|
348
396
|
const countBaseline = ownEntities.length + ownMounts.length + memberSum;
|
|
349
397
|
// C-R1 (studio-issues #6): mapping table must be sized to maxLocalId+1,
|
|
@@ -469,10 +517,16 @@ export function worldSpawnSceneMembers(
|
|
|
469
517
|
const childHandle = childHandleRes.value;
|
|
470
518
|
|
|
471
519
|
// Recursively instantiate the child. Its synthetic root attaches as a
|
|
472
|
-
// child of the mount entity
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
520
|
+
// child of the mount entity; runtime observations share the same result
|
|
521
|
+
// accumulator and bubble to the top-level instance.
|
|
522
|
+
const childRes = worldInstantiateSceneRec(
|
|
523
|
+
world,
|
|
524
|
+
childHandle,
|
|
525
|
+
mountEntity,
|
|
526
|
+
stack,
|
|
527
|
+
diagnostics,
|
|
528
|
+
mountKeys?.get(mountLid),
|
|
529
|
+
);
|
|
476
530
|
if (!childRes.ok) return childRes;
|
|
477
531
|
|
|
478
532
|
// R2/B-2: cross-check mount.memberCount === child.totalSlots BEFORE
|
|
@@ -482,7 +536,12 @@ export function worldSpawnSceneMembers(
|
|
|
482
536
|
const childInstRes = world.get(childRes.value, sceneInstanceToken);
|
|
483
537
|
if (!childInstRes.ok) return childInstRes;
|
|
484
538
|
const childMapping = (childInstRes.value as unknown as { mapping: Uint32Array }).mapping;
|
|
485
|
-
mountInstances.push({
|
|
539
|
+
mountInstances.push({
|
|
540
|
+
mount,
|
|
541
|
+
root: childRes.value,
|
|
542
|
+
mapping: childMapping,
|
|
543
|
+
...(mountKeys?.get(mountLid) === undefined ? {} : { key: mountKeys.get(mountLid) }),
|
|
544
|
+
});
|
|
486
545
|
if (childMapping.length !== mount.memberCount) {
|
|
487
546
|
return err({
|
|
488
547
|
code: 'pack-mount-count-mismatch' as PackErrorCode,
|
|
@@ -614,6 +673,8 @@ export function worldInstantiateSceneAsset(
|
|
|
614
673
|
parent: EntityHandle | undefined,
|
|
615
674
|
stack: Set<number>,
|
|
616
675
|
diagnostics: SceneInstantiateDiagnostic[],
|
|
676
|
+
instanceKey?: string,
|
|
677
|
+
sceneSourceKey?: string,
|
|
617
678
|
): Result<EntityHandle, EcsError> {
|
|
618
679
|
const sceneInstanceToken = world.components.resolve('SceneInstance');
|
|
619
680
|
if (sceneInstanceToken === undefined) {
|
|
@@ -621,12 +682,26 @@ export function worldInstantiateSceneAsset(
|
|
|
621
682
|
}
|
|
622
683
|
const childOfToken = world.components.resolve('ChildOf');
|
|
623
684
|
|
|
624
|
-
const
|
|
685
|
+
const compiled = compileKeyedSceneAsset(world, handle, asset, {
|
|
686
|
+
resolveSource: (source, parentHandle) => worldResolveMountSource(world, source, parentHandle),
|
|
687
|
+
resolveAsset: (childHandle) => worldResolveSceneAsset(world, childHandle),
|
|
688
|
+
stack,
|
|
689
|
+
});
|
|
690
|
+
if (!compiled.ok) return err(compiled.error as EcsError);
|
|
691
|
+
const compiledAsset = compiled.value.asset;
|
|
692
|
+
const membersRes = worldSpawnSceneMembers(
|
|
693
|
+
world,
|
|
694
|
+
handle,
|
|
695
|
+
compiledAsset,
|
|
696
|
+
stack,
|
|
697
|
+
diagnostics,
|
|
698
|
+
compiled.value.mountKeyByLocalId,
|
|
699
|
+
);
|
|
625
700
|
if (!membersRes.ok) return membersRes;
|
|
626
701
|
const { mapping, entityToLocalId, rootEntities, mountEntitiesNeedingRootParent, totalSlots } =
|
|
627
702
|
membersRes.value;
|
|
628
703
|
const { mountInstances } = membersRes.value;
|
|
629
|
-
const ownMounts =
|
|
704
|
+
const ownMounts = compiledAsset.mounts ?? [];
|
|
630
705
|
|
|
631
706
|
// 3. Spawn the synthetic root entity carrying SceneInstance.
|
|
632
707
|
// First alloc the state ref so the SceneInstance.state column has a
|
|
@@ -697,7 +772,11 @@ export function worldInstantiateSceneAsset(
|
|
|
697
772
|
const memberEntityRaw = mapping[lid as unknown as number];
|
|
698
773
|
if (memberEntityRaw !== undefined && memberEntityRaw !== ENTITY_NULL_RAW) {
|
|
699
774
|
const memberEntity = memberEntityRaw as unknown as EntityHandle;
|
|
700
|
-
const applyRes = worldApplyMountOverride(
|
|
775
|
+
const applyRes = worldApplyMountOverride(
|
|
776
|
+
world,
|
|
777
|
+
memberEntity,
|
|
778
|
+
worldRemapMountOverride(world, ov, mapping),
|
|
779
|
+
);
|
|
701
780
|
if (!applyRes.ok) {
|
|
702
781
|
return applyRes as Result<EntityHandle, EcsError>;
|
|
703
782
|
}
|
|
@@ -707,16 +786,11 @@ export function worldInstantiateSceneAsset(
|
|
|
707
786
|
|
|
708
787
|
const detached = new Set<LocalEntityId>();
|
|
709
788
|
const bindings = new Map<string, EntityHandle>();
|
|
710
|
-
for (const entity of asset.entities) {
|
|
711
|
-
if (entity.bindingKey === undefined) continue;
|
|
712
|
-
const live = mapping[entity.localId as unknown as number];
|
|
713
|
-
if (live !== undefined && live !== ENTITY_NULL_RAW) {
|
|
714
|
-
bindings.set(entity.bindingKey, live as unknown as EntityHandle);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
789
|
const state: Record<string, unknown> = {
|
|
718
790
|
source: handle,
|
|
719
|
-
sceneSourceKey:
|
|
791
|
+
...(sceneSourceKey === undefined ? {} : { sceneSourceKey }),
|
|
792
|
+
keyByLocalId: new Map(compiled.value.keyByLocalId),
|
|
793
|
+
...(instanceKey === undefined ? {} : { instanceKey }),
|
|
720
794
|
bindings,
|
|
721
795
|
entityToLocalId,
|
|
722
796
|
detachedLocalIds: detached,
|
|
@@ -732,6 +806,10 @@ export function worldInstantiateSceneAsset(
|
|
|
732
806
|
// re-use the slot we allocated above by writing directly into the
|
|
733
807
|
// payloads map via a `_setUniqueRefPayload` shim.
|
|
734
808
|
worldSetUniqueRefPayload(world, stateRef, state);
|
|
809
|
+
// Populate direct and nested keyed addresses only after this root state is
|
|
810
|
+
// visible. Child SceneInstance states were published by the recursive spawn
|
|
811
|
+
// above, so the same walk can project the complete address closure.
|
|
812
|
+
collectSceneEntityBindings(world, rootEntity, [], bindings);
|
|
735
813
|
|
|
736
814
|
// 5. Wire ChildOf for every owned root entity (no ChildOf at layer-1)
|
|
737
815
|
// to the synthetic root.
|
|
@@ -792,7 +870,20 @@ export function worldInstantiateSceneAssetFlat(
|
|
|
792
870
|
stack: Set<number>,
|
|
793
871
|
diagnostics: SceneInstantiateDiagnostic[],
|
|
794
872
|
): Result<{ roots: EntityHandle[]; mountEntities: EntityHandle[] }, EcsError> {
|
|
795
|
-
const
|
|
873
|
+
const compiled = compileKeyedSceneAsset(world, handle, asset, {
|
|
874
|
+
resolveSource: (source, parentHandle) => worldResolveMountSource(world, source, parentHandle),
|
|
875
|
+
resolveAsset: (childHandle) => worldResolveSceneAsset(world, childHandle),
|
|
876
|
+
stack,
|
|
877
|
+
});
|
|
878
|
+
if (!compiled.ok) return err(compiled.error as EcsError);
|
|
879
|
+
const membersRes = worldSpawnSceneMembers(
|
|
880
|
+
world,
|
|
881
|
+
handle,
|
|
882
|
+
compiled.value.asset,
|
|
883
|
+
stack,
|
|
884
|
+
diagnostics,
|
|
885
|
+
compiled.value.mountKeyByLocalId,
|
|
886
|
+
);
|
|
796
887
|
if (!membersRes.ok) return membersRes;
|
|
797
888
|
const { rootEntities, mountEntitiesNeedingRootParent, mountEntities, mountInstances } =
|
|
798
889
|
membersRes.value;
|
|
@@ -811,7 +902,11 @@ export function worldInstantiateSceneAssetFlat(
|
|
|
811
902
|
const memberEntityRaw = childMapping[childLocalId];
|
|
812
903
|
if (memberEntityRaw === undefined || memberEntityRaw === ENTITY_NULL_RAW) continue;
|
|
813
904
|
const memberEntity = memberEntityRaw as unknown as EntityHandle;
|
|
814
|
-
const applyRes = worldApplyMountOverride(
|
|
905
|
+
const applyRes = worldApplyMountOverride(
|
|
906
|
+
world,
|
|
907
|
+
memberEntity,
|
|
908
|
+
worldRemapMountOverride(world, ov, childMapping),
|
|
909
|
+
);
|
|
815
910
|
if (!applyRes.ok) {
|
|
816
911
|
return applyRes as Result<
|
|
817
912
|
{ roots: EntityHandle[]; mountEntities: EntityHandle[] },
|
|
@@ -849,21 +944,15 @@ export function worldInstantiateSceneAssetFlat(
|
|
|
849
944
|
}
|
|
850
945
|
/** @internal Build ComponentData[] for one SceneEntity, remapping localIds.
|
|
851
946
|
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* and may carry a stale / deprecated / typo'd field. The remap below builds a
|
|
856
|
-
* fresh `remappedRaw` and simply SKIPS keys absent from the schema (no input
|
|
857
|
-
* mutation — the source `raw` is never deleted-from), recording each skipped
|
|
858
|
-
* key as a non-fatal `SceneInstantiateDiagnostic` into the passed accumulator.
|
|
859
|
-
* All known fields still write through, so one bad field cannot blank the
|
|
860
|
-
* entity or the scene (C-AC-02/03/04).
|
|
947
|
+
* SceneAsset payloads use the same schema contract as explicit ECS writes.
|
|
948
|
+
* Unknown fields fail before the first entity is spawned, with the component
|
|
949
|
+
* schema's structured error. The source object is never mutated.
|
|
861
950
|
*/
|
|
862
951
|
export function worldBuildSceneEntityComponentDatas(
|
|
863
952
|
world: World,
|
|
864
|
-
node:
|
|
953
|
+
node: CompiledSceneEntity,
|
|
865
954
|
mapping: Uint32Array,
|
|
866
|
-
|
|
955
|
+
_diagnostics: SceneInstantiateDiagnostic[],
|
|
867
956
|
): Result<ComponentData[], EcsError> {
|
|
868
957
|
const out: ComponentData[] = [];
|
|
869
958
|
const nodeLocalId = node.localId as unknown as number;
|
|
@@ -877,13 +966,21 @@ export function worldBuildSceneEntityComponentDatas(
|
|
|
877
966
|
const remappedRaw: Record<string, unknown> = {};
|
|
878
967
|
for (const fieldName of Object.keys(raw)) {
|
|
879
968
|
const fieldType = schema[fieldName];
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
// validateComponentDataKeys gate stays green.
|
|
969
|
+
// Do not mutate the source `raw`. SceneAsset compilation normally catches
|
|
970
|
+
// this earlier; this guard keeps the private numeric projection fail-fast
|
|
971
|
+
// for callers that provide a precompiled asset.
|
|
884
972
|
if (fieldType === undefined) {
|
|
885
|
-
|
|
886
|
-
|
|
973
|
+
return err({
|
|
974
|
+
code: 'spawn-data-unknown-field',
|
|
975
|
+
expected: `field name in {${Object.keys(schema).sort().join(', ')}}`,
|
|
976
|
+
hint: `unknown field '${fieldName}' on component '${compName}' at scene localId ${nodeLocalId}`,
|
|
977
|
+
detail: {
|
|
978
|
+
component: compName,
|
|
979
|
+
field: fieldName,
|
|
980
|
+
entity: nodeLocalId,
|
|
981
|
+
knownFields: Object.keys(schema).sort(),
|
|
982
|
+
},
|
|
983
|
+
} as unknown as EcsError);
|
|
887
984
|
}
|
|
888
985
|
const value = (raw as Record<string, unknown>)[fieldName];
|
|
889
986
|
const kind = classifyEntityField(token, fieldName);
|
|
@@ -905,6 +1002,46 @@ export function worldBuildSceneEntityComponentDatas(
|
|
|
905
1002
|
}
|
|
906
1003
|
return ok(out);
|
|
907
1004
|
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Resolve the private local-slot values produced by keyed SceneAsset
|
|
1008
|
+
* compilation before an instance override is written to a live ECS row.
|
|
1009
|
+
* Override references are authored in the declaring parent namespace, while
|
|
1010
|
+
* `worldApplyMountOverride` deliberately accepts ordinary live component data.
|
|
1011
|
+
*/
|
|
1012
|
+
function worldRemapMountOverride(
|
|
1013
|
+
world: World,
|
|
1014
|
+
override: MountOverride,
|
|
1015
|
+
mapping: Uint32Array,
|
|
1016
|
+
): MountOverride {
|
|
1017
|
+
const token = world.components.resolve(override.comp);
|
|
1018
|
+
if (token === undefined) return override;
|
|
1019
|
+
const remapField = (field: string, value: unknown): unknown => {
|
|
1020
|
+
const kind = classifyEntityField(token as Component, field);
|
|
1021
|
+
if (kind === null) return value;
|
|
1022
|
+
const toLive = (slot: number): number => {
|
|
1023
|
+
if (slot < 0 || slot >= mapping.length) return ENTITY_NULL_RAW;
|
|
1024
|
+
return mapping[slot] ?? ENTITY_NULL_RAW;
|
|
1025
|
+
};
|
|
1026
|
+
return remapEntityFieldValue(value, kind, toLive);
|
|
1027
|
+
};
|
|
1028
|
+
if (override.field !== undefined) {
|
|
1029
|
+
return { ...override, value: remapField(override.field, override.value) };
|
|
1030
|
+
}
|
|
1031
|
+
if (
|
|
1032
|
+
typeof override.value !== 'object' ||
|
|
1033
|
+
override.value === null ||
|
|
1034
|
+
Array.isArray(override.value)
|
|
1035
|
+
) {
|
|
1036
|
+
return override;
|
|
1037
|
+
}
|
|
1038
|
+
const value: Record<string, unknown> = {};
|
|
1039
|
+
for (const [field, fieldValue] of Object.entries(override.value as Record<string, unknown>)) {
|
|
1040
|
+
value[field] = remapField(field, fieldValue);
|
|
1041
|
+
}
|
|
1042
|
+
return { ...override, value };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
908
1045
|
/**
|
|
909
1046
|
* @internal feat-20260713 M2 / w8: apply one MountOverride to a live member
|
|
910
1047
|
* entity column. The `field?` shape is the add-or-patch discriminant:
|
|
@@ -1058,7 +1195,7 @@ export function worldSpawnMountEntity(
|
|
|
1058
1195
|
mapping: Uint32Array,
|
|
1059
1196
|
diagnostics: SceneInstantiateDiagnostic[],
|
|
1060
1197
|
): Result<EntityHandle, EcsError> {
|
|
1061
|
-
const fakeNode:
|
|
1198
|
+
const fakeNode: CompiledSceneEntity = {
|
|
1062
1199
|
localId: mount.localId,
|
|
1063
1200
|
components: mount.components ?? {},
|
|
1064
1201
|
};
|
|
@@ -1203,8 +1340,11 @@ export function worldResolveSceneEntity(
|
|
|
1203
1340
|
): Result<EntityHandle, EcsError> {
|
|
1204
1341
|
const state = worldResolveSceneInstanceStatePayload(world, root);
|
|
1205
1342
|
if (!state.ok) return state;
|
|
1343
|
+
// Anonymous POD scenes remain addressable with an explicit empty source key.
|
|
1344
|
+
// Never let the caller supply the identity used for the comparison: that
|
|
1345
|
+
// would make an anonymous instance accept a fabricated persistent ref.
|
|
1206
1346
|
const resolved = resolveSceneEntity(ref, {
|
|
1207
|
-
sceneSourceKey: state.value.sceneSourceKey ??
|
|
1347
|
+
sceneSourceKey: state.value.sceneSourceKey ?? '',
|
|
1208
1348
|
bindings: state.value.bindings,
|
|
1209
1349
|
});
|
|
1210
1350
|
if (!resolved.ok) return err(resolved.error as unknown as EcsError);
|
|
@@ -1428,9 +1568,8 @@ export function worldRemoveSceneOverride<S extends ComponentSchema>(
|
|
|
1428
1568
|
// Look up the source SceneAsset layer-1 value.
|
|
1429
1569
|
const assetRes = worldResolveSceneAsset(world, state.source);
|
|
1430
1570
|
if (!assetRes.ok) return assetRes;
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
);
|
|
1571
|
+
const key = state.keyByLocalId.get(lid as unknown as number);
|
|
1572
|
+
const node = key === undefined ? undefined : assetRes.value.entities[key];
|
|
1434
1573
|
const layer1 = node?.components[component.name] as Record<string, unknown> | undefined;
|
|
1435
1574
|
if (layer1 !== undefined && field in layer1) {
|
|
1436
1575
|
const r = world.set(member, component, { [field]: layer1[field] } as Partial<InputShapeOf<S>>);
|
|
@@ -1489,9 +1628,7 @@ export function worldGetSceneAssetForInstance(
|
|
|
1489
1628
|
* reachable from indegree-0 (the fallback caller handles cycle reporting via
|
|
1490
1629
|
* `pack-cyclic-reference` at the upstream scanner / runtime path).
|
|
1491
1630
|
*/
|
|
1492
|
-
function sceneTopoSort(
|
|
1493
|
-
nodes: readonly import('@forgeax/engine-types').SceneEntity[],
|
|
1494
|
-
): readonly number[] {
|
|
1631
|
+
function sceneTopoSort(nodes: readonly CompiledSceneEntity[]): readonly number[] {
|
|
1495
1632
|
const n = nodes.length;
|
|
1496
1633
|
const childrenOf: number[][] = Array.from({ length: n }, () => []);
|
|
1497
1634
|
const indeg = new Uint32Array(n);
|
package/src/instances/state.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
-
import type { Handle, LocalEntityId
|
|
2
|
+
import type { Handle, LocalEntityId } from '@forgeax/engine-types';
|
|
3
|
+
import type { MountOverride } from './runtime-types.js';
|
|
3
4
|
|
|
4
5
|
/** Internal state retained by a SceneInstance root. */
|
|
5
6
|
export interface SceneInstanceStatePayload {
|
|
6
7
|
readonly source: Handle<'SceneAsset', 'shared'>;
|
|
7
8
|
readonly sceneSourceKey?: string;
|
|
9
|
+
/** Authored key for each private numeric slot, retained for collection. */
|
|
10
|
+
readonly keyByLocalId: Map<number, string>;
|
|
11
|
+
/** Authored key of this instance when it is nested in a parent scene. */
|
|
12
|
+
readonly instanceKey?: string;
|
|
8
13
|
readonly bindings: Map<string, EntityHandle>;
|
|
9
14
|
readonly entityToLocalId: Map<EntityHandle, LocalEntityId>;
|
|
10
15
|
readonly detachedLocalIds: Set<LocalEntityId>;
|