@uzuhq/code-sdk 0.8.7 → 0.8.9

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/README.md CHANGED
@@ -296,6 +296,24 @@ changeRoom(null); // デフォルトルーム(全員が同じ音声チャン
296
296
  - 初期状態ではすべてのプレイヤーがデフォルトルーム(`null`)に所属する
297
297
  - `roomId` に文字列を指定すると、そのプレイヤーは指定ルームへ移動する
298
298
 
299
+ ### `finishGame(): void`
300
+
301
+ ゲームを終えて、この端末のプレイ画面を閉じる。ホストはイベントを終了扱いにし、呼んだプレイヤーを退出させる。
302
+
303
+ ```ts
304
+ import { finishGame } from '@uzuhq/code-sdk';
305
+
306
+ onState(state) {
307
+ if (state.debriefClosed) finishGame();
308
+ }
309
+ ```
310
+
311
+ - 結果や感想戦を見終えて閉じるときに呼ぶ。勝敗が決まった瞬間 (`gameover` など) に呼ぶと、結果を見る前に画面が閉じる
312
+ - 途中退出は UZU メニューの「退出」が担うので、ゲーム内の exit / quit ボタンには使わない
313
+ - 閉じるのは呼んだ端末だけ。全員を閉じたいときは、終了を表す state を全員へ配り、各端末で呼ぶ
314
+ - 再描画のたびに呼んでもよい (ホストは処理中の 2 回目以降を無視する)
315
+ - 対応していない古いアプリや `uzu dev` では何も起きないので、呼んだあとも画面を操作不能にしない
316
+
299
317
  ### `onPlayersChanged(handler: (players: Record<string, PlayerVoiceState>) => void): void`
300
318
 
301
319
  プレイヤーのリアルタイム状態(音声状態)が変化したときのハンドラを登録する。
package/dist/index.d.ts CHANGED
@@ -232,6 +232,27 @@ declare function firstFrameReady(): void;
232
232
  * ```
233
233
  */
234
234
  declare function gameReady(): void;
235
+ /**
236
+ * ゲームを終えてプレイ画面を閉じるよう Flutter ホストに通知する。
237
+ *
238
+ * 閉じるのは**呼んだ端末だけ**。全員を閉じたいときは、終了を表す state を全員へ配り、
239
+ * 各端末の `onState` から呼ぶ。ホストはイベントを終了扱いにし (finishedAt を立てるのは
240
+ * 最初の 1 人だけ)、呼んだプレイヤーを退出させてから画面を閉じる。
241
+ *
242
+ * 呼び出しタイミング: 結果や感想戦を見終えて閉じるときなど、シナリオとしてゲームが完全に
243
+ * 終わった瞬間。勝敗が決まった瞬間 (`gameover` など) に呼ぶと、結果を見る前に画面が閉じる。
244
+ *
245
+ * 対応していない古いアプリや `uzu dev` harness では何も起きない。呼んだあとも画面を
246
+ * 操作不能にしないこと。
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * onState(state) {
251
+ * if (state.debriefClosed) finishGame();
252
+ * }
253
+ * ```
254
+ */
255
+ declare function finishGame(): void;
235
256
  /** Flutter からの playersChanged メッセージを受信するハンドラを登録する。 */
236
257
  declare function onPlayersChanged(handler: PlayersChangedHandler): void;
237
258
  /**
@@ -245,4 +266,4 @@ declare function onPlayersChanged(handler: PlayersChangedHandler): void;
245
266
  */
246
267
  declare function run<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerActionMap<S> = Record<never, never>>(config: GameConfig<S, A, SA>): void;
247
268
  //#endregion
248
- export { type ActionArgs, type ActionContext, type ActionHandler, type ActionMap, type BridgeChannel, type BridgeMessage, type ConnectionCallbacks, type ConnectionState, DEFAULT_ICON_URLS, type Deadline, type DeadlineArgs, type DeadlineContext, type DevHooksCtx, type Duration, type Emit, type EventHandler, type EventSubscription, GAME_START, type GameConfig, type GameLogic, type GameTime, type Insets, type JsonMergePatch, type JsonPatchOp, type Operation, type PayloadMap, type PlayScreenMessage, type PlayerVoiceState, type PlayersChangedMessage, type PredictionWarning, type ReconnectableWSOptions, ReconnectableWebSocket, type RunHandle, type Seat, type SeatKind, type SeededRandom, SeededRandomImpl, type SendAction, type ServerActionArgs, type ServerActionContext, type ServerActionHandler, type ServerActionMap, type ServerEvent, type ServerOnlyAction, type ServerOnlyActionContext, type ServerOnlyActionHandlerFn, type SetupArgs, type SetupContext, type UpdateArgs, type UpdateContext, type UzuDevHooks, type UzuInsets, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
269
+ export { type ActionArgs, type ActionContext, type ActionHandler, type ActionMap, type BridgeChannel, type BridgeMessage, type ConnectionCallbacks, type ConnectionState, DEFAULT_ICON_URLS, type Deadline, type DeadlineArgs, type DeadlineContext, type DevHooksCtx, type Duration, type Emit, type EventHandler, type EventSubscription, GAME_START, type GameConfig, type GameLogic, type GameTime, type Insets, type JsonMergePatch, type JsonPatchOp, type Operation, type PayloadMap, type PlayScreenMessage, type PlayerVoiceState, type PlayersChangedMessage, type PredictionWarning, type ReconnectableWSOptions, ReconnectableWebSocket, type RunHandle, type Seat, type SeatKind, type SeededRandom, SeededRandomImpl, type SendAction, type ServerActionArgs, type ServerActionContext, type ServerActionHandler, type ServerActionMap, type ServerEvent, type ServerOnlyAction, type ServerOnlyActionContext, type ServerOnlyActionHandlerFn, type SetupArgs, type SetupContext, type UpdateArgs, type UpdateContext, type UzuDevHooks, type UzuInsets, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, finishGame, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
package/dist/index.js CHANGED
@@ -1301,9 +1301,43 @@ function onInsetsChange(cb) {
1301
1301
  };
1302
1302
  }
1303
1303
  //#endregion
1304
+ //#region src/scenario-callback.ts
1305
+ /**
1306
+ * @docs
1307
+ * - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
1308
+ *
1309
+ * SDK からシナリオのコード (描画 / イベント購読 / bridge の購読) を呼ぶときの例外境界。
1310
+ * シナリオのコールバックを呼ぶ箇所は、すべてここを通す。
1311
+ *
1312
+ * これらは state 配信やメッセージ受信のたびに同じ経路を通る。例外をそのまま上へ通すと
1313
+ * 呼び出し元の state 適用 (confirmedState 更新 / ack 処理 / seq 前進) が途中で止まり、
1314
+ * 以降の配信でも同じ経路で投げ続ける。WebSocket もホストアプリも生きたまま画面だけが
1315
+ * 永久に更新されなくなる — 2026-09-09 の UZU TOKYO 公演で実際に起きた壊れ方がこれ。
1316
+ *
1317
+ * ここで断ち切れば state 適用は最後まで走り、次の配信で描画がやり直される
1318
+ * (onState は毎回フル state から再実行されるので冪等)。同じ理由で、購読が複数ある
1319
+ * ところでは 1 件の失敗を他の購読へ波及させない。
1320
+ */
1321
+ /**
1322
+ * シナリオ側のコールバックを実行し、例外を境界で止める。
1323
+ *
1324
+ * 例外は握り潰さず `console.error` に出す。ホスト側のログ収集がこれを拾う。
1325
+ */
1326
+ const callScenario = (label, fn) => {
1327
+ try {
1328
+ fn();
1329
+ } catch (err) {
1330
+ console.error(`[uzu] シナリオの ${label} で例外が発生しました (処理は継続します)`, err);
1331
+ }
1332
+ };
1333
+ //#endregion
1304
1334
  //#region src/run/optimistic-action-client.ts
1305
1335
  function createOptimisticActionClient(config) {
1306
- const { logic, playerId, onState, events, sendAction } = config;
1336
+ const { logic, playerId, events, sendAction } = config;
1337
+ /** 呼び出し箇所ごとに守らず、シナリオの描画へ出る唯一の口をここで塞ぐ。 */
1338
+ const onState = (state, id) => {
1339
+ callScenario("描画 (onState)", () => config.onState(state, id));
1340
+ };
1307
1341
  /** サーバー確定 state (楽観的更新のベース) */
1308
1342
  let confirmedState = null;
1309
1343
  /** 表示用 state (pending actions 適用済み) */
@@ -1336,7 +1370,11 @@ function createOptimisticActionClient(config) {
1336
1370
  /** 再適用時はイベントを発火しない (送信時に既に発火済み) */
1337
1371
  const noopEmit = () => {};
1338
1372
  const dispatchEvents = (evts) => {
1339
- for (const e of evts) events?.[e.name]?.handler(e.data);
1373
+ for (const e of evts) {
1374
+ const handler = events?.[e.name]?.handler;
1375
+ if (!handler) continue;
1376
+ callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1377
+ }
1340
1378
  };
1341
1379
  /** events の同一性キー。name と data が一致すれば「同じ出来事」とみなす。 */
1342
1380
  const eventKey = (e) => `${e.name}\u0000${JSON.stringify(e.data ?? {})}`;
@@ -1423,7 +1461,7 @@ function createOptimisticActionClient(config) {
1423
1461
  const subscription = events?.[eventName];
1424
1462
  if (!subscription?.predict) return;
1425
1463
  const payloadData = data ?? {};
1426
- subscription.handler(payloadData);
1464
+ callScenario(`event ハンドラ "${eventName}"`, () => subscription.handler(payloadData));
1427
1465
  firedPredictions.push({
1428
1466
  seq,
1429
1467
  name: eventName,
@@ -1448,7 +1486,9 @@ function createOptimisticActionClient(config) {
1448
1486
  time
1449
1487
  });
1450
1488
  onState(target, playerId);
1451
- } catch {}
1489
+ } catch (err) {
1490
+ console.warn(`[uzu] action "${type}" の先読み実行で例外`, err);
1491
+ }
1452
1492
  sendAction({
1453
1493
  action: type,
1454
1494
  payload: payload ?? {},
@@ -1552,13 +1592,7 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1552
1592
  config.inputs((type, payload) => {
1553
1593
  client.send(type, payload);
1554
1594
  });
1555
- ws.addEventListener("message", (ev) => {
1556
- let parsed;
1557
- try {
1558
- parsed = JSON.parse(ev.data);
1559
- } catch {
1560
- return;
1561
- }
1595
+ const handleMessage = (parsed) => {
1562
1596
  const msgType = parsed.type;
1563
1597
  console.log(`[SDK ServerAction] ⬅ recv type=${msgType}`);
1564
1598
  client.observeGameTime(parsed.gameTime);
@@ -1649,6 +1683,21 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1649
1683
  client.reset(parsed.state);
1650
1684
  return;
1651
1685
  }
1686
+ };
1687
+ ws.addEventListener("message", (ev) => {
1688
+ let parsed;
1689
+ try {
1690
+ parsed = JSON.parse(ev.data);
1691
+ } catch {
1692
+ return;
1693
+ }
1694
+ if (parsed === null || typeof parsed !== "object") return;
1695
+ const msg = parsed;
1696
+ try {
1697
+ handleMessage(msg);
1698
+ } catch (err) {
1699
+ console.error(`[SDK ServerAction] ❌ message 処理で例外 type=${String(msg.type)}`, err);
1700
+ }
1652
1701
  });
1653
1702
  }
1654
1703
  //#endregion
@@ -1657,7 +1706,7 @@ function runLocalServerAction(config) {
1657
1706
  const { logic, inputs, events } = config;
1658
1707
  const onState = (next, id) => {
1659
1708
  observeGameTime(gameTime());
1660
- config.onState(next, id, "player");
1709
+ callScenario("描画 (onState)", () => config.onState(next, id, "player"));
1661
1710
  };
1662
1711
  const tickRate = logic.tickRate ?? 0;
1663
1712
  const clock = new GameClock();
@@ -1744,7 +1793,11 @@ function runLocalServerAction(config) {
1744
1793
  wakeupTimer = setTimeout(fireDue, Math.max(0, clock.toWall(next) - Date.now()));
1745
1794
  };
1746
1795
  const dispatchEvents = (evts) => {
1747
- for (const e of evts) events?.[e.name]?.handler(e.data);
1796
+ for (const e of evts) {
1797
+ const handler = events?.[e.name]?.handler;
1798
+ if (!handler) continue;
1799
+ callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1800
+ }
1748
1801
  };
1749
1802
  clock.restart();
1750
1803
  const setupTime = gameTime();
@@ -2022,6 +2075,29 @@ function firstFrameReady() {
2022
2075
  function gameReady() {
2023
2076
  sendRaw("sdk", "gameReady", {});
2024
2077
  }
2078
+ /**
2079
+ * ゲームを終えてプレイ画面を閉じるよう Flutter ホストに通知する。
2080
+ *
2081
+ * 閉じるのは**呼んだ端末だけ**。全員を閉じたいときは、終了を表す state を全員へ配り、
2082
+ * 各端末の `onState` から呼ぶ。ホストはイベントを終了扱いにし (finishedAt を立てるのは
2083
+ * 最初の 1 人だけ)、呼んだプレイヤーを退出させてから画面を閉じる。
2084
+ *
2085
+ * 呼び出しタイミング: 結果や感想戦を見終えて閉じるときなど、シナリオとしてゲームが完全に
2086
+ * 終わった瞬間。勝敗が決まった瞬間 (`gameover` など) に呼ぶと、結果を見る前に画面が閉じる。
2087
+ *
2088
+ * 対応していない古いアプリや `uzu dev` harness では何も起きない。呼んだあとも画面を
2089
+ * 操作不能にしないこと。
2090
+ *
2091
+ * @example
2092
+ * ```ts
2093
+ * onState(state) {
2094
+ * if (state.debriefClosed) finishGame();
2095
+ * }
2096
+ * ```
2097
+ */
2098
+ function finishGame() {
2099
+ sendRaw("sdk", "finishGame", {});
2100
+ }
2025
2101
  /** Flutter からの playersChanged メッセージを受信するハンドラを登録する。 */
2026
2102
  function onPlayersChanged(handler) {
2027
2103
  _playersChangedHandlers.push(handler);
@@ -2118,7 +2194,7 @@ function handleMessage(msg) {
2118
2194
  return;
2119
2195
  case "playersChanged": {
2120
2196
  const players = payload?.players ?? {};
2121
- _playersChangedHandlers.forEach((fn) => fn(players));
2197
+ for (const fn of _playersChangedHandlers) callScenario("playersChanged ハンドラ", () => fn(players));
2122
2198
  return;
2123
2199
  }
2124
2200
  case "requestPause":
@@ -2138,9 +2214,9 @@ function handleMessage(msg) {
2138
2214
  if (channel === "game") {
2139
2215
  const h = gameHandlers.get(type) || [];
2140
2216
  console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
2141
- h.forEach((fn) => fn(payload ?? {}));
2217
+ for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload ?? {}));
2142
2218
  return;
2143
2219
  }
2144
2220
  }
2145
2221
  //#endregion
2146
- export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
2222
+ export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, finishGame, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-sdk",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "UZU PlayScreen SDK - Flutter ↔ JS ゲーム通信ライブラリ",
5
5
  "type": "module",
6
6
  "exports": {