@hology/core 0.0.261 → 0.0.263

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.
Files changed (47) hide show
  1. package/dist/gameplay/index.d.ts +1 -0
  2. package/dist/gameplay/index.js +1 -1
  3. package/dist/gameplay/initiate.js +1 -1
  4. package/dist/gameplay/inject.d.ts +8 -0
  5. package/dist/gameplay/inject.js +1 -1
  6. package/dist/gameplay/input/input.d.ts +1 -0
  7. package/dist/gameplay/input/input.js +1 -1
  8. package/dist/gameplay/services/asset-loader.d.ts +3 -0
  9. package/dist/gameplay/services/asset-loader.js +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -1
  12. package/dist/rendering/graphics-runtime.d.ts +1 -1
  13. package/dist/rendering/graphics-runtime.js +1 -1
  14. package/dist/rendering/node-material-webgpu-resolver.js +1 -1
  15. package/dist/rendering.d.ts +6 -1
  16. package/dist/rendering.js +1 -1
  17. package/dist/scene/asset-resource-loader.d.ts +3 -0
  18. package/dist/scene/asset-resource-loader.js +1 -1
  19. package/dist/scene/assets-provider.d.ts +1 -0
  20. package/dist/scene/assets-provider.js +1 -1
  21. package/dist/scene/custom-param-deserialize.js +1 -1
  22. package/dist/scene/custom-param-runtime-types.js +1 -1
  23. package/dist/scene/lattice-deformation.d.ts +28 -0
  24. package/dist/scene/lattice-deformation.js +4 -0
  25. package/dist/scene/materializer.d.ts +12 -3
  26. package/dist/scene/materializer.js +1 -1
  27. package/dist/scene/model-ref.d.ts +7 -0
  28. package/dist/scene/model-ref.js +4 -0
  29. package/dist/scene/model.d.ts +2 -1
  30. package/dist/scene/model.js +1 -1
  31. package/dist/scene/objects/data-asset.d.ts +6 -1
  32. package/dist/scene/objects/data-asset.js +1 -1
  33. package/dist/scene/runtime-asset-service.d.ts +5 -1
  34. package/dist/scene/runtime-asset-service.js +1 -1
  35. package/dist/scene/storage/packed-json.js +1 -1
  36. package/dist/shader/parameter.d.ts +2 -1
  37. package/dist/test/data-asset-definition.test.js +1 -1
  38. package/dist/test/graphics-runtime.test.js +1 -1
  39. package/dist/test/input.test.js +1 -1
  40. package/dist/test/lattice-deformation.test.d.ts +2 -0
  41. package/dist/test/lattice-deformation.test.js +4 -0
  42. package/dist/test/materializer-prefetch.test.js +1 -1
  43. package/dist/test/packed-json.test.js +1 -1
  44. package/dist/test/runtime-asset-service.test.js +1 -1
  45. package/docs/actor-persistence.md +87 -87
  46. package/package.json +1 -1
  47. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,7 @@
1
+ import type { AssetId } from './model.js';
2
+ /** Stable identity of a model asset; load an instance when it is needed. */
3
+ export declare class ModelRef {
4
+ readonly id: AssetId;
5
+ constructor(id: AssetId);
6
+ }
7
+ //# sourceMappingURL=model-ref.d.ts.map
@@ -0,0 +1,4 @@
1
+ export class ModelRef{constructor(o){this.id=o}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -51,7 +51,8 @@ export declare enum SerializedParamType {
51
51
  Struct = 29,
52
52
  DataAsset = 30,
53
53
  SequenceData = 31,
54
- Scene = 32
54
+ Scene = 32,
55
+ ModelRef = 33
55
56
  }
56
57
  export type VfxInputBinding = {
57
58
  type: 'vfxInput';
@@ -1,4 +1,4 @@
1
- export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene"}(SerializedParamType||(SerializedParamType={}));/*
1
+ export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene",e[e.ModelRef=33]="ModelRef"}(SerializedParamType||(SerializedParamType={}));/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,10 +1,15 @@
1
1
  import type { Asset } from '../model.js';
2
+ import { Observable } from 'rxjs';
2
3
  export declare class DataAssetRef<T = unknown> {
3
4
  readonly asset: Asset;
4
5
  readonly value: T;
6
+ /** Fresh snapshots on saved changes. Unsubscribe when the consumer is destroyed. */
7
+ readonly onChange: Observable<DataAssetRef<T>>;
5
8
  readonly id: string;
6
9
  readonly name: string;
7
10
  readonly definitionId: string;
8
- constructor(asset: Asset, value: T);
11
+ constructor(asset: Asset, value: T,
12
+ /** Fresh snapshots on saved changes. Unsubscribe when the consumer is destroyed. */
13
+ onChange?: Observable<DataAssetRef<T>>);
9
14
  }
10
15
  //# sourceMappingURL=data-asset.d.ts.map
@@ -1,4 +1,4 @@
1
- export class DataAssetRef{constructor(t,s){this.asset=t,this.value=s,this.id=t.id,this.name=t.name,this.definitionId=t.dataAsset?.definition??""}}/*
1
+ import{EMPTY as t}from"rxjs";export class DataAssetRef{constructor(s,i,e=t){this.asset=s,this.value=i,this.onChange=e,this.id=s.id,this.name=s.name,this.definitionId=s.dataAsset?.definition??""}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -10,14 +10,18 @@ export declare class RuntimeAssetsService extends AssetsProvider {
10
10
  private readonly liveUpdateQueues;
11
11
  private readonly hot;
12
12
  private readonly livePrefabParametersHandler;
13
+ private readonly liveDataAssetHandler;
14
+ readonly supportsLiveDataAssets: boolean;
13
15
  readonly supportsLivePrefabParameters: boolean;
14
16
  onCreate: Subject<Asset>;
15
17
  onDelete: Subject<Asset>;
16
18
  onUpdate: Subject<Asset>;
17
- constructor(backend: RuntimeBackendService, livePrefabParametersEnabled?: boolean);
19
+ constructor(backend: RuntimeBackendService, livePrefabParametersEnabled?: boolean, liveDataAssetsEnabled?: boolean);
18
20
  getAssets(): Promise<Asset[]>;
19
21
  getAsset(id: string): Promise<Asset>;
20
22
  private loadAssets;
23
+ /** Updates the catalog; subscribed consumers decide how to apply the change. */
24
+ applyLiveDataAsset(asset: Asset): Promise<boolean>;
21
25
  dispose(): void;
22
26
  private queueLivePrefabParameters;
23
27
  }
@@ -1,4 +1,4 @@
1
- import{AssetsProvider as e}from"../scene/assets-provider.js";import{Subject as t}from"rxjs";export class RuntimeAssetsService extends e{constructor(e,s=!0){super(),this.backend=e,this.assets=new Map,this.loaded=!1,this.loadingAssets=null,this.liveUpdateQueues=new Map,this.hot=import.meta.hot,this.livePrefabParametersHandler=e=>{(function(e){const t=e;return 1===t?.version&&"prefab"===t.asset?.type&&null!=t.asset.prefab})(e)&&this.queueLivePrefabParameters(e.asset)},this.onCreate=new t,this.onDelete=new t,this.onUpdate=new t,this.supportsLivePrefabParameters=s&&null!=this.hot,this.supportsLivePrefabParameters&&this.hot.on("hology:prefab-parameters",this.livePrefabParametersHandler)}async getAssets(){return this.loaded||(this.loadingAssets??(this.loadingAssets=this.loadAssets()),await this.loadingAssets),Array.from(this.assets.values())}async getAsset(e){const t=this.assets.get(e);if(null!=t)return t;const s=await async function(e,t){r.has(e)||r.set(e,t(e));return r.get(e)}(e,()=>this.backend.getAsset(e));return null!=s&&this.assets.set(e,s),s}async loadAssets(){try{const e=await this.backend.getAssets();for(const t of e)this.assets.set(t.id,t);return this.loaded=!0,e}finally{this.loadingAssets=null}}async applyLivePrefabParameters(e){if("prefab"!==e?.type||null==e.prefab)return!1;await this.getAssets();const t=this.assets.get(e.id);if("prefab"!==t?.type||null==t.prefab)return console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: the prefab is not loaded.`),!1;const a=function(e,t){if(e.id!==t.id||"prefab"!==e.type||"prefab"!==t.type||null==e.prefab||null==t.prefab||e.prefab.mainActorId!==t.prefab.mainActorId)return null;const a=s(e.prefab.objects,t.prefab.objects);if(null==a)return null;return{...e,prefab:{...e.prefab,objects:a}}}(t,e);return null==a?(console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: its structure changed. Respawn or restart to use the saved structure.`),!1):(JSON.stringify(t.prefab.eventGraph)!==JSON.stringify(e.prefab.eventGraph)&&console.warn(`Live event graph updates are not supported for prefab ${e.name??e.id}. The running graph was preserved.`),this.assets.set(a.id,a),this.onUpdate.next(a),!0)}dispose(){this.supportsLivePrefabParameters&&this.hot.off("hology:prefab-parameters",this.livePrefabParametersHandler),this.liveUpdateQueues.clear()}queueLivePrefabParameters(e){const t=(this.liveUpdateQueues.get(e.id)??Promise.resolve()).catch(()=>{}).then(()=>this.applyLivePrefabParameters(e)).then(()=>{},t=>{console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}.`,t)});this.liveUpdateQueues.set(e.id,t),t.finally(()=>{this.liveUpdateQueues.get(e.id)===t&&this.liveUpdateQueues.delete(e.id)})}}function s(e,t){if(e.length!==t.length)return null;const r=[];for(let n=0;n<e.length;n++){const i=e[n],o=t[n];if(null==i||null==o||i.id!==o.id||i.type!==o.type||"prefab"===i.type&&i.assetId!==o.assetId)return null;const l=s(i.children??[],o.children??[]);if(null==l)return null;let p={...i,children:null!=i.children||null!=o.children?l:void 0};if("actor"===i.type){if(i.actor?.type!==o.actor?.type||!a(i.actor?.components,o.actor?.components))return null;const e=i.actor?.components??[],t=o.actor?.components??[];p={...p,actor:{...i.actor,params:o.actor?.params,innerParams:o.actor?.innerParams,components:e.map((e,s)=>({...e,params:t[s]?.params,innerParams:t[s]?.innerParams}))}}}else"prefab"===i.type&&(p={...p,prefab:{...i.prefab,params:o.prefab?.params,innerParams:o.prefab?.innerParams}});r.push(p)}return r}function a(e,t){const s=e??[],a=t??[];return s.length===a.length&&s.every((e,t)=>e.type===a[t]?.type)}const r=new Map;/*
1
+ import{AssetsProvider as e}from"../scene/assets-provider.js";import{Subject as t}from"rxjs";export class RuntimeAssetsService extends e{constructor(e,s=!0,a=!0){super(),this.backend=e,this.assets=new Map,this.loaded=!1,this.loadingAssets=null,this.liveUpdateQueues=new Map,this.hot=import.meta.hot,this.livePrefabParametersHandler=e=>{(function(e){const t=e;return 1===t?.version&&"prefab"===t.asset?.type&&null!=t.asset.prefab})(e)&&this.queueLivePrefabParameters(e.asset)},this.liveDataAssetHandler=e=>{const t=e;1===t?.version&&"data"===t.asset?.type&&"string"==typeof t.asset.id&&null!=t.asset.dataAsset&&this.applyLiveDataAsset(t.asset).catch(e=>console.warn("Unable to reload data asset",e))},this.supportsLiveDataAssets=!1,this.supportsLivePrefabParameters=!1,this.onCreate=new t,this.onDelete=new t,this.onUpdate=new t,this.supportsLiveDataAssets=a&&null!=this.hot,this.supportsLiveDataAssets&&this.hot.on("hology:data-asset",this.liveDataAssetHandler),this.supportsLivePrefabParameters=s&&null!=this.hot,this.supportsLivePrefabParameters&&this.hot.on("hology:prefab-parameters",this.livePrefabParametersHandler)}async getAssets(){return this.loaded||(this.loadingAssets??(this.loadingAssets=this.loadAssets()),await this.loadingAssets),Array.from(this.assets.values())}async getAsset(e){const t=this.assets.get(e);if(null!=t)return t;const s=await async function(e,t){r.has(e)||r.set(e,t(e));return r.get(e)}(e,()=>this.backend.getAsset(e));return null!=s&&this.assets.set(e,s),s}async loadAssets(){try{const e=await this.backend.getAssets();for(const t of e)this.assets.set(t.id,t);return this.loaded=!0,e}finally{this.loadingAssets=null}}async applyLivePrefabParameters(e){if("prefab"!==e?.type||null==e.prefab)return!1;await this.getAssets();const t=this.assets.get(e.id);if("prefab"!==t?.type||null==t.prefab)return console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: the prefab is not loaded.`),!1;const a=function(e,t){if(e.id!==t.id||"prefab"!==e.type||"prefab"!==t.type||null==e.prefab||null==t.prefab||e.prefab.mainActorId!==t.prefab.mainActorId)return null;const a=s(e.prefab.objects,t.prefab.objects);if(null==a)return null;return{...e,prefab:{...e.prefab,objects:a}}}(t,e);return null==a?(console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}: its structure changed. Respawn or restart to use the saved structure.`),!1):(JSON.stringify(t.prefab.eventGraph)!==JSON.stringify(e.prefab.eventGraph)&&console.warn(`Live event graph updates are not supported for prefab ${e.name??e.id}. The running graph was preserved.`),this.assets.set(a.id,a),this.onUpdate.next(a),!0)}async applyLiveDataAsset(e){if("data"!==e?.type||null==e.dataAsset)return!1;await this.getAssets();const t=this.assets.get(e.id);return null!=t&&t.dataAsset?.definition!==e.dataAsset.definition?(console.warn(`Data asset ${e.name}: changing definition type requires a restart.`),!1):(this.assets.set(e.id,e),this.onUpdate.next(e),!0)}dispose(){this.supportsLiveDataAssets&&this.hot.off("hology:data-asset",this.liveDataAssetHandler),this.supportsLivePrefabParameters&&this.hot.off("hology:prefab-parameters",this.livePrefabParametersHandler),this.liveUpdateQueues.clear()}queueLivePrefabParameters(e){const t=(this.liveUpdateQueues.get(e.id)??Promise.resolve()).catch(()=>{}).then(()=>this.applyLivePrefabParameters(e)).then(()=>{},t=>{console.warn(`Unable to apply live prefab parameters for ${e.name??e.id}.`,t)});this.liveUpdateQueues.set(e.id,t),t.finally(()=>{this.liveUpdateQueues.get(e.id)===t&&this.liveUpdateQueues.delete(e.id)})}}function s(e,t){if(e.length!==t.length)return null;const r=[];for(let n=0;n<e.length;n++){const i=e[n],o=t[n];if(null==i||null==o||i.id!==o.id||i.type!==o.type||"prefab"===i.type&&i.assetId!==o.assetId)return null;const l=s(i.children??[],o.children??[]);if(null==l)return null;let p={...i,children:null!=i.children||null!=o.children?l:void 0};if("actor"===i.type){if(i.actor?.type!==o.actor?.type||!a(i.actor?.components,o.actor?.components))return null;const e=i.actor?.components??[],t=o.actor?.components??[];p={...p,actor:{...i.actor,params:o.actor?.params,innerParams:o.actor?.innerParams,components:e.map((e,s)=>({...e,params:t[s]?.params,innerParams:t[s]?.innerParams}))}}}else"prefab"===i.type&&(p={...p,prefab:{...i.prefab,params:o.prefab?.params,innerParams:o.prefab?.innerParams}});r.push(p)}return r}function a(e,t){const s=e??[],a=t??[];return s.length===a.length&&s.every((e,t)=>e.type===a[t]?.type)}const r=new Map;/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{gzipSync as r,gunzipSync as e,strFromU8 as t,strToU8 as n}from"fflate";const o="@hology/packed-json:1:gzip:base64:";export function serializeStorageJson(r){return JSON.stringify(a(r),null,2)}export function parseStorageJson(r){return JSON.parse(r,(r,e)=>f(e))}export function decodeStorageValue(r){const e=f(r);return e!==r?decodeStorageValue(e):Array.isArray(r)?r.map(r=>decodeStorageValue(r)):y(r)?Object.fromEntries(Object.entries(r).map(([r,e])=>[r,decodeStorageValue(e)])):r}function a(r){if(Array.isArray(r))return r.map(a);if(!y(r))return r;const e={};for(const[t,n]of Object.entries(r))"vertexMaterials"===t&&Array.isArray(n)?e[t]=l(n):"landscape"===t&&y(n)?e[t]=s(n):"paintedScatter"===t&&y(n)?e[t]=i(n):"secondaryMotion"===t&&y(n)?e[t]=c(n):e[t]=a(n);return e}function s(r){const e=u(r);return Array.isArray(r.heightMaps)&&(e.heightMaps=l(r.heightMaps)),Array.isArray(r.holes)&&(e.holes=l(r.holes)),e}function i(r){const e=u(r);return Array.isArray(r.cells)&&(e.cells=r.cells.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.instances)&&(e.instances=l(r.instances)),e})),e}function c(r){const e=u(r);return Array.isArray(r.regions)&&(e.regions=r.regions.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.paintedVertices)&&(e.paintedVertices=l(r.paintedVertices)),e})),e}function u(r){return Object.fromEntries(Object.entries(r).map(([r,e])=>[r,a(e)]))}function l(e){const t=JSON.stringify(e);return t.length<128?a(e):o+function(r){let e="";const t=32768;for(let n=0;n<r.length;n+=t)e+=String.fromCharCode(...r.subarray(n,n+t));return btoa(e)}(r(n(t),{level:9,mtime:0}))}function f(r){if("string"!=typeof r||!r.startsWith(o))return r;const n=r.slice(34);try{return JSON.parse(t(e(function(r){const e=atob(r),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}(n))))}catch(r){const e=r instanceof Error?`${r.name}: ${r.message}`:String(r);throw Object.assign(new Error(`Failed to decode packed Hology storage data: ${e}`),{cause:r})}}function y(r){return null!=r&&"object"==typeof r&&!Array.isArray(r)}/*
1
+ import{gzipSync as r,gunzipSync as e,strFromU8 as t,strToU8 as n}from"fflate";const o="@hology/packed-json:1:gzip:base64:";export function serializeStorageJson(r){return JSON.stringify(a(r),null,2)}export function parseStorageJson(r){return JSON.parse(r,(r,e)=>f(e))}export function decodeStorageValue(r){const e=f(r);return e!==r?decodeStorageValue(e):Array.isArray(r)?r.map(r=>decodeStorageValue(r)):y(r)?Object.fromEntries(Object.entries(r).map(([r,e])=>[r,decodeStorageValue(e)])):r}function a(r){if(Array.isArray(r))return r.map(a);if(!y(r))return r;const e={};for(const[t,n]of Object.entries(r))"vertexMaterials"===t&&Array.isArray(n)?e[t]=l(n):"latticeDeformation"===t&&y(n)?e[t]=l(n,0):"landscape"===t&&y(n)?e[t]=s(n):"paintedScatter"===t&&y(n)?e[t]=i(n):"secondaryMotion"===t&&y(n)?e[t]=c(n):e[t]=a(n);return e}function s(r){const e=u(r);return Array.isArray(r.heightMaps)&&(e.heightMaps=l(r.heightMaps)),Array.isArray(r.holes)&&(e.holes=l(r.holes)),e}function i(r){const e=u(r);return Array.isArray(r.cells)&&(e.cells=r.cells.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.instances)&&(e.instances=l(r.instances)),e})),e}function c(r){const e=u(r);return Array.isArray(r.regions)&&(e.regions=r.regions.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.paintedVertices)&&(e.paintedVertices=l(r.paintedVertices)),e})),e}function u(r){return Object.fromEntries(Object.entries(r).map(([r,e])=>[r,a(e)]))}function l(e,t=128){const s=JSON.stringify(e);return s.length<t?a(e):o+function(r){let e="";const t=32768;for(let n=0;n<r.length;n+=t)e+=String.fromCharCode(...r.subarray(n,n+t));return btoa(e)}(r(n(s),{level:9,mtime:0}))}function f(r){if("string"!=typeof r||!r.startsWith(o))return r;const n=r.slice(34);try{return JSON.parse(t(e(function(r){const e=atob(r),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}(n))))}catch(r){const e=r instanceof Error?`${r.name}: ${r.message}`:String(r);throw Object.assign(new Error(`Failed to decode packed Hology storage data: ${e}`),{cause:r})}}function y(r){return null!=r&&"object"==typeof r&&!Array.isArray(r)}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -7,11 +7,12 @@ import type { VisualEffect } from '../effects/vfx/vfx-param.js';
7
7
  import type { Prefab, PrefabOf } from '../scene/objects/prefab.js';
8
8
  import type { DataAssetRef } from '../scene/objects/data-asset.js';
9
9
  import type { SceneRef } from '../scene/scene-ref.js';
10
+ import type { ModelRef } from '../scene/model-ref.js';
10
11
  import type { Curve2 } from '../utils/curve.js';
11
12
  import type { ColorLayer, MaskLayer } from './color-layer.js';
12
13
  import type { Sequence } from '../effects/sequence/sequence-data.js';
13
14
  import type { ShaderGraphTarget } from './graph/model.js';
14
- export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | Curve2 | ColorLayer | MaskLayer>;
15
+ export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | ModelRef | Curve2 | ColorLayer | MaskLayer>;
15
16
  export type ParameterType = Type<any> | AbstractType<any>;
16
17
  export type ParameterTypeResolver = () => ParameterType;
17
18
  export type ParameterOption<T> = {
@@ -1,4 +1,4 @@
1
- import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as i,vi as o}from"vitest";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{prepareClassParameters as n,prepareCustomParamsFromType as r,materializeDataAssetRef as d}from"../scene/materializer.js";import{SerializedParamType as m}from"../scene/model.js";import{DataAssetRef as l}from"../scene/objects/data-asset.js";import{DataAssetDefinition as p,getAssignableDataAssetDefinitions as c,getDataAssetDefinitionId as f,getDataAssetDefinitions as u,getDataAssetDefinitionType as y,isDataAssetDefinitionAbstract as v,Parameter as g,ParameterDefinition as B}from"../shader/parameter.js";let w=class{constructor(){this.amount=1}};e([g({type:Number}),t("design:type",Object)],w.prototype,"amount",void 0),w=e([B("test.data.statModifier")],w);let b=class{constructor(){this.displayName="Item"}};e([g({type:String}),t("design:type",Object)],b.prototype,"displayName",void 0),b=e([p("test.data.item",{abstract:!0})],b);let A=class extends b{constructor(){super(...arguments),this.damage=5,this.modifier=new w}};e([g({type:Number}),t("design:type",Object)],A.prototype,"damage",void 0),e([g({type:()=>w}),t("design:type",Object)],A.prototype,"modifier",void 0),A=e([p("test.data.weapon")],A);let h=class extends b{constructor(){super(...arguments),this.charges=1}};e([g({type:Number}),t("design:type",Object)],h.prototype,"charges",void 0),h=e([p("test.data.consumable")],h);class j{constructor(){this.item=null,this.items=[]}}e([g({type:()=>l,dataAssetOf:()=>b}),t("design:type",l)],j.prototype,"item",void 0),e([g({type:()=>l,dataAssetOf:()=>b,array:!0}),t("design:type",Array)],j.prototype,"items",void 0);class N{constructor(){this.items=[]}}function D(e){const t=new Map(e.map(e=>[e.id,e]));return{getAsset:o.fn(async e=>t.get(e)??null),getAssets:o.fn(async()=>e)}}e([g(),t("design:type",Array)],N.prototype,"items",void 0),Reflect.defineMetadata("design:type",Array,N.prototype,"items"),Reflect.defineMetadata("parameter:arrayElementType",l,N.prototype,"items"),Reflect.defineMetadata("parameter:dataAssetOf",b,N.prototype,"items"),a("data asset definitions",()=>{i("registers data asset definitions separately from parameter definitions",()=>{s(y("test.data.item")).toBe(b),s(f(A)).toBe("test.data.weapon"),s(v(b)).toBe(!0),s(v("test.data.item")).toBe(!0);const e=c(b);s(e.map(e=>e.id)).toEqual(["test.data.consumable","test.data.weapon"]);const t=u({includeAbstract:!0});s(t.some(e=>"test.data.item"===e.id)).toBe(!0),s(t.some(e=>"test.data.statModifier"===e.id)).toBe(!1),s(y("test.data.statModifier")).toBeUndefined()}),i("serializes data asset references as asset ids, including arrays",()=>{const e=r(j,{}),t=e.item,a=e.items;s(t.type).toBe(m.DataAsset),s(t.value).toBeNull(),s(a.type).toBe(m.Array),s(a.element).toBe(m.DataAsset),s(a.value).toEqual([])}),i("uses reflected metadata for data asset reference arrays",()=>{const e=r(N,{}).items;s(e.type).toBe(m.Array),s(e.element).toBe(m.DataAsset),s(e.value).toEqual([])}),i("materializes a data asset reference into a wrapper with nested inline structs",async()=>{const e={id:"asset.weapon",name:"Bronze Sword",type:"data",dataAsset:{definition:"test.data.weapon",params:{displayName:{type:m.String,value:"Bronze Sword"},damage:{type:m.Number,value:12},modifier:{type:m.Struct,struct:"test.data.statModifier",value:{amount:{type:m.Number,value:3}}}}}},t=D([e]),a=await n({item:{type:m.DataAsset,value:e.id},items:{type:m.Array,element:m.DataAsset,value:[e.id]}},j,t,{}),i=a.item;s(i).toBeInstanceOf(l),s(i.id).toBe(e.id),s(i.name).toBe("Bronze Sword"),s(i.asset).toBe(e),s(i.definitionId).toBe("test.data.weapon"),s(i.value).toBeInstanceOf(A),s(i.value.displayName).toBe("Bronze Sword"),s(i.value.damage).toBe(12),s(i.value.modifier).toBeInstanceOf(w),s(i.value.modifier.amount).toBe(3),s(a.items).toHaveLength(1),s(a.items[0].value.damage).toBe(12)}),i("resolves missing and invalid data asset references safely",async()=>{const e=o.spyOn(console,"warn").mockImplementation(()=>{}),t=D([{id:"asset.notData",name:"Texture pretending",type:"texture"},{id:"asset.unknownDefinition",name:"Unknown Definition",type:"data",dataAsset:{definition:"test.data.unknown",params:{}}},{id:"asset.abstractDefinition",name:"Abstract Definition",type:"data",dataAsset:{definition:"test.data.item",params:{}}},{id:"asset.invalidParams",name:"Invalid Params",type:"data",dataAsset:{definition:"test.data.weapon",params:"bad"}}]);await s(d("asset.missing",t,{})).resolves.toBeNull(),await s(d("asset.notData",t,{})).resolves.toBeNull(),await s(d("asset.unknownDefinition",t,{})).resolves.toBeNull(),await s(d("asset.abstractDefinition",t,{})).resolves.toBeNull(),await s(d("asset.invalidParams",t,{})).resolves.toBeNull();const a=await n({item:{type:m.DataAsset,value:"asset.missing"}},j,t,{});s(a.item).toBeUndefined(),s(e).toHaveBeenCalled(),e.mockRestore()})});/*
1
+ import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as s,it as i,vi as o}from"vitest";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{prepareClassParameters as n,prepareCustomParamsFromType as r,materializeDataAssetRef as d}from"../scene/materializer.js";import{SerializedParamType as m}from"../scene/model.js";import{DataAssetRef as p}from"../scene/objects/data-asset.js";import{Subject as l}from"rxjs";import{DataAssetDefinition as c,getAssignableDataAssetDefinitions as u,getDataAssetDefinitionId as f,getDataAssetDefinitions as y,getDataAssetDefinitionType as v,isDataAssetDefinitionAbstract as g,Parameter as B,ParameterDefinition as b}from"../shader/parameter.js";let w=class{constructor(){this.amount=1}};e([B({type:Number}),t("design:type",Object)],w.prototype,"amount",void 0),w=e([b("test.data.statModifier")],w);let A=class{constructor(){this.displayName="Item"}};e([B({type:String}),t("design:type",Object)],A.prototype,"displayName",void 0),A=e([c("test.data.item",{abstract:!0})],A);let h=class extends A{constructor(){super(...arguments),this.damage=5,this.modifier=new w}};e([B({type:Number}),t("design:type",Object)],h.prototype,"damage",void 0),e([B({type:()=>w}),t("design:type",Object)],h.prototype,"modifier",void 0),h=e([c("test.data.weapon")],h);let N=class extends A{constructor(){super(...arguments),this.charges=1}};e([B({type:Number}),t("design:type",Object)],N.prototype,"charges",void 0),N=e([c("test.data.consumable")],N);class j{constructor(){this.item=null,this.items=[]}}e([B({type:()=>p,dataAssetOf:()=>A}),t("design:type",p)],j.prototype,"item",void 0),e([B({type:()=>p,dataAssetOf:()=>A,array:!0}),t("design:type",Array)],j.prototype,"items",void 0);class x{constructor(){this.items=[]}}function D(){return{id:"asset.weapon",name:"Bronze Sword",type:"data",dataAsset:{definition:"test.data.weapon",params:{displayName:{type:m.String,value:"Bronze Sword"},damage:{type:m.Number,value:12},modifier:{type:m.Struct,struct:"test.data.statModifier",value:{amount:{type:m.Number,value:3}}}}}}}function O(e){const t=new Map(e.map(e=>[e.id,e]));return{getAsset:o.fn(async e=>t.get(e)??null),getAssets:o.fn(async()=>e)}}e([B(),t("design:type",Array)],x.prototype,"items",void 0),Reflect.defineMetadata("design:type",Array,x.prototype,"items"),Reflect.defineMetadata("parameter:arrayElementType",p,x.prototype,"items"),Reflect.defineMetadata("parameter:dataAssetOf",A,x.prototype,"items"),a("data asset definitions",()=>{i("registers data asset definitions separately from parameter definitions",()=>{s(v("test.data.item")).toBe(A),s(f(h)).toBe("test.data.weapon"),s(g(A)).toBe(!0),s(g("test.data.item")).toBe(!0);const e=u(A);s(e.map(e=>e.id)).toEqual(["test.data.consumable","test.data.weapon"]);const t=y({includeAbstract:!0});s(t.some(e=>"test.data.item"===e.id)).toBe(!0),s(t.some(e=>"test.data.statModifier"===e.id)).toBe(!1),s(v("test.data.statModifier")).toBeUndefined()}),i("serializes data asset references as asset ids, including arrays",()=>{const e=r(j,{}),t=e.item,a=e.items;s(t.type).toBe(m.DataAsset),s(t.value).toBeNull(),s(a.type).toBe(m.Array),s(a.element).toBe(m.DataAsset),s(a.value).toEqual([])}),i("uses reflected metadata for data asset reference arrays",()=>{const e=r(x,{}).items;s(e.type).toBe(m.Array),s(e.element).toBe(m.DataAsset),s(e.value).toEqual([])}),i("materializes a data asset reference into a wrapper with nested inline structs",async()=>{const e=D(),t=O([e]),a=await n({item:{type:m.DataAsset,value:e.id},items:{type:m.Array,element:m.DataAsset,value:[e.id]}},j,t,{}),i=a.item;s(i).toBeInstanceOf(p),s(i.id).toBe(e.id),s(i.name).toBe("Bronze Sword"),s(i.asset).toBe(e),s(i.definitionId).toBe("test.data.weapon"),s(i.value).toBeInstanceOf(h),s(i.value.displayName).toBe("Bronze Sword"),s(i.value.damage).toBe(12),s(i.value.modifier).toBeInstanceOf(w),s(i.value.modifier.amount).toBe(3),s(a.items).toHaveLength(1),s(a.items[0].value.damage).toBe(12)}),i("reloads fresh snapshots, restores removed parameters to defaults, and unsubscribes",async()=>{let e=D();const t=new l,a={supportsLiveDataAssets:!0,onUpdate:t,getAsset:async()=>e},i=await d(e.id,a,{}),n=[],r=i.onChange.subscribe(e=>n.push(e));e={...e,dataAsset:{...e.dataAsset,params:{damage:{type:m.Number,value:25}}}},t.next(e),await o.waitFor(()=>s(n).toHaveLength(1)),s(n[0].value.damage).toBe(25),s(n[0].value.displayName).toBe((new h).displayName),s(i.value.damage).toBe(12),r.unsubscribe(),s(t.observers).toHaveLength(0),t.next(e),s(n).toHaveLength(1)}),i("resolves missing and invalid data asset references safely",async()=>{const e=o.spyOn(console,"warn").mockImplementation(()=>{}),t=O([{id:"asset.notData",name:"Texture pretending",type:"texture"},{id:"asset.unknownDefinition",name:"Unknown Definition",type:"data",dataAsset:{definition:"test.data.unknown",params:{}}},{id:"asset.abstractDefinition",name:"Abstract Definition",type:"data",dataAsset:{definition:"test.data.item",params:{}}},{id:"asset.invalidParams",name:"Invalid Params",type:"data",dataAsset:{definition:"test.data.weapon",params:"bad"}}]);await s(d("asset.missing",t,{})).resolves.toBeNull(),await s(d("asset.notData",t,{})).resolves.toBeNull(),await s(d("asset.unknownDefinition",t,{})).resolves.toBeNull(),await s(d("asset.abstractDefinition",t,{})).resolves.toBeNull(),await s(d("asset.invalidParams",t,{})).resolves.toBeNull();const a=await n({item:{type:m.DataAsset,value:"asset.missing"}},j,t,{});s(a.item).toBeUndefined(),s(e).toHaveBeenCalled(),e.mockRestore()})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as a,it as t,vi as i}from"vitest";import{GraphicsRuntime as n}from"../rendering/graphics-runtime.js";e("GraphicsRuntime",()=>{t("shares a backend until the final view releases it",async()=>{const e=new n,t={id:1},l=i.fn(async()=>t),r=i.fn(),s=await e.acquire("webgpu","default",l),u=await e.acquire("webgpu","default",l);a(s).toBe(t),a(u).toBe(t),a(l).toHaveBeenCalledOnce(),e.release("webgpu","default",r),a(r).not.toHaveBeenCalled(),e.release("webgpu","default",r),a(r).toHaveBeenCalledWith(t)})});/*
1
+ import{describe as e,expect as a,it as i,vi as n}from"vitest";import{GraphicsRuntime as t}from"../rendering/graphics-runtime.js";e("GraphicsRuntime",()=>{i("shares a backend until the final view releases it",async()=>{const e=new t,i={id:1},s=n.fn(async()=>i),r=n.fn(),l=await e.acquire("webgpu","default",s),u=await e.acquire("webgpu","default",s);a(l).toBe(i),a(u).toBe(i),a(s).toHaveBeenCalledOnce(),e.release("webgpu","default",r),a(r).not.toHaveBeenCalled(),e.release("webgpu","default",r),a(r).toHaveBeenCalledWith(i)}),i("reuses an idle backend until its retention period expires",async()=>{n.useFakeTimers();try{const e=new t,i={id:1},s=n.fn(async()=>i),r=n.fn();await e.acquire("webgpu","default",s),e.release("webgpu","default",r,12e4),await n.advanceTimersByTimeAsync(6e4),a(await e.acquire("webgpu","default",s)).toBe(i),a(s).toHaveBeenCalledOnce(),e.release("webgpu","default",r,12e4),await n.advanceTimersByTimeAsync(12e4),a(r).toHaveBeenCalledOnce()}finally{n.useRealTimers()}}),i("evicts an idle backend when a different configuration is acquired",async()=>{const e=new t,i=n.fn();await e.acquire("webgpu","first",async()=>({id:1})),e.release("webgpu","first",i,12e4),await e.acquire("webgpu","second",async()=>({id:2})),a(i).toHaveBeenCalledWith({id:1}),e.release("webgpu","second",i)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{expect as e,test as t,vi as a}from"vitest";import o from"typedi";import{ActionInput as n,ButtonInput as s,ToggleInput as p}from"../gameplay/input/input.js";a.mock("../gameplay/services/render.js",()=>({ViewController:class{}}));import{InputService as i}from"../gameplay/input/input-service.js";import{GamepadButtonBind as l,Keybind as d}from"../gameplay/input/keybind.js";import{ViewController as u}from"../gameplay/services/render.js";t("button input uses hold mode by default",()=>{const t=new s,a=[];t.onStart(()=>a.push(t.activated)),t.onEnd(()=>a.push(t.activated)),t.toggle(!0),e(t.activated).toBe(!0),t.toggle(!0),e(a).toEqual([!0]),t.toggle(!1),e(t.activated).toBe(!1),e(a).toEqual([!0,!1])}),t("button input can switch to toggle mode at runtime",()=>{const t=new s,a=[];t.onStart(()=>a.push(t.activated)),t.onEnd(()=>a.push(t.activated)),t.mode="toggle",t.toggle(!0),e(t.activated).toBe(!1),t.toggle(!1),e(t.activated).toBe(!0),t.toggle(!1),e(t.activated).toBe(!1),e(a).toEqual([!0,!1])}),t("action and toggle inputs remain compatibility presets",()=>{const t=new n;t.toggle(!0),e(t.activated).toBe(!0),t.toggle(!1),e(t.activated).toBe(!1);const a=new p;a.toggle(!0),e(a.activated).toBe(!1),a.toggle(!1),e(a.activated).toBe(!0)}),t("held gamepad buttons can be suppressed until released",()=>{const t={pressed:!0,value:1},a={connected:!0,mapping:"standard",buttons:[t],axes:[]},o=function(){const e=Object.create(i.prototype);return Object.assign(e,{view:{paused:!1},inputs:new Map,deltaCallbacks:new Map,toggleCallbacks:new Map,keybinds:new Map,gamepadButtonBinds:new Map,gamepadAxisBinds:new Map,activeToggleCallbacks:[],prevGamepadButtonStates:new Map,suppressedGamepadButtons:new Map,lastActiveDevice:"kbm",onDeviceChanged:{next:()=>{}}}),e}(),n=[];o.gamepadButtonBinds.set("jump",[new l(0)]),o.toggleCallbacks.set("jump",e=>n.push(e)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[a]}),o.suppressHeldGamepadButtonsUntilRelease(),o.update(0),t.pressed=!1,t.value=0,o.update(0),t.pressed=!0,t.value=1,o.update(0),e(n).toEqual([!0])}),t("mouse button gameplay input requires pointer lock",()=>{let t=null;Object.defineProperty(document,"pointerLockElement",{configurable:!0,get:()=>t}),o.set(u,{paused:!1,htmlElement:document.body});const a=new i,n=[];a.keybinds.set("attack",[new d("MouseLeft")]),a.toggleCallbacks.set("attack",e=>n.push(e)),a.onKeyDown({button:0}),t=document.body,a.onKeyDown({button:0}),e(n).toEqual([!0])});/*
1
+ import{expect as e,test as t,vi as a}from"vitest";import o from"typedi";import{ActionInput as n,ButtonInput as s,ToggleInput as p}from"../gameplay/input/input.js";a.mock("../gameplay/services/render.js",()=>({ViewController:class{}}));import{InputService as l}from"../gameplay/input/input-service.js";import{GamepadButtonBind as i,Keybind as d}from"../gameplay/input/keybind.js";import{ViewController as u}from"../gameplay/services/render.js";t("button input uses hold mode by default",()=>{const t=new s,a=[];t.onStart(()=>a.push(t.activated)),t.onEnd(()=>a.push(t.activated)),t.toggle(!0),e(t.activated).toBe(!0),t.toggle(!0),e(a).toEqual([!0]),t.toggle(!1),e(t.activated).toBe(!1),e(a).toEqual([!0,!1])}),t("removed start listeners do not run on subsequent presses",()=>{const t=new s,o=a.fn(),n=a.fn();t.onStart(o),t.onStart(n),t.removeOnStart(o),t.removeOnStart(o),t.toggle(!0),e(o).not.toHaveBeenCalled(),e(n).toHaveBeenCalledTimes(1)}),t("button input can switch to toggle mode at runtime",()=>{const t=new s,a=[];t.onStart(()=>a.push(t.activated)),t.onEnd(()=>a.push(t.activated)),t.mode="toggle",t.toggle(!0),e(t.activated).toBe(!1),t.toggle(!1),e(t.activated).toBe(!0),t.toggle(!1),e(t.activated).toBe(!1),e(a).toEqual([!0,!1])}),t("action and toggle inputs remain compatibility presets",()=>{const t=new n;t.toggle(!0),e(t.activated).toBe(!0),t.toggle(!1),e(t.activated).toBe(!1);const a=new p;a.toggle(!0),e(a.activated).toBe(!1),a.toggle(!1),e(a.activated).toBe(!0)}),t("held gamepad buttons can be suppressed until released",()=>{const t={pressed:!0,value:1},a={connected:!0,mapping:"standard",buttons:[t],axes:[]},o=function(){const e=Object.create(l.prototype);return Object.assign(e,{view:{paused:!1},inputs:new Map,deltaCallbacks:new Map,toggleCallbacks:new Map,keybinds:new Map,gamepadButtonBinds:new Map,gamepadAxisBinds:new Map,activeToggleCallbacks:[],prevGamepadButtonStates:new Map,suppressedGamepadButtons:new Map,lastActiveDevice:"kbm",onDeviceChanged:{next:()=>{}}}),e}(),n=[];o.gamepadButtonBinds.set("jump",[new i(0)]),o.toggleCallbacks.set("jump",e=>n.push(e)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[a]}),o.suppressHeldGamepadButtonsUntilRelease(),o.update(0),t.pressed=!1,t.value=0,o.update(0),t.pressed=!0,t.value=1,o.update(0),e(n).toEqual([!0])}),t("mouse button gameplay input requires pointer lock",()=>{let t=null;Object.defineProperty(document,"pointerLockElement",{configurable:!0,get:()=>t}),o.set(u,{paused:!1,htmlElement:document.body});const a=new l,n=[];a.keybinds.set("attack",[new d("MouseLeft")]),a.toggleCallbacks.set("attack",e=>n.push(e)),a.onKeyDown({button:0}),t=document.body,a.onKeyDown({button:0}),e(n).toEqual([!0])});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lattice-deformation.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as o,vi as r}from"vitest";import{BoxGeometry as s,BufferGeometry as n,Float32BufferAttribute as a,Group as i,Matrix4 as m,Mesh as f,MeshBasicMaterial as d,PlaneGeometry as l,Vector3 as y}from"three";import{createLatticeDeformation as p,deformLatticeGeometry as c,isValidLattice as g,latticePoint as u,LatticeGeometryCache as w,setLatticeGeometrySourceIdentity as h}from"../scene/lattice-deformation.js";function B(e=new s(2,2,2)){const t=new i,o=new f(e,new d);return t.add(o),{root:t,mesh:o}}function b(e,o){t(e.x).toBeCloseTo(o.x,5),t(e.y).toBeCloseTo(o.y,5),t(e.z).toBeCloseTo(o.z,5)}e("lattice deformation",()=>{o("preserves undeformed geometry, normals and hard edges",()=>{const{root:e,mesh:o}=B(),r=p(e,[3,2,1]),s=c(o.geometry,new m,r);t(Array.from(s.getAttribute("position").array)).toEqual(Array.from(o.geometry.getAttribute("position").array)),t(Array.from(s.getAttribute("normal").array)).toEqual(Array.from(o.geometry.getAttribute("normal").array)),t(s).not.toBe(o.geometry)}),o("transforms positions, normals and tangents under a shear without modifying source buffers",()=>{const e=new n;e.setAttribute("position",new a([0,0,0,1,0,0,0,1,0],3)),e.setAttribute("normal",new a([1,0,0,1,0,0,1,0,0],3)),e.setAttribute("tangent",new a([0,1,0,1,0,1,0,1,0,1,0,1],4));const o=p(new f(new s(2,2,2)),[2,2,2]),r=new y;o.offsets.forEach((e,t)=>{e[0]=.5*u(o,t,r).y});const i=c(e,new m,o);b((new y).fromBufferAttribute(i.getAttribute("position"),2),new y(.5,1,0)),b((new y).fromBufferAttribute(i.getAttribute("normal"),0),new y(1,-.5,0).normalize()),b((new y).fromBufferAttribute(i.getAttribute("tangent"),0),new y(.5,1,0).normalize()),t(Array.from(e.getAttribute("position").array)).toEqual([0,0,0,1,0,0,0,1,0]),t(i.boundingBox.max.x).toBe(1)}),o("uses model local space for transformed child meshes and ignores scene transforms",()=>{const{root:e,mesh:t}=B();t.position.set(3,2,-1),t.rotation.z=Math.PI/2,t.scale.set(2,1,.5),e.position.set(100,200,300),e.scale.setScalar(4);const o=p(e,[1,1,1]);b((new y).fromArray(o.min),new y(2,0,-1.5)),b((new y).fromArray(o.max),new y(4,4,-.5)),o.offsets.forEach(e=>{e[0]=2});const r=c(t.geometry,t.matrix,o);b((new y).fromBufferAttribute(r.getAttribute("position"),0),(new y).fromBufferAttribute(t.geometry.getAttribute("position"),0).add(new y(0,-2,0)))}),o("supports planar models and rejects malformed and oversized cages",()=>{const e=p(new f(new l(2,2)),[1,1,1]);t(g(e)).toBe(!0),t(e.max[2]).toBeGreaterThan(e.min[2]),t(g({...e,offsets:[]})).toBe(!1),t(g({...e,segments:[9,1,1]})).toBe(!1),t(g({...e,min:[NaN,0,0]})).toBe(!1),t(()=>p(new i,[1e4,1e4,1e4])).toThrow("Lattice segments")}),o("reuses identical buffers across copies and restores the original when disabled",()=>{const{root:e,mesh:o}=B(),r=e.clone(),s=r.children[0];r.position.set(9,0,0);const n=o.geometry,a=p(e),i=new w;a.offsets[0][1]=.5,i.apply(e,a),i.apply(r,structuredClone(a)),t(o.geometry).toBe(s.geometry),t(o.geometry).not.toBe(n);const m=o.geometry;i.apply(e,{...a,offsets:a.offsets.map(e=>[e[0],e[1]+1,e[2]])}),t(o.geometry).not.toBe(s.geometry),i.apply(e,a),t(o.geometry).toBe(m),i.apply(e,{...a,enabled:!1}),t(o.geometry).toBe(n),t(s.geometry).toBe(m),i.dispose()}),o("shares independently frozen copies with identical source and pose identities",()=>{const{root:e,mesh:o}=B(),{root:r,mesh:s}=B(o.geometry.clone());h(o.geometry,"same-source-and-pose"),h(s.geometry,"same-source-and-pose");const n=p(e),a=new w;n.offsets[0][0]=.25,a.apply(e,n),a.apply(r,{offsets:n.offsets,max:n.max,min:n.min,segments:n.segments}),t(o.geometry).toBe(s.geometry),a.dispose()}),o("leaves independently authored child objects and editor helpers undeformed",()=>{const{root:e,mesh:o}=B(),r=p(e),s=new i,n=new f(o.geometry);s.userData.src={type:"asset_mesh"},s.add(n),e.add(s);const a=new f(o.geometry);a.name="UCX_model",e.add(a);const m=new f(o.geometry);m.userData.isEditorVisualization=!0,e.add(m);const d=n.geometry,l=new w;r.offsets.forEach(e=>{e[0]=1}),l.apply(e,r),t(o.geometry).not.toBe(d),t(n.geometry).toBe(d),t(m.geometry).toBe(d),t(a.geometry).toBe(d),l.dispose()}),o("isolates geometry variants and disposes idle buffers without disposing live copies",()=>{const{root:e,mesh:o}=B(),s=e.clone(),n=s.children[0],a=p(e),i=new w;a.offsets[0][0]=.25,i.apply(s,a);const m=r.spyOn(n.geometry,"dispose");i.apply(e,a,"painted"),t(o.geometry).not.toBe(n.geometry);for(let t=1;t<20;t++){const o=structuredClone(a);o.offsets[0][0]=t,i.apply(e,o)}t(m).not.toHaveBeenCalled(),i.release(s),i.dispose(),t(m).toHaveBeenCalledOnce()})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as p,SerializedParamType as o}from"../scene/model.js";function m(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),p=s.fn(async e=>new r),o=s.fn(async e=>({texture:new r,layerIndex:0})),m=s.fn(),f={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},l={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new i,f,l,{getMesh:a,getTexture:p,getTextureArray:o},{renderer:{initTexture:m},initTexture:m,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:p,getTextureArray:o,initTexture:m}}function f(e){return{name:e.id,...e}}function l(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function c(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:o.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("prefetches unique materialized asset meshes with the same options used by createFromAsset",async()=>{const e=l("mesh-a"),a=l("mesh-b"),s=l("mesh-c"),i=l("mesh-disabled"),r=l("mesh-high",{detailTier:p.high}),n=c("prefab-a",[f({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),f({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),f({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),o=[f({id:"root",type:"group",children:[f({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),f({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),f({id:"disabled-group",type:"group",enabled:!1,children:[f({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),f({id:"prefab-instance",type:"prefab",assetId:n.id}),f({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),f({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=m(o,d);u.detailTier=p.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),p=u("material-skipped",i.id),o=c("prefab-a",[f({id:"prefab-group",type:"group",children:[f({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(d.id)]})]}),f({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),l=new Map([e,a,s,i,r,n,d,p,o].map(e=>[e.id,e])),g=[f({id:"root-group",type:"group",children:[f({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),f({id:"disabled-group",type:"group",enabled:!1,children:[f({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(p.id)]})]}),f({id:"prefab-instance",type:"prefab",assetId:o.id})]}),f({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:b,getTexture:x,initTexture:w}=m(g,l);await b.preInit(),await b.initTextures(),t(x).toHaveBeenCalledTimes(3),t(new Set(x.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(w).toHaveBeenCalledTimes(3)})});/*
1
+ import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as p,SerializedParamType as o}from"../scene/model.js";function m(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),p=s.fn(async e=>new r),o=s.fn(async e=>({texture:new r,layerIndex:0})),m=s.fn(),l={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},f={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new i,l,f,{getMesh:a,getTexture:p,getTextureArray:o},{renderer:{initTexture:m},initTexture:m,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:p,getTextureArray:o,initTexture:m}}function l(e){return{name:e.id,...e}}function f(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function c(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:o.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("prefetches unique materialized asset meshes without creating discarded instances",async()=>{const e=f("mesh-a"),a=f("mesh-b"),s=f("mesh-c"),i=f("mesh-disabled"),r=f("mesh-high",{detailTier:p.high}),n=c("prefab-a",[l({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),l({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),o=[l({id:"root",type:"group",children:[l({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),l({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),l({id:"prefab-instance",type:"prefab",assetId:n.id}),l({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),l({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=m(o,d);u.detailTier=p.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0),t(h.mock.calls.every(([,e])=>!1===e?.clone)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),p=u("material-skipped",i.id),o=c("prefab-a",[l({id:"prefab-group",type:"group",children:[l({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(d.id)]})]}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),f=new Map([e,a,s,i,r,n,d,p,o].map(e=>[e.id,e])),g=[l({id:"root-group",type:"group",children:[l({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(p.id)]})]}),l({id:"prefab-instance",type:"prefab",assetId:o.id})]}),l({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:b,getTexture:x,initTexture:w}=m(g,f);await b.preInit(),await b.initTextures(),t(x).toHaveBeenCalledTimes(3),t(new Set(x.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(w).toHaveBeenCalledTimes(3)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as a,test as t,vi as s}from"vitest";import{decodeStorageValue as r,parseStorageJson as n,serializeStorageJson as i}from"../scene/storage/packed-json.js";const o="@hology/packed-json:1:gzip:base64:";e("packed storage JSON",()=>{t("packs dense scene fields and restores their original runtime shapes",()=>{const e=Array.from({length:100},(e,a)=>({i:a,y:Math.sin(a)})),t=Array.from({length:100},(e,a)=>({i:a,w:[0,.25,.75]})),s={id:"scene",data:[{id:"landscape",name:"Landscape",landscape:{options:{},heightMaps:[{x:0,y:0,points:e}],holes:t},vertexMaterials:t,paintedScatter:{palette:[],brush:{radius:1,density:1},cells:[{key:"0:0",revision:1,instances:Array.from({length:50},(e,a)=>({id:`instance-${a}`,paletteId:"trees",position:[a,0,a],rotation:[0,0,0],scale:[1,1,1]}))}]}}]},r=i(s),l=JSON.parse(r);a(l.data[0].landscape.heightMaps).toMatch(o),a(l.data[0].landscape.holes).toMatch(o),a(l.data[0].vertexMaterials).toMatch(o),a(l.data[0].paintedScatter.cells[0].instances).toMatch(o),a(n(r)).toEqual(s)}),t("packs secondary-motion paint and decodes already-parsed bundle data",()=>{const e={mesh:{secondaryMotion:{version:1,regions:[{id:"cloth",paintedVertices:Array.from({length:100},(e,a)=>[a,a/100])}]}}},t=JSON.parse(i(e));a(t.mesh.secondaryMotion.regions[0].paintedVertices).toMatch(o),a(r(t)).toEqual(e)}),t("leaves small selected values readable",()=>{const e={vertexMaterials:[{i:2,w:[1,0,0]}]},t=i(e);a(JSON.parse(t).vertexMaterials).toEqual(e.vertexMaterials),a(n(t)).toEqual(e)}),t("leaves unrelated large arrays readable",()=>{const e={children:Array.from({length:100},(e,a)=>({id:`${a}`,name:`Object ${a}`}))},t=JSON.parse(i(e));a(t.children).toEqual(e.children)}),t("produces identical packed data at different save times",()=>{const e={vertexMaterials:Array.from({length:100},(e,a)=>({i:a,w:[0,.25,.75]}))};s.useFakeTimers();try{s.setSystemTime(new Date("2026-01-01T00:00:00Z"));const t=i(e);s.setSystemTime(new Date("2026-08-21T12:34:56Z"));const r=i(e);a(r).toBe(t)}finally{s.useRealTimers()}})});/*
1
+ import{describe as e,expect as t,test as a,vi as s}from"vitest";import{decodeStorageValue as r,parseStorageJson as n,serializeStorageJson as o}from"../scene/storage/packed-json.js";const i="@hology/packed-json:1:gzip:base64:";e("packed storage JSON",()=>{a("packs dense scene fields and restores their original runtime shapes",()=>{const e=Array.from({length:100},(e,t)=>({i:t,y:Math.sin(t)})),a=Array.from({length:100},(e,t)=>({i:t,w:[0,.25,.75]})),s={id:"scene",data:[{id:"landscape",name:"Landscape",landscape:{options:{},heightMaps:[{x:0,y:0,points:e}],holes:a},vertexMaterials:a,paintedScatter:{palette:[],brush:{radius:1,density:1},cells:[{key:"0:0",revision:1,instances:Array.from({length:50},(e,t)=>({id:`instance-${t}`,paletteId:"trees",position:[t,0,t],rotation:[0,0,0],scale:[1,1,1]}))}]}}]},r=o(s),l=JSON.parse(r);t(l.data[0].landscape.heightMaps).toMatch(i),t(l.data[0].landscape.holes).toMatch(i),t(l.data[0].vertexMaterials).toMatch(i),t(l.data[0].paintedScatter.cells[0].instances).toMatch(i),t(n(r)).toEqual(s)}),a("packs secondary-motion paint and decodes already-parsed bundle data",()=>{const e={mesh:{secondaryMotion:{version:1,regions:[{id:"cloth",paintedVertices:Array.from({length:100},(e,t)=>[t,t/100])}]}}},a=JSON.parse(o(e));t(a.mesh.secondaryMotion.regions[0].paintedVertices).toMatch(i),t(r(a)).toEqual(e)}),a.each([1,2,8])("packs a lattice with %i segments onto one line and restores it",e=>{const a={enabled:!1,segments:[e,e,e],min:[0,0,0],max:[1,1,1],offsets:Array.from({length:(e+1)**3},(e,t)=>[0===t?-.125:0,0,0])},s={data:[{id:"mesh",latticeDeformation:a}]},l=o(s),c=JSON.parse(l);t(c.data[0].latticeDeformation).toMatch(i),t(l.split("\n").filter(e=>e.includes("latticeDeformation"))).toEqual([` "latticeDeformation": "${c.data[0].latticeDeformation}"`]),t(n(l)).toEqual(s),t(r(c)).toEqual(s),t(n(JSON.stringify(s))).toEqual(s),t(a.offsets).toHaveLength((e+1)**3)}),a("leaves small selected values readable",()=>{const e={vertexMaterials:[{i:2,w:[1,0,0]}]},a=o(e);t(JSON.parse(a).vertexMaterials).toEqual(e.vertexMaterials),t(n(a)).toEqual(e)}),a("leaves unrelated large arrays readable",()=>{const e={children:Array.from({length:100},(e,t)=>({id:`${t}`,name:`Object ${t}`}))},a=JSON.parse(o(e));t(a.children).toEqual(e.children)}),a("produces identical packed data at different save times",()=>{const e={latticeDeformation:{segments:[1,1,1],min:[0,0,0],max:[1,1,1],offsets:Array.from({length:8},()=>[0,0,0])},vertexMaterials:Array.from({length:100},(e,t)=>({i:t,w:[0,.25,.75]}))};s.useFakeTimers();try{s.setSystemTime(new Date("2026-01-01T00:00:00Z"));const a=o(e);s.setSystemTime(new Date("2026-08-21T12:34:56Z"));const r=o(e);t(r).toBe(a)}finally{s.useRealTimers()}})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,it as a,vi as n}from"vitest";import{RuntimeAssetsService as s}from"../scene/runtime-asset-service.js";function o(e){return{getAssets:n.fn(async()=>e),getAsset:n.fn(async t=>e.find(e=>e.id===t)),getScenes:n.fn(),getScene:n.fn(),getSceneData:n.fn()}}function r(e){return{id:"prefab-a",name:"Prefab A",type:"prefab",prefab:{mainActorId:"actor-a",objects:[{id:"actor-a",name:"Actor A",type:"actor",position:[1,2,3],rotation:[0,0,0],scale:[1,1,1],actor:{type:"TestActor",params:{label:{type:0,value:e}},components:[{type:"TestComponent",params:{label:{type:0,value:`${e}-component`}}}]}}]}}}e("RuntimeAssetsService",()=>{a("shares concurrent asset list loads and reuses the loaded cache",async()=>{const e=[{id:"mesh-a",name:"Mesh A",type:"mesh"},{id:"material-a",name:"Material A",type:"material"}],a={getAssets:n.fn(async()=>(await new Promise(e=>setTimeout(e,5)),e)),getAsset:n.fn(),getScenes:n.fn(),getScene:n.fn(),getSceneData:n.fn()},o=new s(a);await Promise.all([o.getAssets(),o.getAssets()]),await o.getAssets(),t(a.getAssets).toHaveBeenCalledTimes(1)}),a("merges only compatible prefab parameter data into the runtime asset",async()=>{const e=r("old-value"),a=o([e]),c=new s(a,!1),i=[];c.onUpdate.subscribe(e=>i.push(e));const p=r("new-value");p.prefab.objects[0].position=[99,99,99],p.prefab.eventGraph={nodes:[{id:"ignored"}]};const l=n.spyOn(console,"warn").mockImplementation(()=>{});t(await c.applyLivePrefabParameters(p)).toBe(!0);const m=await c.getAsset(e.id);t(m.prefab.objects[0].actor.params.label.value).toBe("new-value"),t(m.prefab.objects[0].actor.components[0].params.label.value).toBe("new-value-component"),t(m.prefab.objects[0].position).toEqual([1,2,3]),t(m.prefab.eventGraph).toBeUndefined(),t(i).toEqual([m]),t(l).toHaveBeenCalledWith(t.stringContaining("event graph updates are not supported")),l.mockRestore()}),a("rejects structural prefab changes without replacing the cached asset",async()=>{const e=r("old-value"),a=o([e]),c=new s(a,!1),i=r("new-value");i.prefab.objects[0].actor.components[0].type="OtherComponent";const p=n.spyOn(console,"warn").mockImplementation(()=>{});t(await c.applyLivePrefabParameters(i)).toBe(!1),t(await c.getAsset(e.id)).toBe(e),t(p).toHaveBeenCalledWith(t.stringContaining("structure changed")),p.mockRestore()})});/*
1
+ import{describe as e,expect as t,it as a,vi as s}from"vitest";import{RuntimeAssetsService as n}from"../scene/runtime-asset-service.js";function o(e){return{getAssets:s.fn(async()=>e),getAsset:s.fn(async t=>e.find(e=>e.id===t)),getScenes:s.fn(),getScene:s.fn(),getSceneData:s.fn()}}function i(e){return{id:"prefab-a",name:"Prefab A",type:"prefab",prefab:{mainActorId:"actor-a",objects:[{id:"actor-a",name:"Actor A",type:"actor",position:[1,2,3],rotation:[0,0,0],scale:[1,1,1],actor:{type:"TestActor",params:{label:{type:0,value:e}},components:[{type:"TestComponent",params:{label:{type:0,value:`${e}-component`}}}]}}]}}}e("RuntimeAssetsService",()=>{a("publishes saved data assets and rejects definition type changes",async()=>{const e={id:"data-a",name:"Data A",type:"data",dataAsset:{definition:"test.ability",params:{}}},a=new n(o([e]),!1),i=s.fn();a.onUpdate.subscribe(i);const r={...e,name:"Updated ability"};t(await a.applyLiveDataAsset(r)).toBe(!0),t(await a.getAsset(e.id)).toBe(r),t(i).toHaveBeenCalledWith(r);const c=s.spyOn(console,"warn").mockImplementation(()=>{});t(await a.applyLiveDataAsset({...r,dataAsset:{definition:"other.ability",params:{}}})).toBe(!1),t(await a.getAsset(e.id)).toBe(r),t(i).toHaveBeenCalledTimes(1),c.mockRestore()}),a("shares concurrent asset list loads and reuses the loaded cache",async()=>{const e=[{id:"mesh-a",name:"Mesh A",type:"mesh"},{id:"material-a",name:"Material A",type:"material"}],a={getAssets:s.fn(async()=>(await new Promise(e=>setTimeout(e,5)),e)),getAsset:s.fn(),getScenes:s.fn(),getScene:s.fn(),getSceneData:s.fn()},o=new n(a);await Promise.all([o.getAssets(),o.getAssets()]),await o.getAssets(),t(a.getAssets).toHaveBeenCalledTimes(1)}),a("merges only compatible prefab parameter data into the runtime asset",async()=>{const e=i("old-value"),a=o([e]),r=new n(a,!1),c=[];r.onUpdate.subscribe(e=>c.push(e));const p=i("new-value");p.prefab.objects[0].position=[99,99,99],p.prefab.eventGraph={nodes:[{id:"ignored"}]};const l=s.spyOn(console,"warn").mockImplementation(()=>{});t(await r.applyLivePrefabParameters(p)).toBe(!0);const m=await r.getAsset(e.id);t(m.prefab.objects[0].actor.params.label.value).toBe("new-value"),t(m.prefab.objects[0].actor.components[0].params.label.value).toBe("new-value-component"),t(m.prefab.objects[0].position).toEqual([1,2,3]),t(m.prefab.eventGraph).toBeUndefined(),t(c).toEqual([m]),t(l).toHaveBeenCalledWith(t.stringContaining("event graph updates are not supported")),l.mockRestore()}),a("rejects structural prefab changes without replacing the cached asset",async()=>{const e=i("old-value"),a=o([e]),r=new n(a,!1),c=i("new-value");c.prefab.objects[0].actor.components[0].type="OtherComponent";const p=s.spyOn(console,"warn").mockImplementation(()=>{});t(await r.applyLivePrefabParameters(c)).toBe(!1),t(await r.getAsset(e.id)).toBe(e),t(p).toHaveBeenCalledWith(t.stringContaining("structure changed")),p.mockRestore()})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,87 +1,87 @@
1
- # Actor persistence
2
-
3
- Persistence is opt-in and storage-independent. Enable it when loading a single-player session:
4
-
5
- ```ts
6
- await application.loadGame(Game, {
7
- sceneName: save.sceneName,
8
- persistence: { data: save.persistence }, // undefined data starts an empty document
9
- })
10
- ```
11
-
12
- `initiateGame()` and React's `HologyScene` accept the same `persistence` option. Games load and migrate their save envelope before loading the scene, then write `inject(ActorPersistence).snapshot()` alongside their other save data. Capture is synchronous and returns detached JSON data; do it before awaiting storage. Sessions without this option do not register actors. Network sessions reject the option.
13
-
14
- ## Actor and component hooks
15
-
16
- ```ts
17
- @Actor({ persistent: true })
18
- class Chest extends BaseActor implements PersistentState<{ opened: boolean }> {
19
- readonly persistenceVersion = 1
20
- opened = false
21
-
22
- onCapturePersistence() { return { opened: this.opened } }
23
- onRestorePersistence(state: { opened: boolean }, context: PersistenceContext) {
24
- if (context.version !== 1 || typeof state?.opened !== 'boolean') {
25
- throw new Error('Unsupported chest state')
26
- }
27
- this.opened = state.opened
28
- }
29
- onPersistenceRestored() { this.updateAppearance() }
30
- }
31
-
32
- @Component()
33
- class Cooldown extends ActorComponent implements PersistentState<{ remaining: number }> {
34
- // Stable within the owning actor, including nested components. Two cooldowns
35
- // need distinct keys, e.g. 'primary-attack' and 'dodge'. Never use array indices.
36
- persistenceKey = 'primary-attack'
37
- readonly persistenceVersion = 1
38
- remaining = 0
39
-
40
- onCapturePersistence() { return { remaining: this.remaining } }
41
- onRestorePersistence(state: { remaining: number }, context: PersistenceContext) {
42
- if (context.version !== 1 || !Number.isFinite(state?.remaining)) {
43
- throw new Error('Unsupported cooldown state')
44
- }
45
- this.remaining = Math.max(0, state.remaining)
46
- }
47
- }
48
- ```
49
-
50
- Override `get persistenceEnabled()` for a policy based on authored parameters (for example, enemy faction). It is evaluated after initialization. A component never opts its actor in. An opted-in actor may have only component hooks, or no hooks if it only needs permanent removal.
51
-
52
- For the common position-and-yaw case, attach `ActorTransformPersistenceComponent` to an opted-in actor. It uses the stable component key `transform` and stores local position and yaw, leaving pitch and roll at their authored values. It restores before begin-play and synchronizes an existing physics body after the restore batch. Attach it only once per actor. `captureActorTransform(actor)` and `restoreActorTransform(actor, state)` provide the same JSON shape for custom hooks; the latter validates the payload and sets the object transform, so custom movement systems must also synchronize their own state and physics. A character movement component, for example, can call `movement.teleport(actor.position, actor.rotation.y)` in the actor's `onPersistenceRestored()` when a saved transform was applied (`transformComponent.restored`).
53
-
54
- Initialize defaults in `onInit()`. Restore runs after all actors/components initialize and references bind, before `onBeginPlay()`, event graphs, game preload and presentation. Do not reset persisted fields in begin-play. Components restore before their actor. `onPersistenceRestored()` runs after the entire batch restores, including actors with no saved entry. Ordinary restore hooks may resolve other actors, but cannot assume those actors have restored yet. Hooks should not spawn participants or mutate the batch.
55
-
56
- Save finite JSON values, not actors, vectors, signals, functions, dates or cyclic objects. Validate or migrate component/actor payload versions in the restore hook. Unknown component entries are retained for later saves. Restore failures stop loading and include identity and component key in the error. Restore by assigning state, without replaying damage, death, item rewards or other gameplay events. Save remaining durations rather than wall-clock deadlines unless the game intentionally advances them while closed.
57
-
58
- ## Identity and removal
59
-
60
- Authored identities are `{ sceneId, path }`: the stable scene metadata ID and the existing object/prefab instance path. Names are not IDs. Renaming does not change identity; recreating an object or moving it across prefab boundaries may require a game save migration.
61
-
62
- Call `persistence.markRemoved(actor)` at the permanent gameplay outcome, such as enemy death, before delayed presentation cleanup. It records removal without destroying the live object. The materializer skips that authored subtree on the next load. For a prefab's exposed actor, this removes the whole exposed placement, including nested prefabs, meshes, collisions and event graphs. Ordinary `world.removeActor()` and unloading capture state but never mark it removed.
63
-
64
- References to removed actors become `null`. Gameplay must handle that possibility. `getIdentity(actor)`, `resolve(identity)` and `isRemoved(identity)` support game save references. Scene parameters can use direct actor references, including arrays; a removed actor resolves to `null` in the corresponding field.
65
-
66
- Scene travel captures outgoing actors and retains unloaded scene records. Game-lifetime actors remain registered and are not restored again when their authored scene is revisited.
67
-
68
- ## Dynamic actors
69
-
70
- The game saves its own spawn descriptors and recreates actors/prefabs explicitly:
71
-
72
- ```ts
73
- await world.spawnActor(Companion, position, rotation, { persistenceId: 'quest-companion' })
74
- await world.spawnPrefab(droppedItemPrefab, position, rotation, { persistenceId: savedDrop.id })
75
- ```
76
-
77
- The actor must still opt in. Dynamic identities are `{ dynamicId, path }`, with an empty path for the main actor and prefab-local paths for other actors. IDs are unique across the session, independent of scene. The game decides which scene to spawn them in. A removed or already active ID is an error; use `isRemoved({ dynamicId, path: '' })` before recreating it. All actors in a prefab restore before any begin-play. References across separately spawned dynamic actors require game-controlled creation order. Synchronous spawning cannot restore persistent state.
78
-
79
- The engine does not recreate dynamically attached components. The game must attach them with stable keys before restoration. Dynamic prefabs are removed as a unit when any persistent member is marked removed.
80
-
81
- ## Cost and testing
82
-
83
- Persistence has no frame callback. Nonparticipating actors receive no persistence record or subscription. Discovery occurs at initialization/capture/restore boundaries, and snapshots traverse registered actors only. Snapshot size and capture frequency remain under game control.
84
-
85
- Core checks: `npm test -- --run src/test/actor-persistence.test.ts src/test/actor-persistence-runtime.test.ts src/test/world-lifecycle.test.ts src/test/world-prefab-spawn.test.ts src/test/prefab-instance-params.test.ts src/test/rendering-host.test.ts` from `packages/core`.
86
-
87
- Maran integration checks, with both repositories side by side: `npm exec -- vitest run --config ../../../legend-of-maran/tests/persistence.config.mts` from `packages/core`.
1
+ # Actor persistence
2
+
3
+ Persistence is opt-in and storage-independent. Enable it when loading a single-player session:
4
+
5
+ ```ts
6
+ await application.loadGame(Game, {
7
+ sceneName: save.sceneName,
8
+ persistence: { data: save.persistence }, // undefined data starts an empty document
9
+ })
10
+ ```
11
+
12
+ `initiateGame()` and React's `HologyScene` accept the same `persistence` option. Games load and migrate their save envelope before loading the scene, then write `inject(ActorPersistence).snapshot()` alongside their other save data. Capture is synchronous and returns detached JSON data; do it before awaiting storage. Sessions without this option do not register actors. Network sessions reject the option.
13
+
14
+ ## Actor and component hooks
15
+
16
+ ```ts
17
+ @Actor({ persistent: true })
18
+ class Chest extends BaseActor implements PersistentState<{ opened: boolean }> {
19
+ readonly persistenceVersion = 1
20
+ opened = false
21
+
22
+ onCapturePersistence() { return { opened: this.opened } }
23
+ onRestorePersistence(state: { opened: boolean }, context: PersistenceContext) {
24
+ if (context.version !== 1 || typeof state?.opened !== 'boolean') {
25
+ throw new Error('Unsupported chest state')
26
+ }
27
+ this.opened = state.opened
28
+ }
29
+ onPersistenceRestored() { this.updateAppearance() }
30
+ }
31
+
32
+ @Component()
33
+ class Cooldown extends ActorComponent implements PersistentState<{ remaining: number }> {
34
+ // Stable within the owning actor, including nested components. Two cooldowns
35
+ // need distinct keys, e.g. 'primary-attack' and 'dodge'. Never use array indices.
36
+ persistenceKey = 'primary-attack'
37
+ readonly persistenceVersion = 1
38
+ remaining = 0
39
+
40
+ onCapturePersistence() { return { remaining: this.remaining } }
41
+ onRestorePersistence(state: { remaining: number }, context: PersistenceContext) {
42
+ if (context.version !== 1 || !Number.isFinite(state?.remaining)) {
43
+ throw new Error('Unsupported cooldown state')
44
+ }
45
+ this.remaining = Math.max(0, state.remaining)
46
+ }
47
+ }
48
+ ```
49
+
50
+ Override `get persistenceEnabled()` for a policy based on authored parameters (for example, enemy faction). It is evaluated after initialization. A component never opts its actor in. An opted-in actor may have only component hooks, or no hooks if it only needs permanent removal.
51
+
52
+ For the common position-and-yaw case, attach `ActorTransformPersistenceComponent` to an opted-in actor. It uses the stable component key `transform` and stores local position and yaw, leaving pitch and roll at their authored values. It restores before begin-play and synchronizes an existing physics body after the restore batch. Attach it only once per actor. `captureActorTransform(actor)` and `restoreActorTransform(actor, state)` provide the same JSON shape for custom hooks; the latter validates the payload and sets the object transform, so custom movement systems must also synchronize their own state and physics. A character movement component, for example, can call `movement.teleport(actor.position, actor.rotation.y)` in the actor's `onPersistenceRestored()` when a saved transform was applied (`transformComponent.restored`).
53
+
54
+ Initialize defaults in `onInit()`. Restore runs after all actors/components initialize and references bind, before `onBeginPlay()`, event graphs, game preload and presentation. Do not reset persisted fields in begin-play. Components restore before their actor. `onPersistenceRestored()` runs after the entire batch restores, including actors with no saved entry. Ordinary restore hooks may resolve other actors, but cannot assume those actors have restored yet. Hooks should not spawn participants or mutate the batch.
55
+
56
+ Save finite JSON values, not actors, vectors, signals, functions, dates or cyclic objects. Validate or migrate component/actor payload versions in the restore hook. Unknown component entries are retained for later saves. Restore failures stop loading and include identity and component key in the error. Restore by assigning state, without replaying damage, death, item rewards or other gameplay events. Save remaining durations rather than wall-clock deadlines unless the game intentionally advances them while closed.
57
+
58
+ ## Identity and removal
59
+
60
+ Authored identities are `{ sceneId, path }`: the stable scene metadata ID and the existing object/prefab instance path. Names are not IDs. Renaming does not change identity; recreating an object or moving it across prefab boundaries may require a game save migration.
61
+
62
+ Call `persistence.markRemoved(actor)` at the permanent gameplay outcome, such as enemy death, before delayed presentation cleanup. It records removal without destroying the live object. The materializer skips that authored subtree on the next load. For a prefab's exposed actor, this removes the whole exposed placement, including nested prefabs, meshes, collisions and event graphs. Ordinary `world.removeActor()` and unloading capture state but never mark it removed.
63
+
64
+ References to removed actors become `null`. Gameplay must handle that possibility. `getIdentity(actor)`, `resolve(identity)` and `isRemoved(identity)` support game save references. Scene parameters can use direct actor references, including arrays; a removed actor resolves to `null` in the corresponding field.
65
+
66
+ Scene travel captures outgoing actors and retains unloaded scene records. Game-lifetime actors remain registered and are not restored again when their authored scene is revisited.
67
+
68
+ ## Dynamic actors
69
+
70
+ The game saves its own spawn descriptors and recreates actors/prefabs explicitly:
71
+
72
+ ```ts
73
+ await world.spawnActor(Companion, position, rotation, { persistenceId: 'quest-companion' })
74
+ await world.spawnPrefab(droppedItemPrefab, position, rotation, { persistenceId: savedDrop.id })
75
+ ```
76
+
77
+ The actor must still opt in. Dynamic identities are `{ dynamicId, path }`, with an empty path for the main actor and prefab-local paths for other actors. IDs are unique across the session, independent of scene. The game decides which scene to spawn them in. A removed or already active ID is an error; use `isRemoved({ dynamicId, path: '' })` before recreating it. All actors in a prefab restore before any begin-play. References across separately spawned dynamic actors require game-controlled creation order. Synchronous spawning cannot restore persistent state.
78
+
79
+ The engine does not recreate dynamically attached components. The game must attach them with stable keys before restoration. Dynamic prefabs are removed as a unit when any persistent member is marked removed.
80
+
81
+ ## Cost and testing
82
+
83
+ Persistence has no frame callback. Nonparticipating actors receive no persistence record or subscription. Discovery occurs at initialization/capture/restore boundaries, and snapshots traverse registered actors only. Snapshot size and capture frequency remain under game control.
84
+
85
+ Core checks: `npm test -- --run src/test/actor-persistence.test.ts src/test/actor-persistence-runtime.test.ts src/test/world-lifecycle.test.ts src/test/world-prefab-spawn.test.ts src/test/prefab-instance-params.test.ts src/test/rendering-host.test.ts` from `packages/core`.
86
+
87
+ Maran integration checks, with both repositories side by side: `npm exec -- vitest run --config ../../../legend-of-maran/tests/persistence.config.mts` from `packages/core`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.261",
3
+ "version": "0.0.263",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",