@hology/core 0.0.252 → 0.0.253

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 (80) hide show
  1. package/dist/effects/sequence/sequencer.js +1 -1
  2. package/dist/gameplay/actors/actor.d.ts +9 -0
  3. package/dist/gameplay/actors/actor.js +1 -1
  4. package/dist/gameplay/actors/builtin/components/character/character-movement.d.ts +2 -0
  5. package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
  6. package/dist/gameplay/actors/controller/actor-controller.d.ts +21 -0
  7. package/dist/gameplay/actors/controller/actor-controller.js +1 -1
  8. package/dist/gameplay/actors/controller/game-state.d.ts +8 -0
  9. package/dist/gameplay/actors/controller/game-state.js +4 -0
  10. package/dist/gameplay/actors/index.d.ts +1 -0
  11. package/dist/gameplay/actors/index.js +1 -1
  12. package/dist/gameplay/game-mode.d.ts +34 -0
  13. package/dist/gameplay/game-mode.js +4 -0
  14. package/dist/gameplay/index.d.ts +7 -1
  15. package/dist/gameplay/index.js +1 -1
  16. package/dist/gameplay/initiate.d.ts +175 -85
  17. package/dist/gameplay/initiate.js +1 -1
  18. package/dist/gameplay/net/browser/index.d.ts +4 -1
  19. package/dist/gameplay/net/browser/index.js +1 -1
  20. package/dist/gameplay/net/index.d.ts +3 -0
  21. package/dist/gameplay/net/index.js +1 -1
  22. package/dist/gameplay/net/net-connection.d.ts +4 -0
  23. package/dist/gameplay/net/net-session.d.ts +3 -0
  24. package/dist/gameplay/net/player-session.d.ts +39 -0
  25. package/dist/gameplay/net/player-session.js +4 -0
  26. package/dist/gameplay/net/service/net-decorator.js +1 -1
  27. package/dist/gameplay/net/service/net-serializer.js +1 -1
  28. package/dist/gameplay/net/service/net-service.d.ts +42 -0
  29. package/dist/gameplay/net/service/net-service.js +1 -1
  30. package/dist/gameplay/network-travel-coordinator.d.ts +27 -0
  31. package/dist/gameplay/network-travel-coordinator.js +4 -0
  32. package/dist/gameplay/player-lifecycle-coordinator.d.ts +29 -0
  33. package/dist/gameplay/player-lifecycle-coordinator.js +4 -0
  34. package/dist/gameplay/rendering-host.d.ts +40 -0
  35. package/dist/gameplay/rendering-host.js +4 -0
  36. package/dist/gameplay/services/physics/physics-system.js +1 -1
  37. package/dist/gameplay/services/render.d.ts +1 -1
  38. package/dist/gameplay/services/render.js +1 -1
  39. package/dist/gameplay/services/scene-transition.d.ts +19 -0
  40. package/dist/gameplay/services/scene-transition.js +4 -0
  41. package/dist/gameplay/services/world.d.ts +16 -6
  42. package/dist/gameplay/services/world.js +1 -1
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +1 -1
  45. package/dist/rendering/graphics-runtime.d.ts +8 -0
  46. package/dist/rendering/graphics-runtime.js +4 -0
  47. package/dist/rendering/intersection-observer.d.ts +5 -0
  48. package/dist/rendering/intersection-observer.js +4 -0
  49. package/dist/rendering/webgpu-experimental-backend.d.ts +12 -1
  50. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  51. package/dist/rendering.d.ts +18 -3
  52. package/dist/rendering.js +1 -1
  53. package/dist/scene/bootstrap.d.ts +2 -0
  54. package/dist/scene/bootstrap.js +1 -1
  55. package/dist/scene/custom-param-runtime-types.js +1 -1
  56. package/dist/scene/materializer.d.ts +3 -1
  57. package/dist/scene/materializer.js +1 -1
  58. package/dist/scene/model.d.ts +2 -1
  59. package/dist/scene/model.js +1 -1
  60. package/dist/scene/objects/prefab.d.ts +3 -0
  61. package/dist/scene/objects/prefab.js +1 -1
  62. package/dist/scene/scene-ref.d.ts +18 -0
  63. package/dist/scene/scene-ref.js +4 -0
  64. package/dist/shader/parameter.d.ts +2 -1
  65. package/dist/test/graphics-runtime.test.d.ts +2 -0
  66. package/dist/test/graphics-runtime.test.js +4 -0
  67. package/dist/test/intersection-observer.test.d.ts +2 -0
  68. package/dist/test/intersection-observer.test.js +4 -0
  69. package/dist/test/net-player-registry.test.d.ts +2 -0
  70. package/dist/test/net-player-registry.test.js +4 -0
  71. package/dist/test/net-serializer.test.d.ts +2 -0
  72. package/dist/test/net-serializer.test.js +4 -0
  73. package/dist/test/player-controller.test.d.ts +2 -0
  74. package/dist/test/player-controller.test.js +4 -0
  75. package/dist/test/rendering-host.test.d.ts +2 -0
  76. package/dist/test/rendering-host.test.js +4 -0
  77. package/dist/test/world-lifecycle.test.js +1 -1
  78. package/dist/test/world-prefab-spawn.test.js +1 -1
  79. package/package.json +1 -1
  80. package/tsconfig.tsbuildinfo +1 -1
@@ -1,112 +1,202 @@
1
- import type { Constructable, ContainerInstance } from "typedi";
2
- import type { ShaderType, ActorType, ActorComponentType } from '../shader/shader.js';
1
+ import type { Constructable, ContainerInstance, ServiceIdentifier } from 'typedi';
2
+ import { Observable } from 'rxjs';
3
+ import type { ActorComponentType, ActorType, ShaderType } from '../shader/shader.js';
4
+ import type { ParameterType } from '../shader/parameter.js';
5
+ import type { DetailTier } from '../scene/model.js';
3
6
  import { World } from './services/world.js';
4
- import { type DirectionalShadowOptions, type DynamicResolutionOptions, type ExperimentalWebGpuPostProcessingOptions, type RenderingBackend, type UpscalingOptions } from '../rendering.js';
5
- import type { Observable } from 'rxjs';
6
- import type { DetailTier } from "../scene/model.js";
7
- import type { NetSession } from "./net/net-session.js";
8
- import type { ParameterType } from "../shader/parameter.js";
9
- import type { VfxBackendPreference } from "../effects/vfx/gpu-sprite-compiler.js";
7
+ import { RenderingView, type DirectionalShadowOptions, type DynamicResolutionOptions, type ExperimentalWebGpuPostProcessingOptions, type RenderingBackend, type UpscalingOptions } from '../rendering.js';
8
+ import type { VfxBackendPreference } from '../effects/vfx/gpu-sprite-compiler.js';
9
+ import type { NetSession } from './net/net-session.js';
10
+ import { RenderingHost, type RenderingHostStageContext } from './rendering-host.js';
11
+ import { type SceneTransitionRequest } from './services/scene-transition.js';
12
+ import { SceneRef } from '../scene/scene-ref.js';
10
13
  type GameClass<T> = Constructable<T>;
11
- export type InitiateGameConfig = {
12
- element: HTMLElement;
13
- sceneName: string;
14
+ export type RenderingConfig = Partial<{
15
+ backend: RenderingBackend;
16
+ resolutionScale: number;
17
+ maxPixelRatio: number;
18
+ dynamicResolution: DynamicResolutionOptions;
19
+ upscaling: UpscalingOptions;
20
+ webgpuPostProcessing: ExperimentalWebGpuPostProcessingOptions;
21
+ webgpuDirectionalShadows: DirectionalShadowOptions;
22
+ webgpuVfx: VfxBackendPreference;
23
+ msaa: number;
24
+ fpsCap: number | null;
25
+ depthPrepass: {
26
+ enabled?: boolean;
27
+ };
28
+ shadows: {
29
+ enabled?: boolean;
30
+ autoUpdate?: boolean;
31
+ };
32
+ bloom: {
33
+ enabled?: boolean;
34
+ };
35
+ reflection: {
36
+ enabled?: boolean;
37
+ };
38
+ occlusionCulling: {
39
+ enabled?: boolean;
40
+ };
41
+ }>;
42
+ type ApplicationConfigBase = {
43
+ /** Optional convenience used when loadGame() is called without an explicit mode class. */
44
+ defaultGameClass?: GameClass<unknown>;
14
45
  dataDir: string;
15
- /** Bypasses callback-based Three file loaders for runtimes with a partial DOM implementation. */
16
46
  directAssetFileLoading?: boolean;
17
- /** Target-specific location of the generated hology_data.json bundle. */
18
47
  runtimeDataUrl?: string | URL;
19
- shaders: {
20
- [name: string]: ShaderType;
21
- };
22
- actors: {
23
- [name: string]: ActorType;
24
- };
25
- components?: {
26
- [name: string]: ActorComponentType;
48
+ shaders: Record<string, ShaderType>;
49
+ actors: Record<string, ActorType>;
50
+ components?: Record<string, ActorComponentType>;
51
+ dataDefinitions?: Record<string, ParameterType>;
52
+ };
53
+ export type InitiateApplicationConfig = ApplicationConfigBase & ({
54
+ element: HTMLElement;
55
+ renderingView?: never;
56
+ renderingHost?: never;
57
+ rendering?: RenderingConfig;
58
+ xr?: {
59
+ enabled: boolean;
27
60
  };
28
- dataDefinitions?: {
29
- [name: string]: ParameterType;
61
+ } | {
62
+ renderingView: RenderingView;
63
+ renderingViewFactory?: () => RenderingView;
64
+ element?: never;
65
+ renderingHost?: never;
66
+ rendering?: never;
67
+ xr?: never;
68
+ } | {
69
+ renderingHost: RenderingHost;
70
+ element?: never;
71
+ renderingView?: never;
72
+ rendering?: never;
73
+ xr?: never;
74
+ });
75
+ export type LoadGameConfig = {
76
+ sceneName: string;
77
+ detailTier?: DetailTier;
78
+ multi?: {
79
+ session: NetSession;
30
80
  };
81
+ };
82
+ export type InitiateGameConfig = ApplicationConfigBase & {
83
+ element: HTMLElement;
84
+ sceneName: string;
85
+ rendering?: RenderingConfig;
31
86
  xr?: {
32
87
  enabled: boolean;
33
88
  };
34
- /**
35
- * What tier of detail to include. If Medium, then any mesh in tier High will not be loaded or rendered but it will also load
36
- * with tier low or undefined.
37
- */
38
89
  detailTier?: DetailTier;
39
- rendering?: Partial<{
40
- backend: RenderingBackend;
41
- resolutionScale: number;
42
- maxPixelRatio: number;
43
- dynamicResolution: DynamicResolutionOptions;
44
- upscaling: UpscalingOptions;
45
- webgpuPostProcessing: ExperimentalWebGpuPostProcessingOptions;
46
- webgpuDirectionalShadows: DirectionalShadowOptions;
47
- /** Preferred VFX simulation backend. `auto` uses GPU when supported and otherwise falls back to CPU. */
48
- webgpuVfx: VfxBackendPreference;
49
- msaa: number;
50
- fpsCap: number | null;
51
- depthPrepass: {
52
- enabled?: boolean;
53
- };
54
- shadows: {
55
- enabled?: boolean;
56
- autoUpdate?: boolean;
57
- };
58
- bloom: {
59
- enabled?: boolean;
60
- };
61
- reflection: {
62
- enabled?: boolean;
63
- };
64
- occlusionCulling: {
65
- enabled?: boolean;
66
- };
67
- }>;
68
90
  multi?: {
69
91
  session: NetSession;
70
92
  };
71
93
  };
72
- export declare function initiateGame<T_Game>(gameClass: GameClass<T_Game>, config: InitiateGameConfig): HologyRuntime<T_Game>;
94
+ export type RuntimeLoadingStage = 'physics' | 'runtime-data' | 'scene' | 'game-preload' | 'shaders' | 'textures' | 'vfx-warmup';
95
+ export type HologyApplicationState = {
96
+ readonly type: 'initializing';
97
+ } | {
98
+ readonly type: 'menu';
99
+ } | {
100
+ readonly type: 'loading';
101
+ readonly sceneName: string;
102
+ readonly progress: number;
103
+ readonly stage: RuntimeLoadingStage;
104
+ } | {
105
+ readonly type: 'playing';
106
+ readonly sceneName: string;
107
+ } | {
108
+ readonly type: 'unloading';
109
+ readonly sceneName?: string;
110
+ } | {
111
+ readonly type: 'error';
112
+ readonly operation: 'initialize' | 'load' | 'unload';
113
+ readonly error: unknown;
114
+ } | {
115
+ readonly type: 'stopped';
116
+ };
117
+ export interface GameSession<TGame = unknown> {
118
+ readonly sceneName: string;
119
+ readonly world: World;
120
+ readonly gameInstance: TGame;
121
+ }
122
+ export type ApplicationRenderStageContext = RenderingHostStageContext;
123
+ export declare function initiateApplication(config: InitiateApplicationConfig): HologyApplicationRuntime;
124
+ /** @deprecated Pass the game class to loadGame(); this overload supplies a default for compatibility. */
125
+ export declare function initiateApplication<TGame>(gameClass: GameClass<TGame>, config: InitiateApplicationConfig): HologyApplicationRuntime<TGame>;
126
+ /** @deprecated Use initiateApplication() followed by runtime.loadGame(). */
127
+ export declare function initiateGame<TGame>(gameClass: GameClass<TGame>, config: InitiateGameConfig): HologyRuntime<TGame>;
73
128
  export declare abstract class GameInstance {
74
- /** Add gameplay objects and preload render resources before the final scene compile. */
75
129
  onPreload(): void | Promise<void>;
76
130
  onStart(): void | Promise<void>;
131
+ onUnload(): void | Promise<void>;
132
+ /** @deprecated Use onUnload(). */
77
133
  onShutdown(): void | Promise<void>;
78
134
  }
79
- export declare function createHologyScene(): void;
80
- /**
81
- * It ideally shouldn't be async.
82
- *
83
- * It should return a runtime and it could have a status to tell you when it is ready.
84
- * There will be stages of loading, etc. Ideally information about loading should be exposed.
85
- *
86
- */
135
+ export declare class HologyApplicationRuntime<TGame = unknown> {
136
+ private readonly config;
137
+ private readonly applicationContainer;
138
+ readonly renderingHost: RenderingHost;
139
+ private readonly defaultGameClass?;
140
+ private readonly stateSubject;
141
+ readonly state: Observable<HologyApplicationState>;
142
+ private readonly shutdownSubject;
143
+ readonly shutdownStarted: Observable<void>;
144
+ readonly ready: Promise<void>;
145
+ private resolveReady;
146
+ private rejectReady;
147
+ private transitionTail;
148
+ private initializationPromise;
149
+ private transitionIntent;
150
+ private session?;
151
+ private shutdownFlag;
152
+ constructor(config: InitiateApplicationConfig, applicationContainer: ContainerInstance, renderingHost: RenderingHost, defaultGameClass?: GameClass<TGame>);
153
+ /** The current compatibility presentation view owned by the rendering host. */
154
+ get renderingView(): RenderingView;
155
+ get currentState(): HologyApplicationState;
156
+ private initialize;
157
+ loadGame(config: LoadGameConfig): Promise<GameSession<TGame>>;
158
+ loadGame<TLoadedGame>(gameClass: GameClass<TLoadedGame>, config: LoadGameConfig): Promise<GameSession<TLoadedGame>>;
159
+ unloadGame(): Promise<void>;
160
+ /** Replace the scene while retaining the current World, game instance, view, and runtime actors. */
161
+ transitionScene(request: SceneTransitionRequest | SceneRef | string): Promise<GameSession<TGame>>;
162
+ shutdown(): Promise<void>;
163
+ getWorld(): World | undefined;
164
+ getGameInstance(): TGame | undefined;
165
+ getApplicationService<T>(service: ServiceIdentifier<T>): T;
166
+ getSessionService<T>(service: ServiceIdentifier<T>): T | undefined;
167
+ getService<T>(service: ServiceIdentifier<T>): T;
168
+ /** Registers an application-lifetime WebGPU pass on the native-resolution presentation target after upscaling. */
169
+ registerRenderStage(callback: (context: ApplicationRenderStageContext) => void): () => void;
170
+ private registerApplicationServices;
171
+ private performLoad;
172
+ private createSession;
173
+ private performSceneTransition;
174
+ private copyApplicationServices;
175
+ private disposeSession;
176
+ private enqueue;
177
+ private checkIntent;
178
+ private assertRunning;
179
+ private loading;
180
+ private setState;
181
+ private getOptionalApplicationService;
182
+ }
183
+ /** Deprecated compatibility façade returned by initiateGame(). */
184
+ export declare class HologyRuntime<TGame = unknown> {
185
+ readonly application: HologyApplicationRuntime<TGame>;
186
+ readonly ready: Promise<boolean>;
187
+ readonly shutdownStarted: Observable<void>;
188
+ constructor(application: HologyApplicationRuntime<TGame>, initialLoad: Promise<GameSession<TGame>>);
189
+ get status(): HologyRuntimeStatus;
190
+ get gameInstance(): TGame;
191
+ get isShutdown(): boolean;
192
+ getWorld(): World;
193
+ getService<T>(service: ServiceIdentifier<T>): T;
194
+ shutdown(): Promise<void>;
195
+ }
87
196
  export declare const enum HologyRuntimeStatus {
88
197
  INITIATING = 0,
89
198
  RUNNING = 5,
90
199
  STOPPED = 10
91
200
  }
92
- export declare class HologyRuntime<T_Game = unknown> {
93
- private containerInstance;
94
- status: HologyRuntimeStatus;
95
- gameInstance?: T_Game;
96
- isShutdown: boolean;
97
- shutdownStarted: Observable<void>;
98
- private _resolver;
99
- /**
100
- * A promise that you can await on to delay some logic until the runtime has finished loading
101
- */
102
- readonly ready: Promise<boolean>;
103
- constructor(containerInstance: ContainerInstance);
104
- getWorld(): World;
105
- getService<T>(serviceType: Constructable<T>): T;
106
- /**
107
- * Stops rendering and all game activity on this client.
108
- */
109
- shutdown(): void;
110
- }
111
201
  export {};
112
202
  //# sourceMappingURL=initiate.d.ts.map
@@ -1,4 +1,4 @@
1
- import e from"typedi";import{loadScene as t}from"../scene/bootstrap.js";import{ActorFactory as n}from"./actors/factory.js";import{World as s}from"./services/world.js";import{SceneMaterializer as o}from"../scene/materializer.js";import{ViewController as i}from"./services/render.js";import{RenderingView as r}from"../rendering.js";import{PhysicsSystem as a}from"./services/physics/physics-system.js";import{MeshComponent as c}from"./actors/builtin/components/mesh-component.js";import{builtInComponents as d}from"./actors/builtin/components/index.js";import{activeContainerInstance as l}from"./actors/internal/container-map.js";import{InputService as m}from"./input/index.js";import{RuntimeAssetsService as p}from"../scene/runtime-asset-service.js";import{AssetResourceLoader as u}from"../scene/asset-resource-loader.js";import{AssetLoader as g}from"./services/asset-loader.js";import{polyfillClient as h}from"./polyfill.js";import{Subject as f}from"rxjs";import{PointerEvents as w}from"./services/pointer-events.js";import{RuntimeBundledBackendService as b}from"../scene/runtime-bundled-backend-service.js";import{Scene as v}from"three";import{ShaderProvider as y}from"./services/shader-provider.js";import{SceneDataService as S}from"../scene/scene-data-service.js";import{NetMode as j}from"./net/net-session.js";import{AssetsProvider as x}from"../scene/assets-provider.js";import{GpuVfxWorldService as I}from"../effects/vfx/gpu-vfx-world-service.js";export function initiateGame(f,w){if(h(),0!=w.element.childNodes.length)return console.error("Can not initialize the game with a non-empty html element"),null;e.has(i);const P=e.of("default"),D=new HologyRuntime(P),G=new n(P,{inEditor:!1});var k;G.classes=w.actors,e.set(n,G),k=w.element,Object.assign(k.style,{position:"absolute",top:"0",left:"0",width:"100%",height:"100%",overflow:"hidden"});const W=new r(w.element,{backend:w.rendering?.backend??"webgpu",enableXR:!0===w.xr?.enabled,maxPixelRatio:w.rendering?.maxPixelRatio,resolutionScale:w.rendering?.resolutionScale,dynamicResolution:w.rendering?.dynamicResolution,upscaling:w.rendering?.upscaling,webgpuPostProcessing:w.rendering?.webgpuPostProcessing,webgpuDirectionalShadows:w.rendering?.webgpuDirectionalShadows,webgpuVfx:w.rendering?.webgpuVfx??"auto",msaa:w.rendering?.msaa,fpsCap:w.rendering?.fpsCap,depthPrepass:{enabled:!0===w.rendering?.depthPrepass?.enabled},bloom:{enabled:!1!==w?.rendering?.bloom?.enabled},reflection:{enabled:!1!==w?.rendering?.reflection?.enabled},occlusionCulling:{enabled:!1!==w?.rendering?.occlusionCulling?.enabled},shadows:{enabled:w.rendering?.shadows?.enabled??!0,autoUpdate:w.rendering?.shadows?.autoUpdate??!0,cascadeUpdateIntervals:[22,40,60,120]}});e.set(r,W);const F=new i(W);e.set(i,F);const H=new b(w.runtimeDataUrl),R=new p(H),z=new u;z.setDataDir(w.dataDir),z.setDirectFileLoading(!0===w.directAssetFileLoading),"webgpu"===W.requestedBackend?z.initKtx2ForWebGpu():z.initKtx2(W.renderer);const A=Object.entries(w.shaders).map(([e,t])=>({name:e,type:t})),E=Object.entries(w.actors).map(([e,t])=>({name:e,type:t})),C={...d,...w.components??{}},U=Object.entries(C).map(([e,t])=>({name:e,type:t})),q=new y(A);e.set(y,q);const O=new g(z,R,A);e.set(x,R),e.set(u,z),e.set(g,O);const B=new v,L=new o(B,new S,R,z,W,A,E,G,U);L.inEditor=!1,L.netMode=w.multi?.session?.mode??j.none,e.set(o,L);const M=e.get(s);return e.set(s,M),M.materializer=L,(async()=>{const n=e.get(a);if(console.info("[Hology] Initializing physics."),await n.start(),console.info("[Hology] Physics initialized."),D.isShutdown)return;if(console.info("[Hology] Loading runtime data."),await H.preloadData(),console.info("[Hology] Runtime data loaded."),D.isShutdown)return;if(M.scene=B,null!=w.multi?.session){const{NetService:e}=await import("./net/service/net-service.js"),t=P.get(e);t.session=w.multi.session,t.start(),D.shutdownStarted.subscribe(()=>t.stop())}const{scene:s,actors:o,materializer:i}=await t(W,w.sceneName,w.dataDir,w.shaders,w.actors,C,G,H,R,z,{detailTier:w.detailTier,nodeMode:w.multi?.session?.mode??j.none});M.scene=s,n.scene=M.scene;for(const e of B.children)M.scene.add(e);if(D.isShutdown)return void W.stop();e.import([c]);for(const e of o)M.addActor(e);n.addFromScene(s),await M.startSceneEventGraphs(i),l.value=P,P.remove(f),P.set({id:f,type:f});const r=P.get(f);if(l.value=null,D.gameInstance=r,r instanceof GameInstance&&await r.onPreload(),D.isShutdown)return void W.stop();if(console.log("Start compile shaders"),console.time("compile shaders"),await W.compileAsync(),console.timeEnd("compile shaders"),"webgpu"===W.activeBackend?console.log("Finished compiling the experimental WebGPU scene."):console.log("Finished compile shaders. Programs: ",W.renderer.info.programs?.length??0),console.log("Start init scene textures"),console.time("init scene textures"),await W.initTexturesAsync(),console.timeEnd("init scene textures"),"webgpu"===W.activeBackend){const e=P.get(I).beginPipelineWarmup();try{W.render(0),await(W.experimentalWebGpuDevice?.queue.onSubmittedWorkDone())}finally{e()}W.render(0),await(W.experimentalWebGpuDevice?.queue.onSubmittedWorkDone()),await new Promise(e=>requestAnimationFrame(()=>e()))}const d=P.get(m);W.loop(e=>{d.update(e)}),D.status=5,D.shutdownStarted.subscribe(()=>{z.disposeAll()}),r instanceof GameInstance&&await r.onStart(),D._resolver(!0)})().catch(e=>{D.status=10,console.error("Failed to initialize Hology runtime.",e),e instanceof Error&&console.error(e.stack)}),D}export class GameInstance{onPreload(){}onStart(){}onShutdown(){}}export function createHologyScene(){}export class HologyRuntime{constructor(e){this.containerInstance=e,this.status=0,this.isShutdown=!1,this.shutdownStarted=new f,this.ready=new Promise(e=>{this._resolver=e})}getWorld(){return this.containerInstance.get(s)}getService(e){return this.containerInstance.get(e)}shutdown(){this.isShutdown=!0;const e=this.shutdownStarted;e.next(),e.complete(),this.gameInstance instanceof GameInstance&&this.gameInstance.onShutdown(),this.containerInstance.get(m).stop();const t=this.containerInstance.get(r);t?.stop();const n=this.getWorld();n.stopSceneEventGraphs();const s=this.containerInstance.get(i);s.setMuted(!0),s.dispose();for(const e of[...n.actors])n.removeActor(e);this.containerInstance.get(a).stop(),this.containerInstance.get(o).dispose();const c=this.containerInstance.get(x);c instanceof p&&c.dispose(),this.containerInstance.get(w).stop(),this.containerInstance.reset(),this.shutdownStarted.complete()}}/*
1
+ import e from"typedi";import{BehaviorSubject as t,Subject as i}from"rxjs";import{loadScene as n}from"../scene/bootstrap.js";import{ActorFactory as s}from"./actors/factory.js";import{SceneMaterializer as o}from"../scene/materializer.js";import{SceneDataService as r}from"../scene/scene-data-service.js";import{World as a}from"./services/world.js";import{ViewController as c}from"./services/render.js";import{RenderingView as p,GraphicsRuntime as h}from"../rendering.js";import{PhysicsSystem as l}from"./services/physics/physics-system.js";import{MeshComponent as d}from"./actors/builtin/components/mesh-component.js";import{builtInComponents as m}from"./actors/builtin/components/index.js";import{InputService as u}from"./input/index.js";import{RuntimeAssetsService as g}from"../scene/runtime-asset-service.js";import{AssetResourceLoader as w}from"../scene/asset-resource-loader.js";import{AssetLoader as f}from"./services/asset-loader.js";import{RuntimeBundledBackendService as y}from"../scene/runtime-bundled-backend-service.js";import{AssetsProvider as S}from"../scene/assets-provider.js";import{ShaderProvider as v}from"./services/shader-provider.js";import{PointerEvents as b}from"./services/pointer-events.js";import{GpuVfxWorldService as I}from"../effects/vfx/gpu-vfx-world-service.js";import{NetMode as j}from"./net/net-session.js";import{polyfillClient as A}from"./polyfill.js";import{withInjectionContext as V}from"./inject.js";import{RenderingHost as N}from"./rendering-host.js";import{SceneTransitionService as R}from"./services/scene-transition.js";import{SceneRef as k}from"../scene/scene-ref.js";let x=0;export function initiateApplication(t,i){const n="function"==typeof t?t:void 0,s=i??t;var o;A(),"element"in s&&null!=s.element&&(o=s.element,Object.assign(o.style,{position:"absolute",top:"0",left:"0",width:"100%",height:"100%",overflow:"hidden"}));const r=s.renderingHost??(null!=s.renderingView?new N(s.renderingView,!1,("renderingViewFactory"in s?s.renderingViewFactory:void 0)??(()=>new p(s.renderingView.container,s.renderingView.options))):function(e,t,i){const n=new h,s=()=>function(e,t,i,n){return new p(e,{graphicsRuntime:n,backend:t?.backend??"webgpu",enableXR:!0===i?.enabled,maxPixelRatio:t?.maxPixelRatio,resolutionScale:t?.resolutionScale,dynamicResolution:t?.dynamicResolution,upscaling:t?.upscaling,webgpuPostProcessing:t?.webgpuPostProcessing,webgpuDirectionalShadows:t?.webgpuDirectionalShadows,webgpuVfx:t?.webgpuVfx??"auto",msaa:t?.msaa,fpsCap:t?.fpsCap,depthPrepass:{enabled:!0===t?.depthPrepass?.enabled},bloom:{enabled:!1!==t?.bloom?.enabled},reflection:{enabled:!1!==t?.reflection?.enabled},occlusionCulling:{enabled:!1!==t?.occlusionCulling?.enabled},shadows:{enabled:t?.shadows?.enabled??!0,autoUpdate:t?.shadows?.autoUpdate??!0,cascadeUpdateIntervals:[22,40,60,120]}})}(e,t,i,n);return new N(s(),!0,s)}(s.element,s.rendering,s.xr)),a=e.of("hology-application-"+ ++x),c=new HologyApplicationRuntime(s,a,r,n??s.defaultGameClass);return c.startInitialization(),c}export function initiateGame(e,t){const{sceneName:i,detailTier:n,multi:s,...o}=t,r=initiateApplication(e,o),a=r.ready.then(()=>r.loadGame({sceneName:i,detailTier:n,multi:s}));return new HologyRuntime(r,a)}export class GameInstance{onPreload(){}onStart(){}onUnload(){return this.onShutdown()}onShutdown(){}}export class HologyApplicationRuntime{constructor(e,n,s,o){this.config=e,this.applicationContainer=n,this.renderingHost=s,this.defaultGameClass=o,this.stateSubject=new t({type:"initializing"}),this.state=this.stateSubject.asObservable(),this.shutdownSubject=new i,this.shutdownStarted=this.shutdownSubject.asObservable(),this.transitionTail=Promise.resolve(),this.initializationPromise=Promise.resolve(),this.transitionIntent=0,this.shutdownFlag=!1,this.ready=new Promise((e,t)=>{this.resolveReady=e,this.rejectReady=t})}get renderingView(){return this.renderingHost.activeView}get currentState(){return this.stateSubject.value}startInitialization(){this.initializationPromise=this.initialize()}async initialize(){try{if(this.registerApplicationServices(),await this.renderingHost.applicationView.compileAsync(),this.shutdownFlag)return void this.rejectReady(z());const e=this.getApplicationService(u);this.renderingHost.start(t=>e.update(t)),this.setState({type:"menu"}),this.resolveReady()}catch(e){this.setState({type:"error",operation:"initialize",error:e}),this.rejectReady(e)}}loadGame(e,t){const i="function"==typeof e?e:this.defaultGameClass,n=t??e;if(null==i)return Promise.reject(new Error("loadGame() requires a game class because no default was configured"));const s=++this.transitionIntent;return this.enqueue(async()=>(await this.ready,this.assertRunning(),this.checkIntent(s),null!=this.session&&await this.disposeSession(this.session,!0),this.checkIntent(s),this.performLoad(i,n,s)))}unloadGame(){const e=++this.transitionIntent;return this.enqueue(async()=>{if(await this.ready,!this.shutdownFlag&&e===this.transitionIntent){try{null!=this.session&&await this.disposeSession(this.session,!0)}catch(t){throw this.shutdownFlag||e!==this.transitionIntent||this.setState({type:"error",operation:"unload",error:t}),t}this.shutdownFlag||e!==this.transitionIntent||this.setState({type:"menu"})}})}transitionScene(e){const t="string"==typeof e?{scene:new k(e,e)}:e instanceof k?{scene:e}:e,i=this.transitionIntent;return this.enqueue(async()=>{await this.ready,this.assertRunning(),this.checkIntent(i);const e=this.session;if(null==e)throw new Error("transitionScene() requires an active game session");try{return await this.performSceneTransition(e,t,i),e}catch(e){throw C(e)||this.shutdownFlag||i!==this.transitionIntent||this.setState({type:"error",operation:"load",error:e}),e}})}async shutdown(){this.shutdownFlag||(this.shutdownFlag=!0,++this.transitionIntent,this.shutdownSubject.next(),this.shutdownSubject.complete(),await this.initializationPromise.catch(()=>{}),await this.transitionTail.catch(()=>{}),null!=this.session&&await this.disposeSession(this.session,!1),this.getApplicationService(u).stop(),this.getOptionalApplicationService(b)?.stop(),this.getApplicationService(w).disposeAll(),this.getApplicationService(g).dispose(),this.getApplicationService(c).dispose(!1),this.renderingHost.shutdown(),e.reset(this.applicationContainer.id),this.setState({type:"stopped"}),this.stateSubject.complete())}getWorld(){return this.session?.world}getGameInstance(){return this.session?.gameInstance}getApplicationService(e){return this.applicationContainer.get(e)}getSessionService(e){return this.session?.container.get(e)}getService(e){return null!=this.session?this.session.container.get(e):this.applicationContainer.get(e)}registerRenderStage(e){return this.renderingHost.registerStage(e)}registerApplicationServices(){const e=new c(this.renderingView);this.applicationContainer.set(p,this.renderingHost.applicationView),this.applicationContainer.set(c,e);const t=new y(this.config.runtimeDataUrl),i=new g(t),n=new w;n.setDataDir(this.config.dataDir),n.setDirectFileLoading(!0===this.config.directAssetFileLoading),"webgpu"===this.renderingHost.applicationView.requestedBackend?n.initKtx2ForWebGpu():n.initKtx2(this.renderingHost.applicationView.renderer);const s=Object.entries(this.config.shaders).map(([e,t])=>({name:e,type:t}));this.applicationContainer.set(y,t),this.applicationContainer.set(S,i),this.applicationContainer.set(g,i),this.applicationContainer.set(w,n),this.applicationContainer.set(f,new f(n,i,s)),this.applicationContainer.set(v,new v(s));const o=V(this.applicationContainer,e=>e.get(u));this.applicationContainer.set(u,o)}async performLoad(t,i,s){const r=this.createSession(i);this.session=r;try{this.loading(i.sceneName,"physics",.08),await r.physics.start(),this.checkIntent(s),this.loading(i.sceneName,"runtime-data",.2),await this.getApplicationService(y).preloadData(),this.checkIntent(s),this.loading(i.sceneName,"scene",.42);const a=await n(r.renderingView,i.sceneName,this.config.dataDir,this.config.shaders,this.config.actors,r.components,r.actorFactory,this.getApplicationService(y),this.getApplicationService(g),this.getApplicationService(w),{detailTier:i.detailTier,nodeMode:i.multi?.session.mode??j.none});if(r.materializer.dispose(),r.materializer=a.materializer,r.sceneRoots=a.sceneRoots,r.container.set(o,a.materializer),r.world.materializer=a.materializer,r.world.scene=a.scene,r.physics.scene=a.scene,this.checkIntent(s),null!=i.multi?.session){const{NetService:e}=await import("./net/service/net-service.js"),t=V(r.container,t=>t.get(e));t.session=i.multi.session,t.start(),r.stopNetwork=()=>t.stop()}e.import([d]);for(const e of a.actors)r.world.addActor(e);if(r.physics.addFromScene(a.scene),await r.world.startSceneEventGraphs(a.materializer),this.checkIntent(s),r.gameInstance=V(r.container,e=>(e.set({id:t,type:t}),e.get(t))),this.loading(i.sceneName,"game-preload",.58),r.gameInstance instanceof GameInstance&&await r.gameInstance.onPreload(),this.checkIntent(s),this.loading(i.sceneName,"shaders",.7),await r.renderingView.compileAsync(),this.checkIntent(s),this.loading(i.sceneName,"textures",.82),await r.renderingView.initTexturesAsync(),this.checkIntent(s),"webgpu"===r.renderingView.activeBackend){this.loading(i.sceneName,"vfx-warmup",.94);const e=r.container.get(I).beginPipelineWarmup();try{const e=performance.now();console.info("[Hology GPU VFX warmup] Rendering loading frame",i.sceneName),r.renderingView.render(0),console.info("[Hology GPU VFX warmup] Render returned; waiting for GPU queue",{sceneName:i.sceneName,renderMs:performance.now()-e}),await(r.renderingView.webGpuDevice?.queue.onSubmittedWorkDone()),console.info("[Hology GPU VFX warmup] GPU queue completed",{sceneName:i.sceneName,totalMs:performance.now()-e})}finally{e()}}return this.checkIntent(s),this.renderingHost.activate(r.renderingView),r.gameInstance instanceof GameInstance&&await r.gameInstance.onStart(),this.checkIntent(s),this.setState({type:"playing",sceneName:i.sceneName}),r}catch(e){throw await this.disposeSession(r,!1),C(e)||this.shutdownFlag||s!==this.transitionIntent||this.setState({type:"error",operation:"load",error:e}),e}}createSession(t){const i=e.of("hology-session-"+ ++x);this.copyApplicationServices(i);const n=this.renderingHost.createView(),h=new c(n);i.set(p,n),i.set(c,h);const d={...m,...this.config.components??{}},u=new s(i,{inEditor:!1});u.classes=this.config.actors,i.set(s,u);const f=new o(n.scene,new r,this.getApplicationService(g),this.getApplicationService(w),n,Object.entries(this.config.shaders).map(([e,t])=>({name:e,type:t})),Object.entries(this.config.actors).map(([e,t])=>({name:e,type:t})),u,Object.entries(d).map(([e,t])=>({name:e,type:t})));f.inEditor=!1,f.netMode=t.multi?.session.mode??j.none,i.set(o,f);const y=V(i,e=>e.get(a));y.scene=n.scene,i.set(a,y);const S=V(i,e=>e.get(l));i.set(l,S);const v=new R;return v.bind(async e=>{await this.transitionScene(e)}),i.set(R,v),{sceneName:t.sceneName,detailTier:t.detailTier,container:i,renderingView:n,viewController:h,actorFactory:u,materializer:f,world:y,physics:S,components:d,sceneTransition:v,sceneRoots:[],netMode:t.multi?.session.mode??j.none}}async performSceneTransition(e,t,i){if(null!=e.stopNetwork)throw new Error("In-session scene transitions are not yet supported for network sessions");const s=t.scene.id.trim();if(0===s.length)throw new Error("A destination scene id is required");const r=e.sceneName;this.setState({type:"unloading",sceneName:r}),e.world.stopSceneEventGraphs();const a=e.world.removeSceneLifetimeActors(e.sceneRoots);for(const t of e.sceneRoots)e.physics.removeRemoved(t),t.removeFromParent();e.sceneRoots=[],e.materializer.dispose(a),this.checkIntent(i),this.loading(s,"scene",.42);const c=await n(e.renderingView,s,this.config.dataDir,this.config.shaders,this.config.actors,e.components,e.actorFactory,this.getApplicationService(y),this.getApplicationService(g),this.getApplicationService(w),{detailTier:e.detailTier,nodeMode:e.netMode});this.checkIntent(i),e.materializer=c.materializer,e.sceneRoots=c.sceneRoots,e.sceneName=s,e.container.set(o,c.materializer),e.world.materializer=c.materializer;for(const t of c.actors)e.world.addActor(t);e.physics.addFromScene(c.scene),await e.world.startSceneEventGraphs(c.materializer),await(t.afterLoad?.(e.world)),this.checkIntent(i),this.loading(s,"shaders",.7),await e.renderingView.compileAsync(),this.checkIntent(i),this.loading(s,"textures",.82),await e.renderingView.initTexturesAsync(),this.checkIntent(i),e.sceneTransition.publish({previousSceneName:r,sceneName:s}),this.setState({type:"playing",sceneName:s})}copyApplicationServices(e){const t=[u,S,g,y,w,f,v];for(const i of t)e.set(i,this.applicationContainer.get(i))}async disposeSession(t,i){if(t.disposed)return;let n;t.disposed=!0,i&&this.setState({type:"unloading",sceneName:t.sceneName});try{t.stopNetwork?.(),t.sceneTransition.dispose(),t.world.stopSceneEventGraphs(),t.gameInstance instanceof GameInstance&&await t.gameInstance.onUnload()}catch(e){n=e}finally{for(const e of[...t.world.actors])t.world.removeActor(e);t.physics.stop(),t.materializer.dispose(),this.renderingHost.activeView===t.renderingView&&this.renderingHost.activate(this.renderingHost.applicationView),t.viewController.dispose(!1),this.renderingHost.releaseView(t.renderingView),e.reset(t.container.id),this.session===t&&(this.session=void 0)}if(null!=n)throw n}enqueue(e){const t=this.transitionTail.then(e,e);return this.transitionTail=t.catch(()=>{}),t}checkIntent(e){if(this.assertRunning(),e!==this.transitionIntent)throw z()}assertRunning(){if(this.shutdownFlag)throw new Error("Hology application has been shut down")}loading(e,t,i){this.setState({type:"loading",sceneName:e,stage:t,progress:i})}setState(e){this.stateSubject.next(e)}getOptionalApplicationService(e){return this.applicationContainer.has(e)?this.applicationContainer.get(e):void 0}}export class HologyRuntime{constructor(e,t){this.application=e,this.ready=t.then(()=>!0),this.shutdownStarted=e.shutdownStarted}get status(){return"playing"===this.application.currentState.type?5:"stopped"===this.application.currentState.type||"error"===this.application.currentState.type?10:0}get gameInstance(){return this.application.getGameInstance()}get isShutdown(){return"stopped"===this.application.currentState.type}getWorld(){return this.application.getWorld()}getService(e){return this.application.getService(e)}shutdown(){return this.application.shutdown()}}function z(){return new DOMException("Superseded by a newer game transition","AbortError")}function C(e){return e instanceof DOMException&&"AbortError"===e.name}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,7 +1,7 @@
1
1
  import { BehaviorSubject, Subject } from 'rxjs';
2
2
  import type { NetReceivedMessage, NetSession } from '../net-session.js';
3
3
  import { NetMode } from '../net-session.js';
4
- import type { NetConnection } from '../net-connection.js';
4
+ import type { NetConnection, NetPlayerInfo } from '../net-connection.js';
5
5
  export type BrowserNetSimulationOptions = {
6
6
  /**
7
7
  * Fixed one-way latency applied to outgoing messages from this session.
@@ -22,6 +22,8 @@ export type BrowserNetSimulationOptions = {
22
22
  reliablePacketLoss?: number;
23
23
  };
24
24
  export type BrowserNetSessionOptions = BrowserNetSimulationOptions & {
25
+ /** Authenticated identity to expose for this simulated participant. */
26
+ player?: NetPlayerInfo;
25
27
  /**
26
28
  * How often this tab refreshes its membership entry. Set to 0 to disable.
27
29
  */
@@ -48,6 +50,7 @@ export declare class BrowserNetSession implements NetSession {
48
50
  * The network mode from this player's point of view.
49
51
  */
50
52
  readonly mode: NetMode;
53
+ readonly localPlayer?: NetPlayerInfo;
51
54
  /**
52
55
  * Convenience function to check if mode === NetworkRole.client
53
56
  */
@@ -1,4 +1,4 @@
1
- import{BehaviorSubject as e,Subject as s}from"rxjs";import{NetMode as t}from"../net-session.js";const n=0,i=0,r=0,o=0;export class BrowserNetSession{get isClient(){return this.mode===t.client}get isServer(){return this.mode<t.client}get isDedicatedServer(){return this.mode===t.dedicatedServer}get clients(){return this.isServer?this.currentMembers().filter(e=>e.mode===t.client).map(e=>a(e)):[]}get server(){if(!this.isServer){const e=this.currentMembers().find(e=>e.mode<t.client);if(null!=e)return a(e)}return null}constructor(t,n,i,r={}){this.sessionId=n,this.localId=i,this.playerLeft=new s,this.playerJoined=new s,this.receivedMessageCount=new e(0),this.storageKey=`net_session_${this.sessionId}`,this.channelName=`net_session_${this.sessionId}`,this.connected=!1,this.storage=null,this.channel=null,this.heartbeatTimer=null,this.receivedMessages=[],this.knownMemberIds=new Set,this.pendingDeliveryTimers=new Set,this.reliableQueues=new Map,this.messageSequence=0,this.onStorage=e=>{if(e.key!==this.storageKey)return;const s=this.getLocalStorage();this.setMembers(s?.members??[],!0)},this.onChannelMessage=e=>{const s=e.data;if(function(e){if(null==e||"object"!=typeof e)return!1;const s=e.kind;return"join"===s||"leave"===s||"heartbeat"===s||"members-request"===s||"members"===s||"message"===s}(s)&&s.sessionId===this.sessionId&&s.sourceConnectionId!==this.localId)switch(s.kind){case"join":case"heartbeat":this.mergeMember(s.member,!0);break;case"leave":this.removeMember(s.member.connectionId,!0);break;case"members-request":this.broadcast({kind:"members",sessionId:this.sessionId,sourceConnectionId:this.localId,members:this.currentMembers()});break;case"members":this.setMembers([...this.currentMembers(),...s.members],!0);break;case"message":this.mergeMember(s.fromMember,!0),s.message.toConnectionId===this.localId&&this.enqueueReceivedMessage(s.message)}},this.onBeforeUnload=()=>{this.disconnect()},this.onUnload=()=>{this.disconnect()},this.mode=t,this.id=n,this.simulation=l(r),this.heartbeatIntervalMs=Math.max(0,r.heartbeatIntervalMs??2e3),this.memberTimeoutMs=Math.max(1,r.memberTimeoutMs??1e4),this.maxQueuedMessages=Math.max(1,r.maxQueuedMessages??1e4)}configureNetworkSimulation(e){this.simulation=l({...this.simulation,...e})}reconnect(){this.disconnect(),this.connect()}disconnect(){this.connected&&this.broadcast({kind:"leave",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()}),this.stopHeartbeat(),this.cancelPendingDeliveries(),this.connected&&(window.removeEventListener("storage",this.onStorage),window.removeEventListener("beforeunload",this.onBeforeUnload),window.removeEventListener("unload",this.onUnload),this.channel?.removeEventListener("message",this.onChannelMessage),this.channel?.close(),this.channel=null,this.connected=!1);const e=this.getLocalStorage(),s=(e?.members??this.storage?.members??[]).filter(e=>e.connectionId!==this.localId);this.writeLocalStorage({members:this.activeMembers(s)}),this.storage=null,this.knownMemberIds.clear(),this.receivedMessages=[],this.receivedMessageCount.next(0)}sendMessage(e,s,t){const n=function(e){const s=(e.id,Number(e.id));return Number.isFinite(s)?s:null}(e);if(null==n)return void console.error(`BrowserNetSession can only send to numeric connection ids. Received ${e.id}`);const i={id:this.createMessageId(),fromConnectionId:this.localId,toConnectionId:n,reliable:s,sentAt:Date.now(),buffer:c(t)};this.sendOrSimulateMessage(i,s)}hasMessage(){return this.receivedMessages.length}readMessage(){const e=this.consumeMessage();if(null!=e){const s=this.connectionForMessage(e);return null==s?(console.error(`No connection found with id ${e.fromConnectionId}`),null):{from:s,buffer:e.buffer}}return null}connect(){if(this.connected)return;if("undefined"==typeof BroadcastChannel)throw new Error("BrowserNetSession requires BroadcastChannel support");this.channel=new BroadcastChannel(this.channelName),this.channel.addEventListener("message",this.onChannelMessage),window.addEventListener("storage",this.onStorage),window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("unload",this.onUnload),this.connected=!0;const e=this.getLocalStorage()??{members:[]},s=this.activeMembers([...e.members,this.createLocalMember()]);this.setMembers(s,!1),this.writeLocalStorage({members:s}),this.broadcast({kind:"join",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()}),this.broadcast({kind:"members-request",sessionId:this.sessionId,sourceConnectionId:this.localId}),this.startHeartbeat()}sendOrSimulateMessage(e,s){if(this.shouldDropMessage(s))return;if(s)return void this.enqueueReliableDelivery(e);const t=this.simulatedDelayMs();if(t<=0)return void this.deliverMessage(e);const n=window.setTimeout(()=>{this.pendingDeliveryTimers.delete(n),this.deliverMessage(e)},t);this.pendingDeliveryTimers.add(n)}deliverMessage(e){e.toConnectionId!==this.localId?this.broadcast({kind:"message",sessionId:this.sessionId,sourceConnectionId:this.localId,fromMember:this.createLocalMember(),message:e}):this.enqueueReceivedMessage(e)}enqueueReceivedMessage(e){this.receivedMessages.length>=this.maxQueuedMessages&&this.receivedMessages.shift(),this.receivedMessages.push(e),this.receivedMessageCount.next(this.receivedMessages.length)}consumeMessage(){const e=this.receivedMessages.shift()??null;return this.receivedMessageCount.next(this.receivedMessages.length),e}connectionForMessage(e){const s=this.server;if(s?.id===e.fromConnectionId)return s;const t=this.clients.find(s=>s.id===e.fromConnectionId);if(null!=t)return t;const n=this.currentMembers().find(s=>s.connectionId===e.fromConnectionId);return null!=n?a(n):null}enqueueReliableDelivery(e){const s=this.simulatedDelayMs(),t=this.reliableQueues.get(e.toConnectionId);if((null==t||0===t.messages.length)&&s<=0)return void this.deliverMessage(e);const n=Date.now(),i=t??{messages:[],timer:null},r=i.messages[i.messages.length-1]?.deliveryAt??n,o=Math.max(n+s,r);i.messages.push({message:e,deliveryAt:o}),this.reliableQueues.set(e.toConnectionId,i),this.scheduleReliableDelivery(e.toConnectionId,i)}scheduleReliableDelivery(e,s){if(null!=s.timer||0===s.messages.length)return;const t=Math.max(0,s.messages[0].deliveryAt-Date.now()),n=window.setTimeout(()=>{this.pendingDeliveryTimers.delete(n),s.timer=null,this.flushReliableDelivery(e)},t);s.timer=n,this.pendingDeliveryTimers.add(n)}flushReliableDelivery(e){const s=this.reliableQueues.get(e);if(null==s)return;const t=Date.now();for(;s.messages.length>0&&s.messages[0].deliveryAt<=t+1;)this.deliverMessage(s.messages.shift().message);s.messages.length>0?this.scheduleReliableDelivery(e,s):this.reliableQueues.delete(e)}shouldDropMessage(e){const s=e?this.simulation.reliablePacketLoss:this.simulation.packetLoss;return s>0&&Math.random()<s}simulatedDelayMs(){const e=this.simulation.jitterMs>0?(2*Math.random()-1)*this.simulation.jitterMs:0;return Math.max(0,this.simulation.latencyMs+e)}startHeartbeat(){this.heartbeatIntervalMs<=0||null!=this.heartbeatTimer||(this.heartbeatTimer=window.setInterval(()=>{const e=this.activeMembers([...this.currentMembers(),this.createLocalMember()]);this.setMembers(e,!0),this.writeLocalStorage({members:e}),this.broadcast({kind:"heartbeat",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()})},this.heartbeatIntervalMs))}stopHeartbeat(){null!=this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}cancelPendingDeliveries(){this.pendingDeliveryTimers.forEach(e=>clearTimeout(e)),this.pendingDeliveryTimers.clear(),this.reliableQueues.clear()}mergeMember(e,s){this.setMembers([...this.currentMembers(),e],s)}removeMember(e,s){this.setMembers(this.currentMembers().filter(s=>s.connectionId!==e),s)}setMembers(e,s){const t=this.storage?.members??[],n=this.activeMembers(this.connected?[...e,this.createLocalMember()]:e);if(s){const e=new Set(n.map(e=>e.connectionId));n.filter(e=>e.connectionId!==this.localId&&!this.knownMemberIds.has(e.connectionId)).forEach(e=>this.playerJoined.next(a(e))),this.knownMemberIds.forEach(s=>{if(s===this.localId||e.has(s))return;const n=t.find(e=>e.connectionId===s);null!=n&&this.playerLeft.next(a(n))})}this.storage={members:n},this.knownMemberIds.clear(),n.forEach(e=>this.knownMemberIds.add(e.connectionId))}currentMembers(){return this.activeMembers(this.storage?.members??this.getLocalStorage()?.members??[])}activeMembers(e){const s=Date.now();return function(e){const s=new Map;for(const t of e){if(!Number.isFinite(t.connectionId))continue;const e=s.get(t.connectionId);(null==e||(t.lastSeenAt??0)>=(e.lastSeenAt??0))&&s.set(t.connectionId,t)}return[...s.values()].sort((e,s)=>e.connectionId-s.connectionId)}(e).filter(e=>e.connectionId===this.localId||null!=e.lastSeenAt&&s-e.lastSeenAt<=this.memberTimeoutMs)}createLocalMember(){return{connectionId:this.localId,mode:this.mode,player:{id:this.localId.toString()},lastSeenAt:Date.now()}}createMessageId(){const e=window.crypto?.randomUUID;return"function"==typeof e?e.call(window.crypto):`${this.localId}-${Date.now()}-${this.messageSequence++}`}broadcast(e){this.channel?.postMessage(e)}getLocalStorage(){const e=localStorage.getItem(this.storageKey);if(null!=e)try{const s=JSON.parse(e);if(Array.isArray(s?.members))return{members:s.members}}catch(e){console.warn(`Failed to parse browser net session ${this.sessionId}`,e)}return null}writeLocalStorage(e){const s=this.activeMembers(e.members);0!==s.length?localStorage.setItem(this.storageKey,JSON.stringify({members:s})):localStorage.removeItem(this.storageKey)}}function a(e){return{id:e.connectionId,player:e.player}}function c(e){const s=new Uint8Array(e),t=new Uint8Array(s.byteLength);return t.set(s),t.buffer}function l(e){return{latencyMs:Math.max(0,e.latencyMs??n),jitterMs:Math.max(0,e.jitterMs??i),packetLoss:h(e.packetLoss??r),reliablePacketLoss:h(e.reliablePacketLoss??o)}}function h(e){return Math.min(1,Math.max(0,e))}/*
1
+ import{BehaviorSubject as e,Subject as s}from"rxjs";import{NetMode as t}from"../net-session.js";const n=0,i=0,r=0,o=0;export class BrowserNetSession{get isClient(){return this.mode===t.client}get isServer(){return this.mode<t.client}get isDedicatedServer(){return this.mode===t.dedicatedServer}get clients(){return this.isServer?this.currentMembers().filter(e=>e.mode===t.client).map(e=>a(e)):[]}get server(){if(!this.isServer){const e=this.currentMembers().find(e=>e.mode<t.client);if(null!=e)return a(e)}return null}constructor(n,i,r,o={}){this.sessionId=i,this.localId=r,this.playerLeft=new s,this.playerJoined=new s,this.receivedMessageCount=new e(0),this.storageKey=`net_session_${this.sessionId}`,this.channelName=`net_session_${this.sessionId}`,this.connected=!1,this.storage=null,this.channel=null,this.heartbeatTimer=null,this.receivedMessages=[],this.knownMemberIds=new Set,this.pendingDeliveryTimers=new Set,this.reliableQueues=new Map,this.messageSequence=0,this.onStorage=e=>{if(e.key!==this.storageKey)return;const s=this.getLocalStorage();this.setMembers(s?.members??[],!0)},this.onChannelMessage=e=>{const s=e.data;if(function(e){if(null==e||"object"!=typeof e)return!1;const s=e.kind;return"join"===s||"leave"===s||"heartbeat"===s||"members-request"===s||"members"===s||"message"===s}(s)&&s.sessionId===this.sessionId&&s.sourceConnectionId!==this.localId)switch(s.kind){case"join":case"heartbeat":this.mergeMember(s.member,!0);break;case"leave":this.removeMember(s.member.connectionId,!0);break;case"members-request":this.broadcast({kind:"members",sessionId:this.sessionId,sourceConnectionId:this.localId,members:this.currentMembers()});break;case"members":this.setMembers([...this.currentMembers(),...s.members],!0);break;case"message":this.mergeMember(s.fromMember,!0),s.message.toConnectionId===this.localId&&this.enqueueReceivedMessage(s.message)}},this.onBeforeUnload=()=>{this.disconnect()},this.onUnload=()=>{this.disconnect()},this.mode=n,this.localPlayer=n===t.dedicatedServer?void 0:o.player??{id:r.toString()},this.id=i,this.simulation=l(o),this.heartbeatIntervalMs=Math.max(0,o.heartbeatIntervalMs??2e3),this.memberTimeoutMs=Math.max(1,o.memberTimeoutMs??1e4),this.maxQueuedMessages=Math.max(1,o.maxQueuedMessages??1e4)}configureNetworkSimulation(e){this.simulation=l({...this.simulation,...e})}reconnect(){this.disconnect(),this.connect()}disconnect(){this.connected&&this.broadcast({kind:"leave",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()}),this.stopHeartbeat(),this.cancelPendingDeliveries(),this.connected&&(window.removeEventListener("storage",this.onStorage),window.removeEventListener("beforeunload",this.onBeforeUnload),window.removeEventListener("unload",this.onUnload),this.channel?.removeEventListener("message",this.onChannelMessage),this.channel?.close(),this.channel=null,this.connected=!1);const e=this.getLocalStorage(),s=(e?.members??this.storage?.members??[]).filter(e=>e.connectionId!==this.localId);this.writeLocalStorage({members:this.activeMembers(s)}),this.storage=null,this.knownMemberIds.clear(),this.receivedMessages=[],this.receivedMessageCount.next(0)}sendMessage(e,s,t){const n=function(e){const s=(e.id,Number(e.id));return Number.isFinite(s)?s:null}(e);if(null==n)return void console.error(`BrowserNetSession can only send to numeric connection ids. Received ${e.id}`);const i={id:this.createMessageId(),fromConnectionId:this.localId,toConnectionId:n,reliable:s,sentAt:Date.now(),buffer:c(t)};this.sendOrSimulateMessage(i,s)}hasMessage(){return this.receivedMessages.length}readMessage(){const e=this.consumeMessage();if(null!=e){const s=this.connectionForMessage(e);return null==s?(console.error(`No connection found with id ${e.fromConnectionId}`),null):{from:s,buffer:e.buffer}}return null}connect(){if(this.connected)return;if("undefined"==typeof BroadcastChannel)throw new Error("BrowserNetSession requires BroadcastChannel support");this.channel=new BroadcastChannel(this.channelName),this.channel.addEventListener("message",this.onChannelMessage),window.addEventListener("storage",this.onStorage),window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("unload",this.onUnload),this.connected=!0;const e=this.getLocalStorage()??{members:[]},s=this.activeMembers([...e.members,this.createLocalMember()]);this.setMembers(s,!1),this.writeLocalStorage({members:s}),this.broadcast({kind:"join",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()}),this.broadcast({kind:"members-request",sessionId:this.sessionId,sourceConnectionId:this.localId}),this.startHeartbeat()}sendOrSimulateMessage(e,s){if(this.shouldDropMessage(s))return;if(s)return void this.enqueueReliableDelivery(e);const t=this.simulatedDelayMs();if(t<=0)return void this.deliverMessage(e);const n=window.setTimeout(()=>{this.pendingDeliveryTimers.delete(n),this.deliverMessage(e)},t);this.pendingDeliveryTimers.add(n)}deliverMessage(e){e.toConnectionId!==this.localId?this.broadcast({kind:"message",sessionId:this.sessionId,sourceConnectionId:this.localId,fromMember:this.createLocalMember(),message:e}):this.enqueueReceivedMessage(e)}enqueueReceivedMessage(e){this.receivedMessages.length>=this.maxQueuedMessages&&this.receivedMessages.shift(),this.receivedMessages.push(e),this.receivedMessageCount.next(this.receivedMessages.length)}consumeMessage(){const e=this.receivedMessages.shift()??null;return this.receivedMessageCount.next(this.receivedMessages.length),e}connectionForMessage(e){const s=this.server;if(s?.id===e.fromConnectionId)return s;const t=this.clients.find(s=>s.id===e.fromConnectionId);if(null!=t)return t;const n=this.currentMembers().find(s=>s.connectionId===e.fromConnectionId);return null!=n?a(n):null}enqueueReliableDelivery(e){const s=this.simulatedDelayMs(),t=this.reliableQueues.get(e.toConnectionId);if((null==t||0===t.messages.length)&&s<=0)return void this.deliverMessage(e);const n=Date.now(),i=t??{messages:[],timer:null},r=i.messages[i.messages.length-1]?.deliveryAt??n,o=Math.max(n+s,r);i.messages.push({message:e,deliveryAt:o}),this.reliableQueues.set(e.toConnectionId,i),this.scheduleReliableDelivery(e.toConnectionId,i)}scheduleReliableDelivery(e,s){if(null!=s.timer||0===s.messages.length)return;const t=Math.max(0,s.messages[0].deliveryAt-Date.now()),n=window.setTimeout(()=>{this.pendingDeliveryTimers.delete(n),s.timer=null,this.flushReliableDelivery(e)},t);s.timer=n,this.pendingDeliveryTimers.add(n)}flushReliableDelivery(e){const s=this.reliableQueues.get(e);if(null==s)return;const t=Date.now();for(;s.messages.length>0&&s.messages[0].deliveryAt<=t+1;)this.deliverMessage(s.messages.shift().message);s.messages.length>0?this.scheduleReliableDelivery(e,s):this.reliableQueues.delete(e)}shouldDropMessage(e){const s=e?this.simulation.reliablePacketLoss:this.simulation.packetLoss;return s>0&&Math.random()<s}simulatedDelayMs(){const e=this.simulation.jitterMs>0?(2*Math.random()-1)*this.simulation.jitterMs:0;return Math.max(0,this.simulation.latencyMs+e)}startHeartbeat(){this.heartbeatIntervalMs<=0||null!=this.heartbeatTimer||(this.heartbeatTimer=window.setInterval(()=>{const e=this.activeMembers([...this.currentMembers(),this.createLocalMember()]);this.setMembers(e,!0),this.writeLocalStorage({members:e}),this.broadcast({kind:"heartbeat",sessionId:this.sessionId,sourceConnectionId:this.localId,member:this.createLocalMember()})},this.heartbeatIntervalMs))}stopHeartbeat(){null!=this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}cancelPendingDeliveries(){this.pendingDeliveryTimers.forEach(e=>clearTimeout(e)),this.pendingDeliveryTimers.clear(),this.reliableQueues.clear()}mergeMember(e,s){this.setMembers([...this.currentMembers(),e],s)}removeMember(e,s){this.setMembers(this.currentMembers().filter(s=>s.connectionId!==e),s)}setMembers(e,s){const t=this.storage?.members??[],n=this.activeMembers(this.connected?[...e,this.createLocalMember()]:e);if(s){const e=new Set(n.map(e=>e.connectionId));n.filter(e=>e.connectionId!==this.localId&&!this.knownMemberIds.has(e.connectionId)).forEach(e=>this.playerJoined.next(a(e))),this.knownMemberIds.forEach(s=>{if(s===this.localId||e.has(s))return;const n=t.find(e=>e.connectionId===s);null!=n&&this.playerLeft.next(a(n))})}this.storage={members:n},this.knownMemberIds.clear(),n.forEach(e=>this.knownMemberIds.add(e.connectionId))}currentMembers(){return this.activeMembers(this.storage?.members??this.getLocalStorage()?.members??[])}activeMembers(e){const s=Date.now();return function(e){const s=new Map;for(const t of e){if(!Number.isFinite(t.connectionId))continue;const e=s.get(t.connectionId);(null==e||(t.lastSeenAt??0)>=(e.lastSeenAt??0))&&s.set(t.connectionId,t)}return[...s.values()].sort((e,s)=>e.connectionId-s.connectionId)}(e).filter(e=>e.connectionId===this.localId||null!=e.lastSeenAt&&s-e.lastSeenAt<=this.memberTimeoutMs)}createLocalMember(){return{connectionId:this.localId,mode:this.mode,player:this.localPlayer,lastSeenAt:Date.now()}}createMessageId(){const e=window.crypto?.randomUUID;return"function"==typeof e?e.call(window.crypto):`${this.localId}-${Date.now()}-${this.messageSequence++}`}broadcast(e){this.channel?.postMessage(e)}getLocalStorage(){const e=localStorage.getItem(this.storageKey);if(null!=e)try{const s=JSON.parse(e);if(Array.isArray(s?.members))return{members:s.members}}catch(e){console.warn(`Failed to parse browser net session ${this.sessionId}`,e)}return null}writeLocalStorage(e){const s=this.activeMembers(e.members);0!==s.length?localStorage.setItem(this.storageKey,JSON.stringify({members:s})):localStorage.removeItem(this.storageKey)}}function a(e){return{id:e.connectionId,player:e.player}}function c(e){const s=new Uint8Array(e),t=new Uint8Array(s.byteLength);return t.set(s),t.buffer}function l(e){return{latencyMs:Math.max(0,e.latencyMs??n),jitterMs:Math.max(0,e.jitterMs??i),packetLoss:h(e.packetLoss??r),reliablePacketLoss:h(e.reliablePacketLoss??o)}}function h(e){return Math.min(1,Math.max(0,e))}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -4,4 +4,7 @@ export * from './service/net-service.js';
4
4
  export * from './service/replication.js';
5
5
  export * from './service/net-decorator.js';
6
6
  export * from './service/net-actor-role.js';
7
+ export * from './player-session.js';
8
+ export * from './net-connection.js';
9
+ export * from './net-session.js';
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
- export*from"./service/rpc-decorator.js";export*from"./service/rpc.js";export*from"./service/net-service.js";export*from"./service/replication.js";export*from"./service/net-decorator.js";export*from"./service/net-actor-role.js";/*
1
+ export*from"./service/rpc-decorator.js";export*from"./service/rpc.js";export*from"./service/net-service.js";export*from"./service/replication.js";export*from"./service/net-decorator.js";export*from"./service/net-actor-role.js";export*from"./player-session.js";export*from"./net-connection.js";export*from"./net-session.js";/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -21,5 +21,9 @@ export interface NetPlayerInfo {
21
21
  * the player ID.
22
22
  */
23
23
  readonly id: string;
24
+ /** Optional authenticated display name. Gameplay may mirror this into PlayerState. */
25
+ readonly displayName?: string;
24
26
  }
27
+ /** Canonical player identity used by ownership, travel, and reconnection. */
28
+ export type NetPlayerId = NetPlayerInfo['id'];
25
29
  //# sourceMappingURL=net-connection.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import type { Observable } from "rxjs";
2
2
  import type { NetConnection } from "./net-connection.js";
3
+ import type { NetPlayerInfo } from './net-connection.js';
3
4
  export declare enum NetMode {
4
5
  /**
5
6
  * Game is played locally so it has authority but no other clients are connected.
@@ -31,6 +32,8 @@ export interface NetSession {
31
32
  * The network mode from this player's point of view.
32
33
  */
33
34
  readonly mode: NetMode;
35
+ /** Authenticated local player. Undefined for a dedicated server. */
36
+ readonly localPlayer?: NetPlayerInfo;
34
37
  /**
35
38
  * The server holds connections to all other clients.
36
39
  */
@@ -0,0 +1,39 @@
1
+ import { type Observable } from 'rxjs';
2
+ import type { BasePlayerController, BasePlayerState } from '../actors/controller/actor-controller.js';
3
+ import type { NetConnection, NetPlayerId, NetPlayerInfo } from './net-connection.js';
4
+ import { type NetSession } from './net-session.js';
5
+ export type PlayerSessionStatus = 'joining' | 'active' | 'travelling' | 'disconnected';
6
+ /** Mutable engine record associated with one stable authenticated player. */
7
+ export interface PlayerSession {
8
+ readonly info: NetPlayerInfo;
9
+ connection?: NetConnection;
10
+ controller?: BasePlayerController;
11
+ playerState?: BasePlayerState;
12
+ /**
13
+ * Zero-based slot for a player controlled on this machine, such as a
14
+ * split-screen player. Undefined for remote players and dedicated servers.
15
+ */
16
+ localPlayerIndex?: number;
17
+ status: PlayerSessionStatus;
18
+ }
19
+ /** Tracks stable players separately from replaceable transport connections. */
20
+ export declare class NetPlayerRegistry {
21
+ private readonly sessions;
22
+ private readonly joinedSubject;
23
+ private readonly leftSubject;
24
+ private subscriptions;
25
+ readonly playerJoined$: Observable<PlayerSession>;
26
+ readonly playerLeft$: Observable<PlayerSession>;
27
+ get values(): readonly PlayerSession[];
28
+ get(playerId: NetPlayerId): PlayerSession | undefined;
29
+ start(session: NetSession): void;
30
+ attachController(playerId: NetPlayerId, controller: BasePlayerController): void;
31
+ attachPlayerState(playerId: NetPlayerId, playerState: BasePlayerState): void;
32
+ setStatus(playerId: NetPlayerId, status: PlayerSessionStatus): void;
33
+ stop(clear?: boolean): void;
34
+ private addConnection;
35
+ private upsert;
36
+ private removeConnection;
37
+ private require;
38
+ }
39
+ //# sourceMappingURL=player-session.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{Subject as t}from"rxjs";import{NetMode as e}from"./net-session.js";export class NetPlayerRegistry{constructor(){this.sessions=new Map,this.joinedSubject=new t,this.leftSubject=new t,this.subscriptions=[],this.playerJoined$=this.joinedSubject.asObservable(),this.playerLeft$=this.leftSubject.asObservable()}get values(){return[...this.sessions.values()]}get(t){return this.sessions.get(t)}start(t){if(this.stop(!1),null!=t.localPlayer&&this.upsert(t.localPlayer,void 0,0),t.mode<e.client){for(const e of t.clients)this.addConnection(e);this.subscriptions.push(t.playerJoined.subscribe(t=>this.addConnection(t)),t.playerLeft.subscribe(t=>this.removeConnection(t)))}}attachController(t,e){const s=this.require(t);s.controller=e,e.playerId=t,null!=s.playerState?e.playerState=s.playerState:null!=e.playerState&&(s.playerState=e.playerState),s.status="active"}attachPlayerState(t,e){const s=this.require(t);s.playerState=e,e.playerId=t,null!=s.info.displayName&&(e.displayName=s.info.displayName),null!=s.controller&&(s.controller.playerState=e)}setStatus(t,e){this.require(t).status=e}stop(t=!0){for(const t of this.subscriptions)t.unsubscribe();this.subscriptions=[],t&&this.sessions.clear()}addConnection(t){null!=t.player&&this.upsert(t.player,t)}upsert(t,e,s){const n=this.sessions.get(t.id);if(null!=n){const t="disconnected"===n.status;return n.connection=e,n.status="active",null!=s&&(n.localPlayerIndex=s),t&&this.joinedSubject.next(n),n}const i={info:t,connection:e,localPlayerIndex:s,status:"joining"};return this.sessions.set(t.id,i),this.joinedSubject.next(i),i}removeConnection(t){const e=t.player?.id;if(null==e)return;const s=this.sessions.get(e);null!=s&&s.connection?.id===t.id&&(s.connection=void 0,s.status="disconnected",this.leftSubject.next(s))}require(t){const e=this.sessions.get(t);if(null==e)throw new Error(`Unknown network player ${JSON.stringify(t)}`);return e}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{BaseActor as t}from"../../actors/actor.js";import{NetService as e}from"./net-service.js";import{inject as n}from"../../inject.js";import{ReplOn as r}from"./replication.js";import{isSignalLike as i}from"./net-utils.js";import{ActorComponent as o}from"../../actors/component.js";export function RunIfServer(){return function(r,i){let o;return i.addInitializer(function(){this instanceof t?o=n(e):console.warn("RunIfServer should not be defined on a non actor")}),function(...t){if(o.isServer)return r.apply(this,t)}}}export function RunIfClient(){return function(r,i){let o;return i.addInitializer(function(){this instanceof t?o=n(e):console.warn("RunIfClient should not be defined on a non actor")}),function(...t){if(!o.isServer)return r.apply(this,t)}}}export function RunIfNotDedicatedServer(){return function(r,i){let o;return i.addInitializer(function(){this instanceof t?o=n(e):console.warn("RunIfClient should not be defined on a non actor")}),function(...t){if(!o.isDedicatedServer)return r.apply(this,t)}}}export function Replicated(s=r.all,c=!0){return function(r,a){let f;a.addInitializer(function(){if(!(this instanceof t||this instanceof o))return void console.warn("RunIfClient should not only be used on actors and actor components");let r,u;if(this instanceof t)r=this;else if(this instanceof o&&(r=this.actor,u=this,null==r))return;f=n(e),f.registerReplicatedProperty(s,c,r,a.name.toString(),this,u);const l=this[a.name];if(i(l)){const t=l.subscribe(t=>{if(f.isServer){if(null==r.__netid)return;f.handlePropertySet(s,c,r,a.name.toString(),t,u)}});return void r.disposed.subscribe(()=>{t()})}let d=l;Object.defineProperty(this,a.name,{set:t=>{f.isServer&&f.handlePropertySet(s,c,r,a.name.toString(),t,u),d=t},get:()=>d})})}}export function makePropertyReplicated(t,e,n,r,i){n.registerReplicatedProperty(i,r,t,e,this,null);let o=t[e];Object.defineProperty(t,e,{set:s=>{n.isServer&&n.handlePropertySet(i,r,t,e,s,null),o=s},get:()=>o})}/*
1
+ import{BaseActor as e}from"../../actors/actor.js";import{NetService as t}from"./net-service.js";import{inject as n}from"../../inject.js";import{ReplOn as r}from"./replication.js";import{isSignalLike as i}from"./net-utils.js";import{ActorComponent as o}from"../../actors/component.js";export function RunIfServer(){return function(r,i){let o;return i.addInitializer(function(){this instanceof e?o=n(t):console.warn("RunIfServer should not be defined on a non actor")}),function(...e){if(o.isServer)return r.apply(this,e)}}}export function RunIfClient(){return function(r,i){let o;return i.addInitializer(function(){this instanceof e?o=n(t):console.warn("RunIfClient should not be defined on a non actor")}),function(...e){if(!o.isServer)return r.apply(this,e)}}}export function RunIfNotDedicatedServer(){return function(r,i){let o;return i.addInitializer(function(){this instanceof e?o=n(t):console.warn("RunIfClient should not be defined on a non actor")}),function(...e){if(!o.isDedicatedServer)return r.apply(this,e)}}}export function Replicated(s=r.all,c=!0){return function(r,a){let f;a.addInitializer(function(){if(!(this instanceof e||this instanceof o))return void console.warn("RunIfClient should not only be used on actors and actor components");let r,u;if(this instanceof e)r=this;else if(this instanceof o&&(r=this.actor,u=this,null==r))return;f=n(t),f.registerReplicatedProperty(s,c,r,a.name.toString(),this,u);const l=this[a.name];if(i(l)){const e=l.subscribe(e=>{if(f.isServer){if(null==r.__netid)return;f.handlePropertySet(s,c,r,a.name.toString(),e,u)}});return void r.disposed.subscribe(()=>{e()})}let d=l;Object.defineProperty(this,a.name,{set:e=>{f.isServer&&f.handlePropertySet(s,c,r,a.name.toString(),e,u),d=e},get:()=>d})})}}export function makePropertyReplicated(e,t,n,r,i){n.registerReplicatedProperty(i,r,e,t,e);let o=e[t];Object.defineProperty(e,t,{set:s=>{n.isServer&&n.handlePropertySet(i,r,e,t,s,null),o=s},get:()=>o})}/*
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{__decorate as r}from"tslib";import{World as t}from"../../services/world.js";import{inject as e}from"../../inject.js";import{Service as s}from"typedi";import{BaseActor as n}from"../../actors/actor.js";import{Euler as o,Quaternion as i,Vector2 as a,Vector3 as c,Vector4 as f}from"three";import{DataAssetRef as u}from"../../../scene/objects/data-asset.js";import{AssetLoader as l}from"../../services/asset-loader.js";const p="-a",m="-v2",d="-v3",w="-v4",y="-e",A="-q",h="-s",j="-m",b="-dar";export class DecodeResult{constructor(){this.error=!1,this.requiresAssetLookups=!1}reset(){this.error=!1,this.missingActorId=void 0,this.requiresAssetLookups=!1}}let O=class{constructor(){this.world=e(t),this.assetLoader=e(l)}encode(r){return JSON.stringify(this.wrapObject(r))}decode(r,t){t.reset();const e=JSON.parse(r);return this.unwrapObject(e,t)}wrapObject(r){if(null!=r&&"object"==typeof r){if(Array.isArray(r))return r.map(r=>this.wrapObject(r));if(r instanceof n){if(null!=r.__netid)return{[p]:r.__netid};console.warn("Missing netid on actor",r)}else{if(r instanceof a)return{[m]:r.toArray([])};if(r instanceof c)return{[d]:r.toArray([])};if(r instanceof f)return{[w]:r.toArray([])};if(r instanceof o)return{[y]:r.toArray([])};if(r instanceof i)return{[A]:r.toArray([])};if(r instanceof Map)return{[j]:Array.from(r.entries()).map(([r,t])=>[r,this.wrapObject(t)])};if(r instanceof Set)return{[h]:Array.from(r.values()).map(r=>this.wrapObject(r))};if(r instanceof u)return{[b]:r.id}}const t={};for(const e of Object.keys(r))t[e]=this.wrapObject(r[e]);return t}return r}unwrapObject(r,t){if(null!=r&&"object"==typeof r){if(Array.isArray(r))return r.map(r=>this.unwrapObject(r,t));{if(null!=r[p]){const e=r[p],s=this.world.actors.find(r=>r.__netid===e);return null==s&&(t.error=!0,t.missingActorId=e,console.warn(`Failed to find replicated actor with netid "${e}"`)),s}if(null!=r[m]){const t=r[m];return(new a).fromArray(t)}if(null!=r[d]){const t=r[d];return(new c).fromArray(t)}if(null!=r[w]){const t=r[w];return(new f).fromArray(t)}if(null!=r[y]){const t=r[y];return(new o).fromArray(t)}if(null!=r[A]){const t=r[A];return(new i).fromArray(t)}if(null!=r[j]){const e=r[j];return new Map(e.map(([r,e])=>[r,this.unwrapObject(e,t)]))}if(null!=r[h]){const e=r[h];return new Set(e.map(r=>this.unwrapObject(r,t)))}if(null!=r[b])return t.requiresAssetLookups=!0,r;const e={};for(const s of Object.keys(r))e[s]=this.unwrapObject(r[s],t);return e}}return r}async decodeAssets(r){if(null!=r&&"object"==typeof r){if(Array.isArray(r))return await Promise.all(r.map(r=>this.decodeAssets(r)));if(r instanceof Map){const t=await Promise.all(Array.from(r.entries()).map(async([r,t])=>[r,await this.decodeAssets(t)]));r.clear();for(const[e,s]of t)r.set(e,s);return r}if(r instanceof Set){const t=await Promise.all(Array.from(r.values()).map(r=>this.decodeAssets(r)));r.clear();for(const e of t)r.add(e);return r}{const t=r;if(null!=t[b])return await this.assetLoader.getDataAssetById(t[b]);for(const r of Object.keys(t))t[r]=await this.decodeAssets(t[r]);return t}}return r}};O=r([s()],O);export{O as NetSerializer};new a,new c,new f,new o,new i;/*
1
+ import{__decorate as r}from"tslib";import{World as t}from"../../services/world.js";import{inject as e}from"../../inject.js";import{Service as s}from"typedi";import{BaseActor as n}from"../../actors/actor.js";import{Euler as o,Quaternion as i,Vector2 as a,Vector3 as c,Vector4 as f}from"three";import{DataAssetRef as u}from"../../../scene/objects/data-asset.js";import{AssetLoader as l}from"../../services/asset-loader.js";const p="-a",d="-v2",m="-v3",w="-v4",y="-e",A="-q",h="-s",j="-m",b="-dar",O="-u";export class DecodeResult{constructor(){this.error=!1,this.requiresAssetLookups=!1}reset(){this.error=!1,this.missingActorId=void 0,this.requiresAssetLookups=!1}}let v=class{constructor(){this.world=e(t),this.assetLoader=e(l)}encode(r){return JSON.stringify(this.wrapObject(r))}decode(r,t){t.reset();const e=JSON.parse(r);return this.unwrapObject(e,t)}wrapObject(r){if(void 0===r)return{[O]:!0};if(null!=r&&"object"==typeof r){if(Array.isArray(r))return r.map(r=>this.wrapObject(r));if(r instanceof n){if(null!=r.__netid)return{[p]:r.__netid};console.warn("Missing netid on actor",r)}else{if(r instanceof a)return{[d]:r.toArray([])};if(r instanceof c)return{[m]:r.toArray([])};if(r instanceof f)return{[w]:r.toArray([])};if(r instanceof o)return{[y]:r.toArray([])};if(r instanceof i)return{[A]:r.toArray([])};if(r instanceof Map)return{[j]:Array.from(r.entries()).map(([r,t])=>[r,this.wrapObject(t)])};if(r instanceof Set)return{[h]:Array.from(r.values()).map(r=>this.wrapObject(r))};if(r instanceof u)return{[b]:r.id}}const t={};for(const e of Object.keys(r))t[e]=this.wrapObject(r[e]);return t}return r}unwrapObject(r,t){if(null!=r&&"object"==typeof r){if(Array.isArray(r))return r.map(r=>this.unwrapObject(r,t));{if(!0===r["-u"])return;if(null!=r[p]){const e=r[p],s=this.world.actors.find(r=>r.__netid===e);return null==s&&(t.error=!0,t.missingActorId=e,console.warn(`Failed to find replicated actor with netid "${e}"`)),s}if(null!=r[d]){const t=r[d];return(new a).fromArray(t)}if(null!=r[m]){const t=r[m];return(new c).fromArray(t)}if(null!=r[w]){const t=r[w];return(new f).fromArray(t)}if(null!=r[y]){const t=r[y];return(new o).fromArray(t)}if(null!=r[A]){const t=r[A];return(new i).fromArray(t)}if(null!=r[j]){const e=r[j];return new Map(e.map(([r,e])=>[r,this.unwrapObject(e,t)]))}if(null!=r[h]){const e=r[h];return new Set(e.map(r=>this.unwrapObject(r,t)))}if(null!=r[b])return t.requiresAssetLookups=!0,r;const e={};for(const s of Object.keys(r))e[s]=this.unwrapObject(r[s],t);return e}}return r}async decodeAssets(r){if(null!=r&&"object"==typeof r){if(Array.isArray(r))return await Promise.all(r.map(r=>this.decodeAssets(r)));if(r instanceof Map){const t=await Promise.all(Array.from(r.entries()).map(async([r,t])=>[r,await this.decodeAssets(t)]));r.clear();for(const[e,s]of t)r.set(e,s);return r}if(r instanceof Set){const t=await Promise.all(Array.from(r.values()).map(r=>this.decodeAssets(r)));r.clear();for(const e of t)r.add(e);return r}{const t=r;if(null!=t[b])return await this.assetLoader.getDataAssetById(t[b]);for(const r of Object.keys(t))t[r]=await this.decodeAssets(t[r]);return t}}return r}};v=r([s()],v);export{v as NetSerializer};new a,new c,new f,new o,new i;/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -2,10 +2,34 @@ import { ActorOptions, BaseActor } from '../../actors/actor.js';
2
2
  import { ActorComponent } from '../../actors/component.js';
3
3
  import { NetConnection } from '../net-connection.js';
4
4
  import { NetMode, NetSession } from '../net-session.js';
5
+ import { NetPlayerRegistry } from '../player-session.js';
5
6
  import { ReplOn } from "./replication.js";
6
7
  import { RunsOn } from './rpc.js';
8
+ import { BaseGameState } from "../../actors/controller/game-state.js";
9
+ export type SceneTravelMessage = {
10
+ travelId: string;
11
+ sceneName: string;
12
+ };
13
+ export type SceneTravelReadyMessage = {
14
+ travelId: string;
15
+ connection: NetConnection;
16
+ };
7
17
  export declare class NetService {
8
18
  session?: NetSession;
19
+ readonly players: NetPlayerRegistry;
20
+ private registeredGameState?;
21
+ private readonly suspendedReplicationConnections;
22
+ private readonly sceneTravelBeginSubject;
23
+ private readonly sceneTravelReadySubject;
24
+ private readonly sceneTravelCompleteSubject;
25
+ private activeSceneTravel?;
26
+ readonly sceneTravelBegin$: import("rxjs").Observable<SceneTravelMessage>;
27
+ readonly sceneTravelReady$: import("rxjs").Observable<SceneTravelReadyMessage>;
28
+ readonly sceneTravelComplete$: import("rxjs").Observable<SceneTravelMessage>;
29
+ /** Travel command retained until a late-starting client coordinator handles it. */
30
+ get pendingSceneTravel(): SceneTravelMessage | undefined;
31
+ /** The single game state after authority creation or replicated client spawn. */
32
+ get gameState(): BaseGameState | undefined;
9
33
  /**
10
34
  * For connection ownership to be determined, actors have
11
35
  * to be registerd with their owning connection.
@@ -136,6 +160,7 @@ export declare class NetService {
136
160
  */
137
161
  private processRemoveActor;
138
162
  setOwningConnection(actor: BaseActor, connection: NetConnection): void;
163
+ clearOwningConnection(actor: BaseActor): void;
139
164
  getOwningConnection(actor: BaseActor): NetConnection;
140
165
  getActorsByOwningConnection(connection: NetConnection): BaseActor[];
141
166
  private isLocallyControlled;
@@ -144,6 +169,19 @@ export declare class NetService {
144
169
  * This relies on role and player controller
145
170
  */
146
171
  isLocalOwner(actor: BaseActor): boolean;
172
+ /** Suspends gameplay replication and asks every remote client to load a scene. */
173
+ beginSceneTravel(travelId: string, sceneName: string): void;
174
+ /** Notifies authority that this client has finished loading the destination. */
175
+ sendSceneTravelReady(travelId: string, sceneName: string): void;
176
+ /** Resynchronizes replicated actors, then tells clients that travel is complete. */
177
+ completeSceneTravel(travelId: string, sceneName: string): void;
178
+ private suspendAndSendSceneTravel;
179
+ private processSceneTravelBegin;
180
+ private processSceneTravelReady;
181
+ private processSceneTravelComplete;
182
+ private createSceneTravelMessage;
183
+ private parseSceneTravelMessage;
184
+ private sendMessage;
147
185
  /**
148
186
  * Sends the current server time (Date.now()) to the specified NetConnection.
149
187
  * The time is sent as an 8-byte (BigInt) value.
@@ -155,6 +193,8 @@ export declare class NetService {
155
193
  */
156
194
  parseServerTime(buffer: ArrayBufferLike): number;
157
195
  private getConnectionKey;
196
+ /** Forgets server-side delivery state after a client has discarded its world. */
197
+ private resetReplicationState;
158
198
  private getPreSpawnMessageQueue;
159
199
  private shouldQueueMessageUntilActorReady;
160
200
  private isPropertyMessage;
@@ -166,6 +206,8 @@ export declare class NetService {
166
206
  private createPropertySnapshotBuffer;
167
207
  private decodeReplicatedValue;
168
208
  private applyReplicatedProperty;
209
+ /** Associates replicated player actors with the stable local player record. */
210
+ private synchronizeReplicatedPlayerActor;
169
211
  private callRepNotify;
170
212
  }
171
213
  //# sourceMappingURL=net-service.d.ts.map