@forgeax/engine-assets-runtime 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +491 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/aabb.unit.test.d.ts +2 -0
- package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-mime-owner.test.d.ts +2 -0
- package/dist/__tests__/decode-image-mime-owner.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
- package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin.test.d.ts +2 -0
- package/dist/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
- package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
- package/dist/aabb.d.ts +3 -0
- package/dist/aabb.d.ts.map +1 -0
- package/dist/asset-kind.d.ts +4 -0
- package/dist/asset-kind.d.ts.map +1 -0
- package/dist/asset-registry.d.ts +554 -0
- package/dist/asset-registry.d.ts.map +1 -0
- package/dist/builtin-asset-registry.d.ts +22 -0
- package/dist/builtin-asset-registry.d.ts.map +1 -0
- package/dist/catalog-source.d.ts +24 -0
- package/dist/catalog-source.d.ts.map +1 -0
- package/dist/decode-image-bytes.d.ts +57 -0
- package/dist/decode-image-bytes.d.ts.map +1 -0
- package/dist/dynamic-texture-store.d.ts +89 -0
- package/dist/dynamic-texture-store.d.ts.map +1 -0
- package/dist/errors/asset.d.ts +201 -0
- package/dist/errors/asset.d.ts.map +1 -0
- package/dist/handles.d.ts +143 -0
- package/dist/handles.d.ts.map +1 -0
- package/dist/image-error.d.ts +6 -0
- package/dist/image-error.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +8359 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal/artifact-cache.d.ts +17 -0
- package/dist/internal/artifact-cache.d.ts.map +1 -0
- package/dist/internal/asset-graph.d.ts +70 -0
- package/dist/internal/asset-graph.d.ts.map +1 -0
- package/dist/internal/catalog-session.d.ts +60 -0
- package/dist/internal/catalog-session.d.ts.map +1 -0
- package/dist/internal/decoder-registry.d.ts +19 -0
- package/dist/internal/decoder-registry.d.ts.map +1 -0
- package/dist/internal/immutable-payload.d.ts +9 -0
- package/dist/internal/immutable-payload.d.ts.map +1 -0
- package/dist/internal/load-asset.d.ts +31 -0
- package/dist/internal/load-asset.d.ts.map +1 -0
- package/dist/internal/pack-reader.d.ts +18 -0
- package/dist/internal/pack-reader.d.ts.map +1 -0
- package/dist/internal/validate-runtime-row.d.ts +7 -0
- package/dist/internal/validate-runtime-row.d.ts.map +1 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +329 -0
- package/dist/internal.mjs.map +1 -0
- package/dist/loader-registry.d.ts +63 -0
- package/dist/loader-registry.d.ts.map +1 -0
- package/dist/loaders/inline-pack.d.ts +72 -0
- package/dist/loaders/inline-pack.d.ts.map +1 -0
- package/dist/loaders/mesh-bin.d.ts +21 -0
- package/dist/loaders/mesh-bin.d.ts.map +1 -0
- package/dist/loaders/pack-artifact.d.ts +20 -0
- package/dist/loaders/pack-artifact.d.ts.map +1 -0
- package/dist/material/dependency-graph.d.ts +9 -0
- package/dist/material/dependency-graph.d.ts.map +1 -0
- package/dist/material/generation-cache.d.ts +24 -0
- package/dist/material/generation-cache.d.ts.map +1 -0
- package/dist/material/inspection.d.ts +24 -0
- package/dist/material/inspection.d.ts.map +1 -0
- package/dist/material/loader.d.ts +59 -0
- package/dist/material/loader.d.ts.map +1 -0
- package/dist/material/runtime-shader.d.ts +14 -0
- package/dist/material/runtime-shader.d.ts.map +1 -0
- package/dist/mesh-bin.d.ts +3 -0
- package/dist/mesh-bin.d.ts.map +1 -0
- package/dist/mipmap-generator.d.ts +132 -0
- package/dist/mipmap-generator.d.ts.map +1 -0
- package/dist/payload-validate.d.ts +60 -0
- package/dist/payload-validate.d.ts.map +1 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/registry/artifact-io.d.ts +16 -0
- package/dist/registry/artifact-io.d.ts.map +1 -0
- package/dist/registry/asset-evidence.d.ts +13 -0
- package/dist/registry/asset-evidence.d.ts.map +1 -0
- package/dist/registry/catalog-state.d.ts +47 -0
- package/dist/registry/catalog-state.d.ts.map +1 -0
- package/dist/registry/catalog.d.ts +48 -0
- package/dist/registry/catalog.d.ts.map +1 -0
- package/dist/registry/instantiate.d.ts +153 -0
- package/dist/registry/instantiate.d.ts.map +1 -0
- package/dist/registry/load-by-guid.d.ts +247 -0
- package/dist/registry/load-by-guid.d.ts.map +1 -0
- package/dist/registry/load-state.d.ts +26 -0
- package/dist/registry/load-state.d.ts.map +1 -0
- package/dist/registry/runtime-projection.d.ts +16 -0
- package/dist/registry/runtime-projection.d.ts.map +1 -0
- package/dist/registry/scene-publication-fence.d.ts +38 -0
- package/dist/registry/scene-publication-fence.d.ts.map +1 -0
- package/dist/registry/validate-material.d.ts +77 -0
- package/dist/registry/validate-material.d.ts.map +1 -0
- package/dist/resolve-asset-handle.d.ts +43 -0
- package/dist/resolve-asset-handle.d.ts.map +1 -0
- package/dist/resolve-tileset-runtime.d.ts +23 -0
- package/dist/resolve-tileset-runtime.d.ts.map +1 -0
- package/dist/scene-handle-fields.d.ts +88 -0
- package/dist/scene-handle-fields.d.ts.map +1 -0
- package/dist/scene-payload.d.ts +15 -0
- package/dist/scene-payload.d.ts.map +1 -0
- package/dist/wire-default-loaders.d.ts +29 -0
- package/dist/wire-default-loaders.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/__tests__/aabb.unit.test.ts +97 -0
- package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
- package/src/__tests__/artifact-io.unit.test.ts +127 -0
- package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
- package/src/__tests__/asset-evidence.unit.test.ts +32 -0
- package/src/__tests__/asset-kind.unit.test.ts +14 -0
- package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
- package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
- package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
- package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
- package/src/__tests__/asset-registry.unit.test.ts +511 -0
- package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
- package/src/__tests__/catalog-replica.unit.test.ts +196 -0
- package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
- package/src/__tests__/catalog-source.unit.test.ts +126 -0
- package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
- package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
- package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
- package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
- package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
- package/src/__tests__/decode-image-bytes.test.ts +90 -0
- package/src/__tests__/decode-image-mime-owner.test.ts +38 -0
- package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
- package/src/__tests__/errors-asset.unit.test.ts +92 -0
- package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
- package/src/__tests__/instantiate-context.unit.test.ts +65 -0
- package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
- package/src/__tests__/instantiate.unit.test.ts +684 -0
- package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
- package/src/__tests__/loader-registry.unit.test.ts +101 -0
- package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
- package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
- package/src/__tests__/material-load-consumer.test-d.ts +42 -0
- package/src/__tests__/material-ready.integration.test.ts +508 -0
- package/src/__tests__/material-stale-generation.unit.test.ts +147 -0
- package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
- package/src/__tests__/mesh-bin.unit.test.ts +152 -0
- package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
- package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
- package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
- package/src/__tests__/payload-validate.unit.test.ts +285 -0
- package/src/__tests__/plugin.test.ts +55 -0
- package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
- package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
- package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
- package/src/__tests__/registry-load-state.unit.test.ts +35 -0
- package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
- package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
- package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
- package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
- package/src/__tests__/scene-payload.unit.test.ts +151 -0
- package/src/__tests__/scene-publication-fence.test.ts +129 -0
- package/src/__tests__/scriptable-pack-public-surface.test-d.ts +27 -0
- package/src/__tests__/static-rig.integration.test.ts +8 -0
- package/src/__tests__/validate-material.unit.test.ts +359 -0
- package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
- package/src/aabb.ts +107 -0
- package/src/asset-kind.ts +6 -0
- package/src/asset-registry.ts +2045 -0
- package/src/builtin-asset-registry.ts +164 -0
- package/src/catalog-source.ts +89 -0
- package/src/decode-image-bytes.ts +126 -0
- package/src/dynamic-texture-store.ts +234 -0
- package/src/errors/asset.ts +347 -0
- package/src/handles.ts +186 -0
- package/src/image-error.ts +45 -0
- package/src/index.ts +189 -0
- package/src/internal/artifact-cache.ts +65 -0
- package/src/internal/asset-graph.ts +420 -0
- package/src/internal/catalog-session.ts +345 -0
- package/src/internal/decoder-registry.ts +175 -0
- package/src/internal/immutable-payload.ts +20 -0
- package/src/internal/load-asset.ts +254 -0
- package/src/internal/pack-reader.ts +183 -0
- package/src/internal/validate-runtime-row.ts +51 -0
- package/src/internal.ts +6 -0
- package/src/loader-registry.ts +157 -0
- package/src/loaders/inline-pack.ts +837 -0
- package/src/loaders/mesh-bin.ts +304 -0
- package/src/loaders/pack-artifact.ts +665 -0
- package/src/material/dependency-graph.ts +46 -0
- package/src/material/generation-cache.ts +177 -0
- package/src/material/inspection.ts +46 -0
- package/src/material/loader.ts +326 -0
- package/src/material/runtime-shader.ts +133 -0
- package/src/mesh-bin.ts +5 -0
- package/src/mipmap-generator.ts +450 -0
- package/src/payload-validate.ts +573 -0
- package/src/plugin.ts +48 -0
- package/src/registry/artifact-io.ts +259 -0
- package/src/registry/asset-evidence.ts +51 -0
- package/src/registry/catalog-state.ts +409 -0
- package/src/registry/catalog.ts +369 -0
- package/src/registry/instantiate.ts +1056 -0
- package/src/registry/load-by-guid.ts +1657 -0
- package/src/registry/load-state.ts +115 -0
- package/src/registry/runtime-projection.ts +63 -0
- package/src/registry/scene-publication-fence.ts +256 -0
- package/src/registry/validate-material.ts +385 -0
- package/src/resolve-asset-handle.ts +147 -0
- package/src/resolve-tileset-runtime.ts +72 -0
- package/src/scene-handle-fields.ts +221 -0
- package/src/scene-payload.ts +207 -0
- package/src/wire-default-loaders.ts +79 -0
|
@@ -0,0 +1,2045 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime - AssetRegistry v2 (feat-20260513-guid-asset-package-system).
|
|
2
|
+
//
|
|
3
|
+
// Entrypoints (feat-20260614 M8 de-handle cut, D-15/D-17/D-19): the registry
|
|
4
|
+
// is a GUID->payload catalogue. It no longer mints or maps handles -- column
|
|
5
|
+
// handles are minted on the World via `world.allocSharedRef('Kind', payload)`,
|
|
6
|
+
// and resolved through the two-tier `resolveAssetHandle` (BuiltinAssetRegistry
|
|
7
|
+
// process-static slots [1,1024) + per-World `world.sharedRefs` slots >=1024).
|
|
8
|
+
//
|
|
9
|
+
// - catalog<T extends Asset>(guid, asset): Result<T, AssetError>
|
|
10
|
+
// stores the GUID->payload entry loadByGuid resolves (dev/inline path)
|
|
11
|
+
// - parseGuid(guidStr): AssetGuid
|
|
12
|
+
// - lookup(guid): Asset | undefined (catalogued payload, no fetch)
|
|
13
|
+
// - loadByGuid<T extends Asset>(guid): Promise<Result<T, AssetError | ImageError | RhiError>>
|
|
14
|
+
// returns the PAYLOAD T (never a handle, D-17)
|
|
15
|
+
// dev/fallback: synchronous catalogue lookup wrapped in Promise
|
|
16
|
+
// prod: fetch(packIndexUrl) -> parse catalog -> fetch entry -> parse Asset
|
|
17
|
+
// - instantiate<T extends SceneAsset>(handle, world, parent?): Result<EntityHandle, ...>
|
|
18
|
+
// handle is a `world.allocSharedRef('SceneAsset', payload)` column handle
|
|
19
|
+
// - inspect(): InspectSnapshot
|
|
20
|
+
//
|
|
21
|
+
// v1 load(url) removed in feat-20260513-guid-asset-package-system (w12).
|
|
22
|
+
// loadByGuid is the replacement; M4/w23 adds real fetch-from-pack-index.
|
|
23
|
+
//
|
|
24
|
+
// feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15: the
|
|
25
|
+
// `createInstancedBuffer` / `updateInstancedBuffer` / `getInstancedGpuBuffer`
|
|
26
|
+
// triplet is removed alongside the `InstancedBufferAsset` POD; per-entity
|
|
27
|
+
// instance transforms are now stored inside the ECS via the `Instances {
|
|
28
|
+
// transforms: 'array<f32>' }` component (the RenderSystem record stage owns
|
|
29
|
+
// the GPU storage buffer + dirty-version upload). Asset closed-union narrows
|
|
30
|
+
// 5 -> 4; the registry surface loses the optional `RhiDevice` constructor
|
|
31
|
+
// argument (no remaining device consumer).
|
|
32
|
+
//
|
|
33
|
+
// Dual-backend audited: the registry is engine-agnostic (no @webgpu/types
|
|
34
|
+
// imports + no rhi-webgpu / rhi-wgpu references); the same instance drives
|
|
35
|
+
// both dual-impl shim backends through the @forgeax/engine-rhi interface
|
|
36
|
+
// SSOT at the consumer site.
|
|
37
|
+
|
|
38
|
+
import type { Component, EcsError, EntityHandle, World } from '@forgeax/engine-ecs';
|
|
39
|
+
import type { PackError } from '@forgeax/engine-pack/errors';
|
|
40
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
41
|
+
import { deriveAssetName } from '@forgeax/engine-pack/name';
|
|
42
|
+
import { err, ok, type Result, type RhiError } from '@forgeax/engine-rhi';
|
|
43
|
+
import type { ShaderRegistry } from '@forgeax/engine-shader';
|
|
44
|
+
import {
|
|
45
|
+
ASSET_ERROR_HINTS,
|
|
46
|
+
type Asset,
|
|
47
|
+
type AssetEnvelope,
|
|
48
|
+
AssetError,
|
|
49
|
+
type AssetEvidence,
|
|
50
|
+
type AssetEvidenceError,
|
|
51
|
+
type AssetRef,
|
|
52
|
+
authoringCapabilityForAssetKind,
|
|
53
|
+
type CatalogEntry,
|
|
54
|
+
catalogOperationsFor,
|
|
55
|
+
type EngineMetrics,
|
|
56
|
+
type Handle,
|
|
57
|
+
handleSlot,
|
|
58
|
+
type ImageError,
|
|
59
|
+
type ImportTransport,
|
|
60
|
+
type InspectEntry,
|
|
61
|
+
type InspectSnapshot,
|
|
62
|
+
type Loader,
|
|
63
|
+
type MaterialAsset,
|
|
64
|
+
type MountOverride,
|
|
65
|
+
migrateLegacyMeshMaterialOverrides,
|
|
66
|
+
type Package,
|
|
67
|
+
type ParseErrorDetail,
|
|
68
|
+
type RuntimeAssetBinding,
|
|
69
|
+
type SceneAsset,
|
|
70
|
+
type SceneEntity,
|
|
71
|
+
type SceneInstanceMount,
|
|
72
|
+
type TagOf,
|
|
73
|
+
type TilesetAsset,
|
|
74
|
+
type TranscodeCaps,
|
|
75
|
+
type MeshAsset as TypesMeshAsset,
|
|
76
|
+
unwrapHandle,
|
|
77
|
+
} from '@forgeax/engine-types';
|
|
78
|
+
import {
|
|
79
|
+
BUILTIN_CUBE,
|
|
80
|
+
BUILTIN_CYLINDER,
|
|
81
|
+
BUILTIN_NINESLICE_QUAD,
|
|
82
|
+
BUILTIN_QUAD,
|
|
83
|
+
BUILTIN_SPHERE,
|
|
84
|
+
BUILTIN_TRIANGLE,
|
|
85
|
+
} from './builtin-asset-registry';
|
|
86
|
+
import type { LoaderRegistry } from './loader-registry';
|
|
87
|
+
import { createDefaultLoaderRegistry } from './wire-default-loaders';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Strip readonly from all fields of T. Used to mutate the MeshAsset.aabb slot
|
|
91
|
+
* after mesh validation passes (the interface is readonly but register-time
|
|
92
|
+
* computation writes the real AABB into the caller's placeholder).
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w4 (D-4 F1): the pre-class
|
|
96
|
+
// constants, loaders, scene-payload / payload-validate / aabb helpers were
|
|
97
|
+
// straight-cut into sibling modules. The thin class value-imports only what its
|
|
98
|
+
// method bodies still reference; the full public consumer face (barrel + tests)
|
|
99
|
+
// is preserved via the `export ... from` re-export block below (pre-w14 shim,
|
|
100
|
+
// removed when consumers repoint in w14/w15).
|
|
101
|
+
import { withMeshAabb } from './aabb';
|
|
102
|
+
import type { CatalogListener, CatalogSource } from './catalog-source';
|
|
103
|
+
import {
|
|
104
|
+
BUILTIN_MESH_GUIDS,
|
|
105
|
+
HANDLE_CUBE,
|
|
106
|
+
HANDLE_CYLINDER,
|
|
107
|
+
HANDLE_NINESLICE_QUAD,
|
|
108
|
+
HANDLE_QUAD,
|
|
109
|
+
HANDLE_SPHERE,
|
|
110
|
+
HANDLE_TRIANGLE,
|
|
111
|
+
} from './handles';
|
|
112
|
+
import type { MaterialLoadError, MaterialReady } from './material/loader';
|
|
113
|
+
import { installMaterialReadyShaders } from './material/runtime-shader';
|
|
114
|
+
import { inferAtlasExtent, validateMeshPayload, validateTilesetPayload } from './payload-validate';
|
|
115
|
+
import { ArtifactReadCache } from './registry/artifact-io';
|
|
116
|
+
import {
|
|
117
|
+
createRuntimeAssetEvidenceAdapter,
|
|
118
|
+
type RuntimeAssetEvidenceAdapter,
|
|
119
|
+
type RuntimeEvidenceSource,
|
|
120
|
+
} from './registry/asset-evidence';
|
|
121
|
+
import { type CatalogRecord, fetchPackIndex } from './registry/catalog';
|
|
122
|
+
import { CatalogReplica, type CatalogReplicaSnapshot } from './registry/catalog-state';
|
|
123
|
+
import {
|
|
124
|
+
instantiateFlat as instantiateFlatImpl,
|
|
125
|
+
instantiate as instantiateImpl,
|
|
126
|
+
type PostSpawnHook,
|
|
127
|
+
resolveHandleGuid,
|
|
128
|
+
resolveMountsRec,
|
|
129
|
+
} from './registry/instantiate';
|
|
130
|
+
import {
|
|
131
|
+
loadByGuid as loadByGuidImpl,
|
|
132
|
+
parseAndReturnAsset as parseAndReturnAssetImpl,
|
|
133
|
+
parseAssetPayload as parseAssetPayloadImpl,
|
|
134
|
+
registerPackagesFromIndex,
|
|
135
|
+
} from './registry/load-by-guid';
|
|
136
|
+
import { LoadStateStore } from './registry/load-state';
|
|
137
|
+
import type {
|
|
138
|
+
ScenePublicationFence,
|
|
139
|
+
ScenePublicationFenceError,
|
|
140
|
+
} from './registry/scene-publication-fence';
|
|
141
|
+
import {
|
|
142
|
+
detectTileNeedsRepeatSampler,
|
|
143
|
+
materialShaderTextureFieldNames as materialShaderTextureFieldNamesImpl,
|
|
144
|
+
validateMaterialPasses,
|
|
145
|
+
validateSpriteSlices,
|
|
146
|
+
} from './registry/validate-material';
|
|
147
|
+
import { extractSceneEntityHandleGuids } from './scene-handle-fields';
|
|
148
|
+
|
|
149
|
+
// Public re-export surface (pre-w14 consumer face preservation): the extracted
|
|
150
|
+
// modules are the new SSOT; asset-registry re-exports them until w14/w15 repoint
|
|
151
|
+
// every consumer to the sibling modules / the new package.
|
|
152
|
+
export {
|
|
153
|
+
HANDLE_CUBE,
|
|
154
|
+
HANDLE_CYLINDER,
|
|
155
|
+
HANDLE_NINESLICE_QUAD,
|
|
156
|
+
HANDLE_QUAD,
|
|
157
|
+
HANDLE_SPHERE,
|
|
158
|
+
HANDLE_TRIANGLE,
|
|
159
|
+
} from './handles';
|
|
160
|
+
export {
|
|
161
|
+
animationClipLoader,
|
|
162
|
+
animationGraphLoader,
|
|
163
|
+
INLINE_PACK_LOADERS,
|
|
164
|
+
materialLoader,
|
|
165
|
+
meshLoader,
|
|
166
|
+
sceneLoader,
|
|
167
|
+
skeletonLoader,
|
|
168
|
+
skinLoader,
|
|
169
|
+
} from './loaders/inline-pack';
|
|
170
|
+
export {
|
|
171
|
+
equirectLoader,
|
|
172
|
+
fontLoader,
|
|
173
|
+
PACK_ARTIFACT_LOADERS,
|
|
174
|
+
textureLoader,
|
|
175
|
+
} from './loaders/pack-artifact';
|
|
176
|
+
export { type TilesetValidateOptions, validateTilesetPayload } from './payload-validate';
|
|
177
|
+
|
|
178
|
+
// ─── Re-exports for engine-runtime-local consumers ──────────────────────────
|
|
179
|
+
//
|
|
180
|
+
// Legacy re-exports: `Asset` widens to the 4-variant engine-types union;
|
|
181
|
+
// `MeshAsset` keeps the engine-types shape (with `attributes`). Consumers
|
|
182
|
+
// that previously imported from `./asset-registry` keep working through
|
|
183
|
+
// the type alias re-exports below.
|
|
184
|
+
|
|
185
|
+
export type { Asset, TypesMeshAsset as MeshAsset };
|
|
186
|
+
|
|
187
|
+
// D-15: the BUILTIN_* mesh payloads moved to builtin-asset-registry.ts (the
|
|
188
|
+
// process-static payload SSOT); the shared runtime vertex layout remains owned
|
|
189
|
+
// by @forgeax/engine-geometry.
|
|
190
|
+
// imported at the top of this file. The constructor still pre-populates the
|
|
191
|
+
// handle->payload map from those imports (the map itself retires in w49).
|
|
192
|
+
|
|
193
|
+
// feat-20260618-asset-and-pack-name-fields M3 (D-1 / D-3): the mutable runtime
|
|
194
|
+
// package object every GUID of the same import path shares. `assetGuids` grows
|
|
195
|
+
// as `registerPackage` adds GUIDs; `assetCount` (the engine-types `Package`
|
|
196
|
+
// view) is derived from `assetGuids.size`, never stored (#2 Derive). The public
|
|
197
|
+
// `packageOf(guid)` projects this to the readonly `Package` interface.
|
|
198
|
+
interface MutablePackage {
|
|
199
|
+
path: string;
|
|
200
|
+
readonly assetGuids: Set<string>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Runtime brand helper ──────────────────────────────────────────────────
|
|
204
|
+
//
|
|
205
|
+
// AC-11 inspect() `.brand` is a 4-member string literal union mirroring the
|
|
206
|
+
// engine-types Asset discriminated union. Map a stored Asset value to its
|
|
207
|
+
// brand via the `.kind` discriminator (+ `.shadingModel` refinement for
|
|
208
|
+
// `MaterialAsset`, preserved for forward compatibility though the runtime
|
|
209
|
+
// brand stays at the asset-kind level per AC-11 spec).
|
|
210
|
+
//
|
|
211
|
+
// feat-20260514 M3 / w15: the `'InstancedBufferAsset'` brand is retired
|
|
212
|
+
// alongside the deleted POD + 3 registry methods; the runtime brand union
|
|
213
|
+
// shrinks 5 -> 4 to mirror the Asset closed-union shape.
|
|
214
|
+
// feat-20260514 w3: re-extends to 5 with the addition of the `'SceneAsset'`
|
|
215
|
+
// brand mirroring the new `'scene'` kind in the Asset discriminated union.
|
|
216
|
+
// feat-20260618-asset-and-pack-name-fields M1 / w3: AssetBrand moved to
|
|
217
|
+
// @forgeax/engine-types (public, single-entry discoverability per charter F1).
|
|
218
|
+
// feat-20260608-tilemap-object-layer-rendering M0: AssetBrand union grows
|
|
219
|
+
// 13 -> 14 with `'TilesetAsset'` in @forgeax/engine-types.
|
|
220
|
+
|
|
221
|
+
// feat-20260622 D-4/D-8: the 14-arm assetBrand switch and ASSET_BRAND Record
|
|
222
|
+
// table are both retired (PR #496 eliminated the brand concept entirely).
|
|
223
|
+
// New Asset union members no longer need a brand mapping; the closed union
|
|
224
|
+
// exhaustive switch in test-d files is the sole type-level guard.
|
|
225
|
+
|
|
226
|
+
// ─── Schema-driven material parse result (feat-20260523 M4-T01) ──────────
|
|
227
|
+
// ─── AssetRegistry class ────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Asset registry (instance-per-engine; `engine.assets: AssetRegistry | null`).
|
|
231
|
+
*
|
|
232
|
+
* The builtin meshes (`HANDLE_CUBE` / `HANDLE_TRIANGLE` / ...) are served by
|
|
233
|
+
* the process-static `BuiltinAssetRegistry`, so AI users see usable handles in
|
|
234
|
+
* the very first frame without registration ceremony (charter proposition 1).
|
|
235
|
+
*
|
|
236
|
+
* @example Catalogue a texture by GUID, load its payload, and bind a material:
|
|
237
|
+
* ```ts
|
|
238
|
+
* const guid = engine.assets.parseGuid('00000000-0000-7000-8000-000000000001');
|
|
239
|
+
* engine.assets.catalog(guid, myTexture); // GUID -> payload
|
|
240
|
+
* const res = await engine.assets.loadByGuid(guid); // payload (D-17)
|
|
241
|
+
* if (!res.ok) {
|
|
242
|
+
* switch (res.error.code) {
|
|
243
|
+
* case 'asset-not-found': // guid not catalogued
|
|
244
|
+
* }
|
|
245
|
+
* return;
|
|
246
|
+
* }
|
|
247
|
+
* const material = world.allocSharedRef('MaterialAsset', { // mint column handle
|
|
248
|
+
* kind: 'material',
|
|
249
|
+
* passes: [{ name: 'Forward', program: { module: 'forgeax::default-standard-pbr' }, tags: { LightMode: 'Forward' }, queue: 2000 }],
|
|
250
|
+
* values: { baseColorTexture: res.value },
|
|
251
|
+
* });
|
|
252
|
+
* world.spawn({ component: MeshRenderer, data: { materials: [material] } });
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
// bug-20260610 Fix B: parsed pack-file body stored in the fetchPackFile
|
|
257
|
+
// in-memory cache + in-flight dedup maps (D-4). Only the raw JSON shape is
|
|
258
|
+
// cached -- parseAssetPayload still runs per-call to look up the per-GUID
|
|
259
|
+
// entry (CON-2 register-before-recurse cycle safety).
|
|
260
|
+
export interface ParsedPackFile {
|
|
261
|
+
schemaVersion?: string;
|
|
262
|
+
kind?: string;
|
|
263
|
+
assets: Array<{
|
|
264
|
+
guid: string;
|
|
265
|
+
kind: string;
|
|
266
|
+
payload: Record<string, unknown>;
|
|
267
|
+
refs?: string[];
|
|
268
|
+
artifacts?: Record<string, import('@forgeax/engine-types').ArtifactDescriptor>;
|
|
269
|
+
}>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export type CatalogReconcileError = AssetError;
|
|
273
|
+
export type CatalogReconcileResult = Result<CatalogReplicaSnapshot, CatalogReconcileError>;
|
|
274
|
+
|
|
275
|
+
function catalogSourceUnconfigured<T>(): Result<T, AssetError> {
|
|
276
|
+
return err(
|
|
277
|
+
new AssetError({
|
|
278
|
+
code: 'catalog-source-unconfigured',
|
|
279
|
+
expected: 'a configured catalog source',
|
|
280
|
+
hint: ASSET_ERROR_HINTS['catalog-source-unconfigured'],
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export class AssetRegistry {
|
|
286
|
+
private catalogSource: CatalogSource | undefined;
|
|
287
|
+
private catalogEnumerating: Promise<Result<readonly CatalogEntry[], AssetError>> | undefined;
|
|
288
|
+
private readonly catalogListeners = new Set<CatalogListener>();
|
|
289
|
+
private catalogSourceDispose: (() => void) | undefined;
|
|
290
|
+
private catalogReplica: CatalogReplica | undefined;
|
|
291
|
+
// feat-20260614 M8 (D-15 / D-17 / D-19): the registry is a GUID -> payload
|
|
292
|
+
// catalogue. It holds NO handle concept -- it cannot mint a column handle
|
|
293
|
+
// (it has no World). `loadByGuid` returns the PAYLOAD; column minting
|
|
294
|
+
// (`world.allocSharedRef`) is the caller's job on the ECS/render side.
|
|
295
|
+
// Sub-asset refs embedded in a payload stay as GUID strings (AssetGuid /
|
|
296
|
+
// dash-form), never minted at load time. Keyed by lowercased GUID string.
|
|
297
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w5 (D-4): public so the
|
|
298
|
+
// extracted `./registry/validate-material` free functions (detectTileNeeds-
|
|
299
|
+
// RepeatSampler) can read it. No underscore + genuinely public is the
|
|
300
|
+
// lint-compliant exposure (D-internal R-internal-C ties `@internal` to `_`).
|
|
301
|
+
readonly assetCatalog: Map<string, AssetEnvelope<Asset>> = new Map();
|
|
302
|
+
|
|
303
|
+
/** Owner-injected component catalog used for scene breadcrumb projection. */
|
|
304
|
+
readonly componentCatalog: ReadonlyMap<string, Component>;
|
|
305
|
+
|
|
306
|
+
// feat-20260618-asset-and-pack-name-fields M3 (D-1): the package index that
|
|
307
|
+
// backs the two-segment asset identity `<packagePath>.<name>`. `packages`
|
|
308
|
+
// maps a lowercased GUID key to its `MutablePackage` (a shared object every
|
|
309
|
+
// GUID of the same import path points at), or `null` for assets with no
|
|
310
|
+
// package (catalog() inline + builtin, D-5). All three registration entry
|
|
311
|
+
// points (catalog / loadByGuid / builtin) funnel through the single
|
|
312
|
+
// `registerPackage` primitive so the XOR name invariant lands once (#1 SSOT).
|
|
313
|
+
private readonly packages: Map<string, MutablePackage | null> = new Map();
|
|
314
|
+
|
|
315
|
+
// Secondary index path -> shared MutablePackage so every GUID of the same
|
|
316
|
+
// import path points at one object (the 1->N promotion + assetCount derive
|
|
317
|
+
// depend on this sharing). Not a duplicate of `packages` (#2): `packages` is
|
|
318
|
+
// the per-GUID lookup; this is the per-path dedup used only inside
|
|
319
|
+
// registerPackage to find-or-create the shared object.
|
|
320
|
+
private readonly packageByPath: Map<string, MutablePackage> = new Map();
|
|
321
|
+
|
|
322
|
+
// Per-GUID stored display names now live on the asset envelope's `name` field
|
|
323
|
+
// (the single home, replacing the retired storedNameOf side table; D-6).
|
|
324
|
+
// resolveName reads `assetCatalog.get(key)?.name` as the `storedName` argument
|
|
325
|
+
// of deriveAssetName. `pendingNames` bridges the one ordering where a name is
|
|
326
|
+
// known before its envelope exists: the prod disk path registers the package
|
|
327
|
+
// (entry names) during resolveCatalogEntry, then catalogues the body later --
|
|
328
|
+
// catalog() drains the pending name into the new envelope, so nothing persists
|
|
329
|
+
// here once the envelope is in place.
|
|
330
|
+
private readonly pendingNames: Map<string, string> = new Map();
|
|
331
|
+
|
|
332
|
+
// ─── Prod pack-index fetch state (M4/w23) ──────────────────────────────
|
|
333
|
+
// When packIndexUrl is configured, loadByGuid fetches pack-index.json on
|
|
334
|
+
// first call, caches the parsed catalog in packIndexCache, then fetches
|
|
335
|
+
// each resource URL resolved against that index and registers the asset.
|
|
336
|
+
packIndexUrl: string | undefined = undefined;
|
|
337
|
+
packIndexCache: Map<string, CatalogRecord> | undefined = undefined;
|
|
338
|
+
/** Current browser-facing asset realm; absent for inline/shipped legacy use. */
|
|
339
|
+
runtimeBinding: RuntimeAssetBinding | undefined = undefined;
|
|
340
|
+
|
|
341
|
+
// tweak-20260609 M1: in-flight Map for recursive loadByGuid dedup + cycle
|
|
342
|
+
// prevention (D-5 / B-10). Maps guidKey → Promise<Result<Handle, ...>> so
|
|
343
|
+
// concurrent calls for the same GUID share the same fetch + register chain,
|
|
344
|
+
// and cycles (A→B→A) terminate when the second visit hits the in-flight
|
|
345
|
+
// entry for A instead of re-entering fetch.
|
|
346
|
+
readonly inFlight: Map<string, Promise<Result<unknown, AssetError | ImageError | RhiError>>> =
|
|
347
|
+
new Map();
|
|
348
|
+
|
|
349
|
+
// bug-20260610 Fix B (M3 / D-4): per-instance pack-file cache keyed by
|
|
350
|
+
// packageUrl (the .pack.json URL). `packFileInFlight` de-duplicates
|
|
351
|
+
// concurrent fetches; `packFileCache` stores resolved bodies so the
|
|
352
|
+
// same URL is fetched at most once per AssetRegistry lifetime (CON-6).
|
|
353
|
+
readonly packFileCache: Map<string, ParsedPackFile> = new Map();
|
|
354
|
+
readonly packFileInFlight: Map<string, Promise<ParsedPackFile>> = new Map();
|
|
355
|
+
readonly artifactCache = new ArtifactReadCache();
|
|
356
|
+
readonly loadState = new LoadStateStore();
|
|
357
|
+
/**
|
|
358
|
+
* Render-facing material readiness owned by the production GUID loader.
|
|
359
|
+
* The map stores the complete tuple result, including structured failures,
|
|
360
|
+
* so record code cannot silently fall back to a generic MaterialAsset.
|
|
361
|
+
*/
|
|
362
|
+
readonly materialReadiness = new Map<string, MaterialReady | MaterialLoadError>();
|
|
363
|
+
private assetEvidenceAdapter: RuntimeAssetEvidenceAdapter = createRuntimeAssetEvidenceAdapter();
|
|
364
|
+
|
|
365
|
+
// feat-20260621-asset-registry-robustness-invalidate-inflight-cach F17c:
|
|
366
|
+
// per-GUID generation counter incremented on each invalidate(guid) call.
|
|
367
|
+
// loadByGuid captures this value at Promise creation and discards the
|
|
368
|
+
// result (returning asset-invalidated) if the generation has changed by
|
|
369
|
+
// the time the fetch completes.
|
|
370
|
+
// F22: invalidateAll increments a single globalGeneration counter instead,
|
|
371
|
+
// which invalidates every in-flight Promise regardless of GUID.
|
|
372
|
+
readonly generations: Map<string, number> = new Map();
|
|
373
|
+
globalGeneration: number = 0;
|
|
374
|
+
/**
|
|
375
|
+
* Monotonic payload-cache epoch. Render-side derived snapshots use this
|
|
376
|
+
* single stamp to skip re-walking an unchanged material parent chain on
|
|
377
|
+
* every frame; catalog/invalidation mutations advance it conservatively.
|
|
378
|
+
*/
|
|
379
|
+
catalogEpoch: number = 0;
|
|
380
|
+
|
|
381
|
+
// F20: per-cache Promise queue to serialise packIndexCache write operations
|
|
382
|
+
// in transportOrFail. The "check -> new Map() -> set" three-step block is
|
|
383
|
+
// not atomic across concurrent transportOrFail calls; chaining through a
|
|
384
|
+
// single queue Promise ensures each patch completes before the next starts.
|
|
385
|
+
packIndexCachePatchQueue: Promise<void> = Promise.resolve();
|
|
386
|
+
|
|
387
|
+
// feat-20260527-sprite-nineslice M4 / w16 + w18 (D-5 + D-9): per-Renderer
|
|
388
|
+
// EngineMetrics shared with the runtime so register-time soft-warns
|
|
389
|
+
// (`nineslice.tile-needs-repeat-sampler` for sliceMode=1 + sampler not
|
|
390
|
+
// 'repeat') and runtime soft-warns (`nineslice.scale-too-small`) increment
|
|
391
|
+
// the SAME counter map. `createRenderer.ts` calls `assets.setMetrics(metrics)`
|
|
392
|
+
// immediately after constructing the registry; standalone test fixtures
|
|
393
|
+
// that do not go through `createRenderer` may leave this null and the
|
|
394
|
+
// soft-warn paths simply no-op (charter P9 graceful degradation: the
|
|
395
|
+
// structured fail-fast branches still fire; only the metric is dropped).
|
|
396
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w5 (D-4): public so the
|
|
397
|
+
// extracted `./registry/validate-material` free functions (detectTileNeeds-
|
|
398
|
+
// RepeatSampler) can read + increment it. No underscore + genuinely public
|
|
399
|
+
// is the lint-compliant exposure (D-internal R-internal-C ties `@internal`
|
|
400
|
+
// to `_`).
|
|
401
|
+
metrics: EngineMetrics | null = null;
|
|
402
|
+
|
|
403
|
+
// feat-20260707 M5 / w33 (D-11): device texture-compression caps the Basis
|
|
404
|
+
// texture / equirect arms feed to `selectTranscodeTarget`. `createRenderer`
|
|
405
|
+
// projects `RhiCaps` -> `TranscodeCaps` and calls `setTranscodeCaps` right
|
|
406
|
+
// after construction (D-8 one-line projection). A standalone registry (test /
|
|
407
|
+
// headless) keeps the all-false default, which drives the uncompressed
|
|
408
|
+
// fallback path (section 8 P3, AC-04) rather than a hard failure.
|
|
409
|
+
transcodeCaps: TranscodeCaps = { bc: false, etc2: false, astc: false };
|
|
410
|
+
|
|
411
|
+
// feat-20260703-collect-nested-sceneinstance-to-mount-roundtrip M1 (D-1):
|
|
412
|
+
// origin reverse-index: a payload object -> its catalog GUID, for payloads
|
|
413
|
+
// that are NOT the current catalog identity. WeakMap so entries auto-GC when
|
|
414
|
+
// the world despawns and the object is no longer held by sharedRefs.
|
|
415
|
+
// _guidForAsset consults it after the catalog identity scan MISSes.
|
|
416
|
+
// SSOT for the "payload-to-GUID provenance" fact (architecture-principles #1).
|
|
417
|
+
//
|
|
418
|
+
// Two writers populate it:
|
|
419
|
+
// 1. instantiate (registry/instantiate.ts): the resolved SceneAsset copy ->
|
|
420
|
+
// its original catalog GUID (the deep-copied envelope is never the catalog
|
|
421
|
+
// identity).
|
|
422
|
+
// 2. feat-20260713 M4 / w15 (D-6, root cause a): catalog() records the
|
|
423
|
+
// SUPERSEDED payload here when re-cataloguing a GUID with a fresh object.
|
|
424
|
+
// A handle minted before the override still points at the old object; this
|
|
425
|
+
// keeps that object reverse-lookupable so save/collect resolves its GUID
|
|
426
|
+
// instead of failing with a GUID-unresolved error (the 2026-07-06 crash).
|
|
427
|
+
//
|
|
428
|
+
// Key type is `object` (not `SceneAsset`) because both material and scene
|
|
429
|
+
// payloads are recorded (material payloads flow through writer 2).
|
|
430
|
+
/** @internal */
|
|
431
|
+
_originIndex: WeakMap<object, string> = new WeakMap();
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Construct a fresh registry pre-populated with the builtin cube + triangle
|
|
435
|
+
* mesh handles (`HANDLE_CUBE` / `HANDLE_TRIANGLE`).
|
|
436
|
+
*
|
|
437
|
+
* feat-20260514 M3 / w15: the previous optional `RhiDevice` constructor
|
|
438
|
+
* argument (consumed by the now-deleted `createInstancedBuffer` triplet)
|
|
439
|
+
* is removed; the registry surface is engine-agnostic again. Per-entity
|
|
440
|
+
* instance transforms now live inside the ECS `Instances { transforms:
|
|
441
|
+
* 'array<f32>' }` component; the RenderSystem record stage owns GPU
|
|
442
|
+
* storage buffer allocation + cap-gate.
|
|
443
|
+
*/
|
|
444
|
+
// feat-20260603-asset-import-loader-injection M1 / w5 (D-7): the registry
|
|
445
|
+
// dispatches `parseAssetPayload` / the texture+font upstream branches through
|
|
446
|
+
// this `LoaderRegistry`. feat-20260623 M3 / w9: the loader registry is now
|
|
447
|
+
// internally built by `createDefaultLoaderRegistry()` (public readonly field)
|
|
448
|
+
// so host apps can reach `engine.assets.loaders.register(...)` without a
|
|
449
|
+
// constructor-injection slot or a phantom passthrough wrapper.
|
|
450
|
+
// The loader set is wired at construction from the complete ordinary Asset
|
|
451
|
+
// vocabulary (including video and audio) plus caller-supplied extensions.
|
|
452
|
+
// Assigned here so the optional loaders cannot appear after a load begins.
|
|
453
|
+
readonly loaders: LoaderRegistry;
|
|
454
|
+
|
|
455
|
+
// feat-20260603-asset-import-loader-injection M4 / w31 (AC-19 / AC-22):
|
|
456
|
+
// the optional `ImportTransport` is the *only* difference between the studio
|
|
457
|
+
// form (transport injected, dev DDC miss triggers lazy import) and the shipped
|
|
458
|
+
// form (transport absent, DDC miss fails fast with `asset-not-imported`).
|
|
459
|
+
// The load path AFTER a successful DDC fetch is identical in both forms --
|
|
460
|
+
// zero branching on transport (AC-23 key invariant). Set at construction (no
|
|
461
|
+
// setter, no illegal intermediate state), same D-7 stance as LoaderRegistry.
|
|
462
|
+
readonly importTransport: ImportTransport | undefined;
|
|
463
|
+
|
|
464
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w9 (D-1): optional post-spawn
|
|
465
|
+
// hook invoked by `instantiate` after the scene subtree spawns. The shipped
|
|
466
|
+
// implementation is runtime's `postSpawnResolveJoints` (auto-wire Skin.joints),
|
|
467
|
+
// injected at the sole production assembly point (createRenderer, w10). When
|
|
468
|
+
// absent (standalone / test registries), instantiate skips joint wiring
|
|
469
|
+
// silently. Public so the extracted `./registry/instantiate` free function can
|
|
470
|
+
// read it (D-internal R-internal-C ties `@internal` to a `_` prefix;
|
|
471
|
+
// genuinely-public is the lint-compliant exposure).
|
|
472
|
+
readonly postSpawnHook: PostSpawnHook | undefined;
|
|
473
|
+
|
|
474
|
+
/** @internal Stored for M2 validation; TS suppressor reference */
|
|
475
|
+
constructor(
|
|
476
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w5 (D-4): public so the
|
|
477
|
+
// extracted `./registry/validate-material` free functions can read it. No
|
|
478
|
+
// underscore + genuinely public is the lint-compliant exposure (an
|
|
479
|
+
// `@internal` tag would require a `_` prefix per D-internal R-internal-C).
|
|
480
|
+
readonly shaderRegistry: ShaderRegistry,
|
|
481
|
+
importTransport?: ImportTransport | undefined,
|
|
482
|
+
// Caller-supplied loaders extend the ordinary default set. Duplicate kinds
|
|
483
|
+
// are rejected by LoaderRegistry so no consumer can observe replacement
|
|
484
|
+
// order as an accidental ownership rule.
|
|
485
|
+
extraLoaders?: readonly Loader[] | undefined,
|
|
486
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w9 (D-1): optional
|
|
487
|
+
// post-spawn hook; createRenderer injects `postSpawnResolveJoints`.
|
|
488
|
+
postSpawnHook?: PostSpawnHook | undefined,
|
|
489
|
+
runtimeBinding?: RuntimeAssetBinding | undefined,
|
|
490
|
+
componentCatalog: ReadonlyMap<string, Component> = new Map(),
|
|
491
|
+
) {
|
|
492
|
+
void this.shaderRegistry;
|
|
493
|
+
this.importTransport = importTransport;
|
|
494
|
+
this.postSpawnHook = postSpawnHook;
|
|
495
|
+
this.runtimeBinding = runtimeBinding;
|
|
496
|
+
this.componentCatalog = componentCatalog;
|
|
497
|
+
this.loaders = createDefaultLoaderRegistry(extraLoaders);
|
|
498
|
+
this.registerBuiltins();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Restore the engine-owned GUID catalogue after a runtime-realm transition.
|
|
503
|
+
*
|
|
504
|
+
* Builtin meshes are process-static engine assets, not game-owned catalog
|
|
505
|
+
* rows. A realm transition clears the user catalog and load caches, but must
|
|
506
|
+
* leave these assets addressable so a scene in the newly bound game can keep
|
|
507
|
+
* resolving its builtin mesh references.
|
|
508
|
+
*/
|
|
509
|
+
private registerBuiltins(): void {
|
|
510
|
+
// feat-20260614 M8 (D-15): builtins are GUID-addressable catalogue rows.
|
|
511
|
+
// The builtin payloads also live process-static in BuiltinAssetRegistry
|
|
512
|
+
// (slot < BUILTIN_BASE) for handle-tier resolution; here they are
|
|
513
|
+
// catalogued by GUID so loadByGuid(builtinGuid) returns the payload and
|
|
514
|
+
// scene refs[] pointing at a builtin GUID resolve without a hand-
|
|
515
|
+
// maintained table (docs/feedbacks/2026-06-03 §6.2 Tier 0).
|
|
516
|
+
const builtinByHandle = new Map<number, Asset>([
|
|
517
|
+
[handleSlot(HANDLE_CUBE), BUILTIN_CUBE],
|
|
518
|
+
[handleSlot(HANDLE_TRIANGLE), BUILTIN_TRIANGLE],
|
|
519
|
+
[handleSlot(HANDLE_QUAD), BUILTIN_QUAD],
|
|
520
|
+
[handleSlot(HANDLE_SPHERE), BUILTIN_SPHERE],
|
|
521
|
+
[handleSlot(HANDLE_NINESLICE_QUAD), BUILTIN_NINESLICE_QUAD],
|
|
522
|
+
[handleSlot(HANDLE_CYLINDER), BUILTIN_CYLINDER],
|
|
523
|
+
]);
|
|
524
|
+
for (const [handle, guidStr] of BUILTIN_MESH_GUIDS) {
|
|
525
|
+
const parsed = AssetGuid.parse(guidStr);
|
|
526
|
+
if (!parsed.ok) {
|
|
527
|
+
throw new Error(`[asset-registry] builtin GUID ${guidStr} is not a valid UUID`);
|
|
528
|
+
}
|
|
529
|
+
const payload = builtinByHandle.get(handleSlot(handle));
|
|
530
|
+
if (payload !== undefined)
|
|
531
|
+
this.assetCatalog.set(guidStr.toLowerCase(), {
|
|
532
|
+
guid: guidStr,
|
|
533
|
+
kind: payload.kind,
|
|
534
|
+
payload,
|
|
535
|
+
refs: [],
|
|
536
|
+
});
|
|
537
|
+
if (payload !== undefined) this.loadState.registerReady(guidStr, payload);
|
|
538
|
+
}
|
|
539
|
+
// D-5: builtin meshes have no import path and no source name -- register
|
|
540
|
+
// them with a null package so resolveName returns '' (the detectable
|
|
541
|
+
// "genuinely no name" signal). They are deliberately NOT given a synthetic
|
|
542
|
+
// package + derived name (memory builtin-guid-preregister-collides).
|
|
543
|
+
this._registerPackage(
|
|
544
|
+
null,
|
|
545
|
+
BUILTIN_MESH_GUIDS.map(([, guidStr]) => guidStr),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* feat-20260527-sprite-nineslice M4 / w16 prep + w18 (D-5 + D-9): inject the
|
|
551
|
+
* per-Renderer `EngineMetrics` so register-time soft-warns can bump the same
|
|
552
|
+
* counter map the runtime reads through `renderer.metrics.snapshot()`. Called
|
|
553
|
+
* by `createRenderer` after constructing both the registry and the metrics
|
|
554
|
+
* instance; safe to skip in standalone tests (the soft-warn arms simply do
|
|
555
|
+
* not record).
|
|
556
|
+
*/
|
|
557
|
+
setMetrics(metrics: EngineMetrics): void {
|
|
558
|
+
this.metrics = metrics;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* feat-20260707 M5 / w33 (D-11): wire the device compression caps used by the
|
|
563
|
+
* Basis texture / equirect transcode arms. `createRenderer` calls this right
|
|
564
|
+
* after construction with `RhiCaps` projected to `TranscodeCaps` (D-8). Left
|
|
565
|
+
* at the all-false default, the loaders transcode to the uncompressed
|
|
566
|
+
* `rgba8unorm` / `rgba16float` fallback (AC-04, section 8 P3).
|
|
567
|
+
*/
|
|
568
|
+
setTranscodeCaps(caps: TranscodeCaps): void {
|
|
569
|
+
this.transcodeCaps = caps;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Inject authoritative runtime evidence without importing CLI or Node policy.
|
|
574
|
+
* `inspect(guid)` and `verifyByGuid(guid)` return the shared state model; an
|
|
575
|
+
* omitted capability remains an explicit structured error, never a pass.
|
|
576
|
+
*/
|
|
577
|
+
configureAssetEvidence(source: RuntimeEvidenceSource): void {
|
|
578
|
+
this.assetEvidenceAdapter = createRuntimeAssetEvidenceAdapter(source);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @internal — read the metrics handle for register-time soft-warn paths.
|
|
583
|
+
* Returns `null` when no `createRenderer` wired the registry to a renderer
|
|
584
|
+
* (the standalone-test path; the structured fail-fast branches still fire).
|
|
585
|
+
*/
|
|
586
|
+
_getMetrics(): EngineMetrics | null {
|
|
587
|
+
return this.metrics;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @internal — reverse-lookup: find the GUID key for a catalogued asset
|
|
592
|
+
* payload by identity comparison (===). Returns the GUID string if found,
|
|
593
|
+
* `undefined` otherwise. This is the SSOT for the inline identity scan
|
|
594
|
+
* idiom that previously existed in two places (instantiate sceneGuidKey
|
|
595
|
+
* lookup and resolveSkinAsset skeleton match).
|
|
596
|
+
*
|
|
597
|
+
* Linear scan of the assetCatalog (Map<string, AssetEnvelope>). The O(n)
|
|
598
|
+
* cost is acceptable for save-path frequencies (OOS-2).
|
|
599
|
+
*/
|
|
600
|
+
_guidForAsset(asset: Asset): string | undefined {
|
|
601
|
+
for (const [key, envelope] of this.assetCatalog) {
|
|
602
|
+
if (envelope.payload === asset) {
|
|
603
|
+
return key;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// feat-20260703 M1 (D-1): fallback to the origin reverse-index. Covers two
|
|
607
|
+
// MISS cases the catalog identity scan cannot: (1) _resolveSceneGuids deep
|
|
608
|
+
// copies — the copy is never the catalogued original; (2) feat-20260713 M4
|
|
609
|
+
// (D-6): a payload superseded by a catalog override, still live behind a
|
|
610
|
+
// handle minted before the override.
|
|
611
|
+
return this._originIndex.get(asset as object);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Public identity projection consumed by scene collection boundaries. */
|
|
615
|
+
guidOf(asset: Asset): string | undefined {
|
|
616
|
+
return this._guidForAsset(asset);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Return the production MaterialReady/Error result for one material GUID. */
|
|
620
|
+
getMaterialReadiness(guid: string): MaterialReady | MaterialLoadError | undefined {
|
|
621
|
+
return this.materialReadiness.get(guid.toLowerCase());
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Record the canonical result produced by the production material loader. */
|
|
625
|
+
recordMaterialReadiness(guid: string, readiness: MaterialReady | MaterialLoadError): void {
|
|
626
|
+
this.materialReadiness.set(guid.toLowerCase(), readiness);
|
|
627
|
+
if (readiness.status === 'Ready') {
|
|
628
|
+
installMaterialReadyShaders(this.shaderRegistry, readiness);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Configure a catalog URL for `loadByGuid`.
|
|
634
|
+
*
|
|
635
|
+
* Call this once during engine initialization with the URL where
|
|
636
|
+
* `pack-index.json` is served (emitted by `@forgeax/engine-vite-plugin-pack`
|
|
637
|
+
* during `vite build`). In a Vite dev host, use `configureRuntimeBinding`
|
|
638
|
+
* instead so the catalog remains bound to its scope and generation. The
|
|
639
|
+
* index is also the canonical base URL for every
|
|
640
|
+
* catalog entry: relative, root-relative, and absolute entry URLs are resolved
|
|
641
|
+
* against it before the registry fetches a pack body. After configuration,
|
|
642
|
+
* `loadByGuid` will fetch the catalog on its first invocation and cache it for
|
|
643
|
+
* subsequent calls.
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* ```ts
|
|
647
|
+
* engine.assets.configurePackIndex('/pack-index.json');
|
|
648
|
+
* const payloadRes = await engine.assets.loadByGuid(guid); // payload, not a handle (D-17)
|
|
649
|
+
* ```
|
|
650
|
+
*/
|
|
651
|
+
configurePackIndex(url: string): void {
|
|
652
|
+
this.packIndexUrl = url;
|
|
653
|
+
this.packIndexCache = undefined; // reset cache if URL changes
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Atomically replace the browser-side asset realm. The binding owns the
|
|
658
|
+
* catalog URL and generation; no cache from the previous game survives the
|
|
659
|
+
* transition.
|
|
660
|
+
*/
|
|
661
|
+
configureRuntimeBinding(binding: RuntimeAssetBinding): void {
|
|
662
|
+
this.clearCatalogSource();
|
|
663
|
+
this.runtimeBinding = binding;
|
|
664
|
+
this.configurePackIndex(binding.catalogUrl);
|
|
665
|
+
this.invalidateAll();
|
|
666
|
+
this.registerBuiltins();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* feat-20260621 F17c: invalidate a single cached asset by GUID so the next
|
|
671
|
+
* `loadByGuid` performs a genuinely fresh fetch. Clears, for this GUID only:
|
|
672
|
+
* the catalogue entry, the in-flight dedup entry, the cached pack-file body
|
|
673
|
+
* (keyed by the index entry's packageUrl), and the pack-index entry. Then
|
|
674
|
+
* increments the per-GUID generation counter so any still in-flight Promise
|
|
675
|
+
* for this GUID discards its result (returns `asset-invalidated`). The body +
|
|
676
|
+
* index clears are targeted (other GUIDs' cached bodies and index entries
|
|
677
|
+
* survive); deleting the index entry forces `resolveCatalogEntry` to re-fetch
|
|
678
|
+
* the pack-index on the next load, re-resolving the packageUrl whose body
|
|
679
|
+
* cache was just dropped. No-op when the GUID is not catalogued.
|
|
680
|
+
*
|
|
681
|
+
* Does NOT touch `packages` (a re-load's registerPackage overwrites them; D-8)
|
|
682
|
+
* and does NOT trigger GPU resource release (OOS-1,
|
|
683
|
+
* q1 boundary: the asset is CPU-only; GPU resources follow the ECS).
|
|
684
|
+
*
|
|
685
|
+
* @param guid - Case-insensitive GUID string or AssetGuid.
|
|
686
|
+
*/
|
|
687
|
+
invalidate(guid: string): void {
|
|
688
|
+
const guidKey = guid.toLowerCase();
|
|
689
|
+
// D-6: the stored name lives on the envelope; preserve it across the delete
|
|
690
|
+
// (the `packages` mapping survives, so resolveName must still see the name
|
|
691
|
+
// until a re-load's registerPackage overwrites it) by parking it on
|
|
692
|
+
// pendingNames -- the next catalog() of this GUID drains it back.
|
|
693
|
+
const survivingEnvelope = this.assetCatalog.get(guidKey);
|
|
694
|
+
const survivingName = survivingEnvelope?.name;
|
|
695
|
+
if (survivingName !== undefined) this.pendingNames.set(guidKey, survivingName);
|
|
696
|
+
if (survivingEnvelope !== undefined) this._originIndex.set(survivingEnvelope.payload, guidKey);
|
|
697
|
+
this.assetCatalog.delete(guidKey);
|
|
698
|
+
this.loadState.remove(guidKey);
|
|
699
|
+
this.materialReadiness.delete(guidKey);
|
|
700
|
+
// R-1 hard fix (research-decisions.md): delete inFlight entry so the
|
|
701
|
+
// next loadByGuid does not hit the old Promise whose generation no
|
|
702
|
+
// longer matches (AC-04 requires a fresh fetch, not asset-invalidated).
|
|
703
|
+
this.inFlight.delete(guidKey);
|
|
704
|
+
// Round-2 M-A: widen the clear so a COMPLETED reload re-fetches fresh
|
|
705
|
+
// bytes instead of serving the stale cached body. Ordering is load-bearing:
|
|
706
|
+
// read packageUrl from the index entry FIRST, then delete the body, then
|
|
707
|
+
// delete the index entry. Targeted delete (not wholesale undefined) keeps
|
|
708
|
+
// other GUIDs' cached bodies/index entries intact (per-GUID precision).
|
|
709
|
+
const entry = this.packIndexCache?.get(guidKey);
|
|
710
|
+
if (entry !== undefined) this.packFileCache.delete(entry.packageUrl);
|
|
711
|
+
this.packIndexCache?.delete(guidKey);
|
|
712
|
+
this.generations.set(guidKey, (this.generations.get(guidKey) ?? 0) + 1);
|
|
713
|
+
this.catalogEpoch++;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Adopt freshly loaded Mesh material slots into an object that is already
|
|
718
|
+
* pinned by live World shared refs. Geometry and GPU-backed fields deliberately
|
|
719
|
+
* remain untouched: this primitive is only for a default-material source
|
|
720
|
+
* override, never a general Mesh reimport. This is the atomic cutover: Catalog,
|
|
721
|
+
* Ready load state, and payload-to-GUID provenance must all name the same
|
|
722
|
+
* object after the call. Call only after invalidate(guid) + loadByGuid(guid)
|
|
723
|
+
* has completed successfully.
|
|
724
|
+
*/
|
|
725
|
+
adoptReloadedMeshMaterialSlots(guid: string, livePayload: Asset): Result<Asset, AssetError> {
|
|
726
|
+
const key = guid.toLowerCase();
|
|
727
|
+
const envelope = this.assetCatalog.get(key);
|
|
728
|
+
const freshPayload = this.loadState.getReady<Asset>(key);
|
|
729
|
+
if (envelope === undefined || freshPayload === undefined || envelope.payload !== freshPayload) {
|
|
730
|
+
return err(
|
|
731
|
+
new AssetError({
|
|
732
|
+
code: 'asset-not-found',
|
|
733
|
+
expected: `GUID ${key} to have one freshly loaded Ready payload`,
|
|
734
|
+
hint: 'complete invalidate + loadByGuid before adopting a live Mesh identity',
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (livePayload.kind !== 'mesh' || freshPayload.kind !== 'mesh') {
|
|
739
|
+
return err(
|
|
740
|
+
new AssetError({
|
|
741
|
+
code: 'asset-parse-failed',
|
|
742
|
+
expected: `GUID ${key} live and fresh payloads to both be MeshAsset`,
|
|
743
|
+
hint: 'use Mesh identity adoption only for a recooked MeshAsset',
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
if (this._guidForAsset(livePayload) !== key) {
|
|
748
|
+
return err(
|
|
749
|
+
new AssetError({
|
|
750
|
+
code: 'asset-not-found',
|
|
751
|
+
expected: `live Mesh payload to retain GUID provenance for ${key}`,
|
|
752
|
+
hint: 'capture the catalogued live payload before invalidating and reloading the same GUID',
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (livePayload === freshPayload) return ok(livePayload);
|
|
757
|
+
|
|
758
|
+
(livePayload as unknown as { materialSlots: TypesMeshAsset['materialSlots'] }).materialSlots =
|
|
759
|
+
freshPayload.materialSlots;
|
|
760
|
+
this._originIndex.set(freshPayload as object, key);
|
|
761
|
+
this.assetCatalog.set(key, { ...envelope, payload: livePayload });
|
|
762
|
+
this.loadState.registerReady(key, livePayload);
|
|
763
|
+
this.catalogEpoch++;
|
|
764
|
+
return ok(livePayload);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* feat-20260621 F17c: invalidate ALL cached assets so the next `loadByGuid`
|
|
769
|
+
* re-fetches both the pack-index and the asset body. Clears assetCatalog,
|
|
770
|
+
* inFlight, and packFileCache (wholesale), and resets packIndexCache to
|
|
771
|
+
* `undefined` (NOT `.clear()` -- an empty Map would short-circuit
|
|
772
|
+
* `resolveCatalogEntry`'s `=== undefined` re-fetch guard and serve
|
|
773
|
+
* asset-not-imported for every later load; undefined forces a fresh
|
|
774
|
+
* fetchPackIndex). Then increments a single globalGeneration counter so every
|
|
775
|
+
* in-flight Promise (regardless of GUID) discards its result. Returns the
|
|
776
|
+
* number of assets that were catalogued before the call.
|
|
777
|
+
*
|
|
778
|
+
* Idempotent: second call on an already-empty catalogue returns clearedCount 0
|
|
779
|
+
* (AC-06). Does NOT trigger GPU resource release (OOS-1).
|
|
780
|
+
*/
|
|
781
|
+
invalidateAll(): { clearedCount: number } {
|
|
782
|
+
const count = this.assetCatalog.size;
|
|
783
|
+
this.assetCatalog.clear();
|
|
784
|
+
this.loadState.clear();
|
|
785
|
+
this.materialReadiness.clear();
|
|
786
|
+
this.inFlight.clear();
|
|
787
|
+
this.globalGeneration++;
|
|
788
|
+
// Round-2 M-A: wholesale clear of the shared body cache, and reset the
|
|
789
|
+
// index cache to UNDEFINED (R2-1) -- NOT .clear(). packFileCache uses
|
|
790
|
+
// .clear() because fetchPackFile checks `.get(packageUrl)` per URL, so an
|
|
791
|
+
// empty Map correctly misses and re-fetches. packIndexCache uses =undefined
|
|
792
|
+
// because resolveCatalogEntry's re-fetch guard tests `=== undefined`; an
|
|
793
|
+
// empty Map would short-circuit it and serve asset-not-imported for every
|
|
794
|
+
// later load -- the exact F17b pollution this feat fixes. The asymmetry is
|
|
795
|
+
// intentional; do not normalise the two operations.
|
|
796
|
+
this.packFileCache.clear();
|
|
797
|
+
this.packIndexCache = undefined;
|
|
798
|
+
this.catalogEpoch++;
|
|
799
|
+
return { clearedCount: count };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Force a re-fetch of the configured pack-index NOW and repopulate the cache,
|
|
804
|
+
* so a synchronous `listCatalog()` immediately reflects assets added on disk
|
|
805
|
+
* since boot (a freshly imported GLB's sub-assets). `loadByGuid`'s lazy
|
|
806
|
+
* re-fetch only fires on a per-GUID miss and `invalidateAll()` merely clears
|
|
807
|
+
* the cache (leaving `listCatalog()` empty until the next load), so neither
|
|
808
|
+
* makes a Content Browser or `loadByGuid`-driven "Add to Scene" see a new
|
|
809
|
+
* asset without a page reload. This does.
|
|
810
|
+
*
|
|
811
|
+
* No-op (returns false) when no pack-index URL is configured (dev inline
|
|
812
|
+
* catalogue path) or the fetch fails — callers keep the stale cache rather
|
|
813
|
+
* than blanking it. Returns true when the cache was repopulated.
|
|
814
|
+
*/
|
|
815
|
+
async refreshCatalog(): Promise<boolean> {
|
|
816
|
+
if (this.packIndexUrl === undefined) return false;
|
|
817
|
+
const result = await fetchPackIndex(this);
|
|
818
|
+
if (!result.ok) return false;
|
|
819
|
+
this.packIndexCache = result.value;
|
|
820
|
+
registerPackagesFromIndex(this, this.packIndexCache);
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
setCatalogSource(source: CatalogSource): void {
|
|
825
|
+
this.clearCatalogSource();
|
|
826
|
+
this.catalogSource = source;
|
|
827
|
+
this.catalogReplica = new CatalogReplica(source);
|
|
828
|
+
this.catalogSourceDispose = this.catalogReplica.subscribe((delta) => {
|
|
829
|
+
for (const listener of [...this.catalogListeners]) {
|
|
830
|
+
try {
|
|
831
|
+
listener(delta);
|
|
832
|
+
} catch {
|
|
833
|
+
// One host listener cannot prevent the remaining listeners from seeing a fact.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
void this.catalogReplica.start();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Stop the catalog transport and remove its replica without clearing payload caches. */
|
|
841
|
+
clearCatalogSource(): void {
|
|
842
|
+
this.catalogReplica?.dispose();
|
|
843
|
+
this.catalogSourceDispose?.();
|
|
844
|
+
this.catalogSourceDispose = undefined;
|
|
845
|
+
this.catalogSource = undefined;
|
|
846
|
+
this.catalogReplica = undefined;
|
|
847
|
+
this.catalogEnumerating = undefined;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
enumerateCatalog(): Promise<Result<readonly CatalogEntry[], AssetError>> {
|
|
851
|
+
if (this.catalogEnumerating !== undefined) return this.catalogEnumerating;
|
|
852
|
+
if (this.catalogSource === undefined || this.catalogReplica === undefined) {
|
|
853
|
+
return Promise.resolve(catalogSourceUnconfigured());
|
|
854
|
+
}
|
|
855
|
+
const promise = this.catalogReplica
|
|
856
|
+
.start()
|
|
857
|
+
.then((result) => (result.ok ? ok(result.value.entries) : result));
|
|
858
|
+
this.catalogEnumerating = promise;
|
|
859
|
+
void promise.then(() => {
|
|
860
|
+
this.catalogEnumerating = undefined;
|
|
861
|
+
});
|
|
862
|
+
return promise;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Read the immutable catalog projection folded by this registry. */
|
|
866
|
+
catalogSnapshot(): CatalogReplicaSnapshot | undefined {
|
|
867
|
+
return this.catalogReplica?.snapshot();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Explicitly recover the registry's single catalog replica from its configured source. */
|
|
871
|
+
reconcileCatalog(): Promise<CatalogReconcileResult> {
|
|
872
|
+
if (this.catalogSource === undefined || this.catalogReplica === undefined) {
|
|
873
|
+
return Promise.resolve(catalogSourceUnconfigured());
|
|
874
|
+
}
|
|
875
|
+
return this.catalogReplica.reconcile();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
subscribeCatalog(listener: CatalogListener): () => void {
|
|
879
|
+
this.catalogListeners.add(listener);
|
|
880
|
+
return () => {
|
|
881
|
+
this.catalogListeners.delete(listener);
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Materialise a `SceneAsset` into an existing `World` and return the
|
|
887
|
+
* synthetic root `Entity` (feat-20260514 w31 sugar wrapper; AC-03 +
|
|
888
|
+
* requirements §IN-3; M3: returns Entity not SceneInstanceId).
|
|
889
|
+
*
|
|
890
|
+
* Before spawning, handle-type component fields (e.g. `assetHandle`,
|
|
891
|
+
* `material`, `skeleton`) containing GUID strings are resolved to fresh
|
|
892
|
+
* user-tier `Handle` numbers via `world.allocSharedRef` (feat-20260614 M8
|
|
893
|
+
* D-19 instantiate-time GUID->handle mint; supersedes the pre-D-17
|
|
894
|
+
* `resolveGuid` map). GUIDs that fail to parse or are not catalogued return
|
|
895
|
+
* `AssetError(code='asset-not-found')` with a hint containing the GUID,
|
|
896
|
+
* node localId, and field name.
|
|
897
|
+
*
|
|
898
|
+
* Errors propagate verbatim through the closed
|
|
899
|
+
* `AssetError | PackError | EcsError` union so AI users that already
|
|
900
|
+
* narrow `loadByGuid<SceneAsset>` results reuse the same `switch
|
|
901
|
+
* (err.code)` exhaustively (charter proposition 3 machine-readable
|
|
902
|
+
* union; plan-strategy §3.3 closed-union transparency).
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```ts
|
|
906
|
+
* const sceneRes = await engine.assets.loadByGuid<SceneAsset>(roomGuid); // payload (D-17)
|
|
907
|
+
* if (!sceneRes.ok) return;
|
|
908
|
+
* const handle = world.allocSharedRef('SceneAsset', sceneRes.value); // mint column handle
|
|
909
|
+
* const r = engine.assets.instantiate(handle, world);
|
|
910
|
+
* if (!r.ok) {
|
|
911
|
+
* switch (r.error.code) {
|
|
912
|
+
* case 'asset-not-found':
|
|
913
|
+
* case 'pack-cyclic-reference':
|
|
914
|
+
* // ... AssetErrorCode | PackErrorCode | EcsErrorCode exhaustive
|
|
915
|
+
* }
|
|
916
|
+
* }
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
instantiate<T extends SceneAsset>(
|
|
920
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
921
|
+
world: World,
|
|
922
|
+
parent?: EntityHandle,
|
|
923
|
+
): Result<EntityHandle, AssetError | PackError | EcsError> {
|
|
924
|
+
return instantiateImpl(this, handle, world, parent) as Result<
|
|
925
|
+
EntityHandle,
|
|
926
|
+
AssetError | PackError | EcsError
|
|
927
|
+
>;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
instantiateWithPublicationFence<T extends SceneAsset>(
|
|
931
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
932
|
+
world: World,
|
|
933
|
+
parent: EntityHandle | undefined,
|
|
934
|
+
expectedPublication: ScenePublicationFence,
|
|
935
|
+
): Result<EntityHandle, AssetError | PackError | EcsError | ScenePublicationFenceError>;
|
|
936
|
+
instantiateWithPublicationFence<T extends SceneAsset>(
|
|
937
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
938
|
+
world: World,
|
|
939
|
+
parent: EntityHandle | undefined,
|
|
940
|
+
expectedPublication: ScenePublicationFence,
|
|
941
|
+
): Result<EntityHandle, AssetError | PackError | EcsError | ScenePublicationFenceError> {
|
|
942
|
+
return instantiateImpl(this, handle, world, parent, expectedPublication);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Materialise a `SceneAsset` FLAT into an existing `World` — the "open a scene
|
|
947
|
+
* for authoring" registry entry (#655): NO synthetic SceneInstance root, NO
|
|
948
|
+
* forced `ChildOf` on top-level members. Returns the top-level entity handles.
|
|
949
|
+
* Nested prefabs (`mounts[]`) still materialise as their own SceneInstance
|
|
950
|
+
* anchors. Use {@link instantiate} (anchor) at runtime / Play and for nested
|
|
951
|
+
* prefabs.
|
|
952
|
+
*/
|
|
953
|
+
instantiateFlat<T extends SceneAsset>(
|
|
954
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
955
|
+
world: World,
|
|
956
|
+
): Result<EntityHandle[], AssetError | PackError | EcsError> {
|
|
957
|
+
return instantiateFlatImpl(this, handle, world) as Result<
|
|
958
|
+
EntityHandle[],
|
|
959
|
+
AssetError | PackError | EcsError
|
|
960
|
+
>;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
instantiateFlatWithPublicationFence<T extends SceneAsset>(
|
|
964
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
965
|
+
world: World,
|
|
966
|
+
expectedPublication: ScenePublicationFence,
|
|
967
|
+
): Result<EntityHandle[], AssetError | PackError | EcsError | ScenePublicationFenceError> {
|
|
968
|
+
return instantiateFlatImpl(this, handle, world, expectedPublication);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private sceneMeshGuidBySlot(
|
|
972
|
+
scene: SceneAsset,
|
|
973
|
+
visited: Set<string> = new Set(),
|
|
974
|
+
): Map<number, string> {
|
|
975
|
+
const result = new Map<number, string>();
|
|
976
|
+
for (const entity of scene.entities) {
|
|
977
|
+
const components = entity.components as Record<string, Record<string, unknown>>;
|
|
978
|
+
const meshGuid = components.MeshFilter?.assetHandle;
|
|
979
|
+
if (typeof meshGuid === 'string') result.set(entity.localId as number, meshGuid);
|
|
980
|
+
}
|
|
981
|
+
for (const mount of scene.mounts ?? []) {
|
|
982
|
+
const mountComponents = mount.components as
|
|
983
|
+
| Record<string, Record<string, unknown>>
|
|
984
|
+
| undefined;
|
|
985
|
+
const mountMeshGuid = mountComponents?.MeshFilter?.assetHandle;
|
|
986
|
+
if (typeof mountMeshGuid === 'string') result.set(mount.localId as number, mountMeshGuid);
|
|
987
|
+
if (typeof mount.source !== 'string') continue;
|
|
988
|
+
const childKey = mount.source.toLowerCase();
|
|
989
|
+
if (visited.has(childKey)) continue;
|
|
990
|
+
const child = this.assetCatalog.get(childKey)?.payload;
|
|
991
|
+
if (child?.kind !== 'scene') continue;
|
|
992
|
+
const childVisited = new Set(visited);
|
|
993
|
+
childVisited.add(childKey);
|
|
994
|
+
const childSlots = this.sceneMeshGuidBySlot(child, childVisited);
|
|
995
|
+
const first = mount.memberFirst as unknown as number;
|
|
996
|
+
for (const [childSlot, meshGuid] of childSlots) result.set(first + childSlot, meshGuid);
|
|
997
|
+
for (const override of mount.overrides ?? []) {
|
|
998
|
+
if (override.comp !== 'MeshFilter') continue;
|
|
999
|
+
const nextMeshGuid =
|
|
1000
|
+
override.field === 'assetHandle'
|
|
1001
|
+
? override.value
|
|
1002
|
+
: override.field === undefined &&
|
|
1003
|
+
typeof override.value === 'object' &&
|
|
1004
|
+
override.value !== null &&
|
|
1005
|
+
!Array.isArray(override.value)
|
|
1006
|
+
? (override.value as Record<string, unknown>).assetHandle
|
|
1007
|
+
: undefined;
|
|
1008
|
+
if (typeof nextMeshGuid === 'string') {
|
|
1009
|
+
result.set(override.localId as unknown as number, nextMeshGuid);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private migrateLegacySceneMaterialOverrides(
|
|
1017
|
+
scene: SceneAsset,
|
|
1018
|
+
sceneGuidKey?: string,
|
|
1019
|
+
): Result<{ readonly scene: SceneAsset; readonly changed: boolean }, AssetError> {
|
|
1020
|
+
const nodes: SceneEntity[] = [];
|
|
1021
|
+
let changed = false;
|
|
1022
|
+
for (const node of scene.entities) {
|
|
1023
|
+
const components = node.components as Record<string, Record<string, unknown>>;
|
|
1024
|
+
const rawMeshGuid = components.MeshFilter?.assetHandle;
|
|
1025
|
+
const meshGuid = typeof rawMeshGuid === 'string' ? rawMeshGuid : undefined;
|
|
1026
|
+
const legacyOverrides = components.MeshRenderer?.materials;
|
|
1027
|
+
const mesh =
|
|
1028
|
+
meshGuid === undefined ? undefined : this.assetCatalog.get(meshGuid.toLowerCase())?.payload;
|
|
1029
|
+
if (
|
|
1030
|
+
mesh?.kind !== 'mesh' ||
|
|
1031
|
+
meshGuid === undefined ||
|
|
1032
|
+
!Array.isArray(legacyOverrides) ||
|
|
1033
|
+
legacyOverrides.length < mesh.submeshes.length ||
|
|
1034
|
+
mesh.submeshes.length <= mesh.materialSlots.length ||
|
|
1035
|
+
!legacyOverrides.every((value) => typeof value === 'string')
|
|
1036
|
+
) {
|
|
1037
|
+
nodes.push(node);
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
const migrated = migrateLegacyMeshMaterialOverrides(
|
|
1041
|
+
legacyOverrides as string[],
|
|
1042
|
+
mesh.submeshes,
|
|
1043
|
+
mesh.materialSlots.length,
|
|
1044
|
+
{
|
|
1045
|
+
meshGuid: meshGuid.toLowerCase(),
|
|
1046
|
+
sceneGuid: sceneGuidKey ?? '<unregistered-scene>',
|
|
1047
|
+
entityId: node.localId as number,
|
|
1048
|
+
},
|
|
1049
|
+
);
|
|
1050
|
+
if (!migrated.ok) {
|
|
1051
|
+
return err(
|
|
1052
|
+
new AssetError({
|
|
1053
|
+
code: 'asset-parse-failed',
|
|
1054
|
+
expected:
|
|
1055
|
+
'legacy per-submesh material overrides to collapse unambiguously onto Mesh v3 slots',
|
|
1056
|
+
hint: migrated.error.hint,
|
|
1057
|
+
detail: migrated.error as unknown as import('@forgeax/engine-types').AssetErrorDetail,
|
|
1058
|
+
}),
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
changed = true;
|
|
1062
|
+
nodes.push({
|
|
1063
|
+
...node,
|
|
1064
|
+
components: {
|
|
1065
|
+
...components,
|
|
1066
|
+
MeshRenderer: { ...components.MeshRenderer, materials: migrated.overrides },
|
|
1067
|
+
},
|
|
1068
|
+
} as SceneEntity);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const mounts: SceneInstanceMount[] = [];
|
|
1072
|
+
for (const mount of scene.mounts ?? []) {
|
|
1073
|
+
const child =
|
|
1074
|
+
typeof mount.source === 'string'
|
|
1075
|
+
? this.assetCatalog.get(mount.source.toLowerCase())?.payload
|
|
1076
|
+
: undefined;
|
|
1077
|
+
let nextMount = mount;
|
|
1078
|
+
let mountChanged = false;
|
|
1079
|
+
const mountComponents = mount.components as
|
|
1080
|
+
| Record<string, Record<string, unknown>>
|
|
1081
|
+
| undefined;
|
|
1082
|
+
const mountMeshGuid = mountComponents?.MeshFilter?.assetHandle;
|
|
1083
|
+
const mountMaterials = mountComponents?.MeshRenderer?.materials;
|
|
1084
|
+
const mountMesh =
|
|
1085
|
+
typeof mountMeshGuid === 'string'
|
|
1086
|
+
? this.assetCatalog.get(mountMeshGuid.toLowerCase())?.payload
|
|
1087
|
+
: undefined;
|
|
1088
|
+
if (
|
|
1089
|
+
mountMesh?.kind === 'mesh' &&
|
|
1090
|
+
typeof mountMeshGuid === 'string' &&
|
|
1091
|
+
Array.isArray(mountMaterials) &&
|
|
1092
|
+
mountMaterials.length >= mountMesh.submeshes.length &&
|
|
1093
|
+
mountMesh.submeshes.length > mountMesh.materialSlots.length &&
|
|
1094
|
+
mountMaterials.every((value) => typeof value === 'string')
|
|
1095
|
+
) {
|
|
1096
|
+
const migrated = migrateLegacyMeshMaterialOverrides(
|
|
1097
|
+
mountMaterials as string[],
|
|
1098
|
+
mountMesh.submeshes,
|
|
1099
|
+
mountMesh.materialSlots.length,
|
|
1100
|
+
{
|
|
1101
|
+
meshGuid: mountMeshGuid.toLowerCase(),
|
|
1102
|
+
sceneGuid: sceneGuidKey ?? '<unregistered-scene>',
|
|
1103
|
+
entityId: mount.localId as unknown as number,
|
|
1104
|
+
},
|
|
1105
|
+
);
|
|
1106
|
+
if (!migrated.ok) {
|
|
1107
|
+
return err(
|
|
1108
|
+
new AssetError({
|
|
1109
|
+
code: 'asset-parse-failed',
|
|
1110
|
+
expected:
|
|
1111
|
+
'legacy mount-entity materials to collapse unambiguously onto Mesh v3 slots',
|
|
1112
|
+
hint: migrated.error.hint,
|
|
1113
|
+
detail: migrated.error as unknown as import('@forgeax/engine-types').AssetErrorDetail,
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
mountChanged = true;
|
|
1118
|
+
nextMount = {
|
|
1119
|
+
...mount,
|
|
1120
|
+
components: {
|
|
1121
|
+
...mountComponents,
|
|
1122
|
+
MeshRenderer: { ...mountComponents?.MeshRenderer, materials: migrated.overrides },
|
|
1123
|
+
},
|
|
1124
|
+
} as SceneInstanceMount;
|
|
1125
|
+
}
|
|
1126
|
+
if (child?.kind !== 'scene' || mount.overrides === undefined) {
|
|
1127
|
+
if (mountChanged) changed = true;
|
|
1128
|
+
mounts.push(nextMount);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const meshByMember = this.sceneMeshGuidBySlot(child);
|
|
1132
|
+
const nextOverrides: MountOverride[] = [];
|
|
1133
|
+
for (const override of mount.overrides) {
|
|
1134
|
+
const childLocalId =
|
|
1135
|
+
(override.localId as unknown as number) - (mount.memberFirst as unknown as number);
|
|
1136
|
+
if (override.comp === 'MeshFilter') {
|
|
1137
|
+
const nextMeshGuid =
|
|
1138
|
+
override.field === 'assetHandle'
|
|
1139
|
+
? override.value
|
|
1140
|
+
: override.field === undefined &&
|
|
1141
|
+
typeof override.value === 'object' &&
|
|
1142
|
+
override.value !== null &&
|
|
1143
|
+
!Array.isArray(override.value)
|
|
1144
|
+
? (override.value as Record<string, unknown>).assetHandle
|
|
1145
|
+
: undefined;
|
|
1146
|
+
if (typeof nextMeshGuid === 'string') meshByMember.set(childLocalId, nextMeshGuid);
|
|
1147
|
+
nextOverrides.push(override);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const legacyMaterials =
|
|
1151
|
+
override.comp === 'MeshRenderer' && override.field === 'materials'
|
|
1152
|
+
? override.value
|
|
1153
|
+
: override.comp === 'MeshRenderer' &&
|
|
1154
|
+
override.field === undefined &&
|
|
1155
|
+
typeof override.value === 'object' &&
|
|
1156
|
+
override.value !== null &&
|
|
1157
|
+
!Array.isArray(override.value)
|
|
1158
|
+
? (override.value as Record<string, unknown>).materials
|
|
1159
|
+
: undefined;
|
|
1160
|
+
const meshGuid = meshByMember.get(childLocalId);
|
|
1161
|
+
const mesh =
|
|
1162
|
+
meshGuid === undefined
|
|
1163
|
+
? undefined
|
|
1164
|
+
: this.assetCatalog.get(meshGuid.toLowerCase())?.payload;
|
|
1165
|
+
if (
|
|
1166
|
+
mesh?.kind !== 'mesh' ||
|
|
1167
|
+
meshGuid === undefined ||
|
|
1168
|
+
!Array.isArray(legacyMaterials) ||
|
|
1169
|
+
legacyMaterials.length < mesh.submeshes.length ||
|
|
1170
|
+
mesh.submeshes.length <= mesh.materialSlots.length ||
|
|
1171
|
+
!legacyMaterials.every((value) => typeof value === 'string')
|
|
1172
|
+
) {
|
|
1173
|
+
nextOverrides.push(override);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const migrated = migrateLegacyMeshMaterialOverrides(
|
|
1177
|
+
legacyMaterials as string[],
|
|
1178
|
+
mesh.submeshes,
|
|
1179
|
+
mesh.materialSlots.length,
|
|
1180
|
+
{
|
|
1181
|
+
meshGuid: meshGuid.toLowerCase(),
|
|
1182
|
+
sceneGuid: sceneGuidKey ?? '<unregistered-scene>',
|
|
1183
|
+
entityId: override.localId as unknown as number,
|
|
1184
|
+
},
|
|
1185
|
+
);
|
|
1186
|
+
if (!migrated.ok) {
|
|
1187
|
+
return err(
|
|
1188
|
+
new AssetError({
|
|
1189
|
+
code: 'asset-parse-failed',
|
|
1190
|
+
expected:
|
|
1191
|
+
'legacy mount material overrides to collapse unambiguously onto Mesh v3 slots',
|
|
1192
|
+
hint: migrated.error.hint,
|
|
1193
|
+
detail: migrated.error as unknown as import('@forgeax/engine-types').AssetErrorDetail,
|
|
1194
|
+
}),
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
mountChanged = true;
|
|
1198
|
+
nextOverrides.push(
|
|
1199
|
+
override.field === 'materials'
|
|
1200
|
+
? { ...override, value: migrated.overrides }
|
|
1201
|
+
: {
|
|
1202
|
+
...override,
|
|
1203
|
+
value: {
|
|
1204
|
+
...(override.value as Record<string, unknown>),
|
|
1205
|
+
materials: migrated.overrides,
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
if (mountChanged) {
|
|
1211
|
+
changed = true;
|
|
1212
|
+
mounts.push({ ...nextMount, overrides: nextOverrides });
|
|
1213
|
+
} else {
|
|
1214
|
+
mounts.push(nextMount);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return ok({
|
|
1218
|
+
scene: changed
|
|
1219
|
+
? {
|
|
1220
|
+
...scene,
|
|
1221
|
+
entities: nodes,
|
|
1222
|
+
...(scene.mounts === undefined ? {} : { mounts }),
|
|
1223
|
+
}
|
|
1224
|
+
: scene,
|
|
1225
|
+
changed,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private preflightSceneMaterialGraph(
|
|
1230
|
+
scene: SceneAsset,
|
|
1231
|
+
sceneGuidKey?: string,
|
|
1232
|
+
visited: Set<string> = new Set(),
|
|
1233
|
+
): Result<void, AssetError> {
|
|
1234
|
+
const key = sceneGuidKey?.toLowerCase();
|
|
1235
|
+
if (key !== undefined) {
|
|
1236
|
+
if (visited.has(key)) return ok(undefined);
|
|
1237
|
+
visited.add(key);
|
|
1238
|
+
}
|
|
1239
|
+
const local = this.migrateLegacySceneMaterialOverrides(scene, sceneGuidKey);
|
|
1240
|
+
if (!local.ok) return local;
|
|
1241
|
+
for (const mount of scene.mounts ?? []) {
|
|
1242
|
+
if (typeof mount.source !== 'string') continue;
|
|
1243
|
+
const childKey = mount.source.toLowerCase();
|
|
1244
|
+
const child = this.assetCatalog.get(childKey)?.payload;
|
|
1245
|
+
if (child?.kind !== 'scene') continue;
|
|
1246
|
+
const nested = this.preflightSceneMaterialGraph(child, childKey, visited);
|
|
1247
|
+
if (!nested.ok) return nested;
|
|
1248
|
+
}
|
|
1249
|
+
return ok(undefined);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* @internal
|
|
1254
|
+
* Transform a SceneAsset whose handle-type component fields hold GUID
|
|
1255
|
+
* strings (post-parseScenePayload intermediate state) into a copy whose
|
|
1256
|
+
* handle fields hold resolved Handle numbers.
|
|
1257
|
+
*
|
|
1258
|
+
* Schema-driven field detection (plan-strategy D-4): for each component
|
|
1259
|
+
* field whose Component.schema fieldType starts with `shared\<`, the
|
|
1260
|
+
* value is treated as a GUID string and resolved via `AssetGuid.parse` +
|
|
1261
|
+
* catalogue lookup + `world.internSharedRef` (feat-20260614 M8 D-15/D-17;
|
|
1262
|
+
* the registry mints nothing). Unknown component names are silently passed
|
|
1263
|
+
* through (the ecs layer's additionalProperties check will catch unknowns at
|
|
1264
|
+
* spawn if appropriate).
|
|
1265
|
+
*
|
|
1266
|
+
* Stop-on-first-error (AC-08): the first unresolvable GUID aborts
|
|
1267
|
+
* iteration and returns `AssetError(code='asset-not-found')` with a hint
|
|
1268
|
+
* containing the GUID string, node localId, and field name for AI-user
|
|
1269
|
+
* debuggability (P3).
|
|
1270
|
+
*/
|
|
1271
|
+
_resolveSceneGuids(
|
|
1272
|
+
scene: SceneAsset,
|
|
1273
|
+
world: World,
|
|
1274
|
+
sceneGuidKey?: string,
|
|
1275
|
+
_visitedMountGuids?: Set<string>,
|
|
1276
|
+
_guidToHandle?: Map<string, number>,
|
|
1277
|
+
_resolvedSceneHandles?: Map<string, number>,
|
|
1278
|
+
_materialGraphPreflighted = false,
|
|
1279
|
+
): Result<SceneAsset, AssetError> {
|
|
1280
|
+
// feat-20260622 M3 / w8: reverse-decode from envelope.refs edges when
|
|
1281
|
+
// sceneGuidKey is provided and the catalog holds an envelope for this
|
|
1282
|
+
// scene. Each edge with sceneEntityId+sourceField.componentName carries
|
|
1283
|
+
// the (entityLocalId, componentName, fieldName, arrayIndex) triple —
|
|
1284
|
+
// no need to walk entities with a process-global component reflection table.
|
|
1285
|
+
// D-15/D-17 dedup contract: the same catalogued payload referenced from
|
|
1286
|
+
// multiple nodes must resolve to ONE user-tier handle. The local GUID map
|
|
1287
|
+
// avoids repeat lookups inside this traversal; World interning preserves
|
|
1288
|
+
// payload identity across separate scene-resolution calls.
|
|
1289
|
+
if (!_materialGraphPreflighted) {
|
|
1290
|
+
const graph = this.preflightSceneMaterialGraph(scene, sceneGuidKey);
|
|
1291
|
+
if (!graph.ok) return graph;
|
|
1292
|
+
}
|
|
1293
|
+
const localMigration = this.migrateLegacySceneMaterialOverrides(scene, sceneGuidKey);
|
|
1294
|
+
if (!localMigration.ok) return localMigration;
|
|
1295
|
+
const sceneForResolution = localMigration.value.scene;
|
|
1296
|
+
const preflightChanged = localMigration.value.changed;
|
|
1297
|
+
|
|
1298
|
+
const resolvedMap = new Map<string, number>();
|
|
1299
|
+
const guidToHandle = _guidToHandle ?? new Map<string, number>();
|
|
1300
|
+
const resolvedSceneHandles = _resolvedSceneHandles ?? new Map<string, number>();
|
|
1301
|
+
const sceneEnvelope =
|
|
1302
|
+
!preflightChanged && sceneGuidKey !== undefined
|
|
1303
|
+
? this.assetCatalog.get(sceneGuidKey)
|
|
1304
|
+
: undefined;
|
|
1305
|
+
// Did the structured-edge branch actually resolve anything? Prod-loaded
|
|
1306
|
+
// packs catalogue refs[] as GUID-only edges (sourceField / sceneEntityId
|
|
1307
|
+
// stripped at the w7 D-10 serialization boundary), so the rich-edge loop
|
|
1308
|
+
// below `continue`-skips every ref and resolves nothing. When that happens
|
|
1309
|
+
// we MUST fall through to the entity-walk fallback — otherwise the handle
|
|
1310
|
+
// fields keep their GUID strings and `spawn` writes the sentinel 0 while
|
|
1311
|
+
// `retainSharedScalarHandle(GUID)` routes `shared-ref-released` (the on-disk
|
|
1312
|
+
// game-scene instantiate crash: enemy MeshFilter.assetHandle).
|
|
1313
|
+
let resolvedFromEdges = false;
|
|
1314
|
+
if (
|
|
1315
|
+
sceneEnvelope !== undefined &&
|
|
1316
|
+
sceneEnvelope.refs !== undefined &&
|
|
1317
|
+
sceneEnvelope.refs.length > 0
|
|
1318
|
+
) {
|
|
1319
|
+
for (const ref of sceneEnvelope.refs) {
|
|
1320
|
+
const { sceneEntityId, sourceField } = ref;
|
|
1321
|
+
if (sceneEntityId === undefined || sourceField === undefined) continue;
|
|
1322
|
+
const { componentName, fieldName, arrayIndex } = sourceField;
|
|
1323
|
+
if (componentName === undefined || fieldName === undefined) continue;
|
|
1324
|
+
|
|
1325
|
+
const fieldPath = `${componentName}.${fieldName}${arrayIndex !== undefined ? `[${arrayIndex}]` : ''}`;
|
|
1326
|
+
|
|
1327
|
+
const envelope = this.assetCatalog.get(ref.guid.toLowerCase());
|
|
1328
|
+
if (envelope === undefined) {
|
|
1329
|
+
return err(
|
|
1330
|
+
new AssetError({
|
|
1331
|
+
code: 'asset-not-found',
|
|
1332
|
+
expected: `GUID ${ref.guid} catalogued in AssetRegistry`,
|
|
1333
|
+
hint:
|
|
1334
|
+
`GUID ${ref.guid} not catalogued; ` +
|
|
1335
|
+
`call loadByGuid('${ref.guid}') before instantiate; ` +
|
|
1336
|
+
`at node localId=${sceneEntityId}, field=${fieldPath}`,
|
|
1337
|
+
}),
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const payload = envelope.payload;
|
|
1341
|
+
const guidKey = ref.guid.toLowerCase();
|
|
1342
|
+
let resolvedSlot = guidToHandle.get(guidKey);
|
|
1343
|
+
if (resolvedSlot === undefined) {
|
|
1344
|
+
resolvedSlot = unwrapHandle(world.internSharedRef(payload.kind, payload));
|
|
1345
|
+
guidToHandle.set(guidKey, resolvedSlot);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const key =
|
|
1349
|
+
`${sceneEntityId}|${componentName}|${fieldName}` +
|
|
1350
|
+
(arrayIndex !== undefined ? `|${arrayIndex}` : '|');
|
|
1351
|
+
resolvedMap.set(key, resolvedSlot);
|
|
1352
|
+
resolvedFromEdges = true;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (!resolvedFromEdges) {
|
|
1356
|
+
// Fallback: positive extraction via extractSceneEntityHandleGuids when
|
|
1357
|
+
// the structured edges resolved nothing — either the scene envelope is
|
|
1358
|
+
// absent (unit tests that build a SceneAsset without cataloguing it) OR
|
|
1359
|
+
// the catalogued refs[] are GUID-only with no per-entity metadata (the
|
|
1360
|
+
// prod on-disk pack path). The entity-component walk recovers the
|
|
1361
|
+
// (localId, componentName, fieldName, arrayIndex) triple the bare edge
|
|
1362
|
+
// dropped, so GUID strings resolve to live handles before spawn.
|
|
1363
|
+
const entries = extractSceneEntityHandleGuids(
|
|
1364
|
+
world.components.entries(),
|
|
1365
|
+
sceneForResolution.entities as unknown as ReadonlyArray<{
|
|
1366
|
+
readonly localId: number;
|
|
1367
|
+
readonly components: Record<string, Record<string, unknown>>;
|
|
1368
|
+
}>,
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
for (const entry of entries) {
|
|
1372
|
+
const fieldPath =
|
|
1373
|
+
`${entry.componentName}.${entry.fieldName}` +
|
|
1374
|
+
(entry.arrayIndex !== undefined ? `[${entry.arrayIndex}]` : '');
|
|
1375
|
+
const resolvedSlot = resolveHandleGuid(
|
|
1376
|
+
this,
|
|
1377
|
+
world,
|
|
1378
|
+
entry.guidString,
|
|
1379
|
+
guidToHandle,
|
|
1380
|
+
fieldPath,
|
|
1381
|
+
`node localId=${entry.entityLocalId}`,
|
|
1382
|
+
);
|
|
1383
|
+
if (!resolvedSlot.ok) return resolvedSlot;
|
|
1384
|
+
|
|
1385
|
+
const key =
|
|
1386
|
+
`${entry.entityLocalId}|${entry.componentName}|${entry.fieldName}` +
|
|
1387
|
+
(entry.arrayIndex !== undefined ? `|${entry.arrayIndex}` : '|');
|
|
1388
|
+
resolvedMap.set(key, resolvedSlot.value);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Build the resolved copy. Handle-type fields (detected above) are
|
|
1393
|
+
// reconstructed from the resolvedMap; all other fields pass through as-is.
|
|
1394
|
+
const resolvedNodes: SceneEntity[] = [];
|
|
1395
|
+
for (const node of sceneForResolution.entities) {
|
|
1396
|
+
const rawComponents = node.components as Record<string, Record<string, unknown>>;
|
|
1397
|
+
const resolvedComponents: Record<string, Record<string, unknown>> = {};
|
|
1398
|
+
|
|
1399
|
+
for (const compName of Object.keys(rawComponents)) {
|
|
1400
|
+
const rawFields = rawComponents[compName];
|
|
1401
|
+
if (!rawFields) {
|
|
1402
|
+
resolvedComponents[compName] = {};
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const resolvedFields: Record<string, unknown> = {};
|
|
1406
|
+
for (const fieldName of Object.keys(rawFields)) {
|
|
1407
|
+
const value = rawFields[fieldName];
|
|
1408
|
+
const plainKey = `${node.localId}|${compName}|${fieldName}|`;
|
|
1409
|
+
const plainResolved = resolvedMap.get(plainKey);
|
|
1410
|
+
if (plainResolved !== undefined) {
|
|
1411
|
+
resolvedFields[fieldName] = plainResolved;
|
|
1412
|
+
} else if (Array.isArray(value)) {
|
|
1413
|
+
const resolvedArr: number[] = [];
|
|
1414
|
+
let hasAnyResolved = false;
|
|
1415
|
+
for (let i = 0; i < value.length; i++) {
|
|
1416
|
+
const arrKey = `${node.localId}|${compName}|${fieldName}|${i}`;
|
|
1417
|
+
const arrResolved = resolvedMap.get(arrKey);
|
|
1418
|
+
if (arrResolved !== undefined) {
|
|
1419
|
+
resolvedArr.push(arrResolved);
|
|
1420
|
+
hasAnyResolved = true;
|
|
1421
|
+
} else if (typeof value[i] === 'number') {
|
|
1422
|
+
resolvedArr.push(value[i]);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
resolvedFields[fieldName] = hasAnyResolved ? resolvedArr : value;
|
|
1426
|
+
} else {
|
|
1427
|
+
resolvedFields[fieldName] = value;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
resolvedComponents[compName] = resolvedFields;
|
|
1431
|
+
}
|
|
1432
|
+
resolvedNodes.push({
|
|
1433
|
+
localId: node.localId,
|
|
1434
|
+
components: resolvedComponents,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// ── m3-i2 / m3-i3: Resolve mounts recursively (breakpoint B fix) ──
|
|
1439
|
+
// For each mount.source (GUID string), look up the child scene in
|
|
1440
|
+
// assetCatalog, recursively resolve its GUIDs, allocSharedRef the
|
|
1441
|
+
// resolved child copy, register it in originIndex (D-7), and produce
|
|
1442
|
+
// a resolved mount with source as the live handle number.
|
|
1443
|
+
// Cycle detection via visited GUID set (R-9): re-entry =>
|
|
1444
|
+
// pack-cyclic-reference / mount-asset, cast through the return type
|
|
1445
|
+
// as world.ts does for its PackError exits.
|
|
1446
|
+
const mountVisited = _visitedMountGuids ?? new Set<string>();
|
|
1447
|
+
if (sceneGuidKey !== undefined) mountVisited.add(sceneGuidKey.toLowerCase());
|
|
1448
|
+
if (sceneForResolution.mounts !== undefined && sceneForResolution.mounts.length > 0) {
|
|
1449
|
+
// feat-20260713 M3 / w13: share the entity-field dedup map so an override
|
|
1450
|
+
// value GUID that also appears as an entity field mints one handle (D-15/D-17).
|
|
1451
|
+
const resolvedMounts = resolveMountsRec(
|
|
1452
|
+
this,
|
|
1453
|
+
sceneForResolution.mounts,
|
|
1454
|
+
world,
|
|
1455
|
+
mountVisited,
|
|
1456
|
+
guidToHandle,
|
|
1457
|
+
resolvedSceneHandles,
|
|
1458
|
+
);
|
|
1459
|
+
if (sceneGuidKey !== undefined) mountVisited.delete(sceneGuidKey.toLowerCase());
|
|
1460
|
+
if (!resolvedMounts.ok) {
|
|
1461
|
+
// Cycle or child-resolution error: cast through as AssetError
|
|
1462
|
+
// (same pattern as world.ts PackError-as-EcsError casts).
|
|
1463
|
+
return resolvedMounts as unknown as Result<SceneAsset, AssetError>;
|
|
1464
|
+
}
|
|
1465
|
+
return ok({
|
|
1466
|
+
kind: 'scene',
|
|
1467
|
+
entities: resolvedNodes,
|
|
1468
|
+
mounts: resolvedMounts.value,
|
|
1469
|
+
} as SceneAsset);
|
|
1470
|
+
}
|
|
1471
|
+
if (sceneGuidKey !== undefined) mountVisited.delete(sceneGuidKey.toLowerCase());
|
|
1472
|
+
return ok({ kind: 'scene', entities: resolvedNodes });
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Register an asset and return a fresh
|
|
1477
|
+
* `Result<Handle<TagOf<T>, 'shared'>, AssetError>`. The brand `target`
|
|
1478
|
+
* tag is derived from the Asset's `kind` discriminator via `AssetTagMap`
|
|
1479
|
+
* (charter F1 single-entry indexability). The runtime representation is
|
|
1480
|
+
* an auto-incrementing u32 starting at 1024 (builtins reserve 1-2).
|
|
1481
|
+
*
|
|
1482
|
+
* feat-20260526 M4: `shadingModel` field is retired in favour of
|
|
1483
|
+
* pass-based MaterialAsset. This generic surface covers the full
|
|
1484
|
+
* `Asset` closed union (mesh / texture / sampler / scene / equirect
|
|
1485
|
+
* / material).
|
|
1486
|
+
*/
|
|
1487
|
+
/**
|
|
1488
|
+
* feat-20260614 M8 (D-15 / D-17): catalogue a payload under its GUID.
|
|
1489
|
+
* Replaces the old `register` / `registerWithGuid` mint pair -- the registry
|
|
1490
|
+
* stores the PAYLOAD and never produces a handle (it owns no World).
|
|
1491
|
+
* Column minting is the caller's job via `world.allocSharedRef`.
|
|
1492
|
+
*
|
|
1493
|
+
* Validates mesh stride + material passes / sprite slices at catalogue entry
|
|
1494
|
+
* (same fail-fast surface as the old register path). Returns
|
|
1495
|
+
* `Result.err(AssetError)` on validation failure; `Result.ok(payload)` with
|
|
1496
|
+
* the stored payload (mesh payloads gain an `aabb`) on success.
|
|
1497
|
+
*/
|
|
1498
|
+
catalog<T = Asset>(
|
|
1499
|
+
guid: AssetGuid | string,
|
|
1500
|
+
asset: T,
|
|
1501
|
+
refs?: readonly AssetRef[],
|
|
1502
|
+
): Result<T, AssetError> {
|
|
1503
|
+
// D-5: narrow T to Asset for kind-discriminate branches. The runtime
|
|
1504
|
+
// catalog only accepts Asset-kind payloads (host custom kinds enter
|
|
1505
|
+
// through loadByGuid + registerParsedAsset, not catalog directly).
|
|
1506
|
+
const a: Asset = asset as unknown as Asset;
|
|
1507
|
+
const meshValidation = validateMeshPayload(a);
|
|
1508
|
+
if (meshValidation !== null) return err(meshValidation);
|
|
1509
|
+
|
|
1510
|
+
// feat-20260608 M0 baseline rebuild: tileset payload fail-fast gate at
|
|
1511
|
+
// register entry — region rectangle bounds-check uses the implicit atlas
|
|
1512
|
+
// extent (columns * tileWidth x rows * tileHeight) when the caller did
|
|
1513
|
+
// not supply an explicit one (charter P3 explicit failure).
|
|
1514
|
+
if (a.kind === 'tileset') {
|
|
1515
|
+
const tilesetAsset = a as TilesetAsset;
|
|
1516
|
+
const tilesetValidation = validateTilesetPayload(
|
|
1517
|
+
tilesetAsset,
|
|
1518
|
+
inferAtlasExtent(tilesetAsset),
|
|
1519
|
+
);
|
|
1520
|
+
if (tilesetValidation !== null) return err(tilesetValidation);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// feat-20260527 M2 / w6: material validation with union paramSchema
|
|
1524
|
+
// semantics across all passes (plan-strategy D-2, D-5).
|
|
1525
|
+
if (a.kind === 'material') {
|
|
1526
|
+
const matValidation = validateMaterialPasses(this, a as MaterialAsset);
|
|
1527
|
+
if (matValidation !== null) return err(matValidation);
|
|
1528
|
+
const sliceValidation = validateSpriteSlices(this, a as MaterialAsset);
|
|
1529
|
+
if (sliceValidation !== null) return err(sliceValidation);
|
|
1530
|
+
detectTileNeedsRepeatSampler(this, a as MaterialAsset);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
let stored: Asset = a;
|
|
1534
|
+
if (a.kind === 'mesh') {
|
|
1535
|
+
stored = withMeshAabb(a as TypesMeshAsset);
|
|
1536
|
+
}
|
|
1537
|
+
const key =
|
|
1538
|
+
typeof guid === 'string' ? guid.toLowerCase() : AssetGuid.format(guid).toLowerCase();
|
|
1539
|
+
const kind = a.kind;
|
|
1540
|
+
// Drain any name recorded by an earlier _registerPackage call whose body had
|
|
1541
|
+
// not yet been catalogued (prod disk path; D-6). Preserve a name already on
|
|
1542
|
+
// a prior envelope for this key (re-catalog of the same GUID).
|
|
1543
|
+
const pendingName = this.pendingNames.get(key);
|
|
1544
|
+
const priorEnvelope = this.assetCatalog.get(key);
|
|
1545
|
+
const priorName = priorEnvelope?.name;
|
|
1546
|
+
const name = pendingName ?? priorName;
|
|
1547
|
+
this.pendingNames.delete(key);
|
|
1548
|
+
// feat-20260713 M4 / w15 (D-6, root cause a): when this GUID is being
|
|
1549
|
+
// re-catalogued with a DIFFERENT payload object, the prior object may still
|
|
1550
|
+
// be live behind a handle minted before the override (asset-registry.ts:513
|
|
1551
|
+
// identity scan would then MISS it). Record the superseded object -> GUID in
|
|
1552
|
+
// the origin reverse-index so `_guidForAsset` keeps resolving it — save/
|
|
1553
|
+
// collect of an owned entity holding that handle no longer fails with a
|
|
1554
|
+
// GUID-unresolved error (the 2026-07-06 crash). Structurally-modified copies
|
|
1555
|
+
// (a fresh object never catalogued) stay uncatalogued and correctly surface a
|
|
1556
|
+
// structured error at collect (requirements edge case "modified payload judged
|
|
1557
|
+
// as a new asset, not silently zeroed").
|
|
1558
|
+
if (priorEnvelope !== undefined && priorEnvelope.payload !== stored) {
|
|
1559
|
+
this._originIndex.set(priorEnvelope.payload, key);
|
|
1560
|
+
}
|
|
1561
|
+
this.assetCatalog.set(key, {
|
|
1562
|
+
guid: key,
|
|
1563
|
+
kind,
|
|
1564
|
+
...(name !== undefined ? { name } : {}),
|
|
1565
|
+
payload: stored,
|
|
1566
|
+
refs: refs ?? [],
|
|
1567
|
+
});
|
|
1568
|
+
this.catalogEpoch++;
|
|
1569
|
+
this.loadState.registerReady(key, stored);
|
|
1570
|
+
// D-1: catalog() inline path defaults every GUID to the no-package state
|
|
1571
|
+
// (null). loadByGuid + builtin override via their own registerPackage calls
|
|
1572
|
+
// before / after this so the package mapping is populated for all assets
|
|
1573
|
+
// through the single primitive (#1 SSOT). Do not clobber a package mapping
|
|
1574
|
+
// a prior registerPackage already established for this GUID.
|
|
1575
|
+
if (!this.packages.has(key)) this.packages.set(key, null);
|
|
1576
|
+
return ok(stored as T);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* @internal feat-20260618-asset-and-pack-name-fields M3 (D-1): the single
|
|
1581
|
+
* package-mapping write primitive. All three registration entry points funnel
|
|
1582
|
+
* here so the XOR name invariant is implemented once (#1 SSOT):
|
|
1583
|
+
* - catalog() inline path -> registerPackage(null, [guid]) (no package)
|
|
1584
|
+
* - loadByGuid disk path -> registerPackage(packageUrl, [g1,g2,...], names)
|
|
1585
|
+
* - constructor builtin -> registerPackage(null, [...guids]) (D-5 null)
|
|
1586
|
+
*
|
|
1587
|
+
* `path === null` registers the GUIDs with no package (resolveName reads their
|
|
1588
|
+
* storedName or returns ''). A non-null `path` finds-or-creates the shared
|
|
1589
|
+
* MutablePackage for that path and adds the GUIDs to it; per-GUID entry names
|
|
1590
|
+
* (D-2: name flows entry -> Package, never the payload) are taken from
|
|
1591
|
+
* `names`. The 1->N promotion branch (D-3) is added by w11. Never throws --
|
|
1592
|
+
* it only writes maps; resolution + validation happen in resolveName / rename.
|
|
1593
|
+
*/
|
|
1594
|
+
_registerPackage(
|
|
1595
|
+
path: string | null,
|
|
1596
|
+
guids: readonly string[],
|
|
1597
|
+
names?: Map<string, string>,
|
|
1598
|
+
): void {
|
|
1599
|
+
if (path === null) {
|
|
1600
|
+
for (const g of guids) {
|
|
1601
|
+
const key = g.toLowerCase();
|
|
1602
|
+
this.packages.set(key, null);
|
|
1603
|
+
const n = names?.get(g) ?? names?.get(key);
|
|
1604
|
+
if (n !== undefined) this.setStoredName(key, n);
|
|
1605
|
+
}
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const pkg = this.packageByPath.get(path) ?? { path, assetGuids: new Set<string>() };
|
|
1610
|
+
this.packageByPath.set(path, pkg);
|
|
1611
|
+
|
|
1612
|
+
// D-3: 1->N promotion. When this path already holds exactly one asset and a
|
|
1613
|
+
// new member is arriving, freeze the original asset's derived basename as its
|
|
1614
|
+
// stored name so it joins the multi-asset branch with a stable name. The
|
|
1615
|
+
// freeze is idempotent: an original that already carries a stored name (the
|
|
1616
|
+
// abnormal single-asset-with-name state, D-4) is left untouched and the
|
|
1617
|
+
// soft-violation counter records it (charter P3 machine-readable signal).
|
|
1618
|
+
const addsNewMember = guids.some((g) => !pkg.assetGuids.has(g.toLowerCase()));
|
|
1619
|
+
if (pkg.assetGuids.size === 1 && addsNewMember) {
|
|
1620
|
+
const [originalKey] = pkg.assetGuids;
|
|
1621
|
+
if (originalKey !== undefined) {
|
|
1622
|
+
if (this.hasStoredName(originalKey)) {
|
|
1623
|
+
this.metrics?.increment('package.xor-invariant-violated');
|
|
1624
|
+
} else {
|
|
1625
|
+
this.setStoredName(originalKey, deriveAssetName(pkg.path, 1));
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
for (const g of guids) {
|
|
1631
|
+
const key = g.toLowerCase();
|
|
1632
|
+
pkg.assetGuids.add(key);
|
|
1633
|
+
this.packages.set(key, pkg);
|
|
1634
|
+
const n = names?.get(g) ?? names?.get(key);
|
|
1635
|
+
if (n !== undefined) this.setStoredName(key, n);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Read the per-GUID stored display name (D-6 home: the envelope's `name`
|
|
1641
|
+
* field, with `pendingNames` covering the prod-disk ordering where the name is
|
|
1642
|
+
* known before the body is catalogued). Single read point for resolveName /
|
|
1643
|
+
* the 1->N promotion XOR check.
|
|
1644
|
+
*/
|
|
1645
|
+
private storedNameFor(key: string): string | undefined {
|
|
1646
|
+
return this.assetCatalog.get(key)?.name ?? this.pendingNames.get(key);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private hasStoredName(key: string): boolean {
|
|
1650
|
+
return this.storedNameFor(key) !== undefined;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Write the per-GUID stored display name. When the envelope exists, replace it
|
|
1655
|
+
* with one carrying the new `name` (the envelope is immutable; D-6 keeps the
|
|
1656
|
+
* payload free of the name). Before the envelope is catalogued (prod disk
|
|
1657
|
+
* path), stash on `pendingNames` so catalog() can drain it into the new
|
|
1658
|
+
* envelope. `name === undefined` clears the name in both homes.
|
|
1659
|
+
*/
|
|
1660
|
+
private setStoredName(key: string, name: string | undefined): void {
|
|
1661
|
+
const envelope = this.assetCatalog.get(key);
|
|
1662
|
+
if (envelope !== undefined) {
|
|
1663
|
+
const { name: _drop, ...rest } = envelope;
|
|
1664
|
+
this.assetCatalog.set(key, name === undefined ? rest : { ...rest, name });
|
|
1665
|
+
this.pendingNames.delete(key);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (name === undefined) this.pendingNames.delete(key);
|
|
1669
|
+
else this.pendingNames.set(key, name);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Return the `Package` this GUID belongs to, or `null` when the asset has no
|
|
1674
|
+
* package (catalog() inline + builtin, D-5), or `undefined` when the GUID was
|
|
1675
|
+
* never registered. The returned `Package` is a readonly snapshot whose
|
|
1676
|
+
* `assetCount` is derived from the live member set (#2 Derive).
|
|
1677
|
+
*/
|
|
1678
|
+
packageOf(guid: AssetGuid | string): Package | null | undefined {
|
|
1679
|
+
const key =
|
|
1680
|
+
typeof guid === 'string' ? guid.toLowerCase() : AssetGuid.format(guid).toLowerCase();
|
|
1681
|
+
const pkg = this.packages.get(key);
|
|
1682
|
+
if (pkg === undefined) return undefined;
|
|
1683
|
+
if (pkg === null) return null;
|
|
1684
|
+
return { path: pkg.path, assetGuids: pkg.assetGuids, assetCount: pkg.assetGuids.size };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Resolve an asset's human-readable display name -- the single source of truth
|
|
1689
|
+
* for the two-segment identity's `name` segment (D-6). Every name consumer
|
|
1690
|
+
* (inspect / catalog builder / CLI) reads this or the same `deriveAssetName`
|
|
1691
|
+
* pure function it delegates to (AC-04); no consumer re-implements the XOR
|
|
1692
|
+
* rule. Returns a deterministic fallback rather than throwing on a missing
|
|
1693
|
+
* name (AC-15): `basename(path)` for a multi-asset entry that lacks a stored
|
|
1694
|
+
* name, or `''` for a no-package asset with no self name (the detectable
|
|
1695
|
+
* "genuinely no name" signal, charter P3). An unregistered GUID is treated as
|
|
1696
|
+
* the no-package branch.
|
|
1697
|
+
*/
|
|
1698
|
+
resolveName(guid: AssetGuid | string): string {
|
|
1699
|
+
const key =
|
|
1700
|
+
typeof guid === 'string' ? guid.toLowerCase() : AssetGuid.format(guid).toLowerCase();
|
|
1701
|
+
const pkg = this.packages.get(key);
|
|
1702
|
+
const storedName = this.storedNameFor(key);
|
|
1703
|
+
const path = pkg == null ? null : pkg.path;
|
|
1704
|
+
const assetCount = pkg == null ? 0 : pkg.assetGuids.size;
|
|
1705
|
+
return deriveAssetName(path, assetCount, storedName);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Rename an asset's display name in memory (D-4). Three classes by package
|
|
1710
|
+
* shape:
|
|
1711
|
+
* - no-package asset -> set the stored self name
|
|
1712
|
+
* - multi-asset package -> set the entry stored name
|
|
1713
|
+
* - single-asset package -> rewrite the package path's leaf segment so the
|
|
1714
|
+
* derived basename becomes `newName` (the package
|
|
1715
|
+
* stays single-asset; the leaf IS the name)
|
|
1716
|
+
*
|
|
1717
|
+
* In-memory only (OOS-1: no disk write-back). Returns structured failures via
|
|
1718
|
+
* the closed `AssetErrorCode` union with no new members (D-4): a name that
|
|
1719
|
+
* collides with another member of the same package -> `asset-invalid-value`;
|
|
1720
|
+
* an unregistered GUID -> `asset-not-found`. AI users consume `.code` through
|
|
1721
|
+
* a `switch`, not by parsing `.message` (charter P3).
|
|
1722
|
+
*/
|
|
1723
|
+
rename(guid: AssetGuid | string, newName: string): Result<void, AssetError> {
|
|
1724
|
+
const key =
|
|
1725
|
+
typeof guid === 'string' ? guid.toLowerCase() : AssetGuid.format(guid).toLowerCase();
|
|
1726
|
+
if (!this.packages.has(key)) {
|
|
1727
|
+
return err(
|
|
1728
|
+
new AssetError({
|
|
1729
|
+
code: 'asset-not-found',
|
|
1730
|
+
expected: `a registered asset for GUID ${key}`,
|
|
1731
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1732
|
+
}),
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const pkg = this.packages.get(key) ?? null;
|
|
1737
|
+
|
|
1738
|
+
const collision = pkg !== null ? this.nameCollisionIn(pkg, key, newName) : null;
|
|
1739
|
+
if (collision !== null) return err(collision);
|
|
1740
|
+
|
|
1741
|
+
if (pkg !== null && pkg.assetGuids.size === 1) {
|
|
1742
|
+
// Single-asset package: the leaf segment IS the derived name; rewrite it so
|
|
1743
|
+
// basename(path) === newName. Keep the directory prefix intact.
|
|
1744
|
+
const slash = pkg.path.lastIndexOf('/');
|
|
1745
|
+
const oldPath = pkg.path;
|
|
1746
|
+
pkg.path = slash >= 0 ? `${pkg.path.slice(0, slash + 1)}${newName}` : newName;
|
|
1747
|
+
this.packageByPath.delete(oldPath);
|
|
1748
|
+
this.packageByPath.set(pkg.path, pkg);
|
|
1749
|
+
this.setStoredName(key, undefined);
|
|
1750
|
+
return ok(undefined);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
this.setStoredName(key, newName);
|
|
1754
|
+
return ok(undefined);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Return an `asset-invalid-value` AssetError if another member of `pkg`
|
|
1759
|
+
* already resolves to `newName`, else null. Extracted from `rename` to keep
|
|
1760
|
+
* the collision-detection control flow flat (D-4 reuses the closed error code;
|
|
1761
|
+
* the detail narrows via the `{ field, value, reason }` union variant).
|
|
1762
|
+
*/
|
|
1763
|
+
private nameCollisionIn(
|
|
1764
|
+
pkg: MutablePackage,
|
|
1765
|
+
selfKey: string,
|
|
1766
|
+
newName: string,
|
|
1767
|
+
): AssetError | null {
|
|
1768
|
+
for (const memberKey of pkg.assetGuids) {
|
|
1769
|
+
if (memberKey !== selfKey && this.resolveName(memberKey) === newName) {
|
|
1770
|
+
return new AssetError({
|
|
1771
|
+
code: 'asset-invalid-value',
|
|
1772
|
+
expected: `a name unique within package "${pkg.path}"`,
|
|
1773
|
+
hint: `another asset in "${pkg.path}" is already named "${newName}"; choose a distinct name`,
|
|
1774
|
+
detail: {
|
|
1775
|
+
field: 'name',
|
|
1776
|
+
value: newName,
|
|
1777
|
+
reason: `duplicate name within package ${pkg.path}`,
|
|
1778
|
+
},
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Parse a dash-form GUID string into an `AssetGuid`. Thin convenience over
|
|
1787
|
+
* `AssetGuid.parse` for the `loadByGuid` / `catalog` call sites; throws
|
|
1788
|
+
* `AssetError` on a malformed GUID (caller-error, mirrors `parseInt`-style
|
|
1789
|
+
* eager validation -- the GUID literal is author-supplied, not user data).
|
|
1790
|
+
*/
|
|
1791
|
+
parseGuid(guidStr: string): AssetGuid {
|
|
1792
|
+
const parsed = AssetGuid.parse(guidStr);
|
|
1793
|
+
if (!parsed.ok) {
|
|
1794
|
+
throw new AssetError({
|
|
1795
|
+
code: 'asset-parse-failed',
|
|
1796
|
+
expected: `valid dash-form GUID, got "${guidStr}"`,
|
|
1797
|
+
hint: ASSET_ERROR_HINTS['asset-parse-failed'],
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
return parsed.value;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Look up a catalogued payload by GUID, or `undefined` on miss. Used by the
|
|
1805
|
+
* ECS/render side (e.g. `walkMaterialParents` in `resolve-asset-handle.ts`)
|
|
1806
|
+
* to resolve a payload's embedded sub-asset GUIDs (D-19) without minting.
|
|
1807
|
+
*/
|
|
1808
|
+
lookup<T = Asset>(guid: AssetGuid | string): T | undefined {
|
|
1809
|
+
const key =
|
|
1810
|
+
typeof guid === 'string' ? guid.toLowerCase() : AssetGuid.format(guid).toLowerCase();
|
|
1811
|
+
const ready = this.loadState.getReady<T>(key);
|
|
1812
|
+
if (ready !== undefined) return ready;
|
|
1813
|
+
if (this.packIndexUrl === undefined)
|
|
1814
|
+
return this.assetCatalog.get(key)?.payload as T | undefined;
|
|
1815
|
+
return undefined;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* feat-20260613-material-paramschema-driven-binding M4 / w23 (D-5 graceful):
|
|
1820
|
+
* Return the texture-field name set for the given material-shader id,
|
|
1821
|
+
* derived from the registered shader's paramSchema via `derive(paramSchema)
|
|
1822
|
+
* .textureFieldNames`. Returns `undefined` when the shader is not yet
|
|
1823
|
+
* registered (cross-worktree shader-late-register, plan R-4).
|
|
1824
|
+
*
|
|
1825
|
+
* Used by `extractFrame` to know which values fields the shader
|
|
1826
|
+
* declares as texture handles; the extract layer validates handle-vs-
|
|
1827
|
+
* scalar typing and drops misclassified slots so the record stage's
|
|
1828
|
+
* MISSING_TEXTURE_HANDLE fallback can take over (white default texture)
|
|
1829
|
+
* rather than letting a stray handle reach `device.createBindGroup`.
|
|
1830
|
+
*
|
|
1831
|
+
* feat-20260705-runtime-tier2-decomposition M1 / w5 (D-4): delegates to the
|
|
1832
|
+
* extracted `./registry/validate-material` free function (signature stable).
|
|
1833
|
+
*/
|
|
1834
|
+
materialShaderTextureFieldNames(shaderId: string): ReadonlySet<string> | undefined {
|
|
1835
|
+
return materialShaderTextureFieldNamesImpl(this, shaderId);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Load an asset and all its transitively referenced sub-assets by GUID.
|
|
1840
|
+
* The result is a durable Asset payload. Consumer owners may narrow the
|
|
1841
|
+
* success type, then call their own World/Host projection; this registry does
|
|
1842
|
+
* not create a generic GUID-to-handle materializer.
|
|
1843
|
+
* Delegates to the load-by-guid collaboration module (w7 / D-4); see
|
|
1844
|
+
* registry/load-by-guid.ts for the full DDC / pack-fetch pipeline.
|
|
1845
|
+
*/
|
|
1846
|
+
async loadByGuid<T = Asset>(
|
|
1847
|
+
guid: AssetGuid,
|
|
1848
|
+
parentContext?: {
|
|
1849
|
+
sceneEntityId?: number;
|
|
1850
|
+
componentField?: string;
|
|
1851
|
+
},
|
|
1852
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
1853
|
+
return loadByGuidImpl<T>(this, guid, parentContext);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Dispatch a pack payload through the injected LoaderRegistry. Delegates to
|
|
1858
|
+
* the load-by-guid collaboration module (w7 / D-4). Kept as a class method so
|
|
1859
|
+
* existing structural-cast test access keeps resolving.
|
|
1860
|
+
*/
|
|
1861
|
+
parseAssetPayload(
|
|
1862
|
+
kind: string,
|
|
1863
|
+
payload: Record<string, unknown>,
|
|
1864
|
+
refs?: string[],
|
|
1865
|
+
):
|
|
1866
|
+
| Asset
|
|
1867
|
+
| Record<string, unknown>
|
|
1868
|
+
| undefined
|
|
1869
|
+
| { readonly ok: false; readonly error: ParseErrorDetail } {
|
|
1870
|
+
return parseAssetPayloadImpl(this, kind, payload, refs);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* Parse a pack asset entry and return the payload + refs. Delegates to the
|
|
1875
|
+
* load-by-guid collaboration module (w7 / D-4). Kept as a class method so
|
|
1876
|
+
* existing structural-cast test access keeps resolving.
|
|
1877
|
+
*/
|
|
1878
|
+
parseAndReturnAsset(assetEntry: {
|
|
1879
|
+
kind: string;
|
|
1880
|
+
payload: Record<string, unknown>;
|
|
1881
|
+
refs?: string[];
|
|
1882
|
+
}): Result<{ asset: Asset; refs: readonly string[] }, AssetError> {
|
|
1883
|
+
return parseAndReturnAssetImpl(this, assetEntry);
|
|
1884
|
+
}
|
|
1885
|
+
/** Return the legacy catalog snapshot when no GUID is supplied. */
|
|
1886
|
+
inspect(): InspectSnapshot;
|
|
1887
|
+
/** Return joined source/cook/package/runtime evidence for one GUID. */
|
|
1888
|
+
inspect(guid: string): Promise<Result<AssetEvidence, AssetEvidenceError>>;
|
|
1889
|
+
inspect(guid?: string): InspectSnapshot | Promise<Result<AssetEvidence, AssetEvidenceError>> {
|
|
1890
|
+
if (guid !== undefined) return this.assetEvidenceAdapter.inspect(guid);
|
|
1891
|
+
const assets: InspectEntry[] = [];
|
|
1892
|
+
for (const [guid, envelope] of this.assetCatalog) {
|
|
1893
|
+
assets.push({
|
|
1894
|
+
guid,
|
|
1895
|
+
kind: envelope.payload.kind,
|
|
1896
|
+
name: this.resolveName(guid),
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
return { assets };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/** Verify the same GUID evidence chain through the injected SDK capability. */
|
|
1903
|
+
verifyByGuid(guid: string): Promise<Result<AssetEvidence, AssetEvidenceError>> {
|
|
1904
|
+
return this.assetEvidenceAdapter.verifyByGuid(guid);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* Return a readonly snapshot of all catalogued assets (inlined + pack-index)
|
|
1909
|
+
* for enumeration by asset panels (AC-03 single source of truth).
|
|
1910
|
+
*
|
|
1911
|
+
* Merges entries from the private `packIndexCache` (prod path, carries
|
|
1912
|
+
* `packageUrl`) and `assetCatalog` (inlined / dev path, no URL). Each
|
|
1913
|
+
* GUID appears exactly once. Returns a fresh array on every call — the
|
|
1914
|
+
* internal Maps are never exposed (charter P4 consistent abstraction).
|
|
1915
|
+
*
|
|
1916
|
+
* plan-strategy section 2 D1; requirements AC-03; research Finding 5.
|
|
1917
|
+
*
|
|
1918
|
+
* @example
|
|
1919
|
+
* ```ts
|
|
1920
|
+
* for (const e of registry.listCatalog()) {
|
|
1921
|
+
* console.log(e.guid, e.kind, e.name, e.packageUrl);
|
|
1922
|
+
* }
|
|
1923
|
+
* ```
|
|
1924
|
+
*/
|
|
1925
|
+
listCatalog(): readonly {
|
|
1926
|
+
guid: string;
|
|
1927
|
+
kind: string;
|
|
1928
|
+
name?: string;
|
|
1929
|
+
packageUrl: string;
|
|
1930
|
+
packageId?: CatalogEntry['packageId'];
|
|
1931
|
+
provenance?: CatalogEntry['provenance'];
|
|
1932
|
+
revision?: CatalogEntry['revision'];
|
|
1933
|
+
sourceKey?: CatalogEntry['sourceKey'];
|
|
1934
|
+
sourceIndex?: CatalogEntry['sourceIndex'];
|
|
1935
|
+
sourceOverrides?: CatalogEntry['sourceOverrides'];
|
|
1936
|
+
sourceOverrideDescriptors?: CatalogEntry['sourceOverrideDescriptors'];
|
|
1937
|
+
authoring?: CatalogEntry['authoring'];
|
|
1938
|
+
relations?: CatalogEntry['relations'];
|
|
1939
|
+
diagnostics?: CatalogEntry['diagnostics'];
|
|
1940
|
+
refs?: readonly string[];
|
|
1941
|
+
subject?: import('@forgeax/engine-types').CatalogSubject;
|
|
1942
|
+
execution?: import('@forgeax/engine-types').CookExecution;
|
|
1943
|
+
lifecycle?: import('@forgeax/engine-types').CatalogLifecycle;
|
|
1944
|
+
projection?: import('@forgeax/engine-types').CatalogProjection;
|
|
1945
|
+
/**
|
|
1946
|
+
* On-disk source-file path for external imported assets (FBX / GLB / HDR /
|
|
1947
|
+
* audio / font), relative to the game root. Editors locate the
|
|
1948
|
+
* `.meta.json` sidecar via `sourcePath + '.meta.json'` for CRUD; unlike
|
|
1949
|
+
* `packageUrl` (the runtime load artefact) it is stable across DDC cook
|
|
1950
|
+
* state. `undefined` for inline / dev-path assets (no sidecar, no CRUD).
|
|
1951
|
+
*/
|
|
1952
|
+
sourcePath?: string;
|
|
1953
|
+
}[] {
|
|
1954
|
+
const seen = new Set<string>();
|
|
1955
|
+
const result: {
|
|
1956
|
+
guid: string;
|
|
1957
|
+
kind: string;
|
|
1958
|
+
name?: string;
|
|
1959
|
+
packageUrl: string;
|
|
1960
|
+
packageId?: CatalogEntry['packageId'];
|
|
1961
|
+
provenance?: CatalogEntry['provenance'];
|
|
1962
|
+
revision?: CatalogEntry['revision'];
|
|
1963
|
+
sourceKey?: CatalogEntry['sourceKey'];
|
|
1964
|
+
sourceIndex?: CatalogEntry['sourceIndex'];
|
|
1965
|
+
sourceOverrides?: CatalogEntry['sourceOverrides'];
|
|
1966
|
+
sourceOverrideDescriptors?: CatalogEntry['sourceOverrideDescriptors'];
|
|
1967
|
+
authoring?: CatalogEntry['authoring'];
|
|
1968
|
+
relations?: CatalogEntry['relations'];
|
|
1969
|
+
diagnostics?: CatalogEntry['diagnostics'];
|
|
1970
|
+
refs?: readonly string[];
|
|
1971
|
+
subject?: import('@forgeax/engine-types').CatalogSubject;
|
|
1972
|
+
execution?: import('@forgeax/engine-types').CookExecution;
|
|
1973
|
+
lifecycle?: import('@forgeax/engine-types').CatalogLifecycle;
|
|
1974
|
+
projection?: import('@forgeax/engine-types').CatalogProjection;
|
|
1975
|
+
sourcePath?: string;
|
|
1976
|
+
cookReceiptUrl?: string;
|
|
1977
|
+
}[] = [];
|
|
1978
|
+
|
|
1979
|
+
// Prod entries: packIndexCache carries packageUrl + optional name + refs.
|
|
1980
|
+
if (this.packIndexCache) {
|
|
1981
|
+
for (const [guidKey, entry] of this.packIndexCache) {
|
|
1982
|
+
seen.add(guidKey);
|
|
1983
|
+
result.push({
|
|
1984
|
+
guid: guidKey,
|
|
1985
|
+
kind: entry.kind,
|
|
1986
|
+
...(entry.name !== undefined ? { name: entry.name } : {}),
|
|
1987
|
+
packageUrl: entry.packageUrl,
|
|
1988
|
+
...(entry.packageId !== undefined ? { packageId: entry.packageId } : {}),
|
|
1989
|
+
...(entry.provenance !== undefined ? { provenance: entry.provenance } : {}),
|
|
1990
|
+
...(entry.revision !== undefined ? { revision: entry.revision } : {}),
|
|
1991
|
+
...(entry.sourceKey !== undefined ? { sourceKey: entry.sourceKey } : {}),
|
|
1992
|
+
...(entry.sourceIndex !== undefined ? { sourceIndex: entry.sourceIndex } : {}),
|
|
1993
|
+
...(entry.sourceOverrides !== undefined
|
|
1994
|
+
? { sourceOverrides: entry.sourceOverrides }
|
|
1995
|
+
: {}),
|
|
1996
|
+
...(entry.sourceOverrideDescriptors !== undefined
|
|
1997
|
+
? { sourceOverrideDescriptors: entry.sourceOverrideDescriptors }
|
|
1998
|
+
: {}),
|
|
1999
|
+
authoring: entry.authoring ?? authoringCapabilityForAssetKind(entry.kind),
|
|
2000
|
+
...(entry.relations !== undefined ? { relations: entry.relations } : {}),
|
|
2001
|
+
...(entry.diagnostics !== undefined ? { diagnostics: entry.diagnostics } : {}),
|
|
2002
|
+
...(entry.refs !== undefined ? { refs: entry.refs } : {}),
|
|
2003
|
+
...(entry.sourcePath !== undefined ? { sourcePath: entry.sourcePath } : {}),
|
|
2004
|
+
...(entry.cookReceiptUrl !== undefined ? { cookReceiptUrl: entry.cookReceiptUrl } : {}),
|
|
2005
|
+
...(entry.subject !== undefined ? { subject: entry.subject } : {}),
|
|
2006
|
+
...(entry.execution !== undefined ? { execution: entry.execution } : {}),
|
|
2007
|
+
...(entry.lifecycle !== undefined ? { lifecycle: entry.lifecycle } : {}),
|
|
2008
|
+
...(entry.projection !== undefined ? { projection: entry.projection } : {}),
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Inlined / dev-path entries: assetCatalog, no pack-index URL. The envelope
|
|
2014
|
+
// holds the authoritative AssetRef[] graph; flatten it to plain GUID edges
|
|
2015
|
+
// so both catalog paths expose the same refs: readonly string[] shape.
|
|
2016
|
+
for (const [guidKey, envelope] of this.assetCatalog) {
|
|
2017
|
+
if (!seen.has(guidKey)) {
|
|
2018
|
+
const name = envelope.name ?? this.resolveName(guidKey);
|
|
2019
|
+
result.push({
|
|
2020
|
+
guid: guidKey,
|
|
2021
|
+
kind: envelope.payload.kind,
|
|
2022
|
+
name,
|
|
2023
|
+
packageUrl: '',
|
|
2024
|
+
authoring: authoringCapabilityForAssetKind(envelope.payload.kind),
|
|
2025
|
+
subject: 'internal-asset',
|
|
2026
|
+
execution: 'direct',
|
|
2027
|
+
lifecycle: 'current',
|
|
2028
|
+
projection: {
|
|
2029
|
+
subject: 'internal-asset',
|
|
2030
|
+
execution: 'direct',
|
|
2031
|
+
lifecycle: 'current',
|
|
2032
|
+
operations: catalogOperationsFor({
|
|
2033
|
+
subject: 'internal-asset',
|
|
2034
|
+
execution: 'direct',
|
|
2035
|
+
lifecycle: 'current',
|
|
2036
|
+
}),
|
|
2037
|
+
},
|
|
2038
|
+
...(envelope.refs.length > 0 ? { refs: envelope.refs.map((r) => r.guid) } : {}),
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
return result;
|
|
2044
|
+
}
|
|
2045
|
+
}
|