@forgeax/engine-runtime 0.1.27 → 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 +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -30
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-dynamic-geometry.browser.test.ts +326 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -1,23 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Coverage:
|
|
6
|
-
// (a) AC-10 happy path: mounts[].source (integer index) resolved through
|
|
7
|
-
// refs[] to a GUID string.
|
|
8
|
-
// (b) AC-10 back-compat: payload with no mounts field passes through.
|
|
9
|
-
// (c) AC-10 back-compat: mounts with source already a string (GUID) passes
|
|
10
|
-
// through unchanged.
|
|
11
|
-
// (d) AC-10 error path: mounts[].source integer index out-of-bounds returns
|
|
12
|
-
// a ParseSceneError.
|
|
13
|
-
// (e) AC-11: mount.source does NOT enter HANDLE_FIELD_NAMES -- it is resolved
|
|
14
|
-
// positionally by field name within the mounts[] array, not through the
|
|
15
|
-
// handle-field allowlist (verify no false collision with existing handle
|
|
16
|
-
// fields named 'source').
|
|
17
|
-
// (f) mounts.memberFirst / mounts.memberCount / mounts.parent (LocalEntityId)
|
|
18
|
-
// are NOT refs[] indices -- they are local entity ids preserved as-is.
|
|
1
|
+
// Keyed SceneAsset registry and decode closure.
|
|
2
|
+
// The public payload uses entity keys and nested `instance` declarations;
|
|
3
|
+
// numeric slots are derived only by the Scene owner at instantiate time.
|
|
19
4
|
|
|
20
|
-
import { AnimationPlayer } from '@forgeax/engine-animation';
|
|
21
5
|
import {
|
|
22
6
|
type Asset,
|
|
23
7
|
AssetRegistry,
|
|
@@ -27,478 +11,41 @@ import {
|
|
|
27
11
|
import { type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
28
12
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
29
13
|
import { MeshFilter, SceneInstance } from '@forgeax/engine-render';
|
|
30
|
-
import { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
BUILTIN_BASE,
|
|
34
|
-
type Handle,
|
|
35
|
-
type LoadContext,
|
|
36
|
-
type MeshAsset,
|
|
37
|
-
type SceneAsset,
|
|
38
|
-
unwrapHandle,
|
|
39
|
-
} from '@forgeax/engine-types';
|
|
14
|
+
import { ChildOf, Transform, worldResolveSceneEntity } from '@forgeax/engine-scene';
|
|
15
|
+
import type { Handle, LoadContext, MeshAsset, SceneAsset } from '@forgeax/engine-types';
|
|
16
|
+
import { BUILTIN_BASE } from '@forgeax/engine-types';
|
|
40
17
|
import { describe, expect, it } from 'vitest';
|
|
41
18
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
42
19
|
|
|
43
|
-
/** Access the private parseAssetPayload method via structural view-cast. */
|
|
44
|
-
function accessParseScenePayload(reg: AssetRegistry) {
|
|
45
|
-
// biome-ignore lint/suspicious/noExplicitAny: private method access for unit test
|
|
46
|
-
const internal = reg as any as {
|
|
47
|
-
parseAssetPayload(kind: string, payload: Record<string, unknown>, refs?: string[]): unknown;
|
|
48
|
-
};
|
|
49
|
-
return (
|
|
50
|
-
kind: string,
|
|
51
|
-
payload: Record<string, unknown>,
|
|
52
|
-
refs?: string[],
|
|
53
|
-
): SceneAsset | undefined => {
|
|
54
|
-
const result = internal.parseAssetPayload(kind, payload, refs);
|
|
55
|
-
if (result === undefined) return undefined;
|
|
56
|
-
if (
|
|
57
|
-
typeof result === 'object' &&
|
|
58
|
-
result !== null &&
|
|
59
|
-
'kind' in result &&
|
|
60
|
-
result.kind === 'scene'
|
|
61
|
-
) {
|
|
62
|
-
return result as SceneAsset;
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
20
|
const GUID_A = 'cbe42beb-8975-5096-b3a1-3dda4cb4c077';
|
|
69
21
|
const GUID_B = 'f6af7007-158f-4d92-9e47-93bf2f213e1f';
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
describe('w27 - parseScenePayload mounts[].source refs resolution (AC-10)', () => {
|
|
73
|
-
it('(a) resolves mounts[].source integer index through refs[] to GUID string', () => {
|
|
74
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
75
|
-
const fn = accessParseScenePayload(reg);
|
|
76
|
-
const refs = [GUID_A];
|
|
77
|
-
const payload = {
|
|
78
|
-
entities: [{ localId: 0, components: {} }],
|
|
79
|
-
mounts: [
|
|
80
|
-
{
|
|
81
|
-
localId: 1,
|
|
82
|
-
source: 0,
|
|
83
|
-
memberFirst: 2,
|
|
84
|
-
memberCount: 3,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
const asset = fn('scene', payload, refs);
|
|
89
|
-
expect(asset).toBeDefined();
|
|
90
|
-
if (!asset) return;
|
|
91
|
-
expect(asset.kind).toBe('scene');
|
|
92
|
-
expect(asset.mounts).toBeDefined();
|
|
93
|
-
if (!asset.mounts) return;
|
|
94
|
-
expect(asset.mounts.length).toBe(1);
|
|
95
|
-
const mount = asset.mounts[0];
|
|
96
|
-
expect(mount).toBeDefined();
|
|
97
|
-
if (!mount) return;
|
|
98
|
-
// source 0 -> refs[0] = GUID_A
|
|
99
|
-
expect(mount.source).toBe(GUID_A);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('(b) payload with no mounts field passes through (back-compat)', () => {
|
|
103
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
104
|
-
const fn = accessParseScenePayload(reg);
|
|
105
|
-
const payload = {
|
|
106
|
-
entities: [{ localId: 0, components: {} }],
|
|
107
|
-
};
|
|
108
|
-
const asset = fn('scene', payload);
|
|
109
|
-
expect(asset).toBeDefined();
|
|
110
|
-
if (!asset) return;
|
|
111
|
-
expect(asset.kind).toBe('scene');
|
|
112
|
-
// mounts absent or empty after parse
|
|
113
|
-
expect(asset.mounts ?? []).toEqual([]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('(c) mounts with source already a string passes through unchanged', () => {
|
|
117
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
118
|
-
const fn = accessParseScenePayload(reg);
|
|
119
|
-
const refs = [GUID_A];
|
|
120
|
-
const payload = {
|
|
121
|
-
entities: [{ localId: 0, components: {} }],
|
|
122
|
-
mounts: [
|
|
123
|
-
{
|
|
124
|
-
localId: 1,
|
|
125
|
-
source: GUID_B, // already a GUID string, not an index
|
|
126
|
-
memberFirst: 2,
|
|
127
|
-
memberCount: 3,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
};
|
|
131
|
-
const asset = fn('scene', payload, refs);
|
|
132
|
-
expect(asset).toBeDefined();
|
|
133
|
-
if (!asset) return;
|
|
134
|
-
expect(asset.mounts).toBeDefined();
|
|
135
|
-
if (!asset.mounts) return;
|
|
136
|
-
expect(asset.mounts[0]?.source).toBe(GUID_B);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('(d) mounts[].source integer index out-of-bounds returns ParseSceneError', () => {
|
|
140
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
141
|
-
const fn = accessParseScenePayload(reg);
|
|
142
|
-
const refs = [GUID_A]; // only 1 entry, indices 0 valid, 1+ invalid
|
|
143
|
-
const payload = {
|
|
144
|
-
entities: [{ localId: 0, components: {} }],
|
|
145
|
-
mounts: [
|
|
146
|
-
{
|
|
147
|
-
localId: 1,
|
|
148
|
-
source: 5, // out-of-bounds: refs.length == 1
|
|
149
|
-
memberFirst: 2,
|
|
150
|
-
memberCount: 3,
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
};
|
|
154
|
-
const asset = fn('scene', payload, refs);
|
|
155
|
-
expect(asset).not.toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('(e) mounts[].source is NOT resolved through HANDLE_FIELD_NAMES (AC-11 positional)', () => {
|
|
159
|
-
// Even if 'source' were in HANDLE_FIELD_NAMES (it is not), mount.source
|
|
160
|
-
// should be resolved through the mounts[] array pipeline, not the handle-
|
|
161
|
-
// field allowlist. This test verifies no collision.
|
|
162
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
163
|
-
const fn = accessParseScenePayload(reg);
|
|
164
|
-
// Use a mock where entities[].components has a field named 'source' that is
|
|
165
|
-
// NOT in HANDLE_FIELD_NAMES. This verifies mount.source resolution is
|
|
166
|
-
// independent of entities[].components resolution.
|
|
167
|
-
const refs = [GUID_A, GUID_B];
|
|
168
|
-
const payload = {
|
|
169
|
-
entities: [{ localId: 0, components: { Transform: { source: 1 } } }],
|
|
170
|
-
mounts: [
|
|
171
|
-
{
|
|
172
|
-
localId: 2,
|
|
173
|
-
source: 0, // refs[0] = GUID_A
|
|
174
|
-
memberFirst: 3,
|
|
175
|
-
memberCount: 2,
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
};
|
|
179
|
-
const asset = fn('scene', payload, refs);
|
|
180
|
-
expect(asset).toBeDefined();
|
|
181
|
-
if (!asset) return;
|
|
182
|
-
// mount.source should be resolved via mounts pipeline
|
|
183
|
-
expect(asset.mounts).toBeDefined();
|
|
184
|
-
if (!asset.mounts) return;
|
|
185
|
-
expect(asset.mounts[0]?.source).toBe(GUID_A);
|
|
186
|
-
// entities[].components.Transform.source is NOT a handle field, so it
|
|
187
|
-
// should stay as the raw number (1), not be resolved.
|
|
188
|
-
const comp = asset.entities[0]?.components as Record<string, Record<string, unknown>>;
|
|
189
|
-
expect(comp.Transform?.source).toBe(1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('(f) mounts memberFirst/memberCount/localId/parent are preserved as numbers (not refs indices)', () => {
|
|
193
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
194
|
-
const fn = accessParseScenePayload(reg);
|
|
195
|
-
const refs = [GUID_A];
|
|
196
|
-
const payload = {
|
|
197
|
-
entities: [{ localId: 0, components: {} }],
|
|
198
|
-
mounts: [
|
|
199
|
-
{
|
|
200
|
-
localId: 5,
|
|
201
|
-
source: 0,
|
|
202
|
-
memberFirst: 10,
|
|
203
|
-
memberCount: 4,
|
|
204
|
-
parent: 3,
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
};
|
|
208
|
-
const asset = fn('scene', payload, refs);
|
|
209
|
-
expect(asset).toBeDefined();
|
|
210
|
-
if (!asset) return;
|
|
211
|
-
expect(asset.mounts).toBeDefined();
|
|
212
|
-
if (!asset.mounts) return;
|
|
213
|
-
const mount = asset.mounts[0];
|
|
214
|
-
expect(mount).toBeDefined();
|
|
215
|
-
if (!mount) return;
|
|
216
|
-
expect(mount.localId).toBe(5);
|
|
217
|
-
expect(mount.memberFirst).toBe(10);
|
|
218
|
-
expect(mount.memberCount).toBe(4);
|
|
219
|
-
expect(mount.parent).toBe(3);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('(g) multiple mounts with interleaved source indices resolved correctly', () => {
|
|
223
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
224
|
-
const fn = accessParseScenePayload(reg);
|
|
225
|
-
const refs = [GUID_A, GUID_B, GUID_C];
|
|
226
|
-
const payload = {
|
|
227
|
-
entities: [{ localId: 0, components: {} }],
|
|
228
|
-
mounts: [
|
|
229
|
-
{ localId: 1, source: 0, memberFirst: 2, memberCount: 3 },
|
|
230
|
-
{ localId: 2, source: 2, memberFirst: 5, memberCount: 1 },
|
|
231
|
-
{ localId: 3, source: 1, memberFirst: 6, memberCount: 4 },
|
|
232
|
-
],
|
|
233
|
-
};
|
|
234
|
-
const asset = fn('scene', payload, refs);
|
|
235
|
-
expect(asset).toBeDefined();
|
|
236
|
-
if (!asset) return;
|
|
237
|
-
expect(asset.mounts).toBeDefined();
|
|
238
|
-
if (!asset.mounts) return;
|
|
239
|
-
expect(asset.mounts.length).toBe(3);
|
|
240
|
-
expect(asset.mounts[0]?.source).toBe(GUID_A);
|
|
241
|
-
expect(asset.mounts[1]?.source).toBe(GUID_C);
|
|
242
|
-
expect(asset.mounts[2]?.source).toBe(GUID_B);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// -- M4 / F21: sceneLoader returns ParseSceneError (AC-09 + AC-10) [w15] --
|
|
247
|
-
|
|
248
|
-
describe('sceneLoader returns ParseSceneError (F21 / AC-09 + AC-10) [w15]', () => {
|
|
249
|
-
it('AC-09 concurrent scene loads: A fails with index error, B succeeds -- no cross-contamination', () => {
|
|
250
|
-
// A: scene payload with an entity field that has a refs[] index out of bounds.
|
|
251
|
-
const payloadA = {
|
|
252
|
-
entities: [
|
|
253
|
-
{
|
|
254
|
-
localId: 100,
|
|
255
|
-
components: {
|
|
256
|
-
MeshRenderer: { material: 5 }, // scalar handle, out-of-bounds: refs has only 1 entry
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
};
|
|
261
|
-
const refsA = [GUID_A]; // indices: 0 valid, 5 invalid
|
|
262
|
-
|
|
263
|
-
// B: valid scene payload with a single entity, no mounts.
|
|
264
|
-
const payloadB = {
|
|
265
|
-
entities: [
|
|
266
|
-
{
|
|
267
|
-
localId: 200,
|
|
268
|
-
components: {
|
|
269
|
-
Transform: { pos: [1, 2, 3] },
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
};
|
|
274
|
-
const refsB = [GUID_B];
|
|
275
|
-
|
|
276
|
-
// LoadContext without reportParseError (F21: removed).
|
|
277
|
-
const ctx: LoadContext = {
|
|
278
|
-
fetchBinary: async () => ({ ok: false, error: new Error('not wired') }),
|
|
279
|
-
resolveRef: async () => ({ ok: false, error: new Error('not wired') }),
|
|
280
|
-
transcodeCaps: { bc: false, etc2: false, astc: false },
|
|
281
|
-
device: undefined,
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Concurrent calls to sceneLoader.load.
|
|
285
|
-
const resultA = sceneLoader.load(payloadA, refsA, ctx);
|
|
286
|
-
const resultB = sceneLoader.load(payloadB, refsB, ctx);
|
|
287
|
-
|
|
288
|
-
// B: must return a valid SceneAsset.
|
|
289
|
-
expect(resultB).toBeDefined();
|
|
290
|
-
expect(typeof resultB).toBe('object');
|
|
291
|
-
if (resultB !== null) {
|
|
292
|
-
// Avoid the Promise arm by guarding on 'then'.
|
|
293
|
-
// biome-ignore lint/suspicious/noExplicitAny: guard
|
|
294
|
-
if (typeof (resultB as any).then !== 'function') {
|
|
295
|
-
expect((resultB as { kind: string }).kind).toBe('scene');
|
|
296
|
-
const sceneB = resultB as { kind: string; entities: Array<{ localId: number }> };
|
|
297
|
-
const entityIds = sceneB.entities.map((e) => e.localId);
|
|
298
|
-
expect(entityIds).not.toContain(100);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// A: should return the structured error { ok: false, error: ParseErrorDetail }.
|
|
303
|
-
expect(resultA).toBeDefined();
|
|
304
|
-
expect(typeof resultA).toBe('object');
|
|
305
|
-
if (resultA !== null && typeof resultA === 'object') {
|
|
306
|
-
// biome-ignore lint/suspicious/noExplicitAny: guard
|
|
307
|
-
if (typeof (resultA as any).then !== 'function') {
|
|
308
|
-
const errResult = resultA as {
|
|
309
|
-
ok: boolean;
|
|
310
|
-
error?: {
|
|
311
|
-
localId: number;
|
|
312
|
-
component: string;
|
|
313
|
-
field: string;
|
|
314
|
-
index: number;
|
|
315
|
-
refsLength: number;
|
|
316
|
-
};
|
|
317
|
-
};
|
|
318
|
-
expect(errResult.ok).toBe(false);
|
|
319
|
-
expect(errResult.error).toBeDefined();
|
|
320
|
-
if (errResult.error) {
|
|
321
|
-
expect(errResult.error.localId).toBe(100);
|
|
322
|
-
expect(errResult.error.component).toBe('MeshRenderer');
|
|
323
|
-
expect(errResult.error.field).toBe('material');
|
|
324
|
-
expect(errResult.error.index).toBe(5);
|
|
325
|
-
expect(errResult.error.refsLength).toBe(1);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('AC-10: sceneLoader.load with valid payload returns SceneAsset (no shared-slot error)', () => {
|
|
332
|
-
const payload = {
|
|
333
|
-
entities: [
|
|
334
|
-
{
|
|
335
|
-
localId: 1,
|
|
336
|
-
components: {
|
|
337
|
-
Transform: { pos: [0, 0, 0] },
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
],
|
|
341
|
-
};
|
|
342
|
-
const refs = [GUID_A];
|
|
343
|
-
|
|
344
|
-
const ctx: LoadContext = {
|
|
345
|
-
fetchBinary: async () => ({ ok: false, error: new Error('not wired') }),
|
|
346
|
-
resolveRef: async () => ({ ok: false, error: new Error('not wired') }),
|
|
347
|
-
transcodeCaps: { bc: false, etc2: false, astc: false },
|
|
348
|
-
device: undefined,
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const result = sceneLoader.load(payload, refs, ctx);
|
|
352
|
-
expect(result).toBeDefined();
|
|
353
|
-
// Must be a SceneAsset, not an error { ok: false }.
|
|
354
|
-
if (result !== undefined && typeof result === 'object' && result !== null) {
|
|
355
|
-
// biome-ignore lint/suspicious/noExplicitAny: guard
|
|
356
|
-
if (typeof (result as any).then !== 'function') {
|
|
357
|
-
expect('ok' in (result as Record<string, unknown>)).toBe(false);
|
|
358
|
-
expect((result as { kind: string }).kind).toBe('scene');
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// -- M4 / F21: integration - concurrent scene load + grep zero-hits (AC-09/AC-10) [w19] --
|
|
365
|
-
|
|
366
|
-
describe('integration: concurrent scene load via parseAssetPayload (F21 / AC-09 + AC-10) [w19]', () => {
|
|
367
|
-
it('two concurrent parseAssetPayload calls: A fails with ParseSceneError, B succeeds without cross-contamination', () => {
|
|
368
|
-
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
369
|
-
|
|
370
|
-
// A: refs index out of bounds -> ParseSceneError from sceneLoader.
|
|
371
|
-
const payloadA = {
|
|
372
|
-
entities: [
|
|
373
|
-
{
|
|
374
|
-
localId: 100,
|
|
375
|
-
components: {
|
|
376
|
-
MeshRenderer: { material: 5 }, // out-of-bounds (refs has only 1 entry)
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
};
|
|
381
|
-
const refsA = [GUID_A];
|
|
382
|
-
|
|
383
|
-
// B: valid scene payload.
|
|
384
|
-
const payloadB = {
|
|
385
|
-
entities: [
|
|
386
|
-
{
|
|
387
|
-
localId: 200,
|
|
388
|
-
components: {
|
|
389
|
-
Transform: { pos: [1, 2, 3] },
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
],
|
|
393
|
-
};
|
|
394
|
-
const refsB = [GUID_B];
|
|
395
|
-
|
|
396
|
-
// Access parseAndReturnAsset privately.
|
|
397
|
-
const internal = reg as unknown as {
|
|
398
|
-
parseAndReturnAsset(entry: {
|
|
399
|
-
kind: string;
|
|
400
|
-
payload: Record<string, unknown>;
|
|
401
|
-
refs?: string[];
|
|
402
|
-
}): { ok: boolean; value?: unknown; error?: { code: string; detail?: { localId: number } } };
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const resultA = internal.parseAndReturnAsset({
|
|
406
|
-
kind: 'scene',
|
|
407
|
-
payload: payloadA,
|
|
408
|
-
refs: refsA,
|
|
409
|
-
});
|
|
410
|
-
const resultB = internal.parseAndReturnAsset({
|
|
411
|
-
kind: 'scene',
|
|
412
|
-
payload: payloadB,
|
|
413
|
-
refs: refsB,
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// A: must be an error with detail containing A's localId.
|
|
417
|
-
expect(resultA.ok).toBe(false);
|
|
418
|
-
if (!resultA.ok) {
|
|
419
|
-
expect(resultA.error?.code).toBe('asset-parse-failed');
|
|
420
|
-
const detail = resultA.error?.detail as { localId: number } | undefined;
|
|
421
|
-
expect(detail?.localId).toBe(100);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// B: must be a valid SceneAsset.
|
|
425
|
-
// feat-20260622 M4 / w12: parseAndReturnAsset returns { asset, refs } so
|
|
426
|
-
// the recursive core can read envelope.refs (D-5); unwrap `.asset` here.
|
|
427
|
-
expect(resultB.ok).toBe(true);
|
|
428
|
-
if (resultB.ok) {
|
|
429
|
-
const asset = (
|
|
430
|
-
resultB.value as { asset: { kind: string; entities: Array<{ localId: number }> } }
|
|
431
|
-
).asset;
|
|
432
|
-
expect(asset.kind).toBe('scene');
|
|
433
|
-
// B's entities must NOT contain A's localId 100.
|
|
434
|
-
const entityIds = asset.entities.map((e) => e.localId);
|
|
435
|
-
expect(entityIds).not.toContain(100);
|
|
436
|
-
expect(entityIds).toContain(200);
|
|
437
|
-
}
|
|
438
|
-
});
|
|
22
|
+
const GUID_MESH = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
439
23
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
payload: Record<string, unknown>;
|
|
446
|
-
refs?: string[];
|
|
447
|
-
}): { ok: boolean; value?: unknown };
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const payload = {
|
|
451
|
-
entities: [
|
|
452
|
-
{
|
|
453
|
-
localId: 1,
|
|
454
|
-
components: {
|
|
455
|
-
Transform: { pos: [0, 0, 0] },
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
],
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const result = internal.parseAndReturnAsset({
|
|
462
|
-
kind: 'scene',
|
|
463
|
-
payload,
|
|
464
|
-
});
|
|
465
|
-
expect(result.ok).toBe(true);
|
|
466
|
-
if (result.ok) {
|
|
467
|
-
// feat-20260622 M4 / w12: parseAndReturnAsset returns { asset, refs }.
|
|
468
|
-
expect((result.value as { asset: { kind: string } }).asset.kind).toBe('scene');
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// ── feat-20260713 M3 / w10 + w11 — apply-side override value GUID→handle
|
|
474
|
-
// down-drill (AC-06, plan-strategy D-2 / D-8) ───────────────────────────────
|
|
475
|
-
//
|
|
476
|
-
// M3 opens the fourth resolution input source: `mounts[].overrides[].value`.
|
|
477
|
-
// Pre-M3 `resolveMountsRec` only `{...m}` shallow-copies each mount + resolves
|
|
478
|
-
// `source`; the GUID strings inside an override value were never resolved, so the
|
|
479
|
-
// ecs value gate (M2 / w9) rejected them as `shared-field-invalid-value` and the
|
|
480
|
-
// whole instantiate errored. After M3 the override value's shared<...> /
|
|
481
|
-
// array<shared<...>> GUID strings resolve to live handles in assets-runtime, so
|
|
482
|
-
// the ecs apply loop only ever sees numeric handles.
|
|
483
|
-
//
|
|
484
|
-
// TDD red phase: these assert the GREEN behaviour (instantiate ok + resolved
|
|
485
|
-
// handle read back on the member). They are RED until w12/w13 land because
|
|
486
|
-
// pre-fix `reg.instantiate` returns err (M2 value gate rejects the GUID string).
|
|
24
|
+
function guid(value: string): AssetGuid {
|
|
25
|
+
const parsed = AssetGuid.parse(value);
|
|
26
|
+
if (!parsed.ok) throw new Error(`bad GUID: ${value}`);
|
|
27
|
+
return parsed.value;
|
|
28
|
+
}
|
|
487
29
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
30
|
+
function context(): LoadContext {
|
|
31
|
+
return {
|
|
32
|
+
fetchBinary: async () => ({ ok: false, error: new Error('not wired') }),
|
|
33
|
+
resolveRef: async () => ({ ok: false, error: new Error('not wired') }),
|
|
34
|
+
transcodeCaps: { bc: false, etc2: false, astc: false },
|
|
35
|
+
device: undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
494
38
|
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
39
|
+
function childScene(): SceneAsset {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'scene',
|
|
42
|
+
entities: {
|
|
43
|
+
'entity-0': { components: { Transform: {}, MeshFilter: { assetHandle: 0 } } },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
499
46
|
}
|
|
500
47
|
|
|
501
|
-
function
|
|
48
|
+
function meshAsset(): MeshAsset {
|
|
502
49
|
return {
|
|
503
50
|
kind: 'mesh',
|
|
504
51
|
vertices: new Float32Array([
|
|
@@ -510,297 +57,190 @@ function mkMeshAsset(): MeshAsset {
|
|
|
510
57
|
submeshes: [
|
|
511
58
|
{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
|
|
512
59
|
],
|
|
513
|
-
|
|
514
60
|
materialSlots: [{ slotName: 'Default' }],
|
|
515
61
|
};
|
|
516
62
|
}
|
|
517
63
|
|
|
518
|
-
function
|
|
519
|
-
return { kind: 'animation-clip', duration, channels: [] };
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function makeMountWorld(): World {
|
|
64
|
+
function worldWithSceneComponents(): World {
|
|
523
65
|
const world = new World();
|
|
524
|
-
for (const component of [Transform, ChildOf, MeshFilter,
|
|
66
|
+
for (const component of [Transform, ChildOf, MeshFilter, SceneInstance]) {
|
|
525
67
|
world.components.register(component).unwrap();
|
|
526
68
|
}
|
|
527
69
|
return world;
|
|
528
70
|
}
|
|
529
71
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
function childScene(): SceneAsset {
|
|
549
|
-
// One member entity carrying Transform only (localId 0 in child namespace).
|
|
550
|
-
return { kind: 'scene', entities: [{ localId: 0 as never, components: { Transform: {} } }] };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
it('scalar shared<> override value GUID resolves to a live handle on the member', () => {
|
|
554
|
-
const reg = mkReg();
|
|
555
|
-
const world = makeMountWorld();
|
|
556
|
-
reg.catalog(pgOv(OV_MESH_GUID), mkMeshAsset());
|
|
557
|
-
reg.catalog(pgOv(SCENE_CHILD_GUID), childScene() as Asset);
|
|
558
|
-
|
|
559
|
-
// Parent: no owned entities; a mount of the child whose single member (parent
|
|
560
|
-
// namespace localId 2) gets a MeshFilter ADDED with a GUID-string assetHandle.
|
|
561
|
-
const parent: SceneAsset = {
|
|
562
|
-
kind: 'scene',
|
|
563
|
-
entities: [],
|
|
564
|
-
mounts: [
|
|
565
|
-
{
|
|
566
|
-
localId: 1 as never,
|
|
567
|
-
source: SCENE_CHILD_GUID,
|
|
568
|
-
memberFirst: 2 as never,
|
|
569
|
-
memberCount: 1,
|
|
570
|
-
overrides: [
|
|
571
|
-
{ localId: 2 as never, comp: 'MeshFilter', value: { assetHandle: OV_MESH_GUID } },
|
|
572
|
-
],
|
|
573
|
-
},
|
|
574
|
-
],
|
|
72
|
+
describe('keyed SceneAsset decode', () => {
|
|
73
|
+
it('resolves only wire refs declared by instance.source and preserves ordinary fields', () => {
|
|
74
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
75
|
+
const parse = (
|
|
76
|
+
registry as unknown as {
|
|
77
|
+
parseAssetPayload(
|
|
78
|
+
kind: string,
|
|
79
|
+
payload: Record<string, unknown>,
|
|
80
|
+
refs?: readonly string[],
|
|
81
|
+
): unknown;
|
|
82
|
+
}
|
|
83
|
+
).parseAssetPayload.bind(registry);
|
|
84
|
+
const payload = {
|
|
85
|
+
entities: {
|
|
86
|
+
root: { components: { Transform: { source: 1 } } },
|
|
87
|
+
child: { components: {}, instance: { source: 0 } },
|
|
88
|
+
},
|
|
575
89
|
};
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
expect(mf).toBeDefined();
|
|
585
|
-
const resolved = mf?.assetHandle as number;
|
|
586
|
-
// Not the sentinel 0, not a GUID string — a live user-tier handle.
|
|
587
|
-
expect(typeof resolved).toBe('number');
|
|
588
|
-
expect(resolved).toBeGreaterThanOrEqual(BUILTIN_BASE);
|
|
589
|
-
// And it resolves back to the catalogued mesh payload.
|
|
590
|
-
const payload = resolveAssetHandle<MeshAsset>(
|
|
591
|
-
world,
|
|
592
|
-
resolved as unknown as Handle<string, 'shared'>,
|
|
593
|
-
);
|
|
594
|
-
expect(payload.ok).toBe(true);
|
|
595
|
-
if (payload.ok) expect(payload.value.kind).toBe('mesh');
|
|
90
|
+
const asset = parse('scene', payload, [GUID_A]);
|
|
91
|
+
expect(asset).toMatchObject({
|
|
92
|
+
kind: 'scene',
|
|
93
|
+
entities: {
|
|
94
|
+
root: { components: { Transform: { source: 1 } } },
|
|
95
|
+
child: { instance: { source: GUID_A } },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
596
98
|
});
|
|
597
99
|
|
|
598
|
-
it('
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
100
|
+
it('rejects an instance source refs index outside the envelope', () => {
|
|
101
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
102
|
+
const parse = (
|
|
103
|
+
registry as unknown as {
|
|
104
|
+
parseAssetPayload(
|
|
105
|
+
kind: string,
|
|
106
|
+
payload: Record<string, unknown>,
|
|
107
|
+
refs?: readonly string[],
|
|
108
|
+
): unknown;
|
|
109
|
+
}
|
|
110
|
+
).parseAssetPayload.bind(registry);
|
|
111
|
+
expect(
|
|
112
|
+
parse('scene', { entities: { child: { components: {}, instance: { source: 7 } } } }, [
|
|
113
|
+
GUID_A,
|
|
114
|
+
]),
|
|
115
|
+
).toBeUndefined();
|
|
116
|
+
});
|
|
606
117
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
entities: [],
|
|
610
|
-
mounts: [
|
|
611
|
-
{
|
|
612
|
-
localId: 1 as never,
|
|
613
|
-
source: SCENE_CHILD_GUID,
|
|
614
|
-
memberFirst: 2 as never,
|
|
615
|
-
memberCount: 1,
|
|
616
|
-
// clips[0] = GUID string (resolve); clips[1] = number (pass through).
|
|
617
|
-
overrides: [
|
|
618
|
-
{
|
|
619
|
-
localId: 2 as never,
|
|
620
|
-
comp: 'AnimationPlayer',
|
|
621
|
-
value: { clips: [OV_CLIP_A_GUID, preHandle] },
|
|
622
|
-
},
|
|
623
|
-
],
|
|
624
|
-
},
|
|
625
|
-
],
|
|
118
|
+
it('loads keyed payloads concurrently without cross contaminating entity keys', () => {
|
|
119
|
+
const payloadA = {
|
|
120
|
+
entities: { 'entity-a': { components: { Transform: { pos: [0, 0, 0] } } } },
|
|
626
121
|
};
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const ap = firstMemberWith(world, r.value, AnimationPlayer);
|
|
635
|
-
expect(ap).toBeDefined();
|
|
636
|
-
const clips = ap?.clips as ArrayLike<number>;
|
|
637
|
-
// slot 0: resolved handle; slot 1: number passthrough.
|
|
638
|
-
expect(typeof clips[0]).toBe('number');
|
|
639
|
-
expect(clips[0]).toBeGreaterThanOrEqual(BUILTIN_BASE);
|
|
640
|
-
expect(clips[1]).toBe(preHandle);
|
|
641
|
-
const payload = resolveAssetHandle<AnimationClip>(
|
|
642
|
-
world,
|
|
643
|
-
clips[0] as unknown as Handle<string, 'shared'>,
|
|
644
|
-
);
|
|
645
|
-
expect(payload.ok).toBe(true);
|
|
646
|
-
if (payload.ok) expect(payload.value.duration).toBe(1.5);
|
|
122
|
+
const payloadB = {
|
|
123
|
+
entities: { 'entity-b': { components: { Transform: { pos: [1, 0, 0] } } } },
|
|
124
|
+
};
|
|
125
|
+
const a = sceneLoader.load(payloadA, [GUID_A], context());
|
|
126
|
+
const b = sceneLoader.load(payloadB, [GUID_B], context());
|
|
127
|
+
expect(a).toMatchObject({ kind: 'scene', entities: { 'entity-a': {} } });
|
|
128
|
+
expect(b).toMatchObject({ kind: 'scene', entities: { 'entity-b': {} } });
|
|
647
129
|
});
|
|
130
|
+
});
|
|
648
131
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const parent: SceneAsset = {
|
|
132
|
+
describe('keyed nested registry instantiate', () => {
|
|
133
|
+
it('projects the authored source key from the Catalog into scene bindings', () => {
|
|
134
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
135
|
+
const world = worldWithSceneComponents();
|
|
136
|
+
const scene: SceneAsset = {
|
|
655
137
|
kind: 'scene',
|
|
656
|
-
entities:
|
|
657
|
-
|
|
138
|
+
entities: { player: { components: { Transform: {} } } },
|
|
139
|
+
};
|
|
140
|
+
expect(registry.catalog(guid(GUID_A), scene as Asset).ok).toBe(true);
|
|
141
|
+
registry.packIndexCache = new Map([
|
|
142
|
+
[
|
|
143
|
+
GUID_A,
|
|
658
144
|
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
memberCount: 1,
|
|
663
|
-
overrides: [
|
|
664
|
-
{
|
|
665
|
-
localId: 2 as never,
|
|
666
|
-
comp: 'MeshFilter',
|
|
667
|
-
value: { assetHandle: OV_UNCATALOGUED_GUID },
|
|
668
|
-
},
|
|
669
|
-
],
|
|
145
|
+
packageUrl: '/assets/scene.pack.json',
|
|
146
|
+
kind: 'scene',
|
|
147
|
+
sourceKey: 'scene/showcase',
|
|
670
148
|
},
|
|
671
149
|
],
|
|
672
|
-
|
|
673
|
-
reg.catalog(pgOv(SCENE_PARENT_GUID), parent as Asset);
|
|
150
|
+
]);
|
|
674
151
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
expect(world.inspect().entityCount).toBe(before);
|
|
152
|
+
const result = registry.instantiate(world.allocSharedRef('SceneAsset', scene), world);
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
if (!result.ok) return;
|
|
155
|
+
const resolved = worldResolveSceneEntity(world, result.value, {
|
|
156
|
+
sceneSourceKey: 'scene/showcase',
|
|
157
|
+
address: 'player',
|
|
158
|
+
});
|
|
159
|
+
expect(resolved.ok).toBe(true);
|
|
684
160
|
});
|
|
685
161
|
|
|
686
|
-
it('
|
|
687
|
-
const
|
|
688
|
-
const world =
|
|
689
|
-
|
|
690
|
-
// Child member already carries MeshFilter (patch replaces its assetHandle).
|
|
691
|
-
const child: SceneAsset = {
|
|
692
|
-
kind: 'scene',
|
|
693
|
-
entities: [
|
|
694
|
-
{ localId: 0 as never, components: { Transform: {}, MeshFilter: { assetHandle: 0 } } },
|
|
695
|
-
],
|
|
696
|
-
};
|
|
697
|
-
reg.catalog(pgOv(SCENE_CHILD_GUID), child as Asset);
|
|
698
|
-
|
|
162
|
+
it('resolves direct and nested authored addresses from one instance', () => {
|
|
163
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
164
|
+
const world = worldWithSceneComponents();
|
|
165
|
+
expect(registry.catalog(guid(GUID_B), childScene() as Asset).ok).toBe(true);
|
|
699
166
|
const parent: SceneAsset = {
|
|
700
167
|
kind: 'scene',
|
|
701
|
-
entities:
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
localId: 1 as never,
|
|
705
|
-
source: SCENE_CHILD_GUID,
|
|
706
|
-
memberFirst: 2 as never,
|
|
707
|
-
memberCount: 1,
|
|
708
|
-
// field present -> PATCH one field, value is that single field's value.
|
|
709
|
-
overrides: [
|
|
710
|
-
{ localId: 2 as never, comp: 'MeshFilter', field: 'assetHandle', value: OV_MESH_GUID },
|
|
711
|
-
],
|
|
712
|
-
},
|
|
713
|
-
],
|
|
168
|
+
entities: {
|
|
169
|
+
house: { components: {}, instance: { source: GUID_B } },
|
|
170
|
+
},
|
|
714
171
|
};
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
expect(
|
|
172
|
+
const result = registry.instantiate(world.allocSharedRef('SceneAsset', parent), world);
|
|
173
|
+
expect(result.ok).toBe(true);
|
|
174
|
+
if (!result.ok) return;
|
|
175
|
+
const direct = worldResolveSceneEntity(world, result.value, {
|
|
176
|
+
sceneSourceKey: '',
|
|
177
|
+
address: 'house',
|
|
178
|
+
});
|
|
179
|
+
const nested = worldResolveSceneEntity(world, result.value, {
|
|
180
|
+
sceneSourceKey: '',
|
|
181
|
+
address: ['house', 'entity-0'],
|
|
182
|
+
});
|
|
183
|
+
expect(direct.ok).toBe(true);
|
|
184
|
+
expect(nested.ok).toBe(true);
|
|
185
|
+
if (direct.ok && nested.ok) expect(direct.value).not.toBe(nested.value);
|
|
727
186
|
});
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
describe('feat-20260713 M3 / w11 — envelope-less scene override value resolution', () => {
|
|
731
|
-
function mkReg(): AssetRegistry {
|
|
732
|
-
return new AssetRegistry(makeMockShaderRegistry());
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
it('override value GUID resolves even when the PARENT scene has no catalogued envelope', () => {
|
|
736
|
-
const reg = mkReg();
|
|
737
|
-
const world = makeMountWorld();
|
|
738
|
-
// Sub-asset + child scene are catalogued; the PARENT scene is NOT (built +
|
|
739
|
-
// allocSharedRef'd directly, no reg.catalog). sceneGuidKey is undefined so
|
|
740
|
-
// _resolveSceneGuids takes the entity-walk fallback branch; the override
|
|
741
|
-
// value must still resolve through resolveMountsRec's down-drill.
|
|
742
|
-
reg.catalog(pgOv(OV_CLIP_B_GUID), mkClipAsset(3.0));
|
|
743
|
-
reg.catalog(pgOv(SCENE_CHILD_GUID), {
|
|
744
|
-
kind: 'scene',
|
|
745
|
-
entities: [{ localId: 0 as never, components: { Transform: {} } }],
|
|
746
|
-
} as Asset);
|
|
747
187
|
|
|
188
|
+
it('resolves nested instance source and override asset GUID before spawning', () => {
|
|
189
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
190
|
+
const world = worldWithSceneComponents();
|
|
191
|
+
expect(registry.catalog(guid(GUID_MESH), meshAsset()).ok).toBe(true);
|
|
192
|
+
expect(registry.catalog(guid(GUID_B), childScene() as Asset).ok).toBe(true);
|
|
748
193
|
const parent: SceneAsset = {
|
|
749
194
|
kind: 'scene',
|
|
750
|
-
entities:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
],
|
|
195
|
+
entities: {
|
|
196
|
+
mount: {
|
|
197
|
+
components: {},
|
|
198
|
+
instance: {
|
|
199
|
+
source: GUID_B,
|
|
200
|
+
overrides: [
|
|
201
|
+
{ target: ['entity-0'], components: { MeshFilter: { assetHandle: GUID_MESH } } },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
760
204
|
},
|
|
761
|
-
|
|
205
|
+
},
|
|
762
206
|
};
|
|
763
|
-
|
|
764
|
-
// Confirm the parent really is envelope-less (no _guidForAsset hit).
|
|
765
|
-
const guidForAsset = (
|
|
766
|
-
reg as unknown as { _guidForAsset(a: Asset): string | undefined }
|
|
767
|
-
)._guidForAsset(parent as Asset);
|
|
768
|
-
expect(guidForAsset).toBeUndefined();
|
|
769
|
-
|
|
770
207
|
const handle = world.allocSharedRef('SceneAsset', parent);
|
|
771
|
-
const
|
|
772
|
-
expect(
|
|
773
|
-
if (!
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
expect(
|
|
780
|
-
|
|
208
|
+
const result = registry.instantiate(handle, world);
|
|
209
|
+
expect(result.ok).toBe(true);
|
|
210
|
+
if (!result.ok) return;
|
|
211
|
+
const instance = world.get(result.value, SceneInstance);
|
|
212
|
+
expect(instance.ok).toBe(true);
|
|
213
|
+
if (!instance.ok) return;
|
|
214
|
+
const member = instance.value.mapping[1] as unknown as EntityHandle;
|
|
215
|
+
const mesh = world.get(member, MeshFilter);
|
|
216
|
+
expect(mesh.ok).toBe(true);
|
|
217
|
+
if (!mesh.ok) return;
|
|
218
|
+
expect(mesh.value.assetHandle as unknown as number).toBeGreaterThanOrEqual(BUILTIN_BASE);
|
|
219
|
+
const payload = resolveAssetHandle<MeshAsset>(
|
|
781
220
|
world,
|
|
782
|
-
|
|
221
|
+
mesh.value.assetHandle as unknown as Handle<string, 'shared'>,
|
|
783
222
|
);
|
|
784
223
|
expect(payload.ok).toBe(true);
|
|
785
|
-
if (payload.ok) expect(payload.value.
|
|
786
|
-
});
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
// -- M4 / F21: static assertions (grep zero hits) [w19] --
|
|
790
|
-
|
|
791
|
-
describe('static assertions: lastParseSceneError / reportParseError zero hits (F21 / AC-10) [w19]', () => {
|
|
792
|
-
it('lastParseSceneError is absent from asset-registry.ts', () => {
|
|
793
|
-
// This is a documentation assertion: grep lastParseSceneError
|
|
794
|
-
// in packages/runtime/src/asset-registry.ts must return zero hits.
|
|
795
|
-
// Verified at the CI sweep step; this test exists to document
|
|
796
|
-
// the requirement and provides a placeholder that always passes.
|
|
797
|
-
expect(true).toBe(true);
|
|
224
|
+
if (payload.ok) expect(payload.value.kind).toBe('mesh');
|
|
798
225
|
});
|
|
799
226
|
|
|
800
|
-
it('
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
227
|
+
it('rejects recursive keyed instances before creating a usable root', () => {
|
|
228
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
229
|
+
const world = worldWithSceneComponents();
|
|
230
|
+
const a: SceneAsset = {
|
|
231
|
+
kind: 'scene',
|
|
232
|
+
entities: { b: { components: {}, instance: { source: GUID_B } } },
|
|
233
|
+
};
|
|
234
|
+
const b: SceneAsset = {
|
|
235
|
+
kind: 'scene',
|
|
236
|
+
entities: { a: { components: {}, instance: { source: GUID_A } } },
|
|
237
|
+
};
|
|
238
|
+
expect(registry.catalog(guid(GUID_A), a as Asset).ok).toBe(true);
|
|
239
|
+
expect(registry.catalog(guid(GUID_B), b as Asset).ok).toBe(true);
|
|
240
|
+
const handle = world.allocSharedRef('SceneAsset', a);
|
|
241
|
+
const before = world.inspect().entityCount;
|
|
242
|
+
const result = registry.instantiate(handle, world);
|
|
243
|
+
expect(result.ok).toBe(false);
|
|
244
|
+
expect(world.inspect().entityCount).toBe(before);
|
|
805
245
|
});
|
|
806
246
|
});
|