@uzuhq/code-sdk 0.8.3 → 0.8.5
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 +17 -162
- package/dist/dev-globals.d.ts +2 -2
- package/dist/{dev-hooks-DOtJHP55.d.ts → dev-hooks-DmOV2p0j.d.ts} +2 -51
- package/dist/index.d.ts +16 -31
- package/dist/index.js +10 -336
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,24 +25,6 @@ SDK チャネルのメッセージは `playSound()`, `setMicEnabled()` などの
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
## 3 つのパラダイム
|
|
29
|
-
|
|
30
|
-
SDK は用途に応じて 3 つの API パターンを提供する。
|
|
31
|
-
|
|
32
|
-
| パラダイム | API | state 管理 | 向いているゲーム |
|
|
33
|
-
| ---------- | --------------------- | ---------------------------------- | ---------------------------- |
|
|
34
|
-
| **Relay** | `init()` + `onRoom()` | ゲーム側の責務 | 独自プロトコルが必要なゲーム |
|
|
35
|
-
| **sync()** | `sync()` | SDK + サーバーが JSON Patch で同期 | ターン制・ボードゲーム |
|
|
36
|
-
| **run()** | `run()` | SDK + reducer がサーバーで逐次実行 | リアルタイム・アクション全般 |
|
|
37
|
-
|
|
38
|
-
### 選択基準
|
|
39
|
-
|
|
40
|
-
- 同時操作で同じリソースを奪い合う → **`run()`**(reducer が最新 state に対して逐次実行、条件保証あり)
|
|
41
|
-
- 各プレイヤーが自分の領域だけ変更 → **`sync()`** で十分
|
|
42
|
-
- 独自のメッセージプロトコルが必要 → **Relay**
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
28
|
## 初期化
|
|
47
29
|
|
|
48
30
|
### `init(): void`
|
|
@@ -54,132 +36,19 @@ import { init } from '@uzuhq/code-sdk';
|
|
|
54
36
|
init();
|
|
55
37
|
```
|
|
56
38
|
|
|
57
|
-
- `run()`
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## パターン 1: Relay
|
|
63
|
-
|
|
64
|
-
### `onRoom(callback: (room: RoomLike) => void): void`
|
|
65
|
-
|
|
66
|
-
Room に接続されたときのコールバックを登録する。
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
import { init, onRoom } from '@uzuhq/code-sdk';
|
|
70
|
-
|
|
71
|
-
init();
|
|
72
|
-
|
|
73
|
-
onRoom((room) => {
|
|
74
|
-
console.log('My ID:', room.myId);
|
|
75
|
-
|
|
76
|
-
room.on('attack', (msg) => {
|
|
77
|
-
console.log(`${msg.__from} sent ${msg.lines} lines`);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
room.broadcast({ type: 'attack', lines: 2 });
|
|
81
|
-
room.send(targetId, { type: 'whisper', text: 'hello' });
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Room API
|
|
86
|
-
|
|
87
|
-
| メソッド/プロパティ | 型 | 説明 |
|
|
88
|
-
| ------------------------ | ------------------------------------------------------ | -------------------------------------------------------- |
|
|
89
|
-
| `room.myId` | `string` (readonly) | 自分の playerId |
|
|
90
|
-
| `room.broadcast(msg)` | `(msg: Record<string, unknown>) => void` | 自分以外の全員に送信 |
|
|
91
|
-
| `room.send(id, msg)` | `(id: string, msg: Record<string, unknown>) => void` | 特定プレイヤーに送信 |
|
|
92
|
-
| `room.on(type, handler)` | `(type: string, handler: (data: any) => void) => void` | メッセージハンドラ登録。受信データに `__from` が含まれる |
|
|
39
|
+
- `run()` が内部で呼ぶので、通常は直接書かなくてよい
|
|
40
|
+
- マルチプレイの state 同期を使わない 1 人用シナリオ (サウンド・ボイス・safe area だけ使う)
|
|
41
|
+
では、これだけを最初に呼ぶ
|
|
93
42
|
|
|
94
43
|
---
|
|
95
44
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
### `sync<S>(config: SyncConfig<S>): void`
|
|
99
|
-
|
|
100
|
-
JSON Patch ベースの状態同期を開始する。
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
import { sync, SERVER_TIME } from '@uzuhq/code-sdk';
|
|
104
|
-
import type { Seat } from '@uzuhq/code-sdk';
|
|
105
|
-
|
|
106
|
-
sync<GameState>({
|
|
107
|
-
playerCount: 2,
|
|
108
|
-
|
|
109
|
-
initialState(players: Seat[]) {
|
|
110
|
-
return { board: createBoard(8), currentPlayer: players[0].id };
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
onState(state, myPlayerId, serverTime) {
|
|
114
|
-
currentState = state;
|
|
115
|
-
render();
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
inputs(patch, set) {
|
|
119
|
-
canvas.addEventListener('click', (e) => {
|
|
120
|
-
const { row, col } = getCellFromClick(e);
|
|
121
|
-
// 複数操作をまとめて送信
|
|
122
|
-
patch([
|
|
123
|
-
{ op: 'replace', path: `/board/${row}/${col}`, value: myPlayerId },
|
|
124
|
-
{ op: 'replace', path: '/lastMoveAt', value: SERVER_TIME },
|
|
125
|
-
]);
|
|
126
|
-
// 単一値のショートカット
|
|
127
|
-
set('/currentPlayer', getNextPlayer());
|
|
128
|
-
});
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
connection: {
|
|
132
|
-
onConnectionStateChange(state) {
|
|
133
|
-
console.log('Connection:', state);
|
|
134
|
-
},
|
|
135
|
-
onPatchFailed(error) {
|
|
136
|
-
console.error('Patch failed:', error);
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### SyncConfig
|
|
143
|
-
|
|
144
|
-
| キー | 型 | 必須 | 説明 |
|
|
145
|
-
| -------------- | ------------------------------------------------------------ | ---- | ---------------------------- |
|
|
146
|
-
| `playerCount` | `number` | Yes | プレイヤー数 |
|
|
147
|
-
| `initialState` | `(players: Seat[]) => S` | Yes | 初期 state を生成 |
|
|
148
|
-
| `onState` | `(state: S, myPlayerId: string, serverTime: number) => void` | Yes | state 更新時のコールバック |
|
|
149
|
-
| `inputs` | `(patch: PatchFn, set: SetFn) => void` | Yes | 入力ハンドラ登録 |
|
|
150
|
-
| `events` | `Record<string, (data: Record<string, unknown>) => void>` | No | ゲームイベントハンドラ |
|
|
151
|
-
| `connection` | `ConnectionCallbacks` | No | 接続状態・エラーコールバック |
|
|
152
|
-
|
|
153
|
-
### PatchFn / SetFn
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
type PatchFn = (ops: Operation[]) => void; // 複数操作をまとめて送信
|
|
157
|
-
type SetFn = (path: string, value: unknown) => void; // 単一パスの replace ショートカット
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Operation (JSON Patch)
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
interface Operation {
|
|
164
|
-
op: 'replace' | 'add' | 'remove';
|
|
165
|
-
path: string; // JSON Pointer パス (例: '/players/alice/score')
|
|
166
|
-
value?: unknown; // 値。SERVER_TIME sentinel 使用可
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### 楽観的更新
|
|
171
|
-
|
|
172
|
-
`sync()` は送信した patch をローカルに即座に適用する。サーバーからの権威的な state を受信すると上書きする。
|
|
173
|
-
|
|
174
|
-
**注意**: `sync()` は patch の条件を検証しない。同時に同じパスを変更すると後勝ちになる。条件付きの状態遷移が必要なら `run()` を使う。
|
|
175
|
-
|
|
176
|
-
---
|
|
177
|
-
|
|
178
|
-
## パターン 3: run()
|
|
45
|
+
## ゲームループ
|
|
179
46
|
|
|
180
47
|
### `run<S>(config: GameConfig<S>): void`
|
|
181
48
|
|
|
182
|
-
|
|
49
|
+
ゲームループを開始する。state はサーバー (GameRoom DO) が持ち、送られた action を
|
|
50
|
+
reducer で 1 件ずつ検証・実行して結果を全員へ配る。reducer は常にその時点の最新 state に
|
|
51
|
+
対して走るので、同時操作で条件が壊れることがない。
|
|
183
52
|
|
|
184
53
|
```ts
|
|
185
54
|
import { run } from '@uzuhq/code-sdk';
|
|
@@ -206,15 +75,14 @@ run({
|
|
|
206
75
|
|
|
207
76
|
### GameConfig
|
|
208
77
|
|
|
209
|
-
| キー | 型 | 必須 | 説明
|
|
210
|
-
| ------------------------- | -------------------------------------------------------------- | ---- |
|
|
211
|
-
| `logic` | `GameLogic<S>` | Yes | ゲームロジック定義
|
|
212
|
-
| `onState` | `(state: S, myPlayerId: string) => void` | Yes | state 更新時のコールバック
|
|
213
|
-
| `inputs` | `(sendAction: (action: string, payload: any) => void) => void` | Yes | 入力ハンドラ登録
|
|
214
|
-
| `events` | `Record<string, (data: any) => void>` | No | ゲームイベントハンドラ
|
|
215
|
-
| `playerCount` | `number` | Yes | プレイヤー数
|
|
216
|
-
| `onConnectionStateChange` | `(state: ConnectionState) => void` | No | 接続状態変化コールバック
|
|
217
|
-
| `onPatchFailed` | `(reason: string) => void` | No | サーバー patch 適用失敗コールバック |
|
|
78
|
+
| キー | 型 | 必須 | 説明 |
|
|
79
|
+
| ------------------------- | -------------------------------------------------------------- | ---- | -------------------------- |
|
|
80
|
+
| `logic` | `GameLogic<S>` | Yes | ゲームロジック定義 |
|
|
81
|
+
| `onState` | `(state: S, myPlayerId: string) => void` | Yes | state 更新時のコールバック |
|
|
82
|
+
| `inputs` | `(sendAction: (action: string, payload: any) => void) => void` | Yes | 入力ハンドラ登録 |
|
|
83
|
+
| `events` | `Record<string, (data: any) => void>` | No | ゲームイベントハンドラ |
|
|
84
|
+
| `playerCount` | `number` | Yes | プレイヤー数 |
|
|
85
|
+
| `onConnectionStateChange` | `(state: ConnectionState) => void` | No | 接続状態変化コールバック |
|
|
218
86
|
|
|
219
87
|
### GameLogic
|
|
220
88
|
|
|
@@ -479,17 +347,6 @@ on('attack', (payload) => {
|
|
|
479
347
|
|
|
480
348
|
---
|
|
481
349
|
|
|
482
|
-
## SERVER_TIME
|
|
483
|
-
|
|
484
|
-
サーバー時刻 sentinel 定数。Patch の `value` に指定すると、サーバー側で `Date.now()` に自動置換される。
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
import { SERVER_TIME } from '@uzuhq/code-sdk';
|
|
488
|
-
set('/meta/phaseStartedAt', SERVER_TIME);
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
---
|
|
492
|
-
|
|
493
350
|
## 型定義
|
|
494
351
|
|
|
495
352
|
### BridgeMessage
|
|
@@ -554,7 +411,6 @@ type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnect
|
|
|
554
411
|
```ts
|
|
555
412
|
interface ConnectionCallbacks {
|
|
556
413
|
onConnectionStateChange?: (state: ConnectionState) => void;
|
|
557
|
-
onPatchFailed?: (error: string) => void;
|
|
558
414
|
}
|
|
559
415
|
```
|
|
560
416
|
|
|
@@ -578,7 +434,7 @@ interface PlayerVoiceState {
|
|
|
578
434
|
|
|
579
435
|
## URL パラメータ
|
|
580
436
|
|
|
581
|
-
`init()` / `run()`
|
|
437
|
+
`init()` / `run()` は以下の URL パラメータを読み取る。
|
|
582
438
|
|
|
583
439
|
| パラメータ | 説明 |
|
|
584
440
|
| ----------- | --------------------------------------------- |
|
|
@@ -604,7 +460,6 @@ interface PlayerVoiceState {
|
|
|
604
460
|
|
|
605
461
|
- `run()` の devHarness → **親 frame だけ** (`page.mainFrame()`)
|
|
606
462
|
- `runLocalServerAction` (ソロモード) 単独 frame → その frame
|
|
607
|
-
- sync 系単独 frame → その frame
|
|
608
463
|
- devHarness の **子 iframe には attach しない**
|
|
609
464
|
|
|
610
465
|
```ts
|
|
@@ -653,7 +508,7 @@ await window.__uzu_dev.waitForSnapshot((s) => s.self.isReady === true);
|
|
|
653
508
|
- 部分更新は `mergeRawState`、array 要素単体の書換は `patchRawState` を使う
|
|
654
509
|
- `mergeRawState` で array field に non-array object patch を当てると **throw** する (array が pure object に化けるのを構造的に防止)
|
|
655
510
|
- online ServerAction では 3 API すべて `undefined`
|
|
656
|
-
- `send` は run devHarness 親 frame のみ。非 parent モード (runLocal / emulator
|
|
511
|
+
- `send` は run devHarness 親 frame のみ。非 parent モード (runLocal / emulator) では scenario 側の `window.__uzu.sendAction` を使う
|
|
657
512
|
- `await d.send(...)` / `await d.*RawState(...)` は **(a) handler 完了 (b) parent state broadcast 投函** までを待つ。**子 iframe canvas の paint 完了は含まない** ので、screenshot / visual e2e test では `await new Promise(r => requestAnimationFrame(r))` を別途挟むこと
|
|
658
513
|
- phase 遷移時の field reset や `markReady` 等の **state shape を仮定する helper は SDK には含めない**。scenario 側で `window.__<scene>_dev` を生やして上記 primitive を組み合わせる
|
|
659
514
|
|
package/dist/dev-globals.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as UzuDevHooks } from "./dev-hooks-DmOV2p0j.js";
|
|
2
2
|
//#region src/dev-globals.d.ts
|
|
3
3
|
declare global {
|
|
4
4
|
interface Window {
|
|
@@ -6,7 +6,7 @@ declare global {
|
|
|
6
6
|
* dev harness / Playwright / 単独 page で attach される dev hooks。
|
|
7
7
|
* 本番 (Flutter native ホスト) では undefined。
|
|
8
8
|
* - run() devHarness: 親 frame に attach (authoritative state)
|
|
9
|
-
* -
|
|
9
|
+
* - online: 子 frame で undefined
|
|
10
10
|
*/
|
|
11
11
|
__uzu_dev?: UzuDevHooks<unknown>;
|
|
12
12
|
}
|
|
@@ -352,25 +352,9 @@ interface GameLogic<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerA
|
|
|
352
352
|
deadlines?: Record<string, Deadline<S>>;
|
|
353
353
|
tickRate?: number;
|
|
354
354
|
}
|
|
355
|
-
/** Sentinel value — patch の value にセットすると、サーバーが Date.now() に置換する */
|
|
356
|
-
declare const SERVER_TIME: "__SERVER_TIME__";
|
|
357
355
|
/** デフォルトのプレイヤーアイコン URL 一覧(dev / local モード用) */
|
|
358
356
|
declare const DEFAULT_ICON_URLS: readonly ["https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/4d0da24d-bcf2-4f7b-d1a0-f1bb8c747300/original", "https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/8c75fccb-41d6-429d-e943-06c728a72a00/original", "https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/43f45d11-da38-4d6e-637d-3df78e583500/original"];
|
|
359
357
|
//#endregion
|
|
360
|
-
//#region ../engine-core/src/json-patch.d.ts
|
|
361
|
-
/**
|
|
362
|
-
* @docs
|
|
363
|
-
* - SyncRoom仕様: docs/docs/uzu_code/sync-room.md
|
|
364
|
-
*
|
|
365
|
-
* state 差分同期用の JSON Patch。RFC 6902 の全 op は実装せず、
|
|
366
|
-
* `compare` が生成する replace / add / remove だけを扱う。
|
|
367
|
-
*/
|
|
368
|
-
interface Operation {
|
|
369
|
-
op: 'replace' | 'add' | 'remove';
|
|
370
|
-
path: string;
|
|
371
|
-
value?: unknown;
|
|
372
|
-
}
|
|
373
|
-
//#endregion
|
|
374
358
|
//#region src/types.d.ts
|
|
375
359
|
/** @deprecated 旧フラット形式。新コードでは BridgeMessage を使用 */
|
|
376
360
|
type PlayScreenMessage = {
|
|
@@ -474,29 +458,10 @@ interface GameConfig<S, A extends ActionMap<S> = ActionMap<S>, SA extends Server
|
|
|
474
458
|
*/
|
|
475
459
|
devMinIframeShortEdge?: number;
|
|
476
460
|
}
|
|
477
|
-
type PatchFn = (ops: Operation[]) => void;
|
|
478
|
-
type SetFn = (path: string, value: unknown) => void;
|
|
479
|
-
interface SyncConfig<S = any> extends ConnectionCallbacks {
|
|
480
|
-
initialState: (players: Seat[]) => S;
|
|
481
|
-
onState: (state: S, myPlayerId: string, serverTime: number) => void;
|
|
482
|
-
inputs: (patch: PatchFn, set: SetFn) => void;
|
|
483
|
-
events?: Record<string, (data: Record<string, unknown>) => void>;
|
|
484
|
-
playerCount: number;
|
|
485
|
-
/** Dev harness のデフォルト向き。manifest.json の `orientation` を渡す。 */
|
|
486
|
-
orientation?: 'portrait' | 'landscape';
|
|
487
|
-
/**
|
|
488
|
-
* Dev harness で各 iframe inner viewport 短辺の下限 (CSS px)。
|
|
489
|
-
* default 360。狭い viewport では親 frame に `body { zoom: N }` を当てて
|
|
490
|
-
* iframe 内部の `window.innerWidth/Height` を保証する。
|
|
491
|
-
*/
|
|
492
|
-
devMinIframeShortEdge?: number;
|
|
493
|
-
}
|
|
494
461
|
type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
495
462
|
interface ConnectionCallbacks {
|
|
496
463
|
/** WebSocket 接続状態が変化した時に呼ばれる */
|
|
497
464
|
onConnectionStateChange?: (state: ConnectionState) => void;
|
|
498
|
-
/** サーバーで patch 適用が失敗した時に呼ばれる (sync モード専用) */
|
|
499
|
-
onPatchFailed?: (reason: string) => void;
|
|
500
465
|
}
|
|
501
466
|
//#endregion
|
|
502
467
|
//#region src/host-globals.d.ts
|
|
@@ -671,23 +636,9 @@ interface RunHandle<S = unknown> {
|
|
|
671
636
|
/** 既存 inputs と同等の action 送信 (dev hooks 用) */
|
|
672
637
|
sendAction(type: string, payload?: Record<string, unknown>): void;
|
|
673
638
|
}
|
|
674
|
-
/**
|
|
675
|
-
* sync/* モジュール (sync/dev / sync/local) が dev hooks に state を expose
|
|
676
|
-
* するための handle。online sync では handle 自体を返さない。
|
|
677
|
-
*/
|
|
678
|
-
interface SyncHandle<S = unknown> {
|
|
679
|
-
/** 内部 state の getter (dev hooks 用) */
|
|
680
|
-
getRawState?(): S | null;
|
|
681
|
-
/** 生 state 全置換 */
|
|
682
|
-
setRawState?(state: S): Promise<void>;
|
|
683
|
-
/** RFC 7396 風 Merge Patch */
|
|
684
|
-
mergeRawState?(patch: JsonMergePatch<S>): Promise<void>;
|
|
685
|
-
/** RFC 6902 JSON Patch */
|
|
686
|
-
patchRawState?(ops: JsonPatchOp[]): Promise<void>;
|
|
687
|
-
}
|
|
688
639
|
/**
|
|
689
640
|
* dev-hooks.ts から見た「SDK 内部のコンテキスト」。
|
|
690
|
-
* 各 run/*
|
|
641
|
+
* 各 run/* モジュールがこの形の handle を組み立てて attachDevHooks に渡す。
|
|
691
642
|
*/
|
|
692
643
|
interface DevHooksCtx<S = unknown> {
|
|
693
644
|
/** 最新 snapshot (per-player view)。未初期化なら null。 */
|
|
@@ -738,4 +689,4 @@ interface DevHooksCtx<S = unknown> {
|
|
|
738
689
|
declare function createDevHooks<S>(ctx: DevHooksCtx<S>): UzuDevHooks<S>;
|
|
739
690
|
declare function attachDevHooks<S>(ctx: DevHooksCtx<S>): void;
|
|
740
691
|
//#endregion
|
|
741
|
-
export {
|
|
692
|
+
export { applyJsonMergePatch as $, GameLogic as A, ServerOnlyActionHandlerFn as B, ActionHandler as C, DeadlineArgs as D, Deadline as E, ServerActionHandler as F, Duration as G, SetupContext as H, ServerActionMap as I, minus as J, GAME_START as K, ServerEvent as L, SeededRandom as M, ServerActionArgs as N, DeadlineContext as O, ServerActionContext as P, JsonPatchOp as Q, ServerOnlyAction as R, ActionContext as S, DEFAULT_ICON_URLS as T, UpdateArgs as U, SetupArgs as V, UpdateContext as W, sub as X, plus as Y, JsonMergePatch as Z, PlayerVoiceState as _, createDevHooks as a, SendAction as b, BridgeChannel as c, ConnectionState as d, applyJsonPatch as et, EventHandler as f, PlayScreenMessage as g, PayloadMap as h, attachDevHooks as i, Seat as j, Emit as k, BridgeMessage as l, GameConfig as m, RunHandle as n, PredictionWarning as o, EventSubscription as p, GameTime as q, UzuDevHooks as r, getPredictionWarnings as s, DevHooksCtx as t, ConnectionCallbacks as u, PlayersChangedMessage as v, ActionMap as w, ActionArgs as x, SeatKind as y, ServerOnlyActionContext as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as applyJsonMergePatch, A as GameLogic, B as ServerOnlyActionHandlerFn, C as ActionHandler, D as DeadlineArgs, E as Deadline, F as ServerActionHandler, G as Duration, H as SetupContext, I as ServerActionMap, J as minus, K as GAME_START, L as ServerEvent, M as SeededRandom, N as ServerActionArgs, O as DeadlineContext, P as ServerActionContext, Q as JsonPatchOp, R as ServerOnlyAction, S as ActionContext, T as DEFAULT_ICON_URLS, U as UpdateArgs, V as SetupArgs, W as UpdateContext, X as sub, Y as plus, Z as JsonMergePatch, _ as PlayerVoiceState, a as createDevHooks, b as SendAction, c as BridgeChannel, d as ConnectionState, et as applyJsonPatch, f as EventHandler, g as PlayScreenMessage, h as PayloadMap, i as attachDevHooks, j as Seat, k as Emit, l as BridgeMessage, m as GameConfig, n as RunHandle, o as PredictionWarning, p as EventSubscription, q as GameTime, r as UzuDevHooks, s as getPredictionWarnings, t as DevHooksCtx, u as ConnectionCallbacks, v as PlayersChangedMessage, w as ActionMap, x as ActionArgs, y as SeatKind, z as ServerOnlyActionContext } from "./dev-hooks-DmOV2p0j.js";
|
|
2
|
+
//#region ../engine-core/src/json-patch.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* @docs
|
|
5
|
+
* - play-server 仕様: docs/docs/uzu_code/play-server.md
|
|
6
|
+
*
|
|
7
|
+
* state 差分同期用の JSON Patch。RFC 6902 の全 op は実装せず、
|
|
8
|
+
* `compare` が生成する replace / add / remove だけを扱う。
|
|
9
|
+
*/
|
|
10
|
+
interface Operation {
|
|
11
|
+
op: 'replace' | 'add' | 'remove';
|
|
12
|
+
path: string;
|
|
13
|
+
value?: unknown;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
2
16
|
//#region ../engine-core/src/random.d.ts
|
|
3
17
|
declare class SeededRandomImpl implements SeededRandom {
|
|
4
18
|
private _state;
|
|
@@ -45,30 +59,6 @@ declare function isPaused(): boolean;
|
|
|
45
59
|
/** 停止状態の変化を購読する。 */
|
|
46
60
|
declare function onPauseChange(cb: (paused: boolean) => void): void;
|
|
47
61
|
//#endregion
|
|
48
|
-
//#region src/room.d.ts
|
|
49
|
-
type RoomMessageHandler = (data: Record<string, unknown>) => void;
|
|
50
|
-
/** WebSocket 互換の send/addEventListener インターフェース */
|
|
51
|
-
interface WebSocketLike {
|
|
52
|
-
send(data: string): void;
|
|
53
|
-
addEventListener(type: 'message', handler: (ev: MessageEvent) => void): void;
|
|
54
|
-
}
|
|
55
|
-
/** Room の公開インターフェース。WebSocket / BroadcastChannel どちらでも実装可能。 */
|
|
56
|
-
interface RoomLike {
|
|
57
|
-
readonly myId: string;
|
|
58
|
-
broadcast(msg: Record<string, unknown>): void;
|
|
59
|
-
send(id: string, msg: Record<string, unknown>): void;
|
|
60
|
-
on(type: string, handler: RoomMessageHandler): void;
|
|
61
|
-
}
|
|
62
|
-
declare class Room implements RoomLike {
|
|
63
|
-
readonly myId: string;
|
|
64
|
-
private ws;
|
|
65
|
-
private handlers;
|
|
66
|
-
constructor(ws: WebSocketLike, myId: string);
|
|
67
|
-
broadcast(msg: Record<string, unknown>): void;
|
|
68
|
-
send(id: string, msg: Record<string, unknown>): void;
|
|
69
|
-
on(type: string, handler: RoomMessageHandler): void;
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
62
|
//#region src/reconnectable-ws.d.ts
|
|
73
63
|
/**
|
|
74
64
|
* 自動再接続 WebSocket ラッパー
|
|
@@ -135,11 +125,7 @@ declare class ReconnectableWebSocket {
|
|
|
135
125
|
type GameMessageHandler = (payload: Record<string, unknown>) => void;
|
|
136
126
|
type PlayersChangedHandler = (players: Record<string, PlayerVoiceState>) => void;
|
|
137
127
|
declare const isHosted: boolean;
|
|
138
|
-
declare function getRoom(): RoomLike | null;
|
|
139
|
-
declare function onRoom(callback: (room: RoomLike) => void): void;
|
|
140
128
|
declare function init(opts?: {
|
|
141
|
-
wsEndpoint?: string;
|
|
142
|
-
syncEndpoint?: string;
|
|
143
129
|
playerCount?: number;
|
|
144
130
|
orientation?: 'portrait' | 'landscape';
|
|
145
131
|
/**
|
|
@@ -203,6 +189,5 @@ declare function onPlayersChanged(handler: PlayersChangedHandler): void;
|
|
|
203
189
|
* `serverActions` を持たない logic でもキーが残るよう空 map を既定にする。
|
|
204
190
|
*/
|
|
205
191
|
declare function run<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerActionMap<S> = Record<never, never>>(config: GameConfig<S, A, SA>): void;
|
|
206
|
-
declare function sync<S>(config: SyncConfig<S>): void;
|
|
207
192
|
//#endregion
|
|
208
|
-
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 JsonMergePatch, type JsonPatchOp, type Operation, type
|
|
193
|
+
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 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, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
//#region ../engine-core/src/types.ts
|
|
2
|
-
/** Sentinel value — patch の value にセットすると、サーバーが Date.now() に置換する */
|
|
3
|
-
const SERVER_TIME = "__SERVER_TIME__";
|
|
4
2
|
/** デフォルトのプレイヤーアイコン URL 一覧(dev / local モード用) */
|
|
5
3
|
const DEFAULT_ICON_URLS = [
|
|
6
4
|
"https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/4d0da24d-bcf2-4f7b-d1a0-f1bb8c747300/original",
|
|
@@ -284,7 +282,7 @@ function isServerOnlyAction(handler) {
|
|
|
284
282
|
//#region src/server-clock.ts
|
|
285
283
|
/**
|
|
286
284
|
* @docs
|
|
287
|
-
* -
|
|
285
|
+
* - play-server 仕様: docs/docs/uzu_code/play-server.md
|
|
288
286
|
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
289
287
|
*
|
|
290
288
|
* サーバーのゲーム内時計の推定値をクライアント全体へ配る。
|
|
@@ -400,46 +398,6 @@ function requestResume() {
|
|
|
400
398
|
requester(false);
|
|
401
399
|
}
|
|
402
400
|
//#endregion
|
|
403
|
-
//#region src/room.ts
|
|
404
|
-
var Room = class {
|
|
405
|
-
constructor(ws, myId) {
|
|
406
|
-
this.handlers = /* @__PURE__ */ new Map();
|
|
407
|
-
this.ws = ws;
|
|
408
|
-
this.myId = myId;
|
|
409
|
-
this.ws.addEventListener("message", (ev) => {
|
|
410
|
-
let parsed;
|
|
411
|
-
try {
|
|
412
|
-
parsed = JSON.parse(ev.data);
|
|
413
|
-
} catch {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const type = parsed.type;
|
|
417
|
-
if (!type) return;
|
|
418
|
-
const from = parsed.__from;
|
|
419
|
-
console.log(`[SDK Room] ⬅ recv type=${type} from=${from}`, JSON.stringify(parsed));
|
|
420
|
-
(this.handlers.get(type) || []).forEach((h) => h({
|
|
421
|
-
...parsed,
|
|
422
|
-
__from: from
|
|
423
|
-
}));
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
broadcast(msg) {
|
|
427
|
-
console.log(`[SDK Room] ➡ broadcast type=${msg.type}`, JSON.stringify(msg));
|
|
428
|
-
this.ws.send(JSON.stringify(msg));
|
|
429
|
-
}
|
|
430
|
-
send(id, msg) {
|
|
431
|
-
console.log(`[SDK Room] ➡ send to=${id} type=${msg.type}`, JSON.stringify(msg));
|
|
432
|
-
this.ws.send(JSON.stringify({
|
|
433
|
-
...msg,
|
|
434
|
-
__to: id
|
|
435
|
-
}));
|
|
436
|
-
}
|
|
437
|
-
on(type, handler) {
|
|
438
|
-
if (!this.handlers.has(type)) this.handlers.set(type, []);
|
|
439
|
-
this.handlers.get(type).push(handler);
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
//#endregion
|
|
443
401
|
//#region src/reconnectable-ws.ts
|
|
444
402
|
const DEFAULTS = {
|
|
445
403
|
maxReconnectAttempts: 15,
|
|
@@ -1310,15 +1268,12 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
|
|
|
1310
1268
|
id: p.id,
|
|
1311
1269
|
name: p.nickname,
|
|
1312
1270
|
iconUrl: p.iconUrl,
|
|
1313
|
-
characterId: p.characterId
|
|
1314
|
-
kind: "player"
|
|
1271
|
+
characterId: p.characterId
|
|
1315
1272
|
});
|
|
1316
|
-
const roster = JSON.stringify(players.map(toWire));
|
|
1317
1273
|
const wsUrl = `${gameEndpoint}/${roomId}?${new URLSearchParams({
|
|
1318
1274
|
seatId,
|
|
1319
1275
|
nickname: "Player",
|
|
1320
|
-
players:
|
|
1321
|
-
seats: roster
|
|
1276
|
+
players: JSON.stringify(players.map(toWire))
|
|
1322
1277
|
})}`;
|
|
1323
1278
|
console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
|
|
1324
1279
|
const ws = new ReconnectableWebSocket(wsUrl, {
|
|
@@ -1567,7 +1522,6 @@ function runLocalServerAction(config) {
|
|
|
1567
1522
|
const setupTime = gameTime();
|
|
1568
1523
|
const setupArgs = {
|
|
1569
1524
|
players,
|
|
1570
|
-
seats: players,
|
|
1571
1525
|
ctx: {
|
|
1572
1526
|
random,
|
|
1573
1527
|
...timeCtx(setupTime)
|
|
@@ -1703,217 +1657,15 @@ function runLocalServerAction(config) {
|
|
|
1703
1657
|
};
|
|
1704
1658
|
}
|
|
1705
1659
|
//#endregion
|
|
1706
|
-
//#region src/sync/local.ts
|
|
1707
|
-
function resolveLocalTime(ops) {
|
|
1708
|
-
const now = Date.now();
|
|
1709
|
-
return ops.map((op) => op.value === "__SERVER_TIME__" ? {
|
|
1710
|
-
...op,
|
|
1711
|
-
value: now
|
|
1712
|
-
} : op);
|
|
1713
|
-
}
|
|
1714
|
-
function syncLocal(config) {
|
|
1715
|
-
const { initialState, onState, inputs, events: _events } = config;
|
|
1716
|
-
const localCount = config.playerCount;
|
|
1717
|
-
const players = [];
|
|
1718
|
-
for (let i = 0; i < localCount; i++) players.push({
|
|
1719
|
-
id: `local_${i}`,
|
|
1720
|
-
nickname: `Player ${i + 1}`,
|
|
1721
|
-
iconUrl: DEFAULT_ICON_URLS[i % DEFAULT_ICON_URLS.length]
|
|
1722
|
-
});
|
|
1723
|
-
let state = initialState(players);
|
|
1724
|
-
const myId = players[0].id;
|
|
1725
|
-
const patchFn = (ops) => {
|
|
1726
|
-
applyPatch(state, resolveLocalTime(ops));
|
|
1727
|
-
onState(state, myId, Date.now());
|
|
1728
|
-
};
|
|
1729
|
-
const setFn = (path, value) => {
|
|
1730
|
-
patchFn([{
|
|
1731
|
-
op: "replace",
|
|
1732
|
-
path,
|
|
1733
|
-
value
|
|
1734
|
-
}]);
|
|
1735
|
-
};
|
|
1736
|
-
inputs(patchFn, setFn);
|
|
1737
|
-
onState(state, myId, Date.now());
|
|
1738
|
-
setInterval(() => {
|
|
1739
|
-
onState(state, myId, Date.now());
|
|
1740
|
-
}, 100);
|
|
1741
|
-
return {
|
|
1742
|
-
getRawState: () => state,
|
|
1743
|
-
setRawState: async (next) => {
|
|
1744
|
-
state = next;
|
|
1745
|
-
onState(state, myId, Date.now());
|
|
1746
|
-
},
|
|
1747
|
-
mergeRawState: async (patch) => {
|
|
1748
|
-
applyJsonMergePatch(state, patch);
|
|
1749
|
-
onState(state, myId, Date.now());
|
|
1750
|
-
},
|
|
1751
|
-
patchRawState: async (ops) => {
|
|
1752
|
-
applyJsonPatch(state, ops);
|
|
1753
|
-
onState(state, myId, Date.now());
|
|
1754
|
-
}
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
//#endregion
|
|
1758
|
-
//#region src/sync/online.ts
|
|
1759
|
-
function syncOnline(config, syncEndpoint, roomId, playerId, hostPlayers) {
|
|
1760
|
-
const { initialState, onState, inputs, events: _events } = config;
|
|
1761
|
-
const wsUrl = `${syncEndpoint}/${roomId}?playerId=${encodeURIComponent(playerId)}`;
|
|
1762
|
-
console.log(`[SDK Sync] 🔗 Connecting wsUrl=${wsUrl}`);
|
|
1763
|
-
const ws = new ReconnectableWebSocket(wsUrl, {
|
|
1764
|
-
onConnectionStateChange: (state) => {
|
|
1765
|
-
console.log(`[SDK Sync] 📡 Connection state: ${state}`);
|
|
1766
|
-
config.onConnectionStateChange?.(state);
|
|
1767
|
-
},
|
|
1768
|
-
shouldBuffer: (data) => {
|
|
1769
|
-
try {
|
|
1770
|
-
const parsed = JSON.parse(data);
|
|
1771
|
-
return parsed.type !== "__patch" && parsed.type !== "__patch_ack";
|
|
1772
|
-
} catch {
|
|
1773
|
-
return true;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
});
|
|
1777
|
-
const myId = playerId;
|
|
1778
|
-
const players = hostPlayers;
|
|
1779
|
-
let state = null;
|
|
1780
|
-
let gameInitSent = false;
|
|
1781
|
-
let serverTimeOffset = 0;
|
|
1782
|
-
let localSeq = 0;
|
|
1783
|
-
let requestStatePending = false;
|
|
1784
|
-
const estimateServerTime = () => Date.now() + serverTimeOffset;
|
|
1785
|
-
const resolveToServerTime = (ops) => {
|
|
1786
|
-
const now = estimateServerTime();
|
|
1787
|
-
return ops.map((op) => op.value === "__SERVER_TIME__" ? {
|
|
1788
|
-
...op,
|
|
1789
|
-
value: now
|
|
1790
|
-
} : op);
|
|
1791
|
-
};
|
|
1792
|
-
const sendPatch = (ops) => {
|
|
1793
|
-
if (!ops || ops.length === 0) return;
|
|
1794
|
-
if (state) {
|
|
1795
|
-
applyPatch(state, resolveToServerTime(ops));
|
|
1796
|
-
onState(state, myId, estimateServerTime());
|
|
1797
|
-
}
|
|
1798
|
-
console.log(`[SDK Sync] ➡ send __patch ops=${ops.length}`, JSON.stringify(ops));
|
|
1799
|
-
ws.send(JSON.stringify({
|
|
1800
|
-
type: "__patch",
|
|
1801
|
-
ops
|
|
1802
|
-
}));
|
|
1803
|
-
};
|
|
1804
|
-
const sendSet = (path, value) => {
|
|
1805
|
-
sendPatch([{
|
|
1806
|
-
op: "replace",
|
|
1807
|
-
path,
|
|
1808
|
-
value
|
|
1809
|
-
}]);
|
|
1810
|
-
};
|
|
1811
|
-
inputs(sendPatch, sendSet);
|
|
1812
|
-
setInterval(() => {
|
|
1813
|
-
if (state) onState(state, myId, estimateServerTime());
|
|
1814
|
-
}, 100);
|
|
1815
|
-
const tryInitState = () => {
|
|
1816
|
-
if (gameInitSent) return;
|
|
1817
|
-
gameInitSent = true;
|
|
1818
|
-
const initState = initialState(players);
|
|
1819
|
-
console.log(`[SDK Sync] ➡ send __init_state players=${players.length}`);
|
|
1820
|
-
ws.send(JSON.stringify({
|
|
1821
|
-
type: "__init_state",
|
|
1822
|
-
state: initState
|
|
1823
|
-
}));
|
|
1824
|
-
};
|
|
1825
|
-
ws.addEventListener("message", (ev) => {
|
|
1826
|
-
let parsed;
|
|
1827
|
-
try {
|
|
1828
|
-
parsed = JSON.parse(ev.data);
|
|
1829
|
-
} catch {
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
const msgType = parsed.type;
|
|
1833
|
-
console.log(`[SDK Sync] ⬅ recv type=${msgType}`, JSON.stringify(parsed));
|
|
1834
|
-
if (msgType === "__room_init") {
|
|
1835
|
-
console.log(`[SDK Sync] ✅ Room init myId=${myId} players=${players.length}`);
|
|
1836
|
-
tryInitState();
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
if (msgType === "__reconnected") {
|
|
1840
|
-
console.log(`[SDK Sync] 🔄 Reconnected myId=${myId}`);
|
|
1841
|
-
if (!gameInitSent) tryInitState();
|
|
1842
|
-
return;
|
|
1843
|
-
}
|
|
1844
|
-
if (msgType === "__state_cleared") {
|
|
1845
|
-
console.log(`[SDK Sync] 🗑 State cleared, reinitializing`);
|
|
1846
|
-
state = null;
|
|
1847
|
-
localSeq = 0;
|
|
1848
|
-
requestStatePending = false;
|
|
1849
|
-
gameInitSent = false;
|
|
1850
|
-
tryInitState();
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
if (msgType === "__patch_failed") {
|
|
1854
|
-
console.log(`[SDK Sync] ⚠️ Patch failed: ${parsed.reason}`);
|
|
1855
|
-
config.onPatchFailed?.(parsed.reason);
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
if (msgType === "__patch_ack") {
|
|
1859
|
-
if (!state) return;
|
|
1860
|
-
const ackSeq = parsed.seq ?? 0;
|
|
1861
|
-
const ackSenderId = parsed.senderId;
|
|
1862
|
-
const ackOps = parsed.ops;
|
|
1863
|
-
const serverTime = parsed.serverTime;
|
|
1864
|
-
if (ackSenderId === myId) {
|
|
1865
|
-
localSeq = ackSeq;
|
|
1866
|
-
serverTimeOffset = serverTime - Date.now();
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
if (requestStatePending) return;
|
|
1870
|
-
if (ackSeq === localSeq + 1) {
|
|
1871
|
-
if (!applyPatch(state, ackOps)) {
|
|
1872
|
-
console.log(`[SDK Sync] ⚠️ Local applyPatch failed, requesting full state`);
|
|
1873
|
-
requestStatePending = true;
|
|
1874
|
-
ws.send(JSON.stringify({ type: "__request_state" }));
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
localSeq = ackSeq;
|
|
1878
|
-
serverTimeOffset = serverTime - Date.now();
|
|
1879
|
-
onState(state, myId, serverTime);
|
|
1880
|
-
} else if (ackSeq > localSeq + 1) {
|
|
1881
|
-
console.log(`[SDK Sync] ⚠️ Seq gap detected: expected=${localSeq + 1} received=${ackSeq}, requesting full state`);
|
|
1882
|
-
requestStatePending = true;
|
|
1883
|
-
ws.send(JSON.stringify({ type: "__request_state" }));
|
|
1884
|
-
}
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
if (msgType === "__state") {
|
|
1888
|
-
state = parsed.state;
|
|
1889
|
-
const serverTime = parsed.serverTime;
|
|
1890
|
-
localSeq = parsed.seq ?? 0;
|
|
1891
|
-
requestStatePending = false;
|
|
1892
|
-
serverTimeOffset = serverTime - Date.now();
|
|
1893
|
-
console.log(`[SDK Sync] ✅ State received serverTime=${serverTime} offset=${serverTimeOffset} seq=${localSeq}`);
|
|
1894
|
-
ws.clearBuffer();
|
|
1895
|
-
onState(state, myId, serverTime);
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
return { ws };
|
|
1900
|
-
}
|
|
1901
|
-
//#endregion
|
|
1902
1660
|
//#region src/index.ts
|
|
1903
1661
|
const gameHandlers = /* @__PURE__ */ new Map();
|
|
1904
1662
|
const _playersChangedHandlers = [];
|
|
1905
|
-
let _room = null;
|
|
1906
1663
|
let _lastStateSnap = null;
|
|
1907
|
-
let _onRoomCallbacks = [];
|
|
1908
|
-
let _wsEndpoint = null;
|
|
1909
|
-
let _syncEndpoint = null;
|
|
1910
1664
|
let _gameEndpoint = null;
|
|
1911
|
-
let _syncWs = null;
|
|
1912
1665
|
let _initialized = false;
|
|
1913
1666
|
let _usesRun = false;
|
|
1914
1667
|
let _playerId = null;
|
|
1915
1668
|
let _runHandle = null;
|
|
1916
|
-
let _syncHandle = null;
|
|
1917
1669
|
const _snapshotListeners = /* @__PURE__ */ new Set();
|
|
1918
1670
|
function notifyDevSnapshot(snap) {
|
|
1919
1671
|
_snapshotListeners.forEach((cb) => {
|
|
@@ -1928,10 +1680,10 @@ function attachDevHooksIfNotHosted() {
|
|
|
1928
1680
|
if (window.FlutterHost) return;
|
|
1929
1681
|
attachDevHooks({
|
|
1930
1682
|
getSnapshot: () => _lastStateSnap?.state ?? null,
|
|
1931
|
-
getRawState: _runHandle?.getRawState
|
|
1932
|
-
setRawState: _runHandle?.setRawState
|
|
1933
|
-
mergeRawState: _runHandle?.mergeRawState
|
|
1934
|
-
patchRawState: _runHandle?.patchRawState
|
|
1683
|
+
getRawState: _runHandle?.getRawState,
|
|
1684
|
+
setRawState: _runHandle?.setRawState,
|
|
1685
|
+
mergeRawState: _runHandle?.mergeRawState,
|
|
1686
|
+
patchRawState: _runHandle?.patchRawState,
|
|
1935
1687
|
playerId: () => _lastStateSnap?.myId ?? _playerId,
|
|
1936
1688
|
subscribeSnapshot: (cb) => {
|
|
1937
1689
|
_snapshotListeners.add(cb);
|
|
@@ -1942,13 +1694,6 @@ function attachDevHooksIfNotHosted() {
|
|
|
1942
1694
|
});
|
|
1943
1695
|
}
|
|
1944
1696
|
const isHosted = typeof window !== "undefined" && (!!window.FlutterHost || window.parent !== window);
|
|
1945
|
-
function getRoom() {
|
|
1946
|
-
return _room;
|
|
1947
|
-
}
|
|
1948
|
-
function onRoom(callback) {
|
|
1949
|
-
if (_room) callback(_room);
|
|
1950
|
-
else _onRoomCallbacks.push(callback);
|
|
1951
|
-
}
|
|
1952
1697
|
function init(opts) {
|
|
1953
1698
|
const params = new URLSearchParams(window.location.search);
|
|
1954
1699
|
_playerId = params.get("seatId");
|
|
@@ -1956,14 +1701,10 @@ function init(opts) {
|
|
|
1956
1701
|
const serverParam = params.get("server");
|
|
1957
1702
|
if (serverParam && serverParam.length > 0) {
|
|
1958
1703
|
const base = serverParam.replace(/\/$/, "");
|
|
1959
|
-
_wsEndpoint = `${base}/ws/rooms`;
|
|
1960
|
-
_syncEndpoint = `${base}/ws/sync`;
|
|
1961
1704
|
const revisionId = params.get("revisionId");
|
|
1962
1705
|
if (!revisionId) throw new Error("[UZU SDK] revisionId is required when server is specified. Pass ?revisionId=xxx in the URL.");
|
|
1963
1706
|
_gameEndpoint = `${base}/ws/games/${revisionId}`;
|
|
1964
1707
|
}
|
|
1965
|
-
if (opts?.wsEndpoint) _wsEndpoint = opts.wsEndpoint;
|
|
1966
|
-
if (opts?.syncEndpoint) _syncEndpoint = opts.syncEndpoint;
|
|
1967
1708
|
if (!isHosted) return;
|
|
1968
1709
|
if (!_initialized) {
|
|
1969
1710
|
window.__ps = { onMessage: (msg) => {
|
|
@@ -1993,13 +1734,6 @@ function init(opts) {
|
|
|
1993
1734
|
sendRaw("sdk", "ready", {});
|
|
1994
1735
|
_initialized = true;
|
|
1995
1736
|
}
|
|
1996
|
-
if (!_usesRun) {
|
|
1997
|
-
const roomIdParam = params.get("roomId");
|
|
1998
|
-
if (roomIdParam) {
|
|
1999
|
-
if (_wsEndpoint) connectRoom(roomIdParam);
|
|
2000
|
-
else console.warn("[SDK] roomId is present but server is not configured — skipping Relay connection");
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
1737
|
if (!_usesRun) attachDevHooksIfNotHosted();
|
|
2004
1738
|
}
|
|
2005
1739
|
/** game channel でカスタムメッセージを送信する。 native コマンドには使用不可。 */
|
|
@@ -2104,33 +1838,6 @@ function run(config) {
|
|
|
2104
1838
|
} else throw new Error("[UZU SDK] roomId is set but ?server= is missing. Multiplayer requires a WebSocket server URL — pass ?server=ws://host:port in the URL.");
|
|
2105
1839
|
attachDevHooksIfNotHosted();
|
|
2106
1840
|
}
|
|
2107
|
-
function sync(config) {
|
|
2108
|
-
_usesRun = true;
|
|
2109
|
-
const params = new URLSearchParams(window.location.search);
|
|
2110
|
-
if (!isHosted) return;
|
|
2111
|
-
init();
|
|
2112
|
-
const origOnState = config.onState;
|
|
2113
|
-
const wrappedConfig = {
|
|
2114
|
-
...config,
|
|
2115
|
-
onState(state, myPlayerId, serverTime) {
|
|
2116
|
-
_lastStateSnap = {
|
|
2117
|
-
state,
|
|
2118
|
-
serverTime,
|
|
2119
|
-
myId: myPlayerId
|
|
2120
|
-
};
|
|
2121
|
-
origOnState(state, myPlayerId, serverTime);
|
|
2122
|
-
notifyDevSnapshot(state);
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
const roomId = params.get("roomId");
|
|
2126
|
-
if (!roomId) _syncHandle = syncLocal(wrappedConfig);
|
|
2127
|
-
else if (_syncEndpoint) {
|
|
2128
|
-
const { seatId, players } = resolveSeatParams(params);
|
|
2129
|
-
const { ws } = syncOnline(wrappedConfig, _syncEndpoint, roomId, seatId, players);
|
|
2130
|
-
_syncWs = ws;
|
|
2131
|
-
} else throw new Error("[UZU SDK] roomId is set but ?server= is missing. Multiplayer sync requires a WebSocket server URL — pass ?server=ws://host:port in the URL.");
|
|
2132
|
-
attachDevHooksIfNotHosted();
|
|
2133
|
-
}
|
|
2134
1841
|
/**
|
|
2135
1842
|
* 自分の席種別を URL から取り出す。
|
|
2136
1843
|
*
|
|
@@ -2149,13 +1856,11 @@ function resolveSeatKind(params) {
|
|
|
2149
1856
|
*
|
|
2150
1857
|
* roster (`?players=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
|
|
2151
1858
|
* 載らないまま接続してくる。
|
|
2152
|
-
*
|
|
2153
|
-
* 移行期: 旧ホスト (デプロイ前の mobile / uzutokyo / emulator) は `?seats=` で送ってくる。
|
|
2154
1859
|
*/
|
|
2155
1860
|
function resolveSeatParams(params) {
|
|
2156
1861
|
const seatId = params.get("seatId");
|
|
2157
1862
|
if (!seatId) throw new Error("[UZU SDK] seatId is required. Pass ?seatId=xxx in the URL.");
|
|
2158
|
-
const json = params.get("players")
|
|
1863
|
+
const json = params.get("players");
|
|
2159
1864
|
if (!json) throw new Error("[UZU SDK] players is required. Pass ?players=[...] in the URL.");
|
|
2160
1865
|
return {
|
|
2161
1866
|
seatId,
|
|
@@ -2167,33 +1872,6 @@ function resolveSeatParams(params) {
|
|
|
2167
1872
|
}))
|
|
2168
1873
|
};
|
|
2169
1874
|
}
|
|
2170
|
-
function requireEndpoint(endpoint, name) {
|
|
2171
|
-
if (!endpoint) throw new Error(`[UZU SDK] ${name} is not configured. Pass ?server=ws://host:port or call init({ wsEndpoint, syncEndpoint }).`);
|
|
2172
|
-
return endpoint;
|
|
2173
|
-
}
|
|
2174
|
-
function connectRoom(roomId) {
|
|
2175
|
-
const { seatId } = resolveSeatParams(new URLSearchParams(window.location.search));
|
|
2176
|
-
const wsParams = new URLSearchParams({ playerId: seatId });
|
|
2177
|
-
const wsUrl = `${requireEndpoint(_wsEndpoint, "wsEndpoint")}/${roomId}?${wsParams}`;
|
|
2178
|
-
console.log(`[SDK] 🔗 connectRoom wsUrl=${wsUrl}`);
|
|
2179
|
-
const ws = new ReconnectableWebSocket(wsUrl);
|
|
2180
|
-
ws.addEventListener("message", (ev) => {
|
|
2181
|
-
let parsed;
|
|
2182
|
-
try {
|
|
2183
|
-
parsed = JSON.parse(ev.data);
|
|
2184
|
-
} catch {
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
console.log(`[SDK] ⬅ recv ws type=${parsed.type}`, JSON.stringify(parsed));
|
|
2188
|
-
if (parsed.type === "__room_init") {
|
|
2189
|
-
const myId = parsed.myId;
|
|
2190
|
-
console.log(`[SDK] ✅ Room initialized myId=${myId}`);
|
|
2191
|
-
_room = new Room(ws, myId);
|
|
2192
|
-
_onRoomCallbacks.forEach((cb) => cb(_room));
|
|
2193
|
-
_onRoomCallbacks = [];
|
|
2194
|
-
} else if (parsed.type === "__reconnected") console.log(`[SDK] 🔄 Room reconnected`);
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
1875
|
/** 内部送信関数。 channel + type + payload のエンベロープで送信する。 */
|
|
2198
1876
|
function sendRaw(channel, type, payload) {
|
|
2199
1877
|
const msg = {
|
|
@@ -2214,10 +1892,6 @@ function handleMessage(msg) {
|
|
|
2214
1892
|
console.log(`[SDK] 🔧 handleMessage sdk/getState → responding`);
|
|
2215
1893
|
sendRaw("sdk", "stateResponse", { state: _lastStateSnap });
|
|
2216
1894
|
return;
|
|
2217
|
-
case "clearState":
|
|
2218
|
-
console.log(`[SDK] 🗑 handleMessage sdk/clearState → forwarding to server`);
|
|
2219
|
-
if (_syncWs && _syncWs.connectionState === "connected") _syncWs.send(JSON.stringify({ type: "__clear_state" }));
|
|
2220
|
-
return;
|
|
2221
1895
|
case "playersChanged": {
|
|
2222
1896
|
const players = payload?.players ?? {};
|
|
2223
1897
|
_playersChangedHandlers.forEach((fn) => fn(players));
|
|
@@ -2257,7 +1931,7 @@ const ACTION_ITEM_WIDTH = 38;
|
|
|
2257
1931
|
function calcHudInsets(params) {
|
|
2258
1932
|
const y = 52;
|
|
2259
1933
|
let actionCount = 2;
|
|
2260
|
-
const json = params.get("players")
|
|
1934
|
+
const json = params.get("players");
|
|
2261
1935
|
if (json) try {
|
|
2262
1936
|
actionCount = JSON.parse(json).length >= 2 ? 2 : 0;
|
|
2263
1937
|
} catch {}
|
|
@@ -2269,4 +1943,4 @@ function calcHudInsets(params) {
|
|
|
2269
1943
|
};
|
|
2270
1944
|
}
|
|
2271
1945
|
//#endregion
|
|
2272
|
-
export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket,
|
|
1946
|
+
export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
|