@uzuhq/code-sdk 0.7.7 → 0.8.1
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 +54 -9
- package/dist/dev-globals.d.ts +1 -1
- package/dist/{dev-hooks-ClWM8HzI.d.ts → dev-hooks-BYl_jrcy.d.ts} +73 -24
- package/dist/index.d.ts +10 -4
- package/dist/index.js +453 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,7 +223,7 @@ import type { GameLogic } from '@uzuhq/code-sdk';
|
|
|
223
223
|
|
|
224
224
|
const logic: GameLogic<MyState> = {
|
|
225
225
|
setup({ players, ctx }) {
|
|
226
|
-
// 初期 state を生成。ctx.random は SeededRandom、ctx.
|
|
226
|
+
// 初期 state を生成。ctx.random は SeededRandom、ctx.time はゲーム内時刻 (必ず 0)。
|
|
227
227
|
// players は配役を受け取る参加者だけで、観測者は含まれない
|
|
228
228
|
return { players: {}, items: [] };
|
|
229
229
|
},
|
|
@@ -235,24 +235,24 @@ const logic: GameLogic<MyState> = {
|
|
|
235
235
|
state.players[playerId].x += payload.dx;
|
|
236
236
|
// ctx.emit でイベント発火 (購読側は events で predict を宣言する)
|
|
237
237
|
ctx.emit('sound', { sound: 'step' });
|
|
238
|
-
// 時刻は ctx.
|
|
238
|
+
// 時刻は ctx.after(d) を使う。Date.now() は epoch が違う (Unix ms vs ゲーム内時刻)
|
|
239
239
|
},
|
|
240
240
|
},
|
|
241
241
|
|
|
242
242
|
// サーバーでのみ走る。実時刻・乱数・fetch などクライアントが再現できない処理。
|
|
243
243
|
// actions と同名にすると「同じ action のサーバー側の続き」になる。
|
|
244
244
|
serverActions: {
|
|
245
|
-
async notifyExternal({ state, payload, playerId }) {
|
|
245
|
+
async notifyExternal({ state, payload, playerId, ctx }) {
|
|
246
246
|
await fetch('https://example.com/notify', {
|
|
247
247
|
method: 'POST',
|
|
248
248
|
body: JSON.stringify({ playerId, ...payload }),
|
|
249
249
|
});
|
|
250
|
-
state.notifiedAt =
|
|
250
|
+
state.notifiedAt = ctx.time;
|
|
251
251
|
},
|
|
252
252
|
},
|
|
253
253
|
|
|
254
254
|
update({ state, ctx }) {
|
|
255
|
-
// 毎 tick 実行。ctx.tick / ctx.random / ctx.
|
|
255
|
+
// 毎 tick 実行。ctx.tick / ctx.random / ctx.time / ctx.after / ctx.emit / ctx.playerInputs が使える
|
|
256
256
|
},
|
|
257
257
|
|
|
258
258
|
tickRate: 10, // 秒間 tick 数 (default: 0 = tick なし)
|
|
@@ -287,10 +287,10 @@ deadlines: {
|
|
|
287
287
|
},
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
| |
|
|
291
|
-
| --------- |
|
|
292
|
-
| `at` |
|
|
293
|
-
| `handler` | サーバーでのみ走る。`ctx` は `{
|
|
290
|
+
| | |
|
|
291
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
292
|
+
| `at` | 締切の[ゲーム内時刻](#ゲーム内時計) (`GameTime`)。`null` / `undefined` で締切なし (optional chain の結果をそのまま返せる)。**state だけから決まる軽い純関数にすること** (state が変わるたびに呼ばれる) |
|
|
293
|
+
| `handler` | サーバーでのみ走る。`ctx` は `{ time, after, random, emit }` |
|
|
294
294
|
|
|
295
295
|
締切は state から導出するので、**予約を張り替える処理を書かなくてよい**。action が throw して
|
|
296
296
|
state が巻き戻れば締切も一緒に巻き戻る。発火直前に `at` を評価し直すので、二重発火を
|
|
@@ -299,6 +299,51 @@ handler 側で弾く必要も無い。
|
|
|
299
299
|
時刻をきっかけに何かを起こすなら `tickRate` で毎秒ポーリングせずこちらを使う。
|
|
300
300
|
ポーリング中は Durable Object が hibernate できない。
|
|
301
301
|
|
|
302
|
+
### ゲーム内時計
|
|
303
|
+
|
|
304
|
+
**時刻は Unix epoch ではない。** `ctx.time` はゲーム開始からの経過 ms (`GameTime`) で、
|
|
305
|
+
[緊急一時停止](#緊急一時停止)中は進まない。`Date.now()` とは桁も意味も違う。
|
|
306
|
+
|
|
307
|
+
| API | 意味 |
|
|
308
|
+
| -------------------------- | ----------------------------------------------------------------- |
|
|
309
|
+
| `ctx.time` | そのハンドラが走っているゲーム内時刻。`setup` では必ず `0` |
|
|
310
|
+
| `ctx.after(d)` | 今から `d` ms 後のゲーム内時刻。締切を state に置くときに使う |
|
|
311
|
+
| `gameTime()` | 描画側で読む現在のゲーム内時刻 (推定値)。`performance.now()` 基準 |
|
|
312
|
+
| `plus(t, d)` / `sub(t, d)` | 時刻に長さを足す / 引く |
|
|
313
|
+
| `minus(a, b)` | 2 つの時刻の差 (ms) |
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { gameTime, minus } from '@uzuhq/code-sdk';
|
|
317
|
+
|
|
318
|
+
// 締切を置く
|
|
319
|
+
actions: {
|
|
320
|
+
startPhase: ({ state, ctx }) => { state.phaseEndsAt = ctx.after(5 * 60_000); },
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// 残り時間を描く
|
|
324
|
+
const remain = Math.ceil(minus(state.phaseEndsAt, gameTime()) / 1000);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`GameTime` は brand 型なので実行時はただの数値で、state は素の JSON のまま。ただし型の上では
|
|
328
|
+
`number` と混ざらないので、`Date.now()` を書き込む式も `endsAt - Date.now()` と読む式も
|
|
329
|
+
コンパイルが通らない。
|
|
330
|
+
|
|
331
|
+
### 緊急一時停止
|
|
332
|
+
|
|
333
|
+
プレイヤーが UZU メニューから全員のタイマーを止められる。**シナリオは停止を知らないし、
|
|
334
|
+
知る必要も無い。** 停止中は `update()` が呼ばれず、action はサーバーが弾き、締切も来ない。
|
|
335
|
+
|
|
336
|
+
演出だけを止めたいときに限り、読み取り専用で参照できる。
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { isPaused, onPauseChange } from '@uzuhq/code-sdk';
|
|
340
|
+
|
|
341
|
+
onPauseChange((paused) => (paused ? engine.stop() : engine.start()));
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`GameLogic` からは触れない。停止を state に持ち込むと「停止中は state が変わらない」という
|
|
345
|
+
前提が崩れる。
|
|
346
|
+
|
|
302
347
|
#### `serverOnly(handler)`
|
|
303
348
|
|
|
304
349
|
> **Deprecated**: `serverActions` に直接書く。
|
package/dist/dev-globals.d.ts
CHANGED
|
@@ -79,19 +79,54 @@ declare function applyJsonMergePatch(target: Record<string, unknown>, patch: Jso
|
|
|
79
79
|
*/
|
|
80
80
|
declare function applyJsonPatch(target: Record<string, unknown>, ops: JsonPatchOp[]): void;
|
|
81
81
|
//#endregion
|
|
82
|
-
//#region ../engine-core/src/
|
|
82
|
+
//#region ../engine-core/src/game-time.d.ts
|
|
83
83
|
/**
|
|
84
84
|
* @docs
|
|
85
|
-
* -
|
|
86
|
-
*
|
|
85
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
86
|
+
*
|
|
87
|
+
* ゲーム内時刻。ゲーム開始からの ms で、停止中は進まない。**Unix epoch ではない。**
|
|
88
|
+
*
|
|
89
|
+
* 実行時はただの number。brand はコンパイル時だけの区別なので、state は素の JSON の
|
|
90
|
+
* まま保たれる (structuredClone / json-patch / JSON.stringify がそのまま通る)。
|
|
91
|
+
* 値を作れるのは `ctx.after` / `plus` だけになるので、`Date.now()` を混ぜると型で落ちる。
|
|
92
|
+
*
|
|
93
|
+
* 型が効くのは TypeScript を書いているシナリオだけだが、JS のシナリオでも epoch が
|
|
94
|
+
* 違うぶんは残る (`Date.now()` を混ぜれば締切が 56,000 日後になって初回で気付く)。
|
|
95
|
+
*/
|
|
96
|
+
declare const gameTimeBrand: unique symbol;
|
|
97
|
+
/** brand の印。`GameTime` を組み立てるためだけに使う。 */
|
|
98
|
+
type GameTimeTag = {
|
|
99
|
+
readonly [gameTimeBrand]: 'GameTime';
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* ゲーム内時刻 (ゲーム開始からの ms)。停止中は進まない。
|
|
87
103
|
*
|
|
88
|
-
*
|
|
104
|
+
* `number & Tag` ではなく `(number & Tag) | Tag` にしてあるのは、**読み側を守るため**。
|
|
105
|
+
* 交差だけだと `GameTime` が `number` へ代入できてしまい、
|
|
106
|
+
* `state.endsAt - Date.now()` が型検査を通る。これは `GameTime` を導入した動機
|
|
107
|
+
* そのもののバグで、しかも実行時に静かに壊れる (巨大な負数が
|
|
108
|
+
* `Math.max(0, ...)` に潰されてタイマーが 00:00 で固まる)。
|
|
89
109
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
110
|
+
* union にすると `number` への代入と算術が型で止まり、`GameTime` 同士の比較・`===`・
|
|
111
|
+
* テンプレート・`Number.isFinite`・JSON 往復・一段 cast はそのまま使える。
|
|
112
|
+
* TypeScript コンパイラ自身が `__String` (識別子の内部表現) で同じ形を採っている
|
|
113
|
+
* (microsoft/TypeScript#16915)。
|
|
94
114
|
*/
|
|
115
|
+
type GameTime = (number & GameTimeTag) | GameTimeTag;
|
|
116
|
+
/** 長さ (ms)。差や間隔はこちら。 */
|
|
117
|
+
type Duration = number;
|
|
118
|
+
/**
|
|
119
|
+
* `GameTime` に長さを足す。
|
|
120
|
+
*
|
|
121
|
+
* 算術は型で止まるので、`GameTime` を作り直す道はこの関数と `ctx.after` に限られる。
|
|
122
|
+
*/
|
|
123
|
+
declare const plus: (t: GameTime, d: Duration) => GameTime;
|
|
124
|
+
/** `GameTime` から長さを引く。`plus(t, -d)` と書かせないための対。 */
|
|
125
|
+
declare const sub: (t: GameTime, d: Duration) => GameTime;
|
|
126
|
+
/** 2 つの `GameTime` の差 (ms)。 */
|
|
127
|
+
declare const minus: (a: GameTime, b: GameTime) => Duration;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region ../engine-core/src/types.d.ts
|
|
95
130
|
/**
|
|
96
131
|
* roster に載る席。
|
|
97
132
|
*
|
|
@@ -128,20 +163,24 @@ interface SeededRandom {
|
|
|
128
163
|
*/
|
|
129
164
|
interface ActionContext {
|
|
130
165
|
/**
|
|
131
|
-
*
|
|
166
|
+
* 現在のゲーム内時刻。先読みではクロックオフセットからの推定値。
|
|
132
167
|
*
|
|
133
168
|
* 推定なのでサーバーとは数十 ms ずれる。時刻での分岐に使うと境界で判定が割れるので、
|
|
134
|
-
* 分岐は `update()` (サーバー専用)
|
|
169
|
+
* 分岐は `update()` / `deadlines` (サーバー専用) へ寄せる。
|
|
135
170
|
*
|
|
136
171
|
* 同じ action を再適用しても値は変わらない (初回予測時の値を使い回す)。
|
|
137
172
|
*/
|
|
138
|
-
|
|
173
|
+
time: GameTime;
|
|
174
|
+
/** 今から `d` ms 後のゲーム内時刻。締切を state へ置くときに使う。 */
|
|
175
|
+
after(d: Duration): GameTime;
|
|
139
176
|
emit: Emit;
|
|
140
177
|
}
|
|
141
178
|
/** `deadlines` handler の実行文脈。サーバーでしか走らないので tick 以外を渡せる。 */
|
|
142
179
|
interface DeadlineContext {
|
|
143
|
-
/**
|
|
144
|
-
|
|
180
|
+
/** 発火時点のゲーム内時刻。サーバー専用なので常に正確。 */
|
|
181
|
+
time: GameTime;
|
|
182
|
+
/** 今から `d` ms 後のゲーム内時刻。次の締切を state へ置き直すときに使う。 */
|
|
183
|
+
after(d: Duration): GameTime;
|
|
145
184
|
random: SeededRandom;
|
|
146
185
|
emit: Emit;
|
|
147
186
|
}
|
|
@@ -162,17 +201,21 @@ interface DeadlineArgs<S> {
|
|
|
162
201
|
*/
|
|
163
202
|
interface Deadline<S> {
|
|
164
203
|
/**
|
|
165
|
-
*
|
|
204
|
+
* 締切のゲーム内時刻。締切が無いときは null / undefined。
|
|
166
205
|
*
|
|
167
206
|
* `state.timer?.endsAt` のような optional chain の結果をそのまま返せるよう
|
|
168
207
|
* undefined も受ける。数値以外は「締切なし」として同じに扱う。
|
|
169
208
|
*
|
|
170
|
-
*
|
|
209
|
+
* 戻り値が `GameTime` なのは強制点。`Date.now() + 尺` も素の number も返せないので、
|
|
210
|
+
* 値を得る道が `ctx.after()` / `plus()` に限られ、state の時刻フィールドを
|
|
211
|
+
* `GameTime` で持つ動機が自動的に生まれる。
|
|
212
|
+
*
|
|
213
|
+
* state が変わるたびに呼ばれるので、state だけから決まる軽い純関数にすること。
|
|
171
214
|
* ここで実時刻や乱数を読むと、呼ばれるたびに答えが変わって予約が暴れる。
|
|
172
215
|
*/
|
|
173
216
|
at(args: {
|
|
174
217
|
state: S;
|
|
175
|
-
}):
|
|
218
|
+
}): GameTime | null | undefined;
|
|
176
219
|
/**
|
|
177
220
|
* `at` の時刻を過ぎたときにサーバーで呼ばれる。
|
|
178
221
|
*
|
|
@@ -185,8 +228,10 @@ interface Deadline<S> {
|
|
|
185
228
|
interface ServerActionContext {
|
|
186
229
|
tick: number;
|
|
187
230
|
random: SeededRandom;
|
|
188
|
-
/**
|
|
189
|
-
|
|
231
|
+
/** 現在のゲーム内時刻。同じ dispatch の `actions` に渡る `ctx.time` と同一値。 */
|
|
232
|
+
time: GameTime;
|
|
233
|
+
/** 今から `d` ms 後のゲーム内時刻。 */
|
|
234
|
+
after(d: Duration): GameTime;
|
|
190
235
|
emit: Emit;
|
|
191
236
|
}
|
|
192
237
|
/** @deprecated `ServerActionContext` を使う。 */
|
|
@@ -230,8 +275,10 @@ type ServerOnlyAction<S> = ServerActionHandler<S> & {
|
|
|
230
275
|
interface UpdateContext {
|
|
231
276
|
random: SeededRandom;
|
|
232
277
|
tick: number;
|
|
233
|
-
/**
|
|
234
|
-
|
|
278
|
+
/** 現在のゲーム内時刻。update はサーバーでしか走らないので常に正確。 */
|
|
279
|
+
time: GameTime;
|
|
280
|
+
/** 今から `d` ms 後のゲーム内時刻。 */
|
|
281
|
+
after(d: Duration): GameTime;
|
|
235
282
|
emit: Emit;
|
|
236
283
|
playerInputs: Record<string, Record<string, any>>;
|
|
237
284
|
}
|
|
@@ -246,8 +293,10 @@ interface UpdateArgs<S> {
|
|
|
246
293
|
*/
|
|
247
294
|
interface SetupContext {
|
|
248
295
|
random: SeededRandom;
|
|
249
|
-
/**
|
|
250
|
-
|
|
296
|
+
/** ゲーム内時刻。setup は時計の起点なので必ず `0`。 */
|
|
297
|
+
time: GameTime;
|
|
298
|
+
/** 今から `d` ms 後のゲーム内時刻。setup では `d` そのものになる。 */
|
|
299
|
+
after(d: Duration): GameTime;
|
|
251
300
|
}
|
|
252
301
|
/** `setup()` の引数。 */
|
|
253
302
|
interface SetupArgs {
|
|
@@ -454,7 +503,7 @@ declare global {
|
|
|
454
503
|
//#endregion
|
|
455
504
|
//#region src/dev-prediction-traps.d.ts
|
|
456
505
|
interface PredictionWarning {
|
|
457
|
-
/** 呼び出した action
|
|
506
|
+
/** 呼び出した action 名。描画経路では `'(render)'`。 */
|
|
458
507
|
action: string;
|
|
459
508
|
/** 呼ばれた API 名 (`'Date.now()'` など) */
|
|
460
509
|
api: string;
|
|
@@ -679,4 +728,4 @@ interface DevHooksCtx<S = unknown> {
|
|
|
679
728
|
declare function createDevHooks<S>(ctx: DevHooksCtx<S>): UzuDevHooks<S>;
|
|
680
729
|
declare function attachDevHooks<S>(ctx: DevHooksCtx<S>): void;
|
|
681
730
|
//#endregion
|
|
682
|
-
export {
|
|
731
|
+
export { minus 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, GameTime 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, applyJsonPatch as at, PlayersChangedMessage as b, getPredictionWarnings as c, ConnectionCallbacks as d, plus as et, ConnectionState as f, PatchFn as g, GameConfig as h, UzuDevHooks as i, applyJsonMergePatch as it, Deadline as j, ActionMap as k, BridgeChannel as l, EventSubscription as m, RunHandle as n, JsonMergePatch as nt, createDevHooks as o, EventHandler as p, SetupArgs as q, SyncHandle as r, JsonPatchOp as rt, PredictionWarning as s, DevHooksCtx as t, sub as tt, BridgeMessage as u, PlayScreenMessage as v, SyncConfig as w, SeatKind as x, PlayerVoiceState as y, ServerActionArgs as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as minus, 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 GameTime, 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 applyJsonPatch, b as PlayersChangedMessage, c as getPredictionWarnings, d as ConnectionCallbacks, et as plus, f as ConnectionState, g as PatchFn, h as GameConfig, i as UzuDevHooks, it as applyJsonMergePatch, j as Deadline, k as ActionMap, l as BridgeChannel, m as EventSubscription, n as RunHandle, nt as JsonMergePatch, o as createDevHooks, p as EventHandler, q as SetupArgs, r as SyncHandle, rt as JsonPatchOp, s as PredictionWarning, t as DevHooksCtx, tt as sub, u as BridgeMessage, v as PlayScreenMessage, w as SyncConfig, x as SeatKind, y as PlayerVoiceState, z as ServerActionArgs } from "./dev-hooks-BYl_jrcy.js";
|
|
2
2
|
//#region ../engine-core/src/random.d.ts
|
|
3
3
|
declare class SeededRandomImpl implements SeededRandom {
|
|
4
4
|
private _state;
|
|
@@ -32,12 +32,18 @@ declare function isServerOnlyAction<S>(handler: ActionHandler<S> | ServerActionH
|
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/server-clock.d.ts
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* ゲーム内時刻の推定値。停止中は凍る。
|
|
36
36
|
*
|
|
37
37
|
* カウントダウン描画のように毎秒/毎フレーム呼ぶ用途を想定しているので、
|
|
38
38
|
* state の到着とは無関係にいつでも呼べる。
|
|
39
39
|
*/
|
|
40
|
-
declare function
|
|
40
|
+
declare function gameTime(): GameTime;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/pause-state.d.ts
|
|
43
|
+
/** 緊急停止中か。 */
|
|
44
|
+
declare function isPaused(): boolean;
|
|
45
|
+
/** 停止状態の変化を購読する。 */
|
|
46
|
+
declare function onPauseChange(cb: (paused: boolean) => void): void;
|
|
41
47
|
//#endregion
|
|
42
48
|
//#region src/room.d.ts
|
|
43
49
|
type RoomMessageHandler = (data: Record<string, unknown>) => void;
|
|
@@ -199,4 +205,4 @@ declare function onPlayersChanged(handler: PlayersChangedHandler): void;
|
|
|
199
205
|
declare function run<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerActionMap<S> = Record<never, never>>(config: GameConfig<S, A, SA>): void;
|
|
200
206
|
declare function sync<S>(config: SyncConfig<S>): void;
|
|
201
207
|
//#endregion
|
|
202
|
-
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 Emit, type EventHandler, type EventSubscription, type GameConfig, type GameLogic, 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, getPredictionWarnings, getRoom, init, isHosted, isServerOnlyAction, on, onPlayersChanged, onRoom, playBgm, playSound, run, send,
|
|
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, 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 };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,180 @@ const DEFAULT_ICON_URLS = [
|
|
|
8
8
|
"https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/43f45d11-da38-4d6e-637d-3df78e583500/original"
|
|
9
9
|
];
|
|
10
10
|
//#endregion
|
|
11
|
+
//#region ../engine-core/src/game-time.ts
|
|
12
|
+
/**
|
|
13
|
+
* エンジン内部で素の ms へ落とす。**パッケージの公開 API には出さない** (`index.ts` を見よ)。
|
|
14
|
+
*
|
|
15
|
+
* 公開すると `toMs(a) - toMs(b)` と書けてしまい brand の保護が丸ごと消える。
|
|
16
|
+
* 差を取る正当な用途は `minus` が既に埋めているので、外へ出す理由が無い。
|
|
17
|
+
* 外部 API へ素の数値を渡したいという実需が出たら、そのとき足す。
|
|
18
|
+
*/
|
|
19
|
+
const toMs = (t) => t;
|
|
20
|
+
/**
|
|
21
|
+
* `GameTime` に長さを足す。
|
|
22
|
+
*
|
|
23
|
+
* 算術は型で止まるので、`GameTime` を作り直す道はこの関数と `ctx.after` に限られる。
|
|
24
|
+
*/
|
|
25
|
+
const plus = (t, d) => toMs(t) + d;
|
|
26
|
+
/** `GameTime` から長さを引く。`plus(t, -d)` と書かせないための対。 */
|
|
27
|
+
const sub = (t, d) => toMs(t) - d;
|
|
28
|
+
/** 2 つの `GameTime` の差 (ms)。 */
|
|
29
|
+
const minus = (a, b) => toMs(a) - toMs(b);
|
|
30
|
+
/**
|
|
31
|
+
* エンジン内部専用。作家コードから呼ばない。
|
|
32
|
+
*
|
|
33
|
+
* 時計の実装 (play-server の facet / dev-server / ソロ / SDK の補間) だけがここを通る。
|
|
34
|
+
*/
|
|
35
|
+
const asGameTime = (ms) => ms;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region ../engine-core/src/game-clock.ts
|
|
38
|
+
/**
|
|
39
|
+
* @docs
|
|
40
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
41
|
+
*
|
|
42
|
+
* ゲーム内時計の実体。3 つのサーバー実装 (play-server の facet / cli の dev-server /
|
|
43
|
+
* SDK のソロ) が同じ挙動になるよう、時計の算術と凍結の出入りをここ 1 箇所に置く。
|
|
44
|
+
*
|
|
45
|
+
* ここに置かないもの: 永続化・tick ループ・broadcast・無人凍結の発火条件。
|
|
46
|
+
* それらはランタイムごとに手段が違うので、各実装が持つ。
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* ゲーム開始からの経過時間を数える時計。停止中は進まない。
|
|
50
|
+
*
|
|
51
|
+
* 実体はスカラー 3 本。シナリオの state は 1 バイトも書き換えない (どのフィールドが
|
|
52
|
+
* 時刻かエンジンは知らないので書き換えられない)。
|
|
53
|
+
*/
|
|
54
|
+
var GameClock = class {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.startedAtWall = 0;
|
|
57
|
+
this.pausedTotalMs = 0;
|
|
58
|
+
this.pausedAtWall = null;
|
|
59
|
+
this.frozenBy = /* @__PURE__ */ new Set();
|
|
60
|
+
}
|
|
61
|
+
/** 時計を 0 から始め直す。`setup()` を呼ぶ直前に通す。 */
|
|
62
|
+
restart() {
|
|
63
|
+
this.startedAtWall = Date.now();
|
|
64
|
+
this.pausedTotalMs = 0;
|
|
65
|
+
this.pausedAtWall = null;
|
|
66
|
+
this.frozenBy.clear();
|
|
67
|
+
}
|
|
68
|
+
get frozen() {
|
|
69
|
+
return this.frozenBy.size > 0;
|
|
70
|
+
}
|
|
71
|
+
isFrozenBy(reason) {
|
|
72
|
+
return this.frozenBy.has(reason);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 現在のゲーム内時刻。停止中は `pausedAtWall` で凍るので同じ値を返し続ける。
|
|
76
|
+
*
|
|
77
|
+
* IMPORTANT: [withGameClock] の中で呼んではいけない。実装が `Date.now()` を読むので、
|
|
78
|
+
* 差し替え済みの時刻を実時刻として引き算し、大きく負の値になる。
|
|
79
|
+
* ハンドラへ渡す時刻は必ず外で 1 回取ってから渡すこと。
|
|
80
|
+
*/
|
|
81
|
+
now() {
|
|
82
|
+
return asGameTime((this.pausedAtWall ?? Date.now()) - this.startedAtWall - this.pausedTotalMs);
|
|
83
|
+
}
|
|
84
|
+
/** ゲーム内時刻を、alarm / setTimeout が使う実時刻へ直す。 */
|
|
85
|
+
toWall(at) {
|
|
86
|
+
return toMs(at) + this.startedAtWall + this.pausedTotalMs;
|
|
87
|
+
}
|
|
88
|
+
/** 実際に理由を足したら true。呼び出し側が永続化や tick 停止の要否に使う。 */
|
|
89
|
+
freeze(reason) {
|
|
90
|
+
if (this.frozenBy.has(reason)) return false;
|
|
91
|
+
const wasFrozen = this.frozen;
|
|
92
|
+
this.frozenBy.add(reason);
|
|
93
|
+
if (!wasFrozen) this.pausedAtWall = Date.now();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** 実際に理由を外したら true。 */
|
|
97
|
+
unfreeze(reason) {
|
|
98
|
+
if (!this.frozenBy.delete(reason)) return false;
|
|
99
|
+
if (!this.frozen && this.pausedAtWall !== null) {
|
|
100
|
+
this.pausedTotalMs += Date.now() - this.pausedAtWall;
|
|
101
|
+
this.pausedAtWall = null;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
snapshot() {
|
|
106
|
+
return {
|
|
107
|
+
startedAtWall: this.startedAtWall,
|
|
108
|
+
pausedTotalMs: this.pausedTotalMs,
|
|
109
|
+
pausedAtWall: this.pausedAtWall,
|
|
110
|
+
frozenBy: [...this.frozenBy]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 永続化した内容から復元する。欠損は「未停止」に倒す。
|
|
115
|
+
*
|
|
116
|
+
* 読めなかったせいで世界が止まったままになる方が、動き出すより悪い。
|
|
117
|
+
*/
|
|
118
|
+
restore(saved) {
|
|
119
|
+
this.startedAtWall = saved.startedAtWall ?? 0;
|
|
120
|
+
this.pausedTotalMs = saved.pausedTotalMs ?? 0;
|
|
121
|
+
this.pausedAtWall = saved.pausedAtWall ?? null;
|
|
122
|
+
this.frozenBy.clear();
|
|
123
|
+
for (const reason of saved.frozenBy ?? []) this.frozenBy.add(reason);
|
|
124
|
+
if (this.frozen && this.pausedAtWall === null) this.frozenBy.clear();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* 作家の**同期**ハンドラを、時刻 API を差し替えた状態で走らせる。
|
|
129
|
+
*
|
|
130
|
+
* `sdk/src/dev-prediction-traps.ts` の runPredicted と同じ形。あちらは dev で警告を
|
|
131
|
+
* 出すための差し替えだが、こちらはゲーム内時計を返すための差し替え。
|
|
132
|
+
* `await` を挟まないので、JS が単一スレッドである以上ほかの部屋と混ざらない。
|
|
133
|
+
*
|
|
134
|
+
* IMPORTANT: 3 実装すべてで通すこと。ここが片方だけだと、生 `Date.now()` を書いた
|
|
135
|
+
* シナリオが「dev では壊れるのに本番では動く」(あるいはその逆) になる。
|
|
136
|
+
*
|
|
137
|
+
* 適用しないもの:
|
|
138
|
+
* - `serverActions` — async なので `await` をまたぐと復元が効かない
|
|
139
|
+
* - `deadlines[].at()` — state だけの純関数という契約があり、毎 state 変更で呼ばれる
|
|
140
|
+
*
|
|
141
|
+
* IMPORTANT: `fn` は**同期で完結すること**。`await` をまたぐと差し替えたまま制御が
|
|
142
|
+
* 抜け、無関係な処理まで偽の `Date.now` を読む。`fn` が Promise を返す形にしたく
|
|
143
|
+
* なったら、それは `serverActions` 側へ寄せるべき処理という合図。
|
|
144
|
+
*
|
|
145
|
+
* IMPORTANT: `fn` の中で [GameClock.now] を呼ばないこと。差し替えた `Date.now` を
|
|
146
|
+
* 実時刻として引き算してしまう。ハンドラへ渡す ctx は呼び出す前に組み立てる。
|
|
147
|
+
*/
|
|
148
|
+
function withGameClock(time, fn) {
|
|
149
|
+
const realNow = Date.now;
|
|
150
|
+
const RealDate = Date;
|
|
151
|
+
const ms = toMs(time);
|
|
152
|
+
Date.now = () => ms;
|
|
153
|
+
globalThis.Date = new Proxy(RealDate, { construct: (target, args) => Reflect.construct(target, args.length === 0 ? [ms] : args) });
|
|
154
|
+
try {
|
|
155
|
+
return fn();
|
|
156
|
+
} finally {
|
|
157
|
+
globalThis.Date = RealDate;
|
|
158
|
+
Date.now = realNow;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* `deadlines[].at()` が返した値を締切として使えるかに正規化する。使えなければ null。
|
|
163
|
+
*
|
|
164
|
+
* `null` / `undefined` は「締切なし」で正常なので黙って捨てる。**数値だが有限でない
|
|
165
|
+
* (NaN / Infinity) 場合だけは記録に残す。** これはほぼ確実に `undefined` を含む算術の
|
|
166
|
+
* 結果で、旧 `ctx.now` を読んでいるシナリオを新エンジンで動かすと
|
|
167
|
+
* `undefined + 尺 = NaN` になり、締切が「二度と来ない」形で静かに死ぬ。
|
|
168
|
+
* 黙って捨てるとログにも例外にも出ないので、key ごとに 1 回だけ必ず出す。
|
|
169
|
+
*
|
|
170
|
+
* `report` を受け取るのは、engine-core が `console` を含むどのランタイム API にも
|
|
171
|
+
* 依存できないため (3 ランタイムが lib ES2022 だけでこのファイルを共有している)。
|
|
172
|
+
*
|
|
173
|
+
* @param warned 既に警告した key。呼び出し側がインスタンスごとに持つ。
|
|
174
|
+
*/
|
|
175
|
+
function resolveDeadline(at, key, warned, report) {
|
|
176
|
+
if (at === null || at === void 0) return null;
|
|
177
|
+
if (typeof at === "number" && Number.isFinite(at)) return asGameTime(at);
|
|
178
|
+
if (!warned.has(key)) {
|
|
179
|
+
warned.add(key);
|
|
180
|
+
report(`deadline "${key}" の at() が締切に使えない値を返した: ${String(at)}。この締切は二度と発火しない。廃止された ctx.now を読んでいないか確認すること`);
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
11
185
|
//#region ../engine-core/src/json-patch.ts
|
|
12
186
|
function unescapePointer(token) {
|
|
13
187
|
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
@@ -101,35 +275,119 @@ function isServerOnlyAction(handler) {
|
|
|
101
275
|
/**
|
|
102
276
|
* @docs
|
|
103
277
|
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
278
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
104
279
|
*
|
|
105
|
-
*
|
|
280
|
+
* サーバーのゲーム内時計の推定値をクライアント全体へ配る。
|
|
106
281
|
*
|
|
107
|
-
* state に入っている `endsAt`
|
|
108
|
-
*
|
|
109
|
-
* (残り時間が恒久的に狂う / 期限判定がサーバーと食い違う)。読み取り側も同じ時計に
|
|
110
|
-
* 揃えるための関数。
|
|
282
|
+
* state に入っている `endsAt` のような締切はサーバーのゲーム内時計で打たれている。
|
|
283
|
+
* 読み取り側も同じ時計に揃えないと、残り時間が恒久的に狂う。
|
|
111
284
|
*
|
|
112
285
|
* ```ts
|
|
113
|
-
* const remain = Math.ceil((state.game.timerEndsAt
|
|
286
|
+
* const remain = Math.ceil(minus(state.game.timerEndsAt, gameTime()) / 1000);
|
|
114
287
|
* ```
|
|
115
288
|
*
|
|
289
|
+
* 基準は `performance.now()` (単調時計)。`Date.now()` を長さの計算に使うと、端末の時計が
|
|
290
|
+
* NTP やユーザー操作でずれた瞬間に全部のカウントダウンが飛ぶ。POSIX / Rust / C++ が
|
|
291
|
+
* 揃って禁じている形なので、ここでも採らない。
|
|
292
|
+
*
|
|
116
293
|
* オフセットは transport がサーバーからのメッセージを受けるたびに更新する。
|
|
117
|
-
* 未接続 /
|
|
294
|
+
* 未接続 / 未観測ならオフセット 0 のまま (オフラインでも壊れない)。
|
|
118
295
|
*/
|
|
119
296
|
let offset = 0;
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
297
|
+
/** 停止中に凍らせた値。null なら進んでいる。 */
|
|
298
|
+
let frozenAt = null;
|
|
299
|
+
/**
|
|
300
|
+
* transport から呼ぶ内部関数。サーバーが打刻したゲーム内時刻を観測してオフセットを更新する。
|
|
301
|
+
*
|
|
302
|
+
* 下り片道遅延を無視するので推定は実サーバー時刻より僅かに遅れる。
|
|
303
|
+
* 表示用途では無視できる誤差なので、精度より単純さを取る。
|
|
304
|
+
*/
|
|
305
|
+
function observeGameTime(t) {
|
|
306
|
+
if (typeof t !== "number" || !Number.isFinite(t)) return;
|
|
307
|
+
offset = t - performance.now();
|
|
124
308
|
}
|
|
125
309
|
/**
|
|
126
|
-
*
|
|
310
|
+
* ゲーム内時刻の推定値。停止中は凍る。
|
|
127
311
|
*
|
|
128
312
|
* カウントダウン描画のように毎秒/毎フレーム呼ぶ用途を想定しているので、
|
|
129
313
|
* state の到着とは無関係にいつでも呼べる。
|
|
130
314
|
*/
|
|
131
|
-
function
|
|
132
|
-
return
|
|
315
|
+
function gameTime() {
|
|
316
|
+
return frozenAt ?? asGameTime(performance.now() + offset);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 時計を凍らせる。
|
|
320
|
+
*
|
|
321
|
+
* 停止中はサーバーの broadcast が止まって打刻が来ないので、`__pause_state` を受けた
|
|
322
|
+
* 時点の値で明示的に止める。解除後は通常の打刻でオフセットが入れ替わるため、
|
|
323
|
+
* 特別な補正は要らない。
|
|
324
|
+
*/
|
|
325
|
+
function freezeGameTime() {
|
|
326
|
+
frozenAt = gameTime();
|
|
327
|
+
}
|
|
328
|
+
function unfreezeGameTime() {
|
|
329
|
+
frozenAt = null;
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/pause-state.ts
|
|
333
|
+
/**
|
|
334
|
+
* @docs
|
|
335
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
336
|
+
*
|
|
337
|
+
* 緊急停止の状態を SDK 全体で 1 箇所に持つ。
|
|
338
|
+
*
|
|
339
|
+
* 停止は state ではない。シナリオの state に持ち込むと「停止中は state を変えるコードが
|
|
340
|
+
* 1 行も走らない」という不変条件が崩れるので、`GameLogic` へは渡さず読み取り専用の
|
|
341
|
+
* 関数として出す。用途は演出の停止 (rAF ループを止める等) に限る。
|
|
342
|
+
*
|
|
343
|
+
* ゲーム内時計の凍結もここから駆動する。停止フラグと時計の凍結が別々に動くと、
|
|
344
|
+
* 「止まっているのにカウントダウンだけ進む」がいつか必ず起きる。
|
|
345
|
+
*/
|
|
346
|
+
let paused = false;
|
|
347
|
+
let pausedBy = null;
|
|
348
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
349
|
+
/** transport から呼ぶ内部関数。停止状態が変わったときだけ通知する。 */
|
|
350
|
+
function setPauseState(next, by) {
|
|
351
|
+
pausedBy = next ? by : null;
|
|
352
|
+
if (next === paused) return;
|
|
353
|
+
paused = next;
|
|
354
|
+
if (paused) freezeGameTime();
|
|
355
|
+
else unfreezeGameTime();
|
|
356
|
+
for (const cb of listeners) try {
|
|
357
|
+
cb(paused);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.warn("[uzu] onPauseChange listener threw:", err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** 緊急停止中か。 */
|
|
363
|
+
function isPaused() {
|
|
364
|
+
return paused;
|
|
365
|
+
}
|
|
366
|
+
/** 停止を要求した playerId。停止していなければ null。 */
|
|
367
|
+
function getPausedBy() {
|
|
368
|
+
return pausedBy;
|
|
369
|
+
}
|
|
370
|
+
/** 停止状態の変化を購読する。 */
|
|
371
|
+
function onPauseChange(cb) {
|
|
372
|
+
listeners.add(cb);
|
|
373
|
+
}
|
|
374
|
+
let requester = null;
|
|
375
|
+
function setPauseRequester(fn) {
|
|
376
|
+
requester = fn;
|
|
377
|
+
}
|
|
378
|
+
function requestPause() {
|
|
379
|
+
if (!requester) {
|
|
380
|
+
console.warn("[uzu] requestPause: サーバーへ接続していないので停止できません");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
requester(true);
|
|
384
|
+
}
|
|
385
|
+
function requestResume() {
|
|
386
|
+
if (!requester) {
|
|
387
|
+
console.warn("[uzu] requestResume: サーバーへ接続していないので解除できません");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
requester(false);
|
|
133
391
|
}
|
|
134
392
|
//#endregion
|
|
135
393
|
//#region src/room.ts
|
|
@@ -578,61 +836,75 @@ function replaceRoot(doc, value) {
|
|
|
578
836
|
}
|
|
579
837
|
//#endregion
|
|
580
838
|
//#region src/dev-prediction-traps.ts
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return ()
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
];
|
|
839
|
+
/** 描画経路の警告に使う擬似 action 名。 */
|
|
840
|
+
const RENDER_CONTEXT = "(render)";
|
|
841
|
+
const CLOCK_TRAPS = [{ install: (report) => {
|
|
842
|
+
const RealDate = Date;
|
|
843
|
+
const realNow = Date.now;
|
|
844
|
+
globalThis.Date = new Proxy(RealDate, { construct: (target, args) => {
|
|
845
|
+
if (args.length === 0) report("new Date()");
|
|
846
|
+
return Reflect.construct(target, args);
|
|
847
|
+
} });
|
|
848
|
+
Date.now = () => {
|
|
849
|
+
report("Date.now()");
|
|
850
|
+
return realNow();
|
|
851
|
+
};
|
|
852
|
+
return () => {
|
|
853
|
+
Date.now = realNow;
|
|
854
|
+
globalThis.Date = RealDate;
|
|
855
|
+
};
|
|
856
|
+
} }, { install: (report) => {
|
|
857
|
+
const real = globalThis.performance?.now;
|
|
858
|
+
if (!real) return () => {};
|
|
859
|
+
const bound = real.bind(globalThis.performance);
|
|
860
|
+
globalThis.performance.now = () => {
|
|
861
|
+
report("performance.now()");
|
|
862
|
+
return bound();
|
|
863
|
+
};
|
|
864
|
+
return () => {
|
|
865
|
+
globalThis.performance.now = real;
|
|
866
|
+
};
|
|
867
|
+
} }];
|
|
868
|
+
/** 乱数系。先読みでだけ問題になるので描画経路には張らない。 */
|
|
869
|
+
const RANDOM_TRAPS = [{ install: (report) => {
|
|
870
|
+
const real = Math.random;
|
|
871
|
+
Math.random = () => {
|
|
872
|
+
report("Math.random()");
|
|
873
|
+
return real();
|
|
874
|
+
};
|
|
875
|
+
return () => {
|
|
876
|
+
Math.random = real;
|
|
877
|
+
};
|
|
878
|
+
} }, { install: (report) => {
|
|
879
|
+
const real = globalThis.crypto?.randomUUID;
|
|
880
|
+
if (!real) return () => {};
|
|
881
|
+
const bound = real.bind(globalThis.crypto);
|
|
882
|
+
globalThis.crypto.randomUUID = () => {
|
|
883
|
+
report("crypto.randomUUID()");
|
|
884
|
+
return bound();
|
|
885
|
+
};
|
|
886
|
+
return () => {
|
|
887
|
+
globalThis.crypto.randomUUID = real;
|
|
888
|
+
};
|
|
889
|
+
} }];
|
|
890
|
+
const TRAPS = [...CLOCK_TRAPS, ...RANDOM_TRAPS];
|
|
633
891
|
const warnings = [];
|
|
634
892
|
const reported = /* @__PURE__ */ new Set();
|
|
635
893
|
const printWarning = (action, api) => {
|
|
894
|
+
if (action === RENDER_CONTEXT) {
|
|
895
|
+
console.groupCollapsed(`⚠️ [uzu-code] 描画中に ${api} を呼びました`);
|
|
896
|
+
console.log([
|
|
897
|
+
`${api} は Unix epoch の実時刻を返します。state に入っている締切は`,
|
|
898
|
+
"ゲーム内時刻 (ゲーム開始からの経過 ms) なので、引き算すると桁が合いません。",
|
|
899
|
+
"また実時刻は緊急停止中も進むので、止まっているのにカウントダウンだけ動きます。",
|
|
900
|
+
"",
|
|
901
|
+
"直し方: 時刻は gameTime() から取る。",
|
|
902
|
+
" import { gameTime, minus } from '@uzuhq/code-sdk';",
|
|
903
|
+
" const remain = Math.ceil(minus(state.timerEndsAt, gameTime()) / 1000);"
|
|
904
|
+
].join("\n"));
|
|
905
|
+
console.groupEnd();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
636
908
|
console.groupCollapsed(`⚠️ [uzu-code] action "${action}" が先読み中に ${api} を呼びました`);
|
|
637
909
|
console.log([
|
|
638
910
|
"先読み (楽観的更新) は「サーバーと同じコードを同じ入力で走らせれば同じ結果になる」",
|
|
@@ -670,12 +942,24 @@ const notifyParent = (action, api) => {
|
|
|
670
942
|
* Flutter native ホスト (本番) では計装せず素通しする。判定基準は dev hooks と同じ。
|
|
671
943
|
*/
|
|
672
944
|
const runPredicted = (action, run) => {
|
|
945
|
+
runInstrumented(action, run);
|
|
946
|
+
};
|
|
947
|
+
/**
|
|
948
|
+
* `onState` から同期で走る描画を計装する。
|
|
949
|
+
*
|
|
950
|
+
* 乱数は描画で使ってよいので、時刻の罠だけ張る。ここで拾えるのは同期の描画だけで、
|
|
951
|
+
* `requestAnimationFrame` の中は原理的に拾えない。
|
|
952
|
+
*/
|
|
953
|
+
const runRendered = (run) => {
|
|
954
|
+
runInstrumented(RENDER_CONTEXT, run);
|
|
955
|
+
};
|
|
956
|
+
const runInstrumented = (action, run) => {
|
|
673
957
|
if (typeof window === "undefined" || window.FlutterHost) {
|
|
674
958
|
run();
|
|
675
959
|
return;
|
|
676
960
|
}
|
|
677
961
|
const report = (api) => {
|
|
678
|
-
const key = `${action}
|
|
962
|
+
const key = `${action}\u0000${api}`;
|
|
679
963
|
if (reported.has(key)) return;
|
|
680
964
|
reported.add(key);
|
|
681
965
|
warnings.push({
|
|
@@ -685,7 +969,7 @@ const runPredicted = (action, run) => {
|
|
|
685
969
|
printWarning(action, api);
|
|
686
970
|
notifyParent(action, api);
|
|
687
971
|
};
|
|
688
|
-
const restores = TRAPS.map((trap) => trap.install(report));
|
|
972
|
+
const restores = (action === RENDER_CONTEXT ? CLOCK_TRAPS : TRAPS).map((trap) => trap.install(report));
|
|
689
973
|
try {
|
|
690
974
|
run();
|
|
691
975
|
} finally {
|
|
@@ -810,7 +1094,7 @@ function createOptimisticActionClient(config) {
|
|
|
810
1094
|
let actionSeq = 0;
|
|
811
1095
|
/**
|
|
812
1096
|
* 送信済みだがサーバー未確認の action キュー。
|
|
813
|
-
* `
|
|
1097
|
+
* `time` は送信時に推定した値。再適用でも同じ値を使う (取り直すと表示がガタつく)。
|
|
814
1098
|
*/
|
|
815
1099
|
const pendingActions = [];
|
|
816
1100
|
/**
|
|
@@ -866,7 +1150,7 @@ function createOptimisticActionClient(config) {
|
|
|
866
1150
|
displayState = structuredClone(confirmedState);
|
|
867
1151
|
let i = 0;
|
|
868
1152
|
while (i < pendingActions.length) {
|
|
869
|
-
const { action, payload,
|
|
1153
|
+
const { action, payload, time } = pendingActions[i];
|
|
870
1154
|
const handler = logic.actions[action];
|
|
871
1155
|
if (!handler || isServerOnlyAction(handler)) {
|
|
872
1156
|
pendingActions.splice(i, 1);
|
|
@@ -879,7 +1163,8 @@ function createOptimisticActionClient(config) {
|
|
|
879
1163
|
payload,
|
|
880
1164
|
playerId,
|
|
881
1165
|
ctx: {
|
|
882
|
-
|
|
1166
|
+
time,
|
|
1167
|
+
after: (d) => plus(time, d),
|
|
883
1168
|
emit: noopEmit
|
|
884
1169
|
}
|
|
885
1170
|
}));
|
|
@@ -906,12 +1191,16 @@ function createOptimisticActionClient(config) {
|
|
|
906
1191
|
};
|
|
907
1192
|
return {
|
|
908
1193
|
send(type, payload) {
|
|
1194
|
+
if (isPaused()) {
|
|
1195
|
+
console.warn(`[uzu] 緊急停止中は action を送れません (action="${type}")`);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
909
1198
|
actionSeq++;
|
|
910
1199
|
const seq = actionSeq;
|
|
911
1200
|
const handler = logic.actions[type];
|
|
912
1201
|
if (!handler && !logic.serverActions?.[type]) console.error(`[uzu] unknown action: "${type}". logic.actions / logic.serverActions のどちらにも登録されていません。`);
|
|
913
1202
|
const target = displayState;
|
|
914
|
-
const
|
|
1203
|
+
const time = gameTime();
|
|
915
1204
|
const predictEmit = (eventName, data) => {
|
|
916
1205
|
const subscription = events?.[eventName];
|
|
917
1206
|
if (!subscription?.predict) return;
|
|
@@ -929,7 +1218,8 @@ function createOptimisticActionClient(config) {
|
|
|
929
1218
|
payload: payload ?? {},
|
|
930
1219
|
playerId,
|
|
931
1220
|
ctx: {
|
|
932
|
-
|
|
1221
|
+
time,
|
|
1222
|
+
after: (d) => plus(time, d),
|
|
933
1223
|
emit: predictEmit
|
|
934
1224
|
}
|
|
935
1225
|
}));
|
|
@@ -937,7 +1227,7 @@ function createOptimisticActionClient(config) {
|
|
|
937
1227
|
seq,
|
|
938
1228
|
action: type,
|
|
939
1229
|
payload: payload ?? {},
|
|
940
|
-
|
|
1230
|
+
time
|
|
941
1231
|
});
|
|
942
1232
|
onState(target, playerId);
|
|
943
1233
|
} catch {}
|
|
@@ -947,8 +1237,8 @@ function createOptimisticActionClient(config) {
|
|
|
947
1237
|
seq
|
|
948
1238
|
});
|
|
949
1239
|
},
|
|
950
|
-
|
|
951
|
-
|
|
1240
|
+
observeGameTime(t) {
|
|
1241
|
+
observeGameTime(t);
|
|
952
1242
|
},
|
|
953
1243
|
applyState(state, options = {}) {
|
|
954
1244
|
handleAck(options.ack, options.from, options.events ?? []);
|
|
@@ -1013,6 +1303,10 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
|
|
|
1013
1303
|
}
|
|
1014
1304
|
}
|
|
1015
1305
|
});
|
|
1306
|
+
setPauseRequester((paused) => {
|
|
1307
|
+
console.log(`[SDK ServerAction] ➡ send ${paused ? "__pause" : "__resume"}`);
|
|
1308
|
+
ws.send(JSON.stringify({ type: paused ? "__pause" : "__resume" }));
|
|
1309
|
+
});
|
|
1016
1310
|
/** サーバー seq (delta の連続性チェック用) */
|
|
1017
1311
|
let serverSeq = 0;
|
|
1018
1312
|
/** フル state 再要求中フラグ (多重要求防止) */
|
|
@@ -1052,7 +1346,13 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
|
|
|
1052
1346
|
}
|
|
1053
1347
|
const msgType = parsed.type;
|
|
1054
1348
|
console.log(`[SDK ServerAction] ⬅ recv type=${msgType}`);
|
|
1055
|
-
client.
|
|
1349
|
+
client.observeGameTime(parsed.gameTime);
|
|
1350
|
+
if (msgType === "__pause_state") {
|
|
1351
|
+
const frozen = parsed.frozen === true;
|
|
1352
|
+
console.log(`[SDK ServerAction] ${frozen ? "⏸" : "▶️"} pause_state frozen=${frozen}`);
|
|
1353
|
+
setPauseState(frozen, parsed.by ?? null);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1056
1356
|
if (msgType === "__room_init") {
|
|
1057
1357
|
console.log(`[SDK ServerAction] ✅ Room init myId=${parsed.myId}`);
|
|
1058
1358
|
return;
|
|
@@ -1130,6 +1430,7 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
|
|
|
1130
1430
|
serverSeq = parsed.seq ?? 0;
|
|
1131
1431
|
requestStatePending = false;
|
|
1132
1432
|
console.log(`[SDK ServerAction] 🔄 State restored (reconnect/late join) seq=${serverSeq}`);
|
|
1433
|
+
setPauseState(parsed.frozen === true, parsed.pausedBy ?? null);
|
|
1133
1434
|
client.reset(parsed.state);
|
|
1134
1435
|
return;
|
|
1135
1436
|
}
|
|
@@ -1139,8 +1440,21 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
|
|
|
1139
1440
|
//#region src/run/local-server-action.ts
|
|
1140
1441
|
function runLocalServerAction(config) {
|
|
1141
1442
|
const { logic, inputs, events } = config;
|
|
1142
|
-
const onState = (next, id) =>
|
|
1443
|
+
const onState = (next, id) => {
|
|
1444
|
+
observeGameTime(gameTime());
|
|
1445
|
+
config.onState(next, id, "player");
|
|
1446
|
+
};
|
|
1143
1447
|
const tickRate = logic.tickRate ?? 0;
|
|
1448
|
+
const clock = new GameClock();
|
|
1449
|
+
const gameTime = () => clock.now();
|
|
1450
|
+
const frozen = () => clock.frozen;
|
|
1451
|
+
/** ハンドラへ渡す ctx の時刻部分。1 回の呼び出し内で値が動かないよう束ねる。 */
|
|
1452
|
+
const timeCtx = (t) => ({
|
|
1453
|
+
time: t,
|
|
1454
|
+
after: (d) => plus(t, d)
|
|
1455
|
+
});
|
|
1456
|
+
/** at() が壊れた値を返したと既に記録した締切。ログを 1 回に絞るため。 */
|
|
1457
|
+
const warnedDeadlines = /* @__PURE__ */ new Set();
|
|
1144
1458
|
const random = new SeededRandomImpl(Math.floor(Math.random() * 4294967295));
|
|
1145
1459
|
const players = Array.from({ length: config.playerCount }, (_, i) => ({
|
|
1146
1460
|
id: `local_${i}`,
|
|
@@ -1154,14 +1468,15 @@ function runLocalServerAction(config) {
|
|
|
1154
1468
|
if (!logic.deadlines) return null;
|
|
1155
1469
|
let earliest = null;
|
|
1156
1470
|
for (const [key, deadline] of Object.entries(logic.deadlines)) {
|
|
1157
|
-
let
|
|
1471
|
+
let raw;
|
|
1158
1472
|
try {
|
|
1159
|
-
|
|
1473
|
+
raw = deadline.at({ state });
|
|
1160
1474
|
} catch (err) {
|
|
1161
1475
|
console.error(`[Deadline] ❌ ${key}.at() で例外`, err);
|
|
1162
1476
|
continue;
|
|
1163
1477
|
}
|
|
1164
|
-
|
|
1478
|
+
const at = resolveDeadline(raw, key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
|
|
1479
|
+
if (at === null) continue;
|
|
1165
1480
|
if (earliest === null || at < earliest) earliest = at;
|
|
1166
1481
|
}
|
|
1167
1482
|
return earliest;
|
|
@@ -1169,28 +1484,28 @@ function runLocalServerAction(config) {
|
|
|
1169
1484
|
const fireDue = () => {
|
|
1170
1485
|
wakeupTimer = null;
|
|
1171
1486
|
wakeupAt = null;
|
|
1172
|
-
if (!logic.deadlines) return;
|
|
1173
|
-
const
|
|
1487
|
+
if (!logic.deadlines || frozen()) return;
|
|
1488
|
+
const time = gameTime();
|
|
1174
1489
|
const evts = [];
|
|
1175
1490
|
const firedKeys = [];
|
|
1176
1491
|
for (const [key, deadline] of Object.entries(logic.deadlines)) {
|
|
1177
1492
|
const pending = [];
|
|
1178
1493
|
let snapshot = null;
|
|
1179
1494
|
try {
|
|
1180
|
-
const at = deadline.at({ state });
|
|
1181
|
-
if (
|
|
1495
|
+
const at = resolveDeadline(deadline.at({ state }), key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
|
|
1496
|
+
if (at === null || at > time) continue;
|
|
1182
1497
|
snapshot = structuredClone(state);
|
|
1183
|
-
deadline.handler({
|
|
1498
|
+
withGameClock(time, () => deadline.handler({
|
|
1184
1499
|
state,
|
|
1185
1500
|
ctx: {
|
|
1186
|
-
|
|
1501
|
+
...timeCtx(time),
|
|
1187
1502
|
random,
|
|
1188
1503
|
emit: (name, data) => pending.push({
|
|
1189
1504
|
name,
|
|
1190
1505
|
data: data ?? {}
|
|
1191
1506
|
})
|
|
1192
1507
|
}
|
|
1193
|
-
});
|
|
1508
|
+
}));
|
|
1194
1509
|
evts.push(...pending);
|
|
1195
1510
|
firedKeys.push(key);
|
|
1196
1511
|
} catch (err) {
|
|
@@ -1205,29 +1520,35 @@ function runLocalServerAction(config) {
|
|
|
1205
1520
|
onState(state, myId);
|
|
1206
1521
|
};
|
|
1207
1522
|
const syncWakeup = () => {
|
|
1208
|
-
const next = nextDeadline();
|
|
1523
|
+
const next = frozen() ? null : nextDeadline();
|
|
1209
1524
|
if (next === wakeupAt) return;
|
|
1210
1525
|
if (wakeupTimer) clearTimeout(wakeupTimer);
|
|
1211
1526
|
wakeupTimer = null;
|
|
1212
1527
|
wakeupAt = next;
|
|
1213
1528
|
if (next === null) return;
|
|
1214
|
-
wakeupTimer = setTimeout(fireDue, Math.max(0, next - Date.now()));
|
|
1529
|
+
wakeupTimer = setTimeout(fireDue, Math.max(0, clock.toWall(next) - Date.now()));
|
|
1215
1530
|
};
|
|
1216
1531
|
const dispatchEvents = (evts) => {
|
|
1217
1532
|
for (const e of evts) events?.[e.name]?.handler(e.data);
|
|
1218
1533
|
};
|
|
1534
|
+
clock.restart();
|
|
1535
|
+
const setupTime = gameTime();
|
|
1219
1536
|
const setupArgs = {
|
|
1220
1537
|
players,
|
|
1221
1538
|
seats: players,
|
|
1222
1539
|
ctx: {
|
|
1223
1540
|
random,
|
|
1224
|
-
|
|
1541
|
+
...timeCtx(setupTime)
|
|
1225
1542
|
}
|
|
1226
1543
|
};
|
|
1227
|
-
let state = logic.setup(setupArgs);
|
|
1544
|
+
let state = withGameClock(setupTime, () => logic.setup(setupArgs));
|
|
1228
1545
|
let tick = 0;
|
|
1229
1546
|
const playerInputs = {};
|
|
1230
1547
|
const dispatchAction = (type, payload) => {
|
|
1548
|
+
if (frozen()) {
|
|
1549
|
+
console.warn(`[SDK LocalServerAction] 緊急停止中は action を実行しません (action="${type}")`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1231
1552
|
const plain = logic.actions[type];
|
|
1232
1553
|
const legacyServerOnly = isServerOnlyAction(plain) ? plain : null;
|
|
1233
1554
|
const server = logic.serverActions?.[type] ?? legacyServerOnly;
|
|
@@ -1242,18 +1563,18 @@ function runLocalServerAction(config) {
|
|
|
1242
1563
|
name,
|
|
1243
1564
|
data: data ?? {}
|
|
1244
1565
|
});
|
|
1245
|
-
const
|
|
1566
|
+
const time = gameTime();
|
|
1246
1567
|
if (plain && !legacyServerOnly) {
|
|
1247
1568
|
try {
|
|
1248
|
-
plain({
|
|
1569
|
+
withGameClock(time, () => plain({
|
|
1249
1570
|
state,
|
|
1250
1571
|
payload: payload ?? {},
|
|
1251
1572
|
playerId: myId,
|
|
1252
1573
|
ctx: {
|
|
1253
|
-
|
|
1574
|
+
...timeCtx(time),
|
|
1254
1575
|
emit: plainEmit
|
|
1255
1576
|
}
|
|
1256
|
-
});
|
|
1577
|
+
}));
|
|
1257
1578
|
} catch (err) {
|
|
1258
1579
|
console.warn("[SDK LocalServerAction] Action error:", err);
|
|
1259
1580
|
return;
|
|
@@ -1272,7 +1593,7 @@ function runLocalServerAction(config) {
|
|
|
1272
1593
|
ctx: {
|
|
1273
1594
|
tick,
|
|
1274
1595
|
random,
|
|
1275
|
-
|
|
1596
|
+
...timeCtx(time),
|
|
1276
1597
|
emit: serverEmit
|
|
1277
1598
|
}
|
|
1278
1599
|
});
|
|
@@ -1285,26 +1606,42 @@ function runLocalServerAction(config) {
|
|
|
1285
1606
|
onState(state, myId);
|
|
1286
1607
|
})();
|
|
1287
1608
|
};
|
|
1609
|
+
/**
|
|
1610
|
+
* ソロの停止 / 解除。サーバーが居ないので自分の時計を直接触る。
|
|
1611
|
+
*
|
|
1612
|
+
* `__uzu_dev.pauseTick()` とは別物。あちらは tick を止めるだけの開発者ツールで、
|
|
1613
|
+
* ゲーム内時計も action ゲートも持たない。流用しない。
|
|
1614
|
+
*/
|
|
1615
|
+
const applyPause = (paused) => {
|
|
1616
|
+
if (!(paused ? clock.freeze("emergency-stop") : clock.unfreeze("emergency-stop"))) return;
|
|
1617
|
+
observeGameTime(gameTime());
|
|
1618
|
+
setPauseState(paused, paused ? myId : null);
|
|
1619
|
+
observeGameTime(gameTime());
|
|
1620
|
+
syncWakeup();
|
|
1621
|
+
};
|
|
1622
|
+
setPauseRequester(applyPause);
|
|
1288
1623
|
inputs(dispatchAction);
|
|
1289
1624
|
syncWakeup();
|
|
1290
1625
|
onState(state, myId);
|
|
1291
1626
|
if (tickRate > 0) setInterval(() => {
|
|
1627
|
+
if (frozen()) return;
|
|
1292
1628
|
const tickEvents = [];
|
|
1293
1629
|
const tickEmit = (name, data) => tickEvents.push({
|
|
1294
1630
|
name,
|
|
1295
1631
|
data: data ?? {}
|
|
1296
1632
|
});
|
|
1633
|
+
const time = gameTime();
|
|
1297
1634
|
try {
|
|
1298
|
-
logic.update({
|
|
1635
|
+
withGameClock(time, () => logic.update({
|
|
1299
1636
|
state,
|
|
1300
1637
|
ctx: {
|
|
1301
1638
|
random,
|
|
1302
1639
|
tick,
|
|
1303
|
-
|
|
1640
|
+
...timeCtx(time),
|
|
1304
1641
|
emit: tickEmit,
|
|
1305
1642
|
playerInputs
|
|
1306
1643
|
}
|
|
1307
|
-
});
|
|
1644
|
+
}));
|
|
1308
1645
|
} catch (err) {
|
|
1309
1646
|
console.error(`[SDK LocalServerAction] tick error at tick=${tick}:`, err);
|
|
1310
1647
|
tick++;
|
|
@@ -1615,6 +1952,12 @@ function init(opts) {
|
|
|
1615
1952
|
const { x: fallbackX, y: fallbackY } = calcHudInsets(params);
|
|
1616
1953
|
document.documentElement.style.setProperty("--uzu-hud-inset-x", `${hudX ?? fallbackX}px`);
|
|
1617
1954
|
document.documentElement.style.setProperty("--uzu-hud-inset-y", `${hudY ?? fallbackY}px`);
|
|
1955
|
+
onPauseChange((paused) => {
|
|
1956
|
+
sendRaw("sdk", "pauseState", {
|
|
1957
|
+
paused,
|
|
1958
|
+
by: getPausedBy()
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1618
1961
|
sendRaw("sdk", "ready", {});
|
|
1619
1962
|
_initialized = true;
|
|
1620
1963
|
}
|
|
@@ -1716,7 +2059,7 @@ function run(config) {
|
|
|
1716
2059
|
serverTime: 0,
|
|
1717
2060
|
myId: myPlayerId
|
|
1718
2061
|
};
|
|
1719
|
-
origOnState(state, myPlayerId, mySeatKind);
|
|
2062
|
+
runRendered(() => origOnState(state, myPlayerId, mySeatKind));
|
|
1720
2063
|
notifyDevSnapshot(state);
|
|
1721
2064
|
}
|
|
1722
2065
|
};
|
|
@@ -1848,6 +2191,14 @@ function handleMessage(msg) {
|
|
|
1848
2191
|
_playersChangedHandlers.forEach((fn) => fn(players));
|
|
1849
2192
|
return;
|
|
1850
2193
|
}
|
|
2194
|
+
case "requestPause":
|
|
2195
|
+
console.log(`[SDK] ⏸ handleMessage sdk/requestPause`);
|
|
2196
|
+
requestPause();
|
|
2197
|
+
return;
|
|
2198
|
+
case "requestResume":
|
|
2199
|
+
console.log(`[SDK] ▶️ handleMessage sdk/requestResume`);
|
|
2200
|
+
requestResume();
|
|
2201
|
+
return;
|
|
1851
2202
|
}
|
|
1852
2203
|
return;
|
|
1853
2204
|
}
|
|
@@ -1886,4 +2237,4 @@ function calcHudInsets(params) {
|
|
|
1886
2237
|
};
|
|
1887
2238
|
}
|
|
1888
2239
|
//#endregion
|
|
1889
|
-
export { DEFAULT_ICON_URLS, ReconnectableWebSocket, Room, SERVER_TIME, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, firstFrameReady, gameReady, getPredictionWarnings, getRoom, init, isHosted, isServerOnlyAction, on, onPlayersChanged, onRoom, playBgm, playSound, run, send,
|
|
2240
|
+
export { DEFAULT_ICON_URLS, 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 };
|