@uzuhq/code-sdk 0.8.4 → 0.8.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/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()` / `sync()` を使う場合は内部で `init()` が呼ばれるため不要
58
- - Relay パターン (`onRoom()`) のみ、最初に呼び出す
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` が含まれる |
93
-
94
- ---
95
-
96
- ## パターン 2: sync()
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()` を使う。
39
+ - `run()` が内部で呼ぶので、通常は直接書かなくてよい
40
+ - マルチプレイの state 同期を使わない 1 人用シナリオ (サウンド・ボイス・safe area だけ使う)
41
+ では、これだけを最初に呼ぶ
175
42
 
176
43
  ---
177
44
 
178
- ## パターン 3: run()
45
+ ## ゲームループ
179
46
 
180
47
  ### `run<S>(config: GameConfig<S>): void`
181
48
 
182
- Host-authoritative ゲームループを実行する。サーバー (GameRoom DO) が reducer を逐次実行するため、条件保証あり。
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
 
@@ -445,6 +313,58 @@ onPlayersChanged((players) => {
445
313
 
446
314
  ---
447
315
 
316
+ ## SafeArea / HUD 回避
317
+
318
+ ゲームは常に全画面へ描かれる。ノッチや UZU の HUD を避けるのはゲーム側の仕事で、避けるべき
319
+ 領域はホストが配る。**素の `env(safe-area-inset-*)` は使わない** (iframe / WebView の境界を
320
+ 越えないのでホストによっては 0 になる)。
321
+
322
+ `init()` が以下の CSS カスタムプロパティを起動時に設定する。
323
+
324
+ | 変数 | 意味 |
325
+ | ----------------------------------------------- | ------------------------------------------------------------------------- |
326
+ | `--uzu-safe-area-inset-{top,right,bottom,left}` | デバイスの safe area |
327
+ | `--uzu-hud-inset-x` / `--uzu-hud-inset-y` | HUD 矩形の右下座標(safe area 内側の左上が原点) |
328
+ | `--uzu-content-inset-{top,right,bottom,left}` | 上 2 つを合成済み (`top` にだけ HUD の高さを足す)。**迷ったらこれを使う** |
329
+
330
+ ```css
331
+ .game-ui {
332
+ padding-top: var(--uzu-content-inset-top, env(safe-area-inset-top, 0px));
333
+ padding-left: var(--uzu-content-inset-left, env(safe-area-inset-left, 0px));
334
+ }
335
+ ```
336
+
337
+ ### `getInsets(): UzuInsets`
338
+
339
+ canvas / WebGL のように CSS で避けられない描画向けに、同じ値を数値で読む。
340
+
341
+ ```ts
342
+ import { getInsets } from '@uzuhq/code-sdk';
343
+
344
+ const { safeArea, content, hud } = getInsets();
345
+ // safeArea / content: { top, right, bottom, left } (CSS px)
346
+ // hud: { x, y }
347
+ layout(content);
348
+ ```
349
+
350
+ 値は初回ペイント前に確定しているので setup の中で読んでよい。
351
+
352
+ ### `onInsetsChange(cb: (insets: UzuInsets) => void): () => void`
353
+
354
+ 回転 / PiP で値が変わったときに呼ばれる。戻り値を呼ぶと解除できる。
355
+
356
+ ```ts
357
+ import { onInsetsChange } from '@uzuhq/code-sdk';
358
+
359
+ const off = onInsetsChange((insets) => layout(insets.content));
360
+ ```
361
+
362
+ **CSS 変数の変化は `resize` イベントを発火させない。** `window.addEventListener('resize', …)`
363
+ だけを見ている実装は追従できないので、canvas を持つシナリオは必ずこちらを使う。
364
+ 登録時の即時呼び出しはしないので、初期値は `getInsets()` で読む。
365
+
366
+ ---
367
+
448
368
  ## メッセージング
449
369
 
450
370
  ### `send(type: string, payload?: object): void`
@@ -479,17 +399,6 @@ on('attack', (payload) => {
479
399
 
480
400
  ---
481
401
 
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
402
  ## 型定義
494
403
 
495
404
  ### BridgeMessage
@@ -554,7 +463,6 @@ type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnect
554
463
  ```ts
555
464
  interface ConnectionCallbacks {
556
465
  onConnectionStateChange?: (state: ConnectionState) => void;
557
- onPatchFailed?: (error: string) => void;
558
466
  }
559
467
  ```
560
468
 
@@ -578,15 +486,17 @@ interface PlayerVoiceState {
578
486
 
579
487
  ## URL パラメータ
580
488
 
581
- `init()` / `run()` / `sync()` は以下の URL パラメータを読み取る。
489
+ `init()` / `run()` は以下の URL パラメータを読み取る。
582
490
 
583
- | パラメータ | 説明 |
584
- | ----------- | --------------------------------------------- |
585
- | `?server=` | WebSocket サーバーの URL |
586
- | `?roomId=` | ルーム ID。指定するとオンラインモードになる |
587
- | `?seatId=` | 自分の席 ID |
588
- | `?players=` | JSON エンコードされたプレイヤーリスト |
589
- | `?__dev=N` | Dev ハーネスモード (N 画面の iframe 並列表示) |
491
+ | パラメータ | 説明 |
492
+ | ------------------------------------------- | --------------------------------------------- |
493
+ | `?server=` | WebSocket サーバーの URL |
494
+ | `?roomId=` | ルーム ID。指定するとオンラインモードになる |
495
+ | `?seatId=` | 自分の席 ID |
496
+ | `?players=` | JSON エンコードされたプレイヤーリスト |
497
+ | `?__dev=N` | Dev ハーネスモード (N 画面の iframe 並列表示) |
498
+ | `?uzuSafeAreaInset{Top,Right,Bottom,Left}=` | デバイスの safe area。ホストが配る |
499
+ | `?uzuHudInsetX=` / `?uzuHudInsetY=` | HUD 矩形の右下座標。ホストが配る |
590
500
 
591
501
  ### モードの自動判定
592
502
 
@@ -604,7 +514,6 @@ interface PlayerVoiceState {
604
514
 
605
515
  - `run()` の devHarness → **親 frame だけ** (`page.mainFrame()`)
606
516
  - `runLocalServerAction` (ソロモード) 単独 frame → その frame
607
- - sync 系単独 frame → その frame
608
517
  - devHarness の **子 iframe には attach しない**
609
518
 
610
519
  ```ts
@@ -653,7 +562,7 @@ await window.__uzu_dev.waitForSnapshot((s) => s.self.isReady === true);
653
562
  - 部分更新は `mergeRawState`、array 要素単体の書換は `patchRawState` を使う
654
563
  - `mergeRawState` で array field に non-array object patch を当てると **throw** する (array が pure object に化けるのを構造的に防止)
655
564
  - online ServerAction では 3 API すべて `undefined`
656
- - `send` は run devHarness 親 frame のみ。非 parent モード (runLocal / emulator / sync) では scenario 側の `window.__uzu.sendAction` を使う
565
+ - `send` は run devHarness 親 frame のみ。非 parent モード (runLocal / emulator) では scenario 側の `window.__uzu.sendAction` を使う
657
566
  - `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
567
  - phase 遷移時の field reset や `markReady` 等の **state shape を仮定する helper は SDK には含めない**。scenario 側で `window.__<scene>_dev` を生やして上記 primitive を組み合わせる
659
568
 
@@ -1,4 +1,4 @@
1
- import { i as UzuDevHooks } from "./dev-hooks-DOtJHP55.js";
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
- * - sync() / online: 子 frame で undefined
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/* / sync/* モジュールがこの形の handle を組み立てて attachDevHooks に渡す。
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 { GameTime as $, DEFAULT_ICON_URLS as A, ServerActionContext as B, SetFn as C, ActionContext as D, ActionArgs as E, GameLogic as F, ServerOnlyActionContext as G, ServerActionMap as H, SERVER_TIME as I, SetupContext as J, ServerOnlyActionHandlerFn as K, Seat as L, DeadlineArgs as M, DeadlineContext as N, ActionHandler as O, Emit as P, GAME_START as Q, SeededRandom as R, SendAction as S, Operation as T, ServerEvent as U, ServerActionHandler as V, ServerOnlyAction as W, UpdateContext as X, UpdateArgs as Y, Duration as Z, PayloadMap as _, attachDevHooks as a, applyJsonMergePatch as at, PlayersChangedMessage as b, getPredictionWarnings as c, ConnectionCallbacks as d, minus as et, ConnectionState as f, PatchFn as g, GameConfig as h, UzuDevHooks as i, JsonPatchOp as it, Deadline as j, ActionMap as k, BridgeChannel as l, EventSubscription as m, RunHandle as n, sub as nt, createDevHooks as o, applyJsonPatch as ot, EventHandler as p, SetupArgs as q, SyncHandle as r, JsonMergePatch as rt, PredictionWarning as s, DevHooksCtx as t, plus as tt, BridgeMessage as u, PlayScreenMessage as v, SyncConfig as w, SeatKind as x, PlayerVoiceState as y, ServerActionArgs as z };
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 GameTime, A as DEFAULT_ICON_URLS, B as ServerActionContext, C as SetFn, D as ActionContext, E as ActionArgs, F as GameLogic, G as ServerOnlyActionContext, H as ServerActionMap, I as SERVER_TIME, J as SetupContext, K as ServerOnlyActionHandlerFn, L as Seat, M as DeadlineArgs, N as DeadlineContext, O as ActionHandler, P as Emit, Q as GAME_START, R as SeededRandom, S as SendAction, T as Operation, U as ServerEvent, V as ServerActionHandler, W as ServerOnlyAction, X as UpdateContext, Y as UpdateArgs, Z as Duration, _ as PayloadMap, a as attachDevHooks, at as applyJsonMergePatch, b as PlayersChangedMessage, c as getPredictionWarnings, d as ConnectionCallbacks, et as minus, f as ConnectionState, g as PatchFn, h as GameConfig, i as UzuDevHooks, it as JsonPatchOp, j as Deadline, k as ActionMap, l as BridgeChannel, m as EventSubscription, n as RunHandle, nt as sub, o as createDevHooks, ot as applyJsonPatch, p as EventHandler, q as SetupArgs, r as SyncHandle, rt as JsonMergePatch, s as PredictionWarning, t as DevHooksCtx, tt as plus, u as BridgeMessage, v as PlayScreenMessage, w as SyncConfig, x as SeatKind, y as PlayerVoiceState, z as ServerActionArgs } from "./dev-hooks-DOtJHP55.js";
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 ラッパー
@@ -131,15 +121,66 @@ declare class ReconnectableWebSocket {
131
121
  private handlePong;
132
122
  }
133
123
  //#endregion
124
+ //#region src/insets.d.ts
125
+ /**
126
+ * @docs
127
+ * - SafeArea 対応ガイド: javascript/uzu_code/docs/docs/guides/safe-area.md
128
+ * - Flutterライブラリ仕様: docs/docs/uzu_code/flutter-host.md
129
+ *
130
+ * ホストが配る inset を CSS 変数と JS API の両方へ流し込む層。
131
+ *
132
+ * `env(safe-area-inset-*)` は iframe / WebView の境界を越えない。GamePlayScreen を
133
+ * 開くホスト 4 つのうち 3 つはゲームを iframe に載せるので、デバイスの safe area は
134
+ * ホストが値として配る以外に伝える手が無い ([csswg-drafts#4670] は未解決のままで、
135
+ * `env()` は UA 定義なので CSS からも JS からも上書きできない)。Discord Activities /
136
+ * Telegram Mini Apps / Capacitor も例外なく同じ形を採っている。
137
+ *
138
+ * [csswg-drafts#4670]: https://github.com/w3c/csswg-drafts/issues/4670
139
+ */
140
+ /** 4 辺の inset (CSS px)。 */
141
+ interface Insets {
142
+ top: number;
143
+ right: number;
144
+ bottom: number;
145
+ left: number;
146
+ }
147
+ /** ホストが配る inset の全層。 */
148
+ interface UzuInsets {
149
+ /** デバイスの safe area (ノッチ / 角丸 / ホームインジケータ)。 */
150
+ safeArea: Insets;
151
+ /**
152
+ * safeArea と HUD を合成済みの、そのまま避ければ安全な領域。
153
+ * 迷ったらこれを使う。
154
+ */
155
+ content: Insets;
156
+ /** UZU HUD 矩形の右下座標 (safe area 内側の左上が原点)。 */
157
+ hud: {
158
+ x: number;
159
+ y: number;
160
+ };
161
+ }
162
+ /**
163
+ * 現在の inset を返す。
164
+ *
165
+ * canvas / WebGL は CSS 変数で避けられないので、そこから読む口。値は初回ペイント前に
166
+ * 確定しているので、setup の中で読んでよい。
167
+ */
168
+ declare function getInsets(): UzuInsets;
169
+ /**
170
+ * inset の変化を購読する。戻り値を呼ぶと解除できる。
171
+ *
172
+ * **CSS 変数の変化は `resize` を発火させない**ので、`window.addEventListener('resize')`
173
+ * だけを見ている実装は回転 / PiP に追従できない。canvas を持つシナリオはこちらを使う。
174
+ *
175
+ * 登録時に即時呼び出しはしない。初期値は [getInsets] で読むこと。
176
+ */
177
+ declare function onInsetsChange(cb: (insets: UzuInsets) => void): () => void;
178
+ //#endregion
134
179
  //#region src/index.d.ts
135
180
  type GameMessageHandler = (payload: Record<string, unknown>) => void;
136
181
  type PlayersChangedHandler = (players: Record<string, PlayerVoiceState>) => void;
137
182
  declare const isHosted: boolean;
138
- declare function getRoom(): RoomLike | null;
139
- declare function onRoom(callback: (room: RoomLike) => void): void;
140
183
  declare function init(opts?: {
141
- wsEndpoint?: string;
142
- syncEndpoint?: string;
143
184
  playerCount?: number;
144
185
  orientation?: 'portrait' | 'landscape';
145
186
  /**
@@ -203,6 +244,5 @@ declare function onPlayersChanged(handler: PlayersChangedHandler): void;
203
244
  * `serverActions` を持たない logic でもキーが残るよう空 map を既定にする。
204
245
  */
205
246
  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
247
  //#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 PatchFn, type PayloadMap, type PlayScreenMessage, type PlayerVoiceState, type PlayersChangedMessage, type PredictionWarning, type ReconnectableWSOptions, ReconnectableWebSocket, Room, type RoomLike, type RunHandle, SERVER_TIME, 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 SetFn, type SetupArgs, type SetupContext, type SyncConfig, type SyncHandle, type UpdateArgs, type UpdateContext, type UzuDevHooks, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getPredictionWarnings, getRoom, init, isHosted, isPaused, isServerOnlyAction, minus, on, onPauseChange, onPlayersChanged, onRoom, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub, sync };
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 };
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
- * - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
285
+ * - play-server 仕様: docs/docs/uzu_code/play-server.md
288
286
  * - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
289
287
  *
290
288
  * サーバーのゲーム内時計の推定値をクライアント全体へ配る。
@@ -355,7 +353,7 @@ function unfreezeGameTime() {
355
353
  */
356
354
  let paused = false;
357
355
  let pausedBy = null;
358
- const listeners = /* @__PURE__ */ new Set();
356
+ const listeners$1 = /* @__PURE__ */ new Set();
359
357
  /** transport から呼ぶ内部関数。停止状態が変わったときだけ通知する。 */
360
358
  function setPauseState(next, by) {
361
359
  pausedBy = next ? by : null;
@@ -363,7 +361,7 @@ function setPauseState(next, by) {
363
361
  paused = next;
364
362
  if (paused) freezeGameTime();
365
363
  else unfreezeGameTime();
366
- for (const cb of listeners) try {
364
+ for (const cb of listeners$1) try {
367
365
  cb(paused);
368
366
  } catch (err) {
369
367
  console.warn("[uzu] onPauseChange listener threw:", err);
@@ -379,7 +377,7 @@ function getPausedBy() {
379
377
  }
380
378
  /** 停止状態の変化を購読する。 */
381
379
  function onPauseChange(cb) {
382
- listeners.add(cb);
380
+ listeners$1.add(cb);
383
381
  }
384
382
  let requester = null;
385
383
  function setPauseRequester(fn) {
@@ -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,
@@ -1115,6 +1073,234 @@ function attachDevHooks(ctx) {
1115
1073
  window.__uzu_dev = createDevHooks(ctx);
1116
1074
  }
1117
1075
  //#endregion
1076
+ //#region src/insets.ts
1077
+ const SIDES = [
1078
+ "top",
1079
+ "right",
1080
+ "bottom",
1081
+ "left"
1082
+ ];
1083
+ const SAFE_AREA_PARAM = {
1084
+ top: "uzuSafeAreaInsetTop",
1085
+ right: "uzuSafeAreaInsetRight",
1086
+ bottom: "uzuSafeAreaInsetBottom",
1087
+ left: "uzuSafeAreaInsetLeft"
1088
+ };
1089
+ /** ホストが値を配らない経路 (素のブラウザで直接開く等) の逃げ道。 */
1090
+ const envExpr = (side) => `env(safe-area-inset-${side}, 0px)`;
1091
+ const HUD_GAP = 6;
1092
+ const HUD_ACTION_ITEM_WIDTH = 38;
1093
+ /**
1094
+ * ホストが配った safe area。`null` は「配られていない」で、`env()` に委ねる合図。
1095
+ * 値で持っているあいだは計測が要らないので、回転のたびにレイアウトを読み直さずに済む。
1096
+ */
1097
+ let hostSafeArea = null;
1098
+ let hud = {
1099
+ x: 0,
1100
+ y: 0
1101
+ };
1102
+ let current = null;
1103
+ let envProbe = null;
1104
+ const listeners = /* @__PURE__ */ new Set();
1105
+ const zero = () => ({
1106
+ top: 0,
1107
+ right: 0,
1108
+ bottom: 0,
1109
+ left: 0
1110
+ });
1111
+ const toFinite = (value) => {
1112
+ const n = typeof value === "string" ? Number.parseFloat(value) : value;
1113
+ return typeof n === "number" && Number.isFinite(n) ? n : null;
1114
+ };
1115
+ /**
1116
+ * `env()` の実値を読む計測用の要素。
1117
+ *
1118
+ * `env()` は JS から直接読めず、CSS 変数へ入れても `getComputedStyle` は代入前の
1119
+ * トークン列を返す。padding に置いて算出値を読むのが唯一の手。
1120
+ */
1121
+ function measureEnvInsets() {
1122
+ if (typeof document === "undefined") return zero();
1123
+ if (!envProbe?.isConnected) {
1124
+ const el = document.createElement("div");
1125
+ el.setAttribute("aria-hidden", "true");
1126
+ el.style.cssText = [
1127
+ "position:fixed;top:0;left:0;width:0;height:0",
1128
+ "visibility:hidden;pointer-events:none",
1129
+ ...SIDES.map((side) => `padding-${side}:${envExpr(side)}`)
1130
+ ].join(";");
1131
+ (document.body ?? document.documentElement).appendChild(el);
1132
+ envProbe = el;
1133
+ }
1134
+ const style = getComputedStyle(envProbe);
1135
+ return {
1136
+ top: toFinite(style.paddingTop) ?? 0,
1137
+ right: toFinite(style.paddingRight) ?? 0,
1138
+ bottom: toFinite(style.paddingBottom) ?? 0,
1139
+ left: toFinite(style.paddingLeft) ?? 0
1140
+ };
1141
+ }
1142
+ function setVar(name, value) {
1143
+ if (typeof document === "undefined") return;
1144
+ document.documentElement.style.setProperty(name, value);
1145
+ }
1146
+ function writeSafeAreaVars() {
1147
+ for (const side of SIDES) setVar(`--uzu-safe-area-inset-${side}`, hostSafeArea ? `${hostSafeArea[side]}px` : envExpr(side));
1148
+ }
1149
+ function writeHudVars() {
1150
+ setVar("--uzu-hud-inset-x", `${hud.x}px`);
1151
+ setVar("--uzu-hud-inset-y", `${hud.y}px`);
1152
+ }
1153
+ /**
1154
+ * 合成済みの層。下 2 層を参照する式で書くので、safe area が実数でも `env()` でも
1155
+ * 同じ 1 本で成立する。
1156
+ *
1157
+ * `left` に `--uzu-hud-inset-x` を足さないのは、HUD が左上の**矩形**だから。上辺を
1158
+ * HUD の高さ分だけ空ければその下は全域が安全で、`left` にも足すと L 字全体を避ける
1159
+ * ことになり過剰に削れる。精密に避けたいシナリオは下 2 層を自分で合成する。
1160
+ */
1161
+ function writeContentVars() {
1162
+ setVar("--uzu-content-inset-top", "calc(var(--uzu-safe-area-inset-top, 0px) + var(--uzu-hud-inset-y, 0px))");
1163
+ setVar("--uzu-content-inset-right", "var(--uzu-safe-area-inset-right, 0px)");
1164
+ setVar("--uzu-content-inset-bottom", "var(--uzu-safe-area-inset-bottom, 0px)");
1165
+ setVar("--uzu-content-inset-left", "var(--uzu-safe-area-inset-left, 0px)");
1166
+ }
1167
+ function compute() {
1168
+ const safeArea = hostSafeArea ?? measureEnvInsets();
1169
+ return {
1170
+ safeArea,
1171
+ content: {
1172
+ top: safeArea.top + hud.y,
1173
+ right: safeArea.right,
1174
+ bottom: safeArea.bottom,
1175
+ left: safeArea.left
1176
+ },
1177
+ hud: { ...hud }
1178
+ };
1179
+ }
1180
+ const sameInsets = (a, b) => SIDES.every((side) => a[side] === b[side]);
1181
+ const same = (a, b) => sameInsets(a.safeArea, b.safeArea) && a.hud.x === b.hud.x && a.hud.y === b.hud.y;
1182
+ /** 値が動いたときだけ購読者へ流す。 */
1183
+ function refresh() {
1184
+ const next = compute();
1185
+ if (current && same(current, next)) return;
1186
+ current = next;
1187
+ for (const cb of listeners) try {
1188
+ cb(next);
1189
+ } catch (err) {
1190
+ console.warn("[UZU SDK] onInsetsChange listener threw:", err);
1191
+ }
1192
+ }
1193
+ /**
1194
+ * HUD 回避領域の右下座標を算出する。
1195
+ * クエリパラメータに uzuHudInsetX/Y がない場合のフォールバック値として使用。
1196
+ *
1197
+ * 前提: UZU ボタンは常に表示される (onMenuPressed は常に非 null)。
1198
+ * Dart 側の HudInsets.x() は hasMenu=false を扱えるが、
1199
+ * フォールバック計算では安全側に倒して常に UZU ボタンありで算出する。
1200
+ *
1201
+ * - players が取得できる場合: 2人以上→アクション数2、1人→アクション数0
1202
+ * - players が取得できない場合: アクション数2 (安全側に倒す)
1203
+ */
1204
+ function fallbackHudInsets(params) {
1205
+ let actionCount = 2;
1206
+ const json = params.get("players");
1207
+ if (json) try {
1208
+ actionCount = JSON.parse(json).length >= 2 ? 2 : 0;
1209
+ } catch {}
1210
+ let x = 56;
1211
+ if (actionCount > 0) x += HUD_GAP + HUD_ACTION_ITEM_WIDTH * actionCount;
1212
+ return {
1213
+ x,
1214
+ y: 52
1215
+ };
1216
+ }
1217
+ function readSafeAreaParams(params) {
1218
+ const values = zero();
1219
+ let supplied = false;
1220
+ for (const side of SIDES) {
1221
+ const raw = params.get(SAFE_AREA_PARAM[side]);
1222
+ if (raw === null) continue;
1223
+ const n = toFinite(raw);
1224
+ if (n === null) continue;
1225
+ values[side] = n;
1226
+ supplied = true;
1227
+ }
1228
+ return supplied ? values : null;
1229
+ }
1230
+ /**
1231
+ * URL パラメータから初期値を確定させる。`init()` が 1 度だけ呼ぶ。
1232
+ *
1233
+ * 初期値を URL で受けるのは、**初回ペイント前に確定させる**ため。起動後に push で
1234
+ * 配る形にすると、最初の 1 フレームだけ inset が空のまま描かれる
1235
+ * ([discord/embedded-app-sdk#304] と同じ踏み方) 。
1236
+ *
1237
+ * [discord/embedded-app-sdk#304]: https://github.com/discord/embedded-app-sdk/issues/304
1238
+ */
1239
+ function applyInsets(params) {
1240
+ const hudX = toFinite(params.get("uzuHudInsetX"));
1241
+ const hudY = toFinite(params.get("uzuHudInsetY"));
1242
+ const fallback = fallbackHudInsets(params);
1243
+ hud = {
1244
+ x: hudX ?? fallback.x,
1245
+ y: hudY ?? fallback.y
1246
+ };
1247
+ hostSafeArea = readSafeAreaParams(params);
1248
+ writeSafeAreaVars();
1249
+ writeHudVars();
1250
+ writeContentVars();
1251
+ refresh();
1252
+ if (!hostSafeArea && typeof window !== "undefined") {
1253
+ window.addEventListener("resize", refresh);
1254
+ window.addEventListener("orientationchange", refresh);
1255
+ }
1256
+ }
1257
+ /**
1258
+ * ホストからの `insetsChanged` を反映する。回転 / PiP で値が変わったときに届く。
1259
+ * 内部用 (bridge の受信口から呼ばれる)。
1260
+ */
1261
+ function handleInsetsChanged(payload) {
1262
+ const safeArea = payload.safeArea;
1263
+ if (safeArea) {
1264
+ const next = zero();
1265
+ for (const side of SIDES) next[side] = toFinite(safeArea[side]) ?? 0;
1266
+ hostSafeArea = next;
1267
+ writeSafeAreaVars();
1268
+ }
1269
+ const nextHud = payload.hud;
1270
+ if (nextHud) {
1271
+ hud = {
1272
+ x: toFinite(nextHud.x) ?? hud.x,
1273
+ y: toFinite(nextHud.y) ?? hud.y
1274
+ };
1275
+ writeHudVars();
1276
+ }
1277
+ refresh();
1278
+ }
1279
+ /**
1280
+ * 現在の inset を返す。
1281
+ *
1282
+ * canvas / WebGL は CSS 変数で避けられないので、そこから読む口。値は初回ペイント前に
1283
+ * 確定しているので、setup の中で読んでよい。
1284
+ */
1285
+ function getInsets() {
1286
+ if (!current) refresh();
1287
+ return current ?? compute();
1288
+ }
1289
+ /**
1290
+ * inset の変化を購読する。戻り値を呼ぶと解除できる。
1291
+ *
1292
+ * **CSS 変数の変化は `resize` を発火させない**ので、`window.addEventListener('resize')`
1293
+ * だけを見ている実装は回転 / PiP に追従できない。canvas を持つシナリオはこちらを使う。
1294
+ *
1295
+ * 登録時に即時呼び出しはしない。初期値は [getInsets] で読むこと。
1296
+ */
1297
+ function onInsetsChange(cb) {
1298
+ listeners.add(cb);
1299
+ return () => {
1300
+ listeners.delete(cb);
1301
+ };
1302
+ }
1303
+ //#endregion
1118
1304
  //#region src/run/optimistic-action-client.ts
1119
1305
  function createOptimisticActionClient(config) {
1120
1306
  const { logic, playerId, onState, events, sendAction } = config;
@@ -1310,15 +1496,12 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1310
1496
  id: p.id,
1311
1497
  name: p.nickname,
1312
1498
  iconUrl: p.iconUrl,
1313
- characterId: p.characterId,
1314
- kind: "player"
1499
+ characterId: p.characterId
1315
1500
  });
1316
- const roster = JSON.stringify(players.map(toWire));
1317
1501
  const wsUrl = `${gameEndpoint}/${roomId}?${new URLSearchParams({
1318
1502
  seatId,
1319
1503
  nickname: "Player",
1320
- players: roster,
1321
- seats: roster
1504
+ players: JSON.stringify(players.map(toWire))
1322
1505
  })}`;
1323
1506
  console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
1324
1507
  const ws = new ReconnectableWebSocket(wsUrl, {
@@ -1567,7 +1750,6 @@ function runLocalServerAction(config) {
1567
1750
  const setupTime = gameTime();
1568
1751
  const setupArgs = {
1569
1752
  players,
1570
- seats: players,
1571
1753
  ctx: {
1572
1754
  random,
1573
1755
  ...timeCtx(setupTime)
@@ -1703,217 +1885,15 @@ function runLocalServerAction(config) {
1703
1885
  };
1704
1886
  }
1705
1887
  //#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
1888
  //#region src/index.ts
1903
1889
  const gameHandlers = /* @__PURE__ */ new Map();
1904
1890
  const _playersChangedHandlers = [];
1905
- let _room = null;
1906
1891
  let _lastStateSnap = null;
1907
- let _onRoomCallbacks = [];
1908
- let _wsEndpoint = null;
1909
- let _syncEndpoint = null;
1910
1892
  let _gameEndpoint = null;
1911
- let _syncWs = null;
1912
1893
  let _initialized = false;
1913
1894
  let _usesRun = false;
1914
1895
  let _playerId = null;
1915
1896
  let _runHandle = null;
1916
- let _syncHandle = null;
1917
1897
  const _snapshotListeners = /* @__PURE__ */ new Set();
1918
1898
  function notifyDevSnapshot(snap) {
1919
1899
  _snapshotListeners.forEach((cb) => {
@@ -1928,10 +1908,10 @@ function attachDevHooksIfNotHosted() {
1928
1908
  if (window.FlutterHost) return;
1929
1909
  attachDevHooks({
1930
1910
  getSnapshot: () => _lastStateSnap?.state ?? null,
1931
- getRawState: _runHandle?.getRawState ?? _syncHandle?.getRawState,
1932
- setRawState: _runHandle?.setRawState ?? _syncHandle?.setRawState,
1933
- mergeRawState: _runHandle?.mergeRawState ?? _syncHandle?.mergeRawState,
1934
- patchRawState: _runHandle?.patchRawState ?? _syncHandle?.patchRawState,
1911
+ getRawState: _runHandle?.getRawState,
1912
+ setRawState: _runHandle?.setRawState,
1913
+ mergeRawState: _runHandle?.mergeRawState,
1914
+ patchRawState: _runHandle?.patchRawState,
1935
1915
  playerId: () => _lastStateSnap?.myId ?? _playerId,
1936
1916
  subscribeSnapshot: (cb) => {
1937
1917
  _snapshotListeners.add(cb);
@@ -1942,13 +1922,6 @@ function attachDevHooksIfNotHosted() {
1942
1922
  });
1943
1923
  }
1944
1924
  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
1925
  function init(opts) {
1953
1926
  const params = new URLSearchParams(window.location.search);
1954
1927
  _playerId = params.get("seatId");
@@ -1956,14 +1929,10 @@ function init(opts) {
1956
1929
  const serverParam = params.get("server");
1957
1930
  if (serverParam && serverParam.length > 0) {
1958
1931
  const base = serverParam.replace(/\/$/, "");
1959
- _wsEndpoint = `${base}/ws/rooms`;
1960
- _syncEndpoint = `${base}/ws/sync`;
1961
1932
  const revisionId = params.get("revisionId");
1962
1933
  if (!revisionId) throw new Error("[UZU SDK] revisionId is required when server is specified. Pass ?revisionId=xxx in the URL.");
1963
1934
  _gameEndpoint = `${base}/ws/games/${revisionId}`;
1964
1935
  }
1965
- if (opts?.wsEndpoint) _wsEndpoint = opts.wsEndpoint;
1966
- if (opts?.syncEndpoint) _syncEndpoint = opts.syncEndpoint;
1967
1936
  if (!isHosted) return;
1968
1937
  if (!_initialized) {
1969
1938
  window.__ps = { onMessage: (msg) => {
@@ -1979,11 +1948,7 @@ function init(opts) {
1979
1948
  }
1980
1949
  } catch {}
1981
1950
  });
1982
- const hudX = params.get("uzuHudInsetX");
1983
- const hudY = params.get("uzuHudInsetY");
1984
- const { x: fallbackX, y: fallbackY } = calcHudInsets(params);
1985
- document.documentElement.style.setProperty("--uzu-hud-inset-x", `${hudX ?? fallbackX}px`);
1986
- document.documentElement.style.setProperty("--uzu-hud-inset-y", `${hudY ?? fallbackY}px`);
1951
+ applyInsets(params);
1987
1952
  onPauseChange((paused) => {
1988
1953
  sendRaw("sdk", "pauseState", {
1989
1954
  paused,
@@ -1993,13 +1958,6 @@ function init(opts) {
1993
1958
  sendRaw("sdk", "ready", {});
1994
1959
  _initialized = true;
1995
1960
  }
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
1961
  if (!_usesRun) attachDevHooksIfNotHosted();
2004
1962
  }
2005
1963
  /** game channel でカスタムメッセージを送信する。 native コマンドには使用不可。 */
@@ -2104,33 +2062,6 @@ function run(config) {
2104
2062
  } 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
2063
  attachDevHooksIfNotHosted();
2106
2064
  }
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
2065
  /**
2135
2066
  * 自分の席種別を URL から取り出す。
2136
2067
  *
@@ -2149,13 +2080,11 @@ function resolveSeatKind(params) {
2149
2080
  *
2150
2081
  * roster (`?players=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
2151
2082
  * 載らないまま接続してくる。
2152
- *
2153
- * 移行期: 旧ホスト (デプロイ前の mobile / uzutokyo / emulator) は `?seats=` で送ってくる。
2154
2083
  */
2155
2084
  function resolveSeatParams(params) {
2156
2085
  const seatId = params.get("seatId");
2157
2086
  if (!seatId) throw new Error("[UZU SDK] seatId is required. Pass ?seatId=xxx in the URL.");
2158
- const json = params.get("players") ?? params.get("seats");
2087
+ const json = params.get("players");
2159
2088
  if (!json) throw new Error("[UZU SDK] players is required. Pass ?players=[...] in the URL.");
2160
2089
  return {
2161
2090
  seatId,
@@ -2167,33 +2096,6 @@ function resolveSeatParams(params) {
2167
2096
  }))
2168
2097
  };
2169
2098
  }
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
2099
  /** 内部送信関数。 channel + type + payload のエンベロープで送信する。 */
2198
2100
  function sendRaw(channel, type, payload) {
2199
2101
  const msg = {
@@ -2214,10 +2116,6 @@ function handleMessage(msg) {
2214
2116
  console.log(`[SDK] 🔧 handleMessage sdk/getState → responding`);
2215
2117
  sendRaw("sdk", "stateResponse", { state: _lastStateSnap });
2216
2118
  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
2119
  case "playersChanged": {
2222
2120
  const players = payload?.players ?? {};
2223
2121
  _playersChangedHandlers.forEach((fn) => fn(players));
@@ -2231,6 +2129,9 @@ function handleMessage(msg) {
2231
2129
  console.log(`[SDK] ▶️ handleMessage sdk/requestResume`);
2232
2130
  requestResume();
2233
2131
  return;
2132
+ case "insetsChanged":
2133
+ handleInsetsChanged(payload ?? {});
2134
+ return;
2234
2135
  }
2235
2136
  return;
2236
2137
  }
@@ -2241,32 +2142,5 @@ function handleMessage(msg) {
2241
2142
  return;
2242
2143
  }
2243
2144
  }
2244
- const GAP = 6;
2245
- const ACTION_ITEM_WIDTH = 38;
2246
- /**
2247
- * HUD 回避領域の右下座標を算出する。
2248
- * クエリパラメータに uzuHudInsetX/Y がない場合のフォールバック値として使用。
2249
- *
2250
- * 前提: UZU ボタンは常に表示される (onMenuPressed は常に非 null)。
2251
- * Dart 側の HudInsets.x() は hasMenu=false を扱えるが、
2252
- * フォールバック計算では安全側に倒して常に UZU ボタンありで算出する。
2253
- *
2254
- * - players が取得できる場合: 2人以上→アクション数2、1人→アクション数0
2255
- * - players が取得できない場合: アクション数2 (安全側に倒す)
2256
- */
2257
- function calcHudInsets(params) {
2258
- const y = 52;
2259
- let actionCount = 2;
2260
- const json = params.get("players") ?? params.get("seats");
2261
- if (json) try {
2262
- actionCount = JSON.parse(json).length >= 2 ? 2 : 0;
2263
- } catch {}
2264
- let x = 56;
2265
- if (actionCount > 0) x += GAP + ACTION_ITEM_WIDTH * actionCount;
2266
- return {
2267
- x,
2268
- y
2269
- };
2270
- }
2271
2145
  //#endregion
2272
- export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, Room, SERVER_TIME, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, gameTime, getPredictionWarnings, getRoom, init, isHosted, isPaused, isServerOnlyAction, minus, on, onPauseChange, onPlayersChanged, onRoom, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub, sync };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-sdk",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "UZU PlayScreen SDK - Flutter ↔ JS ゲーム通信ライブラリ",
5
5
  "type": "module",
6
6
  "exports": {