@crowdedkingdoms/construct 0.3.9-dev.1 → 0.3.10-test.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/construct",
3
- "version": "0.3.9-dev.1",
3
+ "version": "0.3.10-test.1",
4
4
  "description": "The Construct framework: the Crowded Kingdoms game platform layer (hosted sign-in, World Stores, voice/webcam/chat, Crowdy Studio dock, app and grid mods, grid programs) and engine loop, shared by the-construct starter and the games built on it. Ships TypeScript source for Vite.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  const {
4
4
  DEFAULT_VOXEL_STATE,
@@ -7,6 +7,7 @@ const {
7
7
  bytesToBase64,
8
8
  parseVoxelSetArgs,
9
9
  routeClientHostCall,
10
+ routeWithFallback,
10
11
  voxelStateToWire,
11
12
  voxelsFromBase64,
12
13
  voxelsListRows,
@@ -144,6 +145,23 @@ describe('routeClientHostCall', () => {
144
145
  }
145
146
  });
146
147
 
148
+ it("hands an SDK call to the host's server calls while no session is bound", async () => {
149
+ const { bindModSession } = await import('./modChunkRuntime');
150
+ bindModSession(null);
151
+ const r = reads();
152
+ for (const fn of ['emit_channel', 'channel_join', 'sessions_list']) {
153
+ const call = { fn, args: { channelId: '5' } } as never;
154
+ await expect(routeClientHostCall(call, r.reads, grid)).rejects.toBeInstanceOf(
155
+ HostCallRefusedError,
156
+ );
157
+ const serverCalls = vi.fn(async () => ({ answered: fn }));
158
+ await expect(
159
+ routeWithFallback(call, () => routeClientHostCall(call, r.reads, grid), serverCalls),
160
+ ).resolves.toEqual({ answered: fn });
161
+ expect(serverCalls).toHaveBeenCalledWith(call);
162
+ }
163
+ });
164
+
147
165
  it('drains pointer_clicks when the game supplies an input sink', async () => {
148
166
  const r = reads();
149
167
  await expect(
@@ -13,7 +13,7 @@
13
13
  * the same route Paint uses. Never hand a mod the raw client.
14
14
  */
15
15
  import type { PlayerCodeGridBounds, PlayerCodeHostCall } from '@crowdedkingdoms/crowdyjs';
16
- import { routeModGameplay } from './modChunkRuntime';
16
+ import { NO_SESSION, routeModGameplay } from './modChunkRuntime';
17
17
  import type { ModSceneStore } from './modScene';
18
18
 
19
19
  export interface ClientVoxelState {
@@ -188,13 +188,14 @@ export async function routeClientHostCall(
188
188
  return scene.store.setInstances(scene.source, args);
189
189
  default: {
190
190
  const answered = await routeModGameplay(call);
191
- if (
192
- answered &&
193
- typeof answered === 'object' &&
194
- 'error' in answered &&
195
- String((answered as { error: unknown }).error).startsWith('unrouted')
196
- ) {
197
- throw new HostCallRefusedError(fn);
191
+ if (answered && typeof answered === 'object' && 'error' in answered) {
192
+ // A call the mod runtime does not route, or an SDK call in a game that never binds a
193
+ // session (one without StudioService), is the host's to answer: `routeWithFallback`
194
+ // hands a refusal to its server calls.
195
+ const error = String((answered as { error: unknown }).error);
196
+ if (error.startsWith('unrouted') || error === NO_SESSION) {
197
+ throw new HostCallRefusedError(fn);
198
+ }
198
199
  }
199
200
  return answered;
200
201
  }
@@ -429,6 +429,9 @@ async function restoreAvatar(): Promise<void> {
429
429
  .catch(() => undefined);
430
430
  }
431
431
 
432
+ /** The answer to an SDK call while no session is bound; the host's router hands such a call on. */
433
+ export const NO_SESSION = 'no session';
434
+
432
435
  const PASS_SDK = new Set([
433
436
  'actors_create',
434
437
  'actors_update',
@@ -494,7 +497,7 @@ const PASS_SDK = new Set([
494
497
 
495
498
  async function passSdk(fn: string, args: Record<string, unknown>): Promise<unknown> {
496
499
  if (!PASS_SDK.has(fn)) return { ok: false, error: `unrouted host call ${fn}` };
497
- if (!port) return { ok: false, error: 'no session' };
500
+ if (!port) return { ok: false, error: NO_SESSION };
498
501
  const client = port.client;
499
502
  switch (fn) {
500
503
  case 'actors_create':