@rpgjs/client 5.0.0-beta.26 → 5.0.0-beta.27

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 (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/Game/Object.js.map +1 -1
  3. package/dist/Gui/Gui.d.ts +2 -2
  4. package/dist/Gui/Gui.js.map +1 -1
  5. package/dist/core/inject.d.ts +4 -4
  6. package/dist/core/inject.js.map +1 -1
  7. package/dist/core/setup.d.ts +3 -3
  8. package/dist/core/setup.js +1 -1
  9. package/dist/core/setup.js.map +1 -1
  10. package/dist/core/setup.spec.d.ts +1 -0
  11. package/dist/i18n.d.ts +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -2
  14. package/dist/module.d.ts +2 -2
  15. package/dist/module.js.map +1 -1
  16. package/dist/public-api-types.spec.d.ts +1 -0
  17. package/dist/services/AbstractSocket.d.ts +3 -3
  18. package/dist/services/AbstractSocket.js.map +1 -1
  19. package/dist/services/loadMap.d.ts +3 -7
  20. package/dist/services/loadMap.js +2 -2
  21. package/dist/services/loadMap.js.map +1 -1
  22. package/dist/services/mapStreaming.d.ts +2 -16
  23. package/dist/services/mapStreaming.js.map +1 -1
  24. package/dist/services/mmorpg.d.ts +4 -26
  25. package/dist/services/mmorpg.js.map +1 -1
  26. package/dist/services/standalone.d.ts +2 -98
  27. package/dist/services/standalone.js.map +1 -1
  28. package/package.json +3 -3
  29. package/src/Game/Object.spec.ts +1 -1
  30. package/src/Game/Object.ts +4 -4
  31. package/src/Gui/Gui.ts +3 -3
  32. package/src/core/inject.ts +6 -5
  33. package/src/core/setup.spec.ts +35 -0
  34. package/src/core/setup.ts +8 -6
  35. package/src/index.ts +12 -1
  36. package/src/module.ts +2 -2
  37. package/src/public-api-types.spec.ts +19 -0
  38. package/src/services/AbstractSocket.ts +2 -2
  39. package/src/services/loadMap.ts +8 -8
  40. package/src/services/mapStreaming.ts +7 -5
  41. package/src/services/mmorpg.ts +6 -7
  42. package/src/services/standalone.ts +4 -5
@@ -1,5 +1,4 @@
1
- import { Context } from '@signe/di';
2
- import { MapChunkHitbox, MapStreamChunk, MapStreamManifest, MapStreamPacket } from '@rpgjs/common';
1
+ import { MapChunkHitbox, MapStreamChunk, MapStreamManifest, MapStreamPacket, RpgProvider } from '@rpgjs/common';
3
2
  import { MapData, LoadMapOptions } from './loadMap';
4
3
  export interface ClientMapStreamingAdapter<TManifestData = unknown, TChunkData = unknown, TState = unknown> {
5
4
  /** CanvasEngine component that renders the provider-specific state. */
@@ -47,16 +46,6 @@ export declare class MapStreamClientController<TManifestData, TChunkData, TState
47
46
  private publish;
48
47
  private updatePredictionBoundary;
49
48
  }
50
- declare class MapStreamingLoadMapService<TManifestData, TChunkData, TState> {
51
- private readonly context;
52
- private readonly options;
53
- private socket?;
54
- private stream?;
55
- private updateMap?;
56
- constructor(context: Context, options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>);
57
- initialize(): void;
58
- load(mapId: string): Promise<MapData>;
59
- }
60
49
  /**
61
50
  * Provide a transport-neutral client map loader backed by authoritative chunks.
62
51
  * Standalone mode may keep a direct loader while MMORPG mode never fetches the
@@ -78,8 +67,5 @@ declare class MapStreamingLoadMapService<TManifestData, TChunkData, TState> {
78
67
  * })
79
68
  * ```
80
69
  */
81
- export declare function provideClientMapStreaming<TManifestData, TChunkData, TState>(options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>): {
82
- provide: string;
83
- useFactory: (context: Context) => MapStreamingLoadMapService<TManifestData, TChunkData, TState>;
84
- }[];
70
+ export declare function provideClientMapStreaming<TManifestData, TChunkData, TState>(options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>): RpgProvider[];
85
71
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"mapStreaming.js","names":[],"sources":["../../src/services/mapStreaming.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport {\n MAP_STREAM_EVENT,\n MAP_STREAM_REQUEST_EVENT,\n type MapChunkHitbox,\n type MapStreamChunk,\n type MapStreamManifest,\n type MapStreamPacket,\n} from \"@rpgjs/common\";\nimport { AbstractWebsocket, WebSocketToken } from \"./AbstractSocket\";\nimport { LoadMapToken, type MapData, type LoadMapOptions } from \"./loadMap\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\n\nexport interface ClientMapStreamingAdapter<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** CanvasEngine component that renders the provider-specific state. */\n component: unknown;\n /** Create empty client render state from public manifest data. */\n createState(manifest: MapStreamManifest<TManifestData>): TState;\n /** Apply one disclosed render chunk to the state. */\n applyChunk(state: TState, chunk: MapStreamChunk<TChunkData>): void;\n /** Remove one chunk that left the retention window. */\n removeChunk(state: TState, key: string): void;\n /** Return the serializable/component-ready map value exposed to CanvasEngine. */\n getData(state: TState): unknown;\n /** Optionally derive component parameters from the public manifest. */\n getParams?(manifest: MapStreamManifest<TManifestData>): Record<string, unknown>;\n}\n\nexport interface ClientMapStreamingOptions<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** Format adapter paired with the server compiler. */\n adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>;\n /** Optional direct loader used only by standalone RPG mode. */\n directLoad?: LoadMapOptions;\n /** Maximum wait for the initial authoritative manifest. Defaults to 10 seconds. */\n timeoutMs?: number;\n}\n\ntype StreamingPhysicsMap = {\n data: { (): MapData | null; set(value: MapData): void };\n replaceStreamedStaticHitboxes(namespace: string, hitboxes: MapChunkHitbox[]): void;\n clearStreamedStaticHitboxes(namespace: string): void;\n};\n\ntype Waiter = {\n resolve(controller: MapStreamClientController<any, any, any>): void;\n reject(error: Error): void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport class MapStreamClientController<TManifestData, TChunkData, TState> {\n private state: TState;\n private manifest: MapStreamManifest<TManifestData>;\n private readonly chunks = new Map<string, MapStreamChunk<TChunkData>>();\n private attachedMap?: StreamingPhysicsMap;\n\n constructor(\n private readonly adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>,\n manifest: MapStreamManifest<TManifestData>,\n ) {\n this.manifest = manifest;\n this.state = adapter.createState(manifest);\n }\n\n get revision(): string {\n return this.manifest.revision;\n }\n\n reset(manifest: MapStreamManifest<TManifestData>): void {\n const attachedMap = this.attachedMap;\n this.detach();\n this.manifest = manifest;\n this.state = this.adapter.createState(manifest);\n this.chunks.clear();\n this.attachedMap = attachedMap;\n }\n\n receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n for (const key of packet.removed) {\n this.chunks.delete(key);\n this.adapter.removeChunk(this.state, key);\n this.attachedMap?.clearStreamedStaticHitboxes(key);\n }\n for (const chunk of packet.chunks) {\n this.chunks.set(chunk.key, chunk);\n this.adapter.applyChunk(this.state, chunk);\n this.attachedMap?.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.publish();\n }\n\n toMapData(): MapData {\n return {\n id: this.manifest.mapId,\n width: this.manifest.width,\n height: this.manifest.height,\n data: this.adapter.getData(this.state),\n component: this.adapter.component,\n params: this.adapter.getParams?.(this.manifest),\n streamController: this,\n };\n }\n\n attach(map: StreamingPhysicsMap): void {\n this.attachedMap = map;\n for (const chunk of this.chunks.values()) {\n map.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.updatePredictionBoundary();\n }\n\n detach(): void {\n if (!this.attachedMap) return;\n for (const key of this.chunks.keys()) {\n this.attachedMap.clearStreamedStaticHitboxes(key);\n }\n this.attachedMap.clearStreamedStaticHitboxes(\"__boundary__\");\n this.attachedMap = undefined;\n }\n\n private publish(): void {\n if (!this.attachedMap) return;\n const current = this.attachedMap.data();\n if (current) {\n this.attachedMap.data.set({ ...current, data: this.adapter.getData(this.state) });\n }\n this.updatePredictionBoundary();\n }\n\n private updatePredictionBoundary(): void {\n const map = this.attachedMap;\n if (!map) return;\n if (this.chunks.size === 0) {\n map.clearStreamedStaticHitboxes(\"__boundary__\");\n return;\n }\n const chunks = [...this.chunks.values()];\n const left = Math.min(...chunks.map((chunk) => chunk.bounds.x));\n const top = Math.min(...chunks.map((chunk) => chunk.bounds.y));\n const right = Math.max(...chunks.map((chunk) => chunk.bounds.x + chunk.bounds.width));\n const bottom = Math.max(...chunks.map((chunk) => chunk.bounds.y + chunk.bounds.height));\n const thickness = 2;\n const barriers: MapChunkHitbox[] = [];\n if (left > 0) barriers.push({ x: left - thickness, y: top, width: thickness, height: bottom - top });\n if (top > 0) barriers.push({ x: left, y: top - thickness, width: right - left, height: thickness });\n if (right < this.manifest.width) barriers.push({ x: right, y: top, width: thickness, height: bottom - top });\n if (bottom < this.manifest.height) barriers.push({ x: left, y: bottom, width: right - left, height: thickness });\n map.replaceStreamedStaticHitboxes(\"__boundary__\", barriers);\n }\n}\n\nclass MapStreamingClientService<TManifestData, TChunkData, TState> {\n private readonly controllers = new Map<string, MapStreamClientController<TManifestData, TChunkData, TState>>();\n private readonly waiters = new Map<string, Waiter[]>();\n\n constructor(\n private readonly socket: AbstractWebsocket,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {\n socket.on(MAP_STREAM_EVENT, (packet) => this.receive(packet));\n }\n\n async load(mapId: string): Promise<MapData> {\n const normalizedId = mapId.replace(/^map-/, \"\");\n const existing = this.controllers.get(normalizedId);\n if (existing) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n return existing.toMapData();\n }\n\n const shouldRequest = !this.waiters.has(normalizedId);\n const controllerPromise = new Promise<MapStreamClientController<TManifestData, TChunkData, TState>>((resolve, reject) => {\n const timeoutMs = Math.max(1, this.options.timeoutMs ?? 10_000);\n const waiter: Waiter = { resolve, reject, timer: undefined as unknown as ReturnType<typeof setTimeout> };\n const timer = setTimeout(() => {\n const current = this.waiters.get(normalizedId) ?? [];\n const remaining = current.filter((entry) => entry !== waiter);\n if (remaining.length > 0) this.waiters.set(normalizedId, remaining);\n else this.waiters.delete(normalizedId);\n reject(new Error(`Map stream '${normalizedId}' was not received after ${timeoutMs}ms`));\n }, timeoutMs);\n waiter.timer = timer;\n const waiters = this.waiters.get(normalizedId) ?? [];\n waiters.push(waiter);\n this.waiters.set(normalizedId, waiters);\n });\n // The map-room connection is established before load() runs. Request the\n // initial packet only after the waiter is registered so fast local and DO\n // transports cannot deliver it before the client is ready.\n if (shouldRequest) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n }\n return (await controllerPromise).toMapData();\n }\n\n private receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n if (!packet || typeof packet.mapId !== \"string\") return;\n const mapId = packet.mapId.replace(/^map-/, \"\");\n let controller = this.controllers.get(mapId);\n if (!controller) {\n if (!packet.manifest) return;\n controller = new MapStreamClientController(this.options.adapter, packet.manifest);\n this.controllers.set(mapId, controller);\n }\n else if (packet.manifest && controller.revision !== packet.manifest.revision) {\n controller.reset(packet.manifest);\n }\n controller.receive(packet);\n\n const waiters = this.waiters.get(mapId) ?? [];\n waiters.forEach((waiter) => {\n clearTimeout(waiter.timer);\n waiter.resolve(controller!);\n });\n this.waiters.delete(mapId);\n }\n}\n\nclass MapStreamingLoadMapService<TManifestData, TChunkData, TState> {\n private socket?: AbstractWebsocket;\n private stream?: MapStreamingClientService<TManifestData, TChunkData, TState>;\n private updateMap?: UpdateMapService;\n\n constructor(\n private readonly context: Context,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {}\n\n initialize(): void {\n if (this.stream) return;\n this.socket = inject<AbstractWebsocket>(this.context, WebSocketToken);\n this.stream = new MapStreamingClientService(this.socket, this.options);\n }\n\n async load(mapId: string): Promise<MapData> {\n this.initialize();\n const map = this.socket?.mode === \"standalone\" && this.options.directLoad\n ? await this.options.directLoad(mapId.replace(/^map-/, \"\"))\n : await this.stream!.load(mapId);\n this.updateMap ??= inject<UpdateMapService>(this.context, UpdateMapToken);\n await this.updateMap.update(map);\n return map;\n }\n}\n\n/**\n * Provide a transport-neutral client map loader backed by authoritative chunks.\n * Standalone mode may keep a direct loader while MMORPG mode never fetches the\n * private source map.\n *\n * @param options - Client adapter, optional standalone loader, and timeout.\n * @returns Dependency-injection providers for the RPGJS map loader.\n *\n * @example\n * ```ts\n * provideClientMapStreaming({\n * adapter: {\n * component: MyMap,\n * createState: (manifest) => ({ manifest, chunks: new Map() }),\n * applyChunk: (state, chunk) => state.chunks.set(chunk.key, chunk.renderData),\n * removeChunk: (state, key) => state.chunks.delete(key),\n * getData: (state) => state,\n * },\n * })\n * ```\n */\nexport function provideClientMapStreaming<TManifestData, TChunkData, TState>(\n options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n) {\n return [\n {\n provide: LoadMapToken,\n useFactory: (context: Context) => new MapStreamingLoadMapService(context, options),\n },\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mapStreaming.js","names":[],"sources":["../../src/services/mapStreaming.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport {\n MAP_STREAM_EVENT,\n MAP_STREAM_REQUEST_EVENT,\n type MapChunkHitbox,\n type MapStreamChunk,\n type MapStreamManifest,\n type MapStreamPacket,\n type RpgContext,\n type RpgProvider,\n} from \"@rpgjs/common\";\nimport { AbstractWebsocket, WebSocketToken } from \"./AbstractSocket\";\nimport { LoadMapToken, type MapData, type LoadMapOptions } from \"./loadMap\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\n\nexport interface ClientMapStreamingAdapter<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** CanvasEngine component that renders the provider-specific state. */\n component: unknown;\n /** Create empty client render state from public manifest data. */\n createState(manifest: MapStreamManifest<TManifestData>): TState;\n /** Apply one disclosed render chunk to the state. */\n applyChunk(state: TState, chunk: MapStreamChunk<TChunkData>): void;\n /** Remove one chunk that left the retention window. */\n removeChunk(state: TState, key: string): void;\n /** Return the serializable/component-ready map value exposed to CanvasEngine. */\n getData(state: TState): unknown;\n /** Optionally derive component parameters from the public manifest. */\n getParams?(manifest: MapStreamManifest<TManifestData>): Record<string, unknown>;\n}\n\nexport interface ClientMapStreamingOptions<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** Format adapter paired with the server compiler. */\n adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>;\n /** Optional direct loader used only by standalone RPG mode. */\n directLoad?: LoadMapOptions;\n /** Maximum wait for the initial authoritative manifest. Defaults to 10 seconds. */\n timeoutMs?: number;\n}\n\ntype StreamingPhysicsMap = {\n data: { (): MapData | null; set(value: MapData): void };\n replaceStreamedStaticHitboxes(namespace: string, hitboxes: MapChunkHitbox[]): void;\n clearStreamedStaticHitboxes(namespace: string): void;\n};\n\ntype Waiter = {\n resolve(controller: MapStreamClientController<any, any, any>): void;\n reject(error: Error): void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport class MapStreamClientController<TManifestData, TChunkData, TState> {\n private state: TState;\n private manifest: MapStreamManifest<TManifestData>;\n private readonly chunks = new Map<string, MapStreamChunk<TChunkData>>();\n private attachedMap?: StreamingPhysicsMap;\n\n constructor(\n private readonly adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>,\n manifest: MapStreamManifest<TManifestData>,\n ) {\n this.manifest = manifest;\n this.state = adapter.createState(manifest);\n }\n\n get revision(): string {\n return this.manifest.revision;\n }\n\n reset(manifest: MapStreamManifest<TManifestData>): void {\n const attachedMap = this.attachedMap;\n this.detach();\n this.manifest = manifest;\n this.state = this.adapter.createState(manifest);\n this.chunks.clear();\n this.attachedMap = attachedMap;\n }\n\n receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n for (const key of packet.removed) {\n this.chunks.delete(key);\n this.adapter.removeChunk(this.state, key);\n this.attachedMap?.clearStreamedStaticHitboxes(key);\n }\n for (const chunk of packet.chunks) {\n this.chunks.set(chunk.key, chunk);\n this.adapter.applyChunk(this.state, chunk);\n this.attachedMap?.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.publish();\n }\n\n toMapData(): MapData {\n return {\n id: this.manifest.mapId,\n width: this.manifest.width,\n height: this.manifest.height,\n data: this.adapter.getData(this.state),\n component: this.adapter.component,\n params: this.adapter.getParams?.(this.manifest),\n streamController: this,\n };\n }\n\n attach(map: StreamingPhysicsMap): void {\n this.attachedMap = map;\n for (const chunk of this.chunks.values()) {\n map.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.updatePredictionBoundary();\n }\n\n detach(): void {\n if (!this.attachedMap) return;\n for (const key of this.chunks.keys()) {\n this.attachedMap.clearStreamedStaticHitboxes(key);\n }\n this.attachedMap.clearStreamedStaticHitboxes(\"__boundary__\");\n this.attachedMap = undefined;\n }\n\n private publish(): void {\n if (!this.attachedMap) return;\n const current = this.attachedMap.data();\n if (current) {\n this.attachedMap.data.set({ ...current, data: this.adapter.getData(this.state) });\n }\n this.updatePredictionBoundary();\n }\n\n private updatePredictionBoundary(): void {\n const map = this.attachedMap;\n if (!map) return;\n if (this.chunks.size === 0) {\n map.clearStreamedStaticHitboxes(\"__boundary__\");\n return;\n }\n const chunks = [...this.chunks.values()];\n const left = Math.min(...chunks.map((chunk) => chunk.bounds.x));\n const top = Math.min(...chunks.map((chunk) => chunk.bounds.y));\n const right = Math.max(...chunks.map((chunk) => chunk.bounds.x + chunk.bounds.width));\n const bottom = Math.max(...chunks.map((chunk) => chunk.bounds.y + chunk.bounds.height));\n const thickness = 2;\n const barriers: MapChunkHitbox[] = [];\n if (left > 0) barriers.push({ x: left - thickness, y: top, width: thickness, height: bottom - top });\n if (top > 0) barriers.push({ x: left, y: top - thickness, width: right - left, height: thickness });\n if (right < this.manifest.width) barriers.push({ x: right, y: top, width: thickness, height: bottom - top });\n if (bottom < this.manifest.height) barriers.push({ x: left, y: bottom, width: right - left, height: thickness });\n map.replaceStreamedStaticHitboxes(\"__boundary__\", barriers);\n }\n}\n\nclass MapStreamingClientService<TManifestData, TChunkData, TState> {\n private readonly controllers = new Map<string, MapStreamClientController<TManifestData, TChunkData, TState>>();\n private readonly waiters = new Map<string, Waiter[]>();\n\n constructor(\n private readonly socket: AbstractWebsocket,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {\n socket.on(MAP_STREAM_EVENT, (packet) => this.receive(packet));\n }\n\n async load(mapId: string): Promise<MapData> {\n const normalizedId = mapId.replace(/^map-/, \"\");\n const existing = this.controllers.get(normalizedId);\n if (existing) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n return existing.toMapData();\n }\n\n const shouldRequest = !this.waiters.has(normalizedId);\n const controllerPromise = new Promise<MapStreamClientController<TManifestData, TChunkData, TState>>((resolve, reject) => {\n const timeoutMs = Math.max(1, this.options.timeoutMs ?? 10_000);\n const waiter: Waiter = { resolve, reject, timer: undefined as unknown as ReturnType<typeof setTimeout> };\n const timer = setTimeout(() => {\n const current = this.waiters.get(normalizedId) ?? [];\n const remaining = current.filter((entry) => entry !== waiter);\n if (remaining.length > 0) this.waiters.set(normalizedId, remaining);\n else this.waiters.delete(normalizedId);\n reject(new Error(`Map stream '${normalizedId}' was not received after ${timeoutMs}ms`));\n }, timeoutMs);\n waiter.timer = timer;\n const waiters = this.waiters.get(normalizedId) ?? [];\n waiters.push(waiter);\n this.waiters.set(normalizedId, waiters);\n });\n // The map-room connection is established before load() runs. Request the\n // initial packet only after the waiter is registered so fast local and DO\n // transports cannot deliver it before the client is ready.\n if (shouldRequest) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n }\n return (await controllerPromise).toMapData();\n }\n\n private receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n if (!packet || typeof packet.mapId !== \"string\") return;\n const mapId = packet.mapId.replace(/^map-/, \"\");\n let controller = this.controllers.get(mapId);\n if (!controller) {\n if (!packet.manifest) return;\n controller = new MapStreamClientController(this.options.adapter, packet.manifest);\n this.controllers.set(mapId, controller);\n }\n else if (packet.manifest && controller.revision !== packet.manifest.revision) {\n controller.reset(packet.manifest);\n }\n controller.receive(packet);\n\n const waiters = this.waiters.get(mapId) ?? [];\n waiters.forEach((waiter) => {\n clearTimeout(waiter.timer);\n waiter.resolve(controller!);\n });\n this.waiters.delete(mapId);\n }\n}\n\nclass MapStreamingLoadMapService<TManifestData, TChunkData, TState> {\n private socket?: AbstractWebsocket;\n private stream?: MapStreamingClientService<TManifestData, TChunkData, TState>;\n private updateMap?: UpdateMapService;\n\n constructor(\n private readonly context: RpgContext,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {}\n\n initialize(): void {\n if (this.stream) return;\n this.socket = inject<AbstractWebsocket>(this.context as Context, WebSocketToken);\n this.stream = new MapStreamingClientService(this.socket, this.options);\n }\n\n async load(mapId: string): Promise<MapData> {\n this.initialize();\n const map = this.socket?.mode === \"standalone\" && this.options.directLoad\n ? await this.options.directLoad(mapId.replace(/^map-/, \"\"))\n : await this.stream!.load(mapId);\n this.updateMap ??= inject<UpdateMapService>(this.context as Context, UpdateMapToken);\n await this.updateMap.update(map);\n return map;\n }\n}\n\n/**\n * Provide a transport-neutral client map loader backed by authoritative chunks.\n * Standalone mode may keep a direct loader while MMORPG mode never fetches the\n * private source map.\n *\n * @param options - Client adapter, optional standalone loader, and timeout.\n * @returns Dependency-injection providers for the RPGJS map loader.\n *\n * @example\n * ```ts\n * provideClientMapStreaming({\n * adapter: {\n * component: MyMap,\n * createState: (manifest) => ({ manifest, chunks: new Map() }),\n * applyChunk: (state, chunk) => state.chunks.set(chunk.key, chunk.renderData),\n * removeChunk: (state, key) => state.chunks.delete(key),\n * getData: (state) => state,\n * },\n * })\n * ```\n */\nexport function provideClientMapStreaming<TManifestData, TChunkData, TState>(\n options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n): RpgProvider[] {\n return [\n {\n provide: LoadMapToken,\n useFactory: (context: RpgContext) => new MapStreamingLoadMapService(context, options),\n },\n ];\n}\n"],"mappings":"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"}
@@ -1,8 +1,5 @@
1
- import { Context } from '@signe/di';
2
- import { RpgGui } from '../Gui/Gui';
3
- import { RpgClientEngine } from '../RpgClientEngine';
4
1
  import { AbstractWebsocket, SocketQuery, SocketUpdateProperties } from './AbstractSocket';
5
- import { UpdateMapService } from '@rpgjs/common';
2
+ import { RpgContext, RpgProvider } from '@rpgjs/common';
6
3
  export interface MmorpgOptions {
7
4
  host?: string;
8
5
  connectionId?: string;
@@ -17,7 +14,7 @@ export interface MmorpgOptions {
17
14
  connectionAcceptanceTimeoutMs?: number;
18
15
  }
19
16
  export declare class BridgeWebsocket extends AbstractWebsocket {
20
- protected context: Context;
17
+ protected context: RpgContext;
21
18
  private options;
22
19
  readonly mode: "mmorpg";
23
20
  private socket;
@@ -25,7 +22,7 @@ export declare class BridgeWebsocket extends AbstractWebsocket {
25
22
  private pendingOn;
26
23
  private acceptedOpenListeners;
27
24
  private targetRoom;
28
- constructor(context: Context, options?: MmorpgOptions);
25
+ constructor(context: RpgContext, options?: MmorpgOptions);
29
26
  private resolveConnectionId;
30
27
  private resolveQuery;
31
28
  connection(listeners?: (data: any) => void): Promise<void>;
@@ -39,23 +36,4 @@ export declare class BridgeWebsocket extends AbstractWebsocket {
39
36
  reconnect(_listeners?: (data: any) => void): Promise<void>;
40
37
  getCurrentRoom(): string;
41
38
  }
42
- declare class UpdateMapStandaloneService extends UpdateMapService {
43
- protected context: Context;
44
- private _options;
45
- constructor(context: Context, _options: MmorpgOptions);
46
- update(_map: any): Promise<void>;
47
- }
48
- export declare function provideMmorpg(options: MmorpgOptions): (typeof RpgClientEngine | typeof RpgGui | {
49
- provide: typeof import('./save').SaveClientService;
50
- useClass: typeof import('./save').SaveClientService;
51
- } | {
52
- provide: string;
53
- useValue: null;
54
- } | {
55
- provide: string;
56
- useFactory: (context: Context) => BridgeWebsocket;
57
- } | {
58
- provide: string;
59
- useFactory: (context: Context) => UpdateMapStandaloneService;
60
- })[];
61
- export {};
39
+ export declare function provideMmorpg(options: MmorpgOptions): RpgProvider[];
@@ -1 +1 @@
1
- {"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n /**\n * Time allowed for a room to restore and send the RPGJS acceptance packet.\n * Increase this for cold edge rooms or local Durable Object startup.\n * Defaults to 10 seconds.\n */\n connectionAcceptanceTimeoutMs?: number;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(\n this.socket.conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n );\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(\n conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n { ignoreCleanClose: true },\n );\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken, type RpgContext, type RpgProvider } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n /**\n * Time allowed for a room to restore and send the RPGJS acceptance packet.\n * Increase this for cold edge rooms or local Durable Object startup.\n * Defaults to 10 seconds.\n */\n connectionAcceptanceTimeoutMs?: number;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: RpgContext, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(\n this.socket.conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n );\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(\n conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n { ignoreCleanClose: true },\n );\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: RpgContext, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions): RpgProvider[] {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: RpgContext) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: RpgContext) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
@@ -1,102 +1,6 @@
1
- import { AbstractWebsocket, SocketUpdateProperties } from './AbstractSocket';
2
- import { Context } from '@signe/di';
3
- import { RpgClientEngine } from '../RpgClientEngine';
4
- import { UpdateMapService } from '@rpgjs/common';
5
- import { RpgGui } from '../Gui/Gui';
1
+ import { RpgProvider } from '@rpgjs/common';
6
2
  interface StandaloneOptions {
7
3
  env?: Record<string, any>;
8
4
  }
9
- declare class BridgeWebsocket extends AbstractWebsocket {
10
- protected context: Context;
11
- private server;
12
- readonly mode: "standalone";
13
- private room;
14
- private socket;
15
- private socketRoom?;
16
- private roomId;
17
- private query?;
18
- private readonly privateId;
19
- private listeners;
20
- private rooms;
21
- private serverInstance;
22
- constructor(context: Context, server: any, options?: StandaloneOptions);
23
- connection(listeners?: (data: any) => void): Promise<any>;
24
- private _connection;
25
- on(key: string, callback: (data: any) => void): void;
26
- off(event: string, callback: (data: any) => void): void;
27
- emit(event: string, data: any): void;
28
- /**
29
- * Update underlying connection properties before a reconnect
30
- *
31
- * Design
32
- * - Dynamically register a factory for the requested room to ensure a fresh server instance
33
- * - Swap the internal ServerIo to target the new room
34
- *
35
- * @param params - Properties to update
36
- * @param params.room - The target room id (e.g. `map-simplemap2`)
37
- *
38
- * @example
39
- * ```ts
40
- * websocket.updateProperties({ room: 'map-simplemap2' })
41
- * await websocket.reconnect()
42
- * ```
43
- */
44
- updateProperties({ room, query }: SocketUpdateProperties): void;
45
- /**
46
- * Reconnect the client to the current Party room
47
- *
48
- * Design
49
- * - Must be called after `updateProperties()` when switching rooms
50
- * - Rebuilds the client <-> server bridge and re-triggers connection listeners
51
- *
52
- * @param listeners - Optional callback to re-bind event handlers on the new socket
53
- *
54
- * @example
55
- * ```ts
56
- * websocket.updateProperties({ room: 'map-dungeon' })
57
- * await websocket.reconnect((socket) => {
58
- * // re-bind events here
59
- * })
60
- * ```
61
- */
62
- reconnect(listeners?: (data: any) => void): Promise<void>;
63
- private connectToRoom;
64
- private normalizeQuery;
65
- private detachCurrentSocket;
66
- getServer(): any;
67
- getSocket(): any;
68
- }
69
- declare class UpdateMapStandaloneService extends UpdateMapService {
70
- private server;
71
- /**
72
- * Update the current room map data on the server side
73
- *
74
- * Design
75
- * - Uses the in-memory server instance stored in context (standalone mode)
76
- * - Builds a local HTTP-like request to the current Party room endpoint
77
- *
78
- * @param map - The map payload to apply on the server
79
- *
80
- * @example
81
- * ```ts
82
- * await updateMapService.update({ width: 1024, height: 768, events: [] })
83
- * ```
84
- */
85
- update(map: any): Promise<void>;
86
- }
87
- export declare function provideRpg(server: any, options?: StandaloneOptions): (typeof RpgClientEngine | typeof RpgGui | {
88
- provide: typeof import('./save').SaveClientService;
89
- useClass: typeof import('./save').SaveClientService;
90
- } | {
91
- provide: string;
92
- useValue: null;
93
- } | {
94
- provide: string;
95
- useFactory: (context: Context) => BridgeWebsocket;
96
- useClass?: undefined;
97
- } | {
98
- provide: string;
99
- useClass: typeof UpdateMapStandaloneService;
100
- useFactory?: undefined;
101
- })[];
5
+ export declare function provideRpg(server: any, options?: StandaloneOptions): RpgProvider[];
102
6
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { Context } from \"@signe/di\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\nimport type { SocketQuery } from \"./AbstractSocket\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"standalone\" as const;\n\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private roomId = \"lobby-1\";\n private query?: SocketQuery;\n private readonly privateId = \"player-client-id\";\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n const room = new ServerIo(roomId, this.rooms);\n const server = new this.server(room)\n await server.onStart();\n await server.subRoom.onStart()\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: Context, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n await this.connectToRoom();\n listeners?.(this.socket)\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties({ room, query }: SocketUpdateProperties) {\n this.roomId = room;\n this.query = query;\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private async connectToRoom() {\n if (this.serverInstance?.room?.id !== this.roomId) {\n const party = await this.room.context.parties.main.get(this.roomId);\n this.serverInstance = party.server;\n this.context.set('server', this.serverInstance);\n this.room = this.serverInstance.room;\n }\n else {\n this.serverInstance = this.context.get('server');\n }\n\n this.socket = new ClientIo(this.serverInstance, this.privateId);\n const url = new URL('http://localhost');\n const query = this.normalizeQuery(this.query);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n }\n\n private normalizeQuery(query?: SocketQuery): Record<string, string> | undefined {\n if (!query) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(query)\n .filter((entry): entry is [string, string] => typeof entry[1] === \"string\")\n );\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAM,kBAAN,cAA8B,kBAAkB;CA0B9C,YAAY,SAA4B,QAAqB,UAA6B,CAAC,GAAG;EAC5F,MAAM,OAAO;EADO,KAAA,UAAA;EAA0B,KAAA,SAAA;cAzBhC;gBAKC;mBAEY;mBAKxB,CAAC;eACU;GACd,SAAS,OAAO,WAAmB;IACjC,MAAM,OAAO,IAAI,SAAS,QAAQ,KAAK,KAAK;IAC5C,MAAM,SAAS,IAAI,KAAK,OAAO,IAAI;IACnC,MAAM,OAAO,QAAQ;IACrB,MAAM,OAAO,QAAQ,QAAQ;IAC7B,OAAO;GACT;GACA,KAAK,CAAC;EACR;EAME,KAAK,MAAM,MAAM,QAAQ,OAAO,CAAC;EACjC,KAAK,OAAO,IAAI,SAAS,WAAW,KAAK,KAAK;CAChD;CAEA,MAAM,WAAW,WAAiC;EAChD,KAAK,iBAAiB,IAAI,KAAK,OAAO,KAAK,IAAI;EAC/C,MAAM,KAAK,eAAe,QAAQ;EAClC,MAAM,KAAK,eAAe,QAAQ,QAAQ;EAC1C,KAAK,QAAQ,IAAI,UAAU,KAAK,cAAc;EAC9C,OAAO,KAAK,YAAY,SAAS;CACnC;CAEA,MAAc,YAAY,WAAiC;EACzD,KAAK,oBAAoB;EACzB,MAAM,KAAK,cAAc;EACzB,YAAY,KAAK,MAAM;EACvB,OAAO,KAAK;CACd;CAEA,GAAG,KAAa,UAA+B;EAC7C,IACE,KAAK,UAAU,MACZ,aAAa,SAAS,UAAU,OAAO,SAAS,aAAa,QAChE,GAEA;EAEF,MAAM,WAAW,UAAU;GACzB,MAAM,SAAS,2BAA2B,KAAK;GAC/C,IAAI,OAAO,SAAS,KAClB,SAAS,OAAO,KAAK;EAEzB;EACA,KAAK,UAAU,KAAK;GAAE,OAAO;GAAK;GAAU;EAAQ,CAAC;EACrD,KAAK,QAAQ,iBAAiB,WAAW,OAAO;CAClD;CAEA,IAAI,OAAe,UAA+B;EAChD,MAAM,YAAmC,CAAC;EAC1C,KAAK,MAAM,YAAY,KAAK,WAAW;GACrC,IAAI,SAAS,UAAU,SAAS,SAAS,aAAa,UAAU;IAC9D,KAAK,QAAQ,oBAAoB,WAAW,SAAS,OAAO;IAC5D;GACF;GACA,UAAU,KAAK,QAAQ;EACzB;EACA,KAAK,YAAY;CACnB;CAEA,KAAK,OAAe,MAAW;EAC7B,KAAK,OAAO,KAAK;GACf,QAAQ;GACR,OAAO;EACT,CAAC;CACH;;;;;;;;;;;;;;;;;CAkBA,iBAAiB,EAAE,MAAM,SAAiC;EACxD,KAAK,SAAS;EACd,KAAK,QAAQ;CACf;;;;;;;;;;;;;;;;;;CAmBA,MAAM,UAAU,WAAgD;EAC9D,MAAM,KAAK,aAAa,WAAW;GACjC,YAAY,MAAM;EACpB,CAAC;CACH;CAEA,MAAc,gBAAgB;EAC5B,IAAI,KAAK,gBAAgB,MAAM,OAAO,KAAK,QAAQ;GACjD,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,QAAQ,KAAK,IAAI,KAAK,MAAM;GAClE,KAAK,iBAAiB,MAAM;GAC5B,KAAK,QAAQ,IAAI,UAAU,KAAK,cAAc;GAC9C,KAAK,OAAO,KAAK,eAAe;EAClC,OAEE,KAAK,iBAAiB,KAAK,QAAQ,IAAI,QAAQ;EAGjD,KAAK,SAAS,IAAI,SAAS,KAAK,gBAAgB,KAAK,SAAS;EAC9D,MAAM,MAAM,IAAI,IAAI,kBAAkB;EACtC,MAAM,QAAQ,KAAK,eAAe,KAAK,KAAK;EAC5C,IAAI,OACF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,aAAa,IAAI,KAAK,KAAK;EAGnC,MAAM,UAAU,IAAI,QAAQ,IAAI,SAAS,GAAG;GAC1C,QAAQ;GACR,SAAS,EACP,gBAAgB,mBAClB;EACF,CAAC;EACD,KAAK,KAAK,QAAQ,IAAI,KAAK,OAAO,IAAI,KAAK,MAAM;EACjD,KAAK,aAAa,KAAK;EACvB,KAAK,UAAU,SAAS,EAAE,cAAc;GACtC,KAAK,OAAO,iBAAiB,WAAW,OAAO;EACjD,CAAC;EACD,MAAM,KAAK,eAAe,UAAU,KAAK,OAAO,MAAa,EAAE,QAAQ,CAAQ;CACjF;CAEA,eAAuB,OAAyD;EAC9E,IAAI,CAAC,OACH;EAEF,OAAO,OAAO,YACZ,OAAO,QAAQ,KAAK,EACjB,QAAQ,UAAqC,OAAO,MAAM,OAAO,QAAQ,CAC9E;CACF;CAEA,sBAA8B;EAC5B,IAAI,CAAC,KAAK,QAAQ;EAClB,KAAK,UAAU,SAAS,EAAE,cAAc;GACtC,KAAK,OAAO,oBAAoB,WAAW,OAAO;EACpD,CAAC;EACD,KAAK,YAAY,SAAS,SAAS,KAAK,OAAO,EAAE;EACjD,KAAK,SAAS,KAAA;EACd,KAAK,aAAa,KAAA;CACpB;CAEA,YAAY;EACV,OAAO,KAAK;CACd;CAEA,YAAY;EACV,OAAO,KAAK;CACd;AACF;AAEA,IAAM,6BAAN,cAAyC,iBAAiB;;;;;;;;;;;;;;;CAiBxD,MAAM,OAAO,KAAU;EACrB,KAAK,SAAS,KAAK,QAAQ,IAAI,QAAQ;EAEvC,MAAM,MAAM;GACV,KAAK,iCAFQ,KAAK,QAAQ,MAAM,MAAM,UAEO;GAC7C,QAAQ;GACR,SAAS,IAAI,QAAQ,CAAC,CAAC;GACvB,MAAM,YAAY;IAChB,OAAO;GACT;EACF;EACA,MAAM,KAAK,OAAO,UAAU,GAAG;CACjC;AACF;AAEA,SAAgB,WAAW,QAAa,UAA6B,CAAC,GAAG;CACvE,OAAO;EACL;GACE,SAAS;GACT,aAAa,YAAqB,IAAI,gBAAgB,SAAS,QAAQ,OAAO;EAChF;EACA;GACE,SAAS;GACT,UAAU;EACZ;EACA,wBAAwB;EACxB,kBAAkB;EAClB;EACA;CACF;AACF"}
1
+ {"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken, type RpgContext, type RpgProvider } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\nimport type { SocketQuery } from \"./AbstractSocket\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"standalone\" as const;\n\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private roomId = \"lobby-1\";\n private query?: SocketQuery;\n private readonly privateId = \"player-client-id\";\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n const room = new ServerIo(roomId, this.rooms);\n const server = new this.server(room)\n await server.onStart();\n await server.subRoom.onStart()\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: RpgContext, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n await this.connectToRoom();\n listeners?.(this.socket)\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties({ room, query }: SocketUpdateProperties) {\n this.roomId = room;\n this.query = query;\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private async connectToRoom() {\n if (this.serverInstance?.room?.id !== this.roomId) {\n const party = await this.room.context.parties.main.get(this.roomId);\n this.serverInstance = party.server;\n this.context.set('server', this.serverInstance);\n this.room = this.serverInstance.room;\n }\n else {\n this.serverInstance = this.context.get('server');\n }\n\n this.socket = new ClientIo(this.serverInstance, this.privateId);\n const url = new URL('http://localhost');\n const query = this.normalizeQuery(this.query);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n }\n\n private normalizeQuery(query?: SocketQuery): Record<string, string> | undefined {\n if (!query) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(query)\n .filter((entry): entry is [string, string] => typeof entry[1] === \"string\")\n );\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}): RpgProvider[] {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: RpgContext) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/client",
3
- "version": "5.0.0-beta.26",
3
+ "version": "5.0.0-beta.27",
4
4
  "description": "RPGJS is a framework for creating RPG/MMORPG games",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,8 +22,8 @@
22
22
  "pixi.js": "^8.9.2"
23
23
  },
24
24
  "dependencies": {
25
- "@rpgjs/common": "5.0.0-beta.25",
26
- "@rpgjs/server": "5.0.0-beta.26",
25
+ "@rpgjs/common": "5.0.0-beta.26",
26
+ "@rpgjs/server": "5.0.0-beta.27",
27
27
  "@rpgjs/ui-css": "5.0.0-beta.23",
28
28
  "@signe/di": "3.1.0",
29
29
  "@signe/room": "3.1.0",
@@ -101,7 +101,7 @@ describe("RpgClientObject animations", () => {
101
101
  const object = createObject();
102
102
  const animationChanges: Array<{ name: string; isPlaying: boolean }> = [];
103
103
 
104
- object.animationName.observable.subscribe((name) => {
104
+ (object.animationName as any).observable.subscribe((name) => {
105
105
  animationChanges.push({
106
106
  name,
107
107
  isPlaying: object.animationIsPlaying(),
@@ -1,6 +1,6 @@
1
1
  import { Hooks, ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
2
2
  import { trigger, signal, type Trigger } from "canvasengine";
3
- import { combineLatest, from, map, of, startWith, Subscription, switchMap } from "rxjs";
3
+ import { combineLatest, from, map, of, startWith, Subscription, switchMap, type Observable } from "rxjs";
4
4
  import { inject } from "../core/inject";
5
5
  import { RpgClientEngine } from "../RpgClientEngine";
6
6
  type Frame = { x: number; y: number; ts: number };
@@ -123,7 +123,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
123
123
  const engine = this.engine;
124
124
  this.hooks.callHooks("client-sprite-onInit", this).subscribe();
125
125
 
126
- this._frames.observable.subscribe(({ items }) => {
126
+ (this._frames as any).observable.subscribe(({ items }) => {
127
127
  if (!this.id) return;
128
128
  //if (this.id == this.engine.playerIdSignal()!) return;
129
129
  this.frames = mergeFreshFramePayload(
@@ -133,8 +133,8 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
133
133
  );
134
134
  });
135
135
 
136
- const graphics$ = this.graphics.observable.pipe(map(({ items }) => items));
137
- const graphicScale$ = this._graphicScale.observable.pipe(
136
+ const graphics$: Observable<unknown> = (this.graphics as any).observable.pipe(map(({ items }) => items));
137
+ const graphicScale$: Observable<unknown> = (this._graphicScale as any).observable.pipe(
138
138
  startWith({ value: this._graphicScale() }),
139
139
  map((payload: any) => payload?.value ?? payload),
140
140
  );
package/src/Gui/Gui.ts CHANGED
@@ -3,7 +3,7 @@ import { signal, Signal, WritableSignal } from "canvasengine";
3
3
  import { AbstractWebsocket, WebSocketToken } from "../services/AbstractSocket";
4
4
  import { DialogboxComponent, ShopComponent, SaveLoadComponent, MainMenuComponent, NotificationComponent, TitleScreenComponent, GameoverComponent, InputComponent } from "../components/gui";
5
5
  import { combineLatest, Subscription } from "rxjs";
6
- import { PrebuiltGui } from "@rpgjs/common";
6
+ import { PrebuiltGui, type RpgContext } from "@rpgjs/common";
7
7
 
8
8
  interface GuiOptions {
9
9
  name?: string;
@@ -145,8 +145,8 @@ export class RpgGui {
145
145
  */
146
146
  attachedGuiDisplayState = signal<Record<string, boolean>>({});
147
147
 
148
- constructor(private context: Context) {
149
- this.webSocket = inject(context, WebSocketToken);
148
+ constructor(private context: RpgContext) {
149
+ this.webSocket = inject(context as Context, WebSocketToken);
150
150
  this.add({
151
151
  name: "rpg-dialog",
152
152
  component: DialogboxComponent,
@@ -1,17 +1,18 @@
1
1
  import { Context, inject as injector } from "@signe/di";
2
+ import type { RpgContext } from "@rpgjs/common";
2
3
 
3
- export let context: Context | null = null
4
+ export let context: RpgContext | null = null
4
5
 
5
- export function inject<T>(service: (new (...args: any[]) => T) | string, _context?: Context): T {
6
+ export function inject<T>(service: (new (...args: any[]) => T) | string, _context?: RpgContext): T {
6
7
  const c = _context ?? context
7
8
  if (!c) throw new Error("Context is not set. use setInject() to set the context");
8
- return injector(c, service);
9
+ return injector(c as Context, service);
9
10
  }
10
11
 
11
- export function setInject(_context: Context) {
12
+ export function setInject(_context: RpgContext) {
12
13
  context = _context;
13
14
  }
14
15
 
15
16
  export function clearInject() {
16
17
  context = null
17
- }
18
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { RpgClientEngine } from "../RpgClientEngine";
3
+ import { inject } from "./inject";
4
+ import { startGame } from "./setup";
5
+
6
+ describe("startGame", () => {
7
+ test("initializes recursively nested RPGJS providers", async () => {
8
+ let engineStarted = false;
9
+
10
+ const context = await startGame({
11
+ providers: [
12
+ [
13
+ [
14
+ {
15
+ provide: "deep-provider",
16
+ useValue: 42,
17
+ },
18
+ ],
19
+ ],
20
+ {
21
+ provide: RpgClientEngine,
22
+ useValue: {
23
+ async start() {
24
+ engineStarted = true;
25
+ },
26
+ },
27
+ },
28
+ ],
29
+ });
30
+
31
+ expect(context.side).toBe("client");
32
+ expect(inject("deep-provider")).toBe(42);
33
+ expect(engineStarted).toBe(true);
34
+ });
35
+ });
package/src/core/setup.ts CHANGED
@@ -1,20 +1,22 @@
1
- import { Context, FactoryProvider, findProvider, findProviders, inject, injector, Providers } from "@signe/di";
1
+ import { Context, inject, injector, type Providers } from "@signe/di";
2
+ import type { RpgContext, RpgProviders } from "@rpgjs/common";
2
3
  import { RpgClientEngine } from "../RpgClientEngine";
3
4
  import { setInject } from "./inject";
4
5
 
5
6
  interface SetupOptions {
6
- providers: Providers;
7
+ providers: RpgProviders;
7
8
  }
8
9
 
9
10
 
10
- export async function startGame(options: SetupOptions) {
11
+ export async function startGame(options: SetupOptions): Promise<RpgContext> {
11
12
  const context = new Context();
12
13
  context['side'] = 'client'
13
- setInject(context);
14
+ setInject(context as unknown as RpgContext);
14
15
 
15
- await injector(context, options.providers);
16
+ const providers = (options.providers as unknown[]).flat(Infinity) as Providers;
17
+ await injector(context, providers);
16
18
 
17
19
  const engine = inject<RpgClientEngine>(context, RpgClientEngine);
18
20
  await engine.start();
19
- return context;
21
+ return context as unknown as RpgContext;
20
22
  }
package/src/index.ts CHANGED
@@ -22,10 +22,21 @@ export * from "./Sound";
22
22
  export * from "./Resource";
23
23
  export * from "./decorators/spritesheet";
24
24
  export * from "./utils/getEntityProp";
25
- export { Context } from "@signe/di";
26
25
  export { KeyboardControls, Input } from "canvasengine";
27
26
  export { Control } from "./services/keyboardControls";
28
27
  export { defineModule } from "@rpgjs/common";
28
+ export type {
29
+ RpgClassProvider,
30
+ RpgContext,
31
+ RpgExistingProvider,
32
+ RpgFactoryProvider,
33
+ RpgProvider,
34
+ RpgProviders,
35
+ RpgProviderToken,
36
+ RpgReadableSignal,
37
+ RpgValueProvider,
38
+ RpgWritableSignal,
39
+ } from "@rpgjs/common";
29
40
  export { RpgClientObject } from "./Game/Object";
30
41
  export { RpgClientPlayer } from "./Game/Player";
31
42
  export { RpgClientEvent } from "./Game/Event";
package/src/module.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { findModules, provideModules, registerI18nMessages } from "@rpgjs/common";
2
- import { FactoryProvider } from "@signe/di";
2
+ import type { RpgFactoryProvider } from "@rpgjs/common";
3
3
  import { RpgClientEngine } from "./RpgClientEngine";
4
4
  import { RpgClient } from "./RpgClient";
5
5
  import { inject } from "@signe/di";
@@ -48,7 +48,7 @@ export type RpgClientModule = RpgClient | (new () => any);
48
48
  * provideClientModules([MyClientModule])
49
49
  * ```
50
50
  */
51
- export function provideClientModules(modules: RpgClientModule[]): FactoryProvider {
51
+ export function provideClientModules(modules: RpgClientModule[]): RpgFactoryProvider {
52
52
  return provideModules(modules, "client", (modules, context) => {
53
53
  const mainModuleClient = findModules(context, 'Client')
54
54
  modules = [...mainModuleClient, ...modules]
@@ -0,0 +1,19 @@
1
+ import { describe, expectTypeOf, test } from "vitest";
2
+ import type { RpgContext, RpgProvider } from "@rpgjs/common";
3
+ import { provideMmorpg, startGame } from "./index";
4
+
5
+ describe("client public API types", () => {
6
+ test("bootstrap and providers expose RPGJS-owned contracts", () => {
7
+ expectTypeOf(provideMmorpg({})).toEqualTypeOf<RpgProvider[]>();
8
+ const assertions = (start: typeof startGame) => {
9
+ expectTypeOf(start({ providers: [] })).toEqualTypeOf<Promise<RpgContext>>();
10
+ };
11
+ expectTypeOf(assertions).toBeFunction();
12
+ });
13
+
14
+ test("the Signe Context class is not re-exported", () => {
15
+ // @ts-expect-error Context is available only from direct advanced Signe usage
16
+ type LegacyContext = typeof import("./index")["Context"];
17
+ expectTypeOf<LegacyContext>();
18
+ });
19
+ });
@@ -1,4 +1,4 @@
1
- import { Context } from "@signe/di";
1
+ import type { RpgContext } from "@rpgjs/common";
2
2
 
3
3
  export const WebSocketToken = "websocket";
4
4
 
@@ -14,7 +14,7 @@ export type WebSocketMode = "standalone" | "mmorpg";
14
14
  export abstract class AbstractWebsocket {
15
15
  readonly mode?: WebSocketMode;
16
16
 
17
- constructor(protected context: Context) {}
17
+ constructor(protected context: RpgContext) {}
18
18
 
19
19
  abstract connection(listeners?: (data: any) => void): Promise<void>;
20
20
  abstract emit(event: string, data: any): void;