@uzuhq/code-sdk 0.7.5 → 0.7.6

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/dist/index.js CHANGED
@@ -323,17 +323,19 @@ function resolveSeatKind(params) {
323
323
  /**
324
324
  * 自席の ID と roster を URL から取り出す。
325
325
  *
326
- * roster (`?seats=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
326
+ * roster (`?players=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
327
327
  * 載らないまま接続してくる。
328
+ *
329
+ * 移行期: 旧ホスト (デプロイ前の mobile / uzutokyo / emulator) は `?seats=` で送ってくる。
328
330
  */
329
331
  function resolveSeatParams(params) {
330
332
  const seatId = params.get('seatId');
331
333
  if (!seatId) {
332
334
  throw new Error('[UZU SDK] seatId is required. Pass ?seatId=xxx in the URL.');
333
335
  }
334
- const json = params.get('seats');
336
+ const json = params.get('players') ?? params.get('seats');
335
337
  if (!json) {
336
- throw new Error('[UZU SDK] seats is required. Pass ?seats=[...] in the URL.');
338
+ throw new Error('[UZU SDK] players is required. Pass ?players=[...] in the URL.');
337
339
  }
338
340
  const raw = JSON.parse(json);
339
341
  const players = raw.map((p) => ({
@@ -448,7 +450,7 @@ const ACTION_ITEM_WIDTH = 38;
448
450
  function calcHudInsets(params) {
449
451
  const y = HUD_TOP + UZU_BUTTON_SIZE;
450
452
  let actionCount = 2; // デフォルト: 安全側 (マイク + チャット想定)
451
- const json = params.get('seats');
453
+ const json = params.get('players') ?? params.get('seats');
452
454
  if (json) {
453
455
  try {
454
456
  const raw = JSON.parse(json);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * URL の roster パラメータ (`?players=` / 旧名 `?seats=`) の解決を固定する unit test。
3
+ *
4
+ * ホスト (mobile / uzutokyo / emulator / dev harness) と SDK の間はデプロイが揃わない。
5
+ * 新旧どちらの名前で渡されても run() が起動し、GameRoom へは新名で送り直すところまでを
6
+ * 1 本で見る。旧名しか読めない SDK / 旧名しか送らないホストが同時に居るのが移行期の前提。
7
+ */
8
+ import { afterEach, describe, expect, it, vi } from 'vitest';
9
+ const logic = {
10
+ setup: () => ({ moves: 0 }),
11
+ actions: {},
12
+ update: () => { },
13
+ };
14
+ const roster = [
15
+ { id: 'p1', nickname: 'P1', iconUrl: '' },
16
+ { id: 'p2', nickname: 'P2', iconUrl: '' },
17
+ ];
18
+ const wire = (seats) => JSON.stringify(seats.map((s) => ({ id: s.id, name: s.nickname, iconUrl: s.iconUrl })));
19
+ /** `new WebSocket(url)` を捕まえるスタブ。接続先 URL だけ見る。 */
20
+ const stubWebSocket = () => {
21
+ const urls = [];
22
+ class FakeWebSocket {
23
+ constructor(url) {
24
+ this.url = url;
25
+ this.onopen = null;
26
+ this.onclose = null;
27
+ this.onerror = null;
28
+ this.onmessage = null;
29
+ this.readyState = FakeWebSocket.OPEN;
30
+ urls.push(url);
31
+ }
32
+ send() { }
33
+ close() { }
34
+ }
35
+ FakeWebSocket.OPEN = 1;
36
+ vi.stubGlobal('WebSocket', FakeWebSocket);
37
+ return urls;
38
+ };
39
+ /**
40
+ * 指定のクエリで SDK を読み込み直して `run()` まで通す。
41
+ *
42
+ * `isHosted` は module 評価時に一度だけ決まるので、`FlutterHost` を先に生やしてから
43
+ * import しないと run() が即 return して何も起きない。
44
+ */
45
+ const runWithQuery = async (query) => {
46
+ vi.resetModules();
47
+ vi.stubGlobal('FlutterHost', { postMessage: () => { } });
48
+ const urls = stubWebSocket();
49
+ window.history.replaceState({}, '', `/?${query}`);
50
+ const sdk = await import('./index.js');
51
+ sdk.run({ logic, playerCount: 2, onState: () => { }, inputs: () => { } });
52
+ return urls;
53
+ };
54
+ const baseQuery = 'roomId=room1&server=ws://localhost:1234&revisionId=rev1&seatId=p1';
55
+ afterEach(() => {
56
+ vi.unstubAllGlobals();
57
+ });
58
+ describe('roster パラメータの解決', () => {
59
+ /** 新ホストは `players=` で送る。 */
60
+ it('players から roster を読む', async () => {
61
+ const urls = await runWithQuery(`${baseQuery}&players=${encodeURIComponent(wire(roster))}`);
62
+ const sent = new URL(urls[0]).searchParams.get('players');
63
+ expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
64
+ });
65
+ /**
66
+ * 移行期の互換措置。デプロイ前のホストは旧名でしか送ってこない。読めないと run() の
67
+ * 入口で throw してシナリオが起動しない。
68
+ */
69
+ it('players が無ければ旧名 seats に落ちる', async () => {
70
+ const urls = await runWithQuery(`${baseQuery}&seats=${encodeURIComponent(wire(roster))}`);
71
+ const sent = new URL(urls[0]).searchParams.get('players');
72
+ expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
73
+ });
74
+ /** 移行期は両方載せたホストが居る。新名を優先し、旧名は fallback にしか使わない。 */
75
+ it('両方あれば players を採る', async () => {
76
+ const old = [{ id: 'old1', nickname: 'Old1', iconUrl: '' }];
77
+ const urls = await runWithQuery(`${baseQuery}&players=${encodeURIComponent(wire(roster))}` +
78
+ `&seats=${encodeURIComponent(wire(old))}`);
79
+ const sent = new URL(urls[0]).searchParams.get('players');
80
+ expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
81
+ });
82
+ /** roster がどちらの名前でも無ければ、黙って空 roster で始めず入口で落とす。 */
83
+ it('どちらも無ければ throw する', async () => {
84
+ await expect(runWithQuery(baseQuery)).rejects.toThrow(/players is required/);
85
+ });
86
+ });
@@ -10,10 +10,14 @@ export function runOnlineServerAction(config, gameEndpoint, roomId, seatId, play
10
10
  characterId: p.characterId,
11
11
  kind: 'player',
12
12
  });
13
+ const roster = JSON.stringify(players.map(toWire));
13
14
  const query = new URLSearchParams({
14
15
  seatId,
15
16
  nickname: 'Player',
16
- seats: JSON.stringify(players.map(toWire)),
17
+ players: roster,
18
+ // 移行期: play-server / dev-server が `players=` を読めるようになる前の revision へ
19
+ // 繋ぐ経路が残る (JIT 再デプロイ前の焼き込み済みテンプレなど)。旧名も併せて送る。
20
+ seats: roster,
17
21
  });
18
22
  const wsUrl = `${gameEndpoint}/${roomId}?${query}`;
19
23
  console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
@@ -74,13 +74,22 @@ describe('runOnlineServerAction の seatKind', () => {
74
74
  expect(url.searchParams.get('seatId')).toBe('admin_0');
75
75
  });
76
76
  /**
77
- * roster (`?seats=`) は player 席だけ。観測者は自分が載っていない roster を送る。
77
+ * roster (`?players=`) は player 席だけ。観測者は自分が載っていない roster を送る。
78
78
  */
79
79
  it('roster には自分が居なくてよい', () => {
80
80
  const { sockets } = connect('admin', 'admin_0');
81
- const raw = JSON.parse(new URL(sockets[0].url).searchParams.get('seats') ?? '[]');
81
+ const raw = JSON.parse(new URL(sockets[0].url).searchParams.get('players') ?? '[]');
82
82
  expect(raw.map((s) => s.id)).toEqual(['p1', 'p2']);
83
83
  });
84
+ /**
85
+ * 移行期の互換措置。`players=` しか送らないと、JIT 再デプロイ前の焼き込み済み
86
+ * テンプレを持つ部屋が roster を pin できず起動しない。旧名にも同じ JSON を載せる。
87
+ */
88
+ it('旧名 seats にも同じ roster を載せる', () => {
89
+ const { sockets } = connect('player');
90
+ const params = new URL(sockets[0].url).searchParams;
91
+ expect(params.get('seats')).toBe(params.get('players'));
92
+ });
84
93
  /** シナリオが GM ビューと観戦ビューを分けられるよう、自分の席種別だけを渡す。 */
85
94
  it('onState の第 3 引数に自分の seatKind を渡す', () => {
86
95
  const { sockets, onState } = connect('admin', 'admin_0');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-sdk",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "UZU PlayScreen SDK - Flutter ↔ JS ゲーム通信ライブラリ",
5
5
  "type": "module",
6
6
  "exports": {