@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 +18 -0
- package/dist/index.d.ts +22 -1
- package/dist/index.js +92 -16
- package/package.json +1 -1
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,
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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 };
|