@uzuhq/code-sdk 0.8.9 → 0.8.11

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/dist/index.js CHANGED
@@ -153,19 +153,19 @@ var GameClock = class {
153
153
  * IMPORTANT: `fn` の中で [GameClock.now] を呼ばないこと。差し替えた `Date.now` を
154
154
  * 実時刻として引き算してしまう。ハンドラへ渡す ctx は呼び出す前に組み立てる。
155
155
  */
156
- function withGameClock(time, fn) {
156
+ const withGameClock = (time, fn) => {
157
157
  const realNow = Date.now;
158
- const RealDate = Date;
158
+ const realDate = Date;
159
159
  const ms = toMs(time);
160
160
  Date.now = () => ms;
161
- globalThis.Date = new Proxy(RealDate, { construct: (target, args) => Reflect.construct(target, args.length === 0 ? [ms] : args) });
161
+ globalThis.Date = new Proxy(realDate, { construct: (target, args) => Reflect.construct(target, args.length === 0 ? [ms] : args) });
162
162
  try {
163
163
  return fn();
164
164
  } finally {
165
- globalThis.Date = RealDate;
165
+ globalThis.Date = realDate;
166
166
  Date.now = realNow;
167
167
  }
168
- }
168
+ };
169
169
  /**
170
170
  * `deadlines[].at()` が返した値を締切として使えるかに正規化する。使えなければ null。
171
171
  *
@@ -180,45 +180,41 @@ function withGameClock(time, fn) {
180
180
  *
181
181
  * @param warned 既に警告した key。呼び出し側がインスタンスごとに持つ。
182
182
  */
183
- function resolveDeadline(at, key, warned, report) {
183
+ const resolveDeadline = (at, key, warned, report) => {
184
184
  if (at === null || at === void 0) return null;
185
185
  if (typeof at === "number" && Number.isFinite(at)) return asGameTime(at);
186
186
  if (!warned.has(key)) {
187
187
  warned.add(key);
188
- report(`deadline "${key}" の at() が締切に使えない値を返した: ${String(at)}。この締切は二度と発火しない。廃止された ctx.now を読んでいないか確認すること`);
188
+ report(`deadline "${key}" の at() が締切に使えない値を返した: ${typeof at === "number" ? String(at) : JSON.stringify(at)}。この締切は二度と発火しない。廃止された ctx.now を読んでいないか確認すること`);
189
189
  }
190
190
  return null;
191
- }
191
+ };
192
192
  //#endregion
193
193
  //#region ../engine-core/src/json-patch.ts
194
- function unescapePointer(token) {
195
- return token.replace(/~1/g, "/").replace(/~0/g, "~");
196
- }
197
- function applyPatch(doc, ops) {
194
+ const isIndexable = (value) => typeof value === "object" && value !== null;
195
+ const unescapePointer = (token) => token.replace(/~1/g, "/").replace(/~0/g, "~");
196
+ /** `op.path` の親までを辿る。途中で潜れなくなったら null。 */
197
+ const resolveParent$1 = (doc, tokens) => {
198
+ let target = doc;
199
+ for (let i = 0; i < tokens.length - 1; i++) {
200
+ if (!isIndexable(target)) return null;
201
+ target = target[tokens[i]];
202
+ }
203
+ return isIndexable(target) ? target : null;
204
+ };
205
+ const applyPatch = (doc, ops) => {
198
206
  for (const op of ops) {
199
207
  const tokens = op.path.split("/").slice(1).map(unescapePointer);
200
208
  if (tokens.length === 0) return false;
201
- if (op.op === "replace" || op.op === "add") {
202
- let target = doc;
203
- for (let i = 0; i < tokens.length - 1; i++) {
204
- target = target?.[tokens[i]];
205
- if (target === void 0 || target === null) return false;
206
- }
207
- const lastKey = tokens[tokens.length - 1];
208
- target[lastKey] = op.value;
209
- } else if (op.op === "remove") {
210
- let target = doc;
211
- for (let i = 0; i < tokens.length - 1; i++) {
212
- target = target?.[tokens[i]];
213
- if (target === void 0 || target === null) return false;
214
- }
215
- const lastKey = tokens[tokens.length - 1];
216
- if (Array.isArray(target)) target.splice(Number(lastKey), 1);
217
- else delete target[lastKey];
218
- }
209
+ const target = resolveParent$1(doc, tokens);
210
+ if (target === null) return false;
211
+ const lastKey = tokens[tokens.length - 1];
212
+ if (op.op === "replace" || op.op === "add") target[lastKey] = op.value;
213
+ else if (Array.isArray(target)) target.splice(Number(lastKey), 1);
214
+ else Reflect.deleteProperty(target, lastKey);
219
215
  }
220
216
  return true;
221
- }
217
+ };
222
218
  //#endregion
223
219
  //#region ../engine-core/src/random.ts
224
220
  var SeededRandomImpl = class SeededRandomImpl {
@@ -267,17 +263,13 @@ var SeededRandomImpl = class SeededRandomImpl {
267
263
  * serverActions: { notifyExternal: async (state) => { ... } }
268
264
  * ```
269
265
  */
270
- function serverOnly(handler) {
271
- return Object.assign(handler, { __serverOnly: true });
272
- }
266
+ const serverOnly = (handler) => Object.assign(handler, { __serverOnly: true });
273
267
  /**
274
268
  * handler が `serverOnly()` で wrap されているか判定する。
275
269
  *
276
270
  * 移行期の互換用。`serverActions` へ移行済みの logic では常に false になる。
277
271
  */
278
- function isServerOnlyAction(handler) {
279
- return typeof handler === "function" && "__serverOnly" in handler && handler.__serverOnly === true;
280
- }
272
+ const isServerOnlyAction = (handler) => typeof handler === "function" && "__serverOnly" in handler && handler.__serverOnly === true;
281
273
  //#endregion
282
274
  //#region src/server-clock.ts
283
275
  /**
@@ -310,19 +302,19 @@ let frozenAt = null;
310
302
  * 下り片道遅延を無視するので推定は実サーバー時刻より僅かに遅れる。
311
303
  * 表示用途では無視できる誤差なので、精度より単純さを取る。
312
304
  */
313
- function observeGameTime(t) {
305
+ const observeGameTime = (t) => {
314
306
  if (typeof t !== "number" || !Number.isFinite(t)) return;
315
307
  offset = t - performance.now();
316
- }
308
+ };
317
309
  /**
318
310
  * ゲーム内時刻の推定値。停止中は凍る。
319
311
  *
320
312
  * カウントダウン描画のように毎秒/毎フレーム呼ぶ用途を想定しているので、
321
313
  * state の到着とは無関係にいつでも呼べる。
322
314
  */
323
- function gameTime() {
315
+ const gameTime = () => {
324
316
  return frozenAt ?? asGameTime(performance.now() + offset);
325
- }
317
+ };
326
318
  /**
327
319
  * 時計を凍らせる。
328
320
  *
@@ -330,12 +322,12 @@ function gameTime() {
330
322
  * 時点の値で明示的に止める。解除後は通常の打刻でオフセットが入れ替わるため、
331
323
  * 特別な補正は要らない。
332
324
  */
333
- function freezeGameTime() {
325
+ const freezeGameTime = () => {
334
326
  frozenAt = gameTime();
335
- }
336
- function unfreezeGameTime() {
327
+ };
328
+ const unfreezeGameTime = () => {
337
329
  frozenAt = null;
338
- }
330
+ };
339
331
  //#endregion
340
332
  //#region src/pause-state.ts
341
333
  /**
@@ -355,7 +347,7 @@ let paused = false;
355
347
  let pausedBy = null;
356
348
  const listeners$1 = /* @__PURE__ */ new Set();
357
349
  /** transport から呼ぶ内部関数。停止状態が変わったときだけ通知する。 */
358
- function setPauseState(next, by) {
350
+ const setPauseState = (next, by) => {
359
351
  pausedBy = next ? by : null;
360
352
  if (next === paused) return;
361
353
  paused = next;
@@ -366,37 +358,37 @@ function setPauseState(next, by) {
366
358
  } catch (err) {
367
359
  console.warn("[uzu] onPauseChange listener threw:", err);
368
360
  }
369
- }
361
+ };
370
362
  /** 緊急停止中か。 */
371
- function isPaused() {
363
+ const isPaused = () => {
372
364
  return paused;
373
- }
365
+ };
374
366
  /** 停止を要求した playerId。停止していなければ null。 */
375
- function getPausedBy() {
367
+ const getPausedBy = () => {
376
368
  return pausedBy;
377
- }
369
+ };
378
370
  /** 停止状態の変化を購読する。 */
379
- function onPauseChange(cb) {
371
+ const onPauseChange = (cb) => {
380
372
  listeners$1.add(cb);
381
- }
373
+ };
382
374
  let requester = null;
383
- function setPauseRequester(fn) {
375
+ const setPauseRequester = (fn) => {
384
376
  requester = fn;
385
- }
386
- function requestPause() {
377
+ };
378
+ const requestPause = () => {
387
379
  if (!requester) {
388
380
  console.warn("[uzu] requestPause: サーバーへ接続していないので停止できません");
389
381
  return;
390
382
  }
391
383
  requester(true);
392
- }
393
- function requestResume() {
384
+ };
385
+ const requestResume = () => {
394
386
  if (!requester) {
395
387
  console.warn("[uzu] requestResume: サーバーへ接続していないので解除できません");
396
388
  return;
397
389
  }
398
390
  requester(false);
399
- }
391
+ };
400
392
  //#endregion
401
393
  //#region src/reconnectable-ws.ts
402
394
  const DEFAULTS = {
@@ -447,14 +439,12 @@ var ReconnectableWebSocket = class {
447
439
  if (this.messageBuffer.length < this.opts.maxBufferSize) this.messageBuffer.push(data);
448
440
  }
449
441
  }
450
- addEventListener(type, handler) {
451
- if (type === "message") this.messageHandlers.push(handler);
442
+ addEventListener(_type, handler) {
443
+ this.messageHandlers.push(handler);
452
444
  }
453
- removeEventListener(type, handler) {
454
- if (type === "message") {
455
- const index = this.messageHandlers.indexOf(handler);
456
- if (index !== -1) this.messageHandlers.splice(index, 1);
457
- }
445
+ removeEventListener(_type, handler) {
446
+ const index = this.messageHandlers.indexOf(handler);
447
+ if (index !== -1) this.messageHandlers.splice(index, 1);
458
448
  }
459
449
  /** バッファをクリア(権威的 state 受信時に呼ぶ) */
460
450
  clearBuffer() {
@@ -545,7 +535,7 @@ var ReconnectableWebSocket = class {
545
535
  return Math.max(0, exponentialDelay + jitter);
546
536
  }
547
537
  flushBuffer() {
548
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
538
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
549
539
  const buffer = this.messageBuffer;
550
540
  this.messageBuffer = [];
551
541
  for (const data of buffer) try {
@@ -600,7 +590,7 @@ var ReconnectableWebSocket = class {
600
590
  *
601
591
  * 型 mismatch (例: object → primitive) は overwrite で許可する。
602
592
  */
603
- function applyJsonMergePatch(target, patch) {
593
+ const applyJsonMergePatch = (target, patch) => {
604
594
  if (Array.isArray(target)) throw new TypeError("[uzu_dev] mergeRawState: cannot deep-merge into an array field. Use patchRawState ({ op: \"replace\", path: \"/.../<index>\" }) for element-level writes, or pass a full array to atomic-replace it.");
605
595
  for (const key of Object.keys(patch)) {
606
596
  const value = patch[key];
@@ -615,7 +605,7 @@ function applyJsonMergePatch(target, patch) {
615
605
  applyJsonMergePatch(current, value);
616
606
  } else target[key] = value;
617
607
  }
618
- }
608
+ };
619
609
  /**
620
610
  * JSON Pointer (RFC 6901) を tokens に分解する。
621
611
  * `/board/1/~04` → `['board', '1', '~4']`
@@ -626,16 +616,16 @@ function applyJsonMergePatch(target, patch) {
626
616
  *
627
617
  * 復号順は ~1 → ~0。逆順で解くと `~01` が `/` に化けるので注意。
628
618
  */
629
- function parseJsonPointer(pointer) {
619
+ const parseJsonPointer = (pointer) => {
630
620
  if (pointer === "") return [];
631
621
  if (!pointer.startsWith("/")) throw new TypeError(`[uzu_dev] invalid JSON Pointer: ${pointer} (must start with "/")`);
632
622
  return pointer.slice(1).split("/").map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
633
- }
623
+ };
634
624
  /**
635
625
  * JSON Pointer の **親 container** と **末端 key** に解決する。
636
626
  * root pointer (`''`) は対応外: 呼び出し側で別途扱う。
637
627
  */
638
- function resolveParent(doc, tokens) {
628
+ const resolveParent = (doc, tokens) => {
639
629
  if (tokens.length === 0) throw new TypeError("[uzu_dev] root pointer (\"\") is not addressable by this helper");
640
630
  let current = doc;
641
631
  for (let i = 0; i < tokens.length - 1; i++) {
@@ -651,12 +641,12 @@ function resolveParent(doc, tokens) {
651
641
  parent: current,
652
642
  key: tokens[tokens.length - 1]
653
643
  };
654
- }
644
+ };
655
645
  /**
656
646
  * JSON Pointer から値を取得する。root pointer は doc 自体。
657
647
  * 存在しない path は throw する。
658
648
  */
659
- function getByPointer(doc, pointer) {
649
+ const getByPointer = (doc, pointer) => {
660
650
  const tokens = parseJsonPointer(pointer);
661
651
  let current = doc;
662
652
  for (const token of tokens) {
@@ -671,12 +661,12 @@ function getByPointer(doc, pointer) {
671
661
  }
672
662
  }
673
663
  return current;
674
- }
664
+ };
675
665
  /**
676
666
  * Array index token を numeric index に解決する。
677
667
  * `'-'` は append (RFC 6902 仕様、`add` 操作のみ許可) で `length` を返す。
678
668
  */
679
- function parseArrayIndex(token, length, allowAppend) {
669
+ const parseArrayIndex = (token, length, allowAppend) => {
680
670
  if (token === "-") {
681
671
  if (!allowAppend) throw new TypeError("[uzu_dev] array append (\"-\") is only valid for `add` operations");
682
672
  return length;
@@ -685,19 +675,19 @@ function parseArrayIndex(token, length, allowAppend) {
685
675
  const idx = Number(token);
686
676
  if (idx < 0 || idx > length) throw new RangeError(`[uzu_dev] array index ${idx} out of range (length=${length})`);
687
677
  return idx;
688
- }
678
+ };
689
679
  /**
690
680
  * 値の深い等価判定 (RFC 6902 `test` 用)。`JSON.stringify` ベース。
691
681
  * `undefined` を含む値や key 順序差まで真面目に見るほどには使わないので十分。
692
682
  */
693
- function deepEqual(a, b) {
683
+ const deepEqual = (a, b) => {
694
684
  if (a === b) return true;
695
685
  try {
696
686
  return JSON.stringify(a) === JSON.stringify(b);
697
687
  } catch {
698
688
  return false;
699
689
  }
700
- }
690
+ };
701
691
  /**
702
692
  * RFC 6902 (JSON Patch) operations を target に in-place 適用する。
703
693
  *
@@ -709,10 +699,10 @@ function deepEqual(a, b) {
709
699
  * doc のフィールドを丸ごと差し替える形で動く (= 結局 `setRawState` で十分な
710
700
  * ケースなので、わざわざ `patchRawState` で使うことはほぼない)。
711
701
  */
712
- function applyJsonPatch(target, ops) {
702
+ const applyJsonPatch = (target, ops) => {
713
703
  for (const op of ops) applyOne(target, op);
714
- }
715
- function applyOne(doc, op) {
704
+ };
705
+ const applyOne = (doc, op) => {
716
706
  switch (op.op) {
717
707
  case "add": {
718
708
  const tokens = parseJsonPointer(op.path);
@@ -736,7 +726,7 @@ function applyOne(doc, op) {
736
726
  parent.splice(idx, 1);
737
727
  } else {
738
728
  if (!(key in parent)) throw new TypeError(`[uzu_dev] cannot remove non-existent path "${op.path}"`);
739
- delete parent[key];
729
+ Reflect.deleteProperty(parent, key);
740
730
  }
741
731
  return;
742
732
  }
@@ -781,36 +771,38 @@ function applyOne(doc, op) {
781
771
  });
782
772
  return;
783
773
  }
784
- case "test":
785
- if (!deepEqual(getByPointer(doc, op.path), op.value)) throw new Error(`[uzu_dev] test failed at "${op.path}"`);
774
+ case "test": {
775
+ const value = getByPointer(doc, op.path);
776
+ if (!deepEqual(value, op.value)) throw new Error(`[uzu_dev] test failed at "${op.path}"`);
786
777
  return;
778
+ }
787
779
  default: throw new TypeError(`[uzu_dev] unknown JSON Patch op: ${JSON.stringify(op)}`);
788
780
  }
789
- }
790
- function isProperPrefixPointer(prefix, path) {
781
+ };
782
+ const isProperPrefixPointer = (prefix, path) => {
791
783
  if (prefix === "") return path !== "";
792
784
  return path === prefix || path.startsWith(`${prefix}/`);
793
- }
785
+ };
794
786
  /**
795
787
  * root pointer (`''`) への add / replace 用。doc を in-place で空にして
796
788
  * value の中身を流し込む。value が object でない場合は Record の形に
797
789
  * 入らないので throw する (root を primitive / array に置換はサポート対象外、
798
790
  * setRawState を使うべき)。
799
791
  */
800
- function replaceRoot(doc, value) {
792
+ const replaceRoot = (doc, value) => {
801
793
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("[uzu_dev] root replace requires a plain object value; use setRawState for full state replacement");
802
- for (const key of Object.keys(doc)) delete doc[key];
794
+ for (const key of Object.keys(doc)) Reflect.deleteProperty(doc, key);
803
795
  Object.assign(doc, value);
804
- }
796
+ };
805
797
  //#endregion
806
798
  //#region src/dev-prediction-traps.ts
807
799
  /** 描画経路の警告に使う擬似 action 名。 */
808
800
  const RENDER_CONTEXT = "(render)";
809
801
  /** 実時刻。先読みでも描画でもサーバーとズレる。 */
810
802
  const WALL_CLOCK_TRAPS = [{ install: (report) => {
811
- const RealDate = Date;
803
+ const realDate = Date;
812
804
  const realNow = Date.now;
813
- globalThis.Date = new Proxy(RealDate, { construct: (target, args) => {
805
+ globalThis.Date = new Proxy(realDate, { construct: (target, args) => {
814
806
  if (args.length === 0) report("new Date()");
815
807
  return Reflect.construct(target, args);
816
808
  } });
@@ -820,7 +812,7 @@ const WALL_CLOCK_TRAPS = [{ install: (report) => {
820
812
  };
821
813
  return () => {
822
814
  Date.now = realNow;
823
- globalThis.Date = RealDate;
815
+ globalThis.Date = realDate;
824
816
  };
825
817
  } }];
826
818
  /**
@@ -838,8 +830,8 @@ const WALL_CLOCK_TRAPS = [{ install: (report) => {
838
830
  * これは正しい検出になる。
839
831
  */
840
832
  const MONOTONIC_TRAPS = [{ install: (report) => {
841
- const real = globalThis.performance?.now;
842
- if (!real) return () => {};
833
+ const real = globalThis.performance.now;
834
+ if (typeof real !== "function") return () => {};
843
835
  const bound = real.bind(globalThis.performance);
844
836
  globalThis.performance.now = () => {
845
837
  report("performance.now()");
@@ -860,8 +852,8 @@ const RANDOM_TRAPS = [{ install: (report) => {
860
852
  Math.random = real;
861
853
  };
862
854
  } }, { install: (report) => {
863
- const real = globalThis.crypto?.randomUUID;
864
- if (!real) return () => {};
855
+ const real = globalThis.crypto.randomUUID;
856
+ if (typeof real !== "function") return () => {};
865
857
  const bound = real.bind(globalThis.crypto);
866
858
  globalThis.crypto.randomUUID = () => {
867
859
  report("crypto.randomUUID()");
@@ -970,7 +962,7 @@ const runInstrumented = (action, run) => {
970
962
  const getPredictionWarnings = () => warnings;
971
963
  //#endregion
972
964
  //#region src/dev-hooks.ts
973
- function createDevHooks(ctx) {
965
+ const createDevHooks = (ctx) => {
974
966
  const getRawState = () => ctx.getRawState ? ctx.getRawState() : null;
975
967
  const waitForSnapshot = (predicate, options) => {
976
968
  const timeoutMs = options?.timeoutMs ?? 1e4;
@@ -981,7 +973,7 @@ function createDevHooks(ctx) {
981
973
  try {
982
974
  ok = predicate(current);
983
975
  } catch (err) {
984
- reject(err);
976
+ reject(err instanceof Error ? err : new Error(String(err)));
985
977
  return;
986
978
  }
987
979
  if (ok) {
@@ -1000,7 +992,7 @@ function createDevHooks(ctx) {
1000
992
  } catch (err) {
1001
993
  clearTimeout(timer);
1002
994
  unsubscribe();
1003
- reject(err);
995
+ reject(err instanceof Error ? err : new Error(String(err)));
1004
996
  return;
1005
997
  }
1006
998
  if (ok) {
@@ -1068,10 +1060,10 @@ function createDevHooks(ctx) {
1068
1060
  hooks.subscribeEvents = (cb) => subscribeEvents(cb);
1069
1061
  }
1070
1062
  return hooks;
1071
- }
1072
- function attachDevHooks(ctx) {
1063
+ };
1064
+ const attachDevHooks = (ctx) => {
1073
1065
  window.__uzu_dev = createDevHooks(ctx);
1074
- }
1066
+ };
1075
1067
  //#endregion
1076
1068
  //#region src/insets.ts
1077
1069
  const SIDES = [
@@ -1118,7 +1110,7 @@ const toFinite = (value) => {
1118
1110
  * `env()` は JS から直接読めず、CSS 変数へ入れても `getComputedStyle` は代入前の
1119
1111
  * トークン列を返す。padding に置いて算出値を読むのが唯一の手。
1120
1112
  */
1121
- function measureEnvInsets() {
1113
+ const measureEnvInsets = () => {
1122
1114
  if (typeof document === "undefined") return zero();
1123
1115
  if (!envProbe?.isConnected) {
1124
1116
  const el = document.createElement("div");
@@ -1138,18 +1130,18 @@ function measureEnvInsets() {
1138
1130
  bottom: toFinite(style.paddingBottom) ?? 0,
1139
1131
  left: toFinite(style.paddingLeft) ?? 0
1140
1132
  };
1141
- }
1142
- function setVar(name, value) {
1133
+ };
1134
+ const setVar = (name, value) => {
1143
1135
  if (typeof document === "undefined") return;
1144
1136
  document.documentElement.style.setProperty(name, value);
1145
- }
1146
- function writeSafeAreaVars() {
1137
+ };
1138
+ const writeSafeAreaVars = () => {
1147
1139
  for (const side of SIDES) setVar(`--uzu-safe-area-inset-${side}`, hostSafeArea ? `${hostSafeArea[side]}px` : envExpr(side));
1148
- }
1149
- function writeHudVars() {
1140
+ };
1141
+ const writeHudVars = () => {
1150
1142
  setVar("--uzu-hud-inset-x", `${hud.x}px`);
1151
1143
  setVar("--uzu-hud-inset-y", `${hud.y}px`);
1152
- }
1144
+ };
1153
1145
  /**
1154
1146
  * 合成済みの層。下 2 層を参照する式で書くので、safe area が実数でも `env()` でも
1155
1147
  * 同じ 1 本で成立する。
@@ -1158,13 +1150,13 @@ function writeHudVars() {
1158
1150
  * HUD の高さ分だけ空ければその下は全域が安全で、`left` にも足すと L 字全体を避ける
1159
1151
  * ことになり過剰に削れる。精密に避けたいシナリオは下 2 層を自分で合成する。
1160
1152
  */
1161
- function writeContentVars() {
1153
+ const writeContentVars = () => {
1162
1154
  setVar("--uzu-content-inset-top", "calc(var(--uzu-safe-area-inset-top, 0px) + var(--uzu-hud-inset-y, 0px))");
1163
1155
  setVar("--uzu-content-inset-right", "var(--uzu-safe-area-inset-right, 0px)");
1164
1156
  setVar("--uzu-content-inset-bottom", "var(--uzu-safe-area-inset-bottom, 0px)");
1165
1157
  setVar("--uzu-content-inset-left", "var(--uzu-safe-area-inset-left, 0px)");
1166
- }
1167
- function compute() {
1158
+ };
1159
+ const compute = () => {
1168
1160
  const safeArea = hostSafeArea ?? measureEnvInsets();
1169
1161
  return {
1170
1162
  safeArea,
@@ -1176,11 +1168,11 @@ function compute() {
1176
1168
  },
1177
1169
  hud: { ...hud }
1178
1170
  };
1179
- }
1171
+ };
1180
1172
  const sameInsets = (a, b) => SIDES.every((side) => a[side] === b[side]);
1181
1173
  const same = (a, b) => sameInsets(a.safeArea, b.safeArea) && a.hud.x === b.hud.x && a.hud.y === b.hud.y;
1182
1174
  /** 値が動いたときだけ購読者へ流す。 */
1183
- function refresh() {
1175
+ const refresh = () => {
1184
1176
  const next = compute();
1185
1177
  if (current && same(current, next)) return;
1186
1178
  current = next;
@@ -1189,7 +1181,7 @@ function refresh() {
1189
1181
  } catch (err) {
1190
1182
  console.warn("[UZU SDK] onInsetsChange listener threw:", err);
1191
1183
  }
1192
- }
1184
+ };
1193
1185
  /**
1194
1186
  * HUD 回避領域の右下座標を算出する。
1195
1187
  * クエリパラメータに uzuHudInsetX/Y がない場合のフォールバック値として使用。
@@ -1201,7 +1193,7 @@ function refresh() {
1201
1193
  * - players が取得できる場合: 2人以上→アクション数2、1人→アクション数0
1202
1194
  * - players が取得できない場合: アクション数2 (安全側に倒す)
1203
1195
  */
1204
- function fallbackHudInsets(params) {
1196
+ const fallbackHudInsets = (params) => {
1205
1197
  let actionCount = 2;
1206
1198
  const json = params.get("players");
1207
1199
  if (json) try {
@@ -1213,8 +1205,8 @@ function fallbackHudInsets(params) {
1213
1205
  x,
1214
1206
  y: 52
1215
1207
  };
1216
- }
1217
- function readSafeAreaParams(params) {
1208
+ };
1209
+ const readSafeAreaParams = (params) => {
1218
1210
  const values = zero();
1219
1211
  let supplied = false;
1220
1212
  for (const side of SIDES) {
@@ -1226,7 +1218,7 @@ function readSafeAreaParams(params) {
1226
1218
  supplied = true;
1227
1219
  }
1228
1220
  return supplied ? values : null;
1229
- }
1221
+ };
1230
1222
  /**
1231
1223
  * URL パラメータから初期値を確定させる。`init()` が 1 度だけ呼ぶ。
1232
1224
  *
@@ -1236,7 +1228,7 @@ function readSafeAreaParams(params) {
1236
1228
  *
1237
1229
  * [discord/embedded-app-sdk#304]: https://github.com/discord/embedded-app-sdk/issues/304
1238
1230
  */
1239
- function applyInsets(params) {
1231
+ const applyInsets = (params) => {
1240
1232
  const hudX = toFinite(params.get("uzuHudInsetX"));
1241
1233
  const hudY = toFinite(params.get("uzuHudInsetY"));
1242
1234
  const fallback = fallbackHudInsets(params);
@@ -1253,12 +1245,12 @@ function applyInsets(params) {
1253
1245
  window.addEventListener("resize", refresh);
1254
1246
  window.addEventListener("orientationchange", refresh);
1255
1247
  }
1256
- }
1248
+ };
1257
1249
  /**
1258
1250
  * ホストからの `insetsChanged` を反映する。回転 / PiP で値が変わったときに届く。
1259
1251
  * 内部用 (bridge の受信口から呼ばれる)。
1260
1252
  */
1261
- function handleInsetsChanged(payload) {
1253
+ const handleInsetsChanged = (payload) => {
1262
1254
  const safeArea = payload.safeArea;
1263
1255
  if (safeArea) {
1264
1256
  const next = zero();
@@ -1275,17 +1267,17 @@ function handleInsetsChanged(payload) {
1275
1267
  writeHudVars();
1276
1268
  }
1277
1269
  refresh();
1278
- }
1270
+ };
1279
1271
  /**
1280
1272
  * 現在の inset を返す。
1281
1273
  *
1282
1274
  * canvas / WebGL は CSS 変数で避けられないので、そこから読む口。値は初回ペイント前に
1283
1275
  * 確定しているので、setup の中で読んでよい。
1284
1276
  */
1285
- function getInsets() {
1277
+ const getInsets = () => {
1286
1278
  if (!current) refresh();
1287
1279
  return current ?? compute();
1288
- }
1280
+ };
1289
1281
  /**
1290
1282
  * inset の変化を購読する。戻り値を呼ぶと解除できる。
1291
1283
  *
@@ -1294,12 +1286,12 @@ function getInsets() {
1294
1286
  *
1295
1287
  * 登録時に即時呼び出しはしない。初期値は [getInsets] で読むこと。
1296
1288
  */
1297
- function onInsetsChange(cb) {
1289
+ const onInsetsChange = (cb) => {
1298
1290
  listeners.add(cb);
1299
1291
  return () => {
1300
1292
  listeners.delete(cb);
1301
1293
  };
1302
- }
1294
+ };
1303
1295
  //#endregion
1304
1296
  //#region src/scenario-callback.ts
1305
1297
  /**
@@ -1331,44 +1323,97 @@ const callScenario = (label, fn) => {
1331
1323
  }
1332
1324
  };
1333
1325
  //#endregion
1334
- //#region src/run/optimistic-action-client.ts
1335
- function createOptimisticActionClient(config) {
1336
- const { logic, playerId, events, sendAction } = config;
1337
- /** 呼び出し箇所ごとに守らず、シナリオの描画へ出る唯一の口をここで塞ぐ。 */
1338
- const onState = (state, id) => {
1339
- callScenario("描画 (onState)", () => config.onState(state, id));
1326
+ //#region src/run/local-server-action.ts
1327
+ const runLocalServerAction = (config) => {
1328
+ const { logic, inputs, events } = config;
1329
+ const onState = (next, id) => {
1330
+ observeGameTime(gameTime());
1331
+ callScenario("描画 (onState)", () => config.onState(next, id, "player"));
1332
+ };
1333
+ const tickRate = logic.tickRate ?? 0;
1334
+ const clock = new GameClock();
1335
+ const gameTime = () => clock.now();
1336
+ const frozen = () => clock.frozen;
1337
+ /** ハンドラへ渡す ctx の時刻部分。1 回の呼び出し内で値が動かないよう束ねる。 */
1338
+ const timeCtx = (t) => ({
1339
+ time: t,
1340
+ after: (d) => plus(t, d)
1341
+ });
1342
+ /** at() が壊れた値を返したと既に記録した締切。ログを 1 回に絞るため。 */
1343
+ const warnedDeadlines = /* @__PURE__ */ new Set();
1344
+ const random = new SeededRandomImpl(Math.floor(Math.random() * 4294967295));
1345
+ const players = Array.from({ length: config.playerCount }, (_, i) => ({
1346
+ id: `local_${i}`,
1347
+ nickname: `Player ${i + 1}`,
1348
+ iconUrl: DEFAULT_ICON_URLS[i % DEFAULT_ICON_URLS.length]
1349
+ }));
1350
+ const myId = players[0].id;
1351
+ let wakeupTimer = null;
1352
+ let wakeupAt = null;
1353
+ const nextDeadline = () => {
1354
+ if (!logic.deadlines) return null;
1355
+ let earliest = null;
1356
+ for (const [key, deadline] of Object.entries(logic.deadlines)) {
1357
+ let raw;
1358
+ try {
1359
+ raw = deadline.at({ state });
1360
+ } catch (err) {
1361
+ console.error(`[Deadline] ❌ ${key}.at() で例外`, err);
1362
+ continue;
1363
+ }
1364
+ const at = resolveDeadline(raw, key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
1365
+ if (at === null) continue;
1366
+ if (earliest === null || at < earliest) earliest = at;
1367
+ }
1368
+ return earliest;
1369
+ };
1370
+ const fireDue = () => {
1371
+ wakeupTimer = null;
1372
+ wakeupAt = null;
1373
+ if (!logic.deadlines || frozen()) return;
1374
+ const time = gameTime();
1375
+ const evts = [];
1376
+ const firedKeys = [];
1377
+ for (const [key, deadline] of Object.entries(logic.deadlines)) {
1378
+ const pending = [];
1379
+ let snapshot = null;
1380
+ try {
1381
+ const at = resolveDeadline(deadline.at({ state }), key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
1382
+ if (at === null || at > time) continue;
1383
+ snapshot = structuredClone(state);
1384
+ withGameClock(time, () => deadline.handler({
1385
+ state,
1386
+ ctx: {
1387
+ ...timeCtx(time),
1388
+ random,
1389
+ emit: (name, data) => pending.push({
1390
+ name,
1391
+ data: data ?? {}
1392
+ })
1393
+ }
1394
+ }));
1395
+ evts.push(...pending);
1396
+ firedKeys.push(key);
1397
+ } catch (err) {
1398
+ if (snapshot !== null) state = snapshot;
1399
+ console.error(`[Deadline] ❌ ${key} で例外`, err);
1400
+ }
1401
+ }
1402
+ syncWakeup();
1403
+ if (firedKeys.length === 0) return;
1404
+ console.log(`[Deadline] ⏰ ${firedKeys.length} 件発火: ${firedKeys.join(", ")}`);
1405
+ dispatchEvents(evts);
1406
+ onState(state, myId);
1407
+ };
1408
+ const syncWakeup = () => {
1409
+ const next = frozen() ? null : nextDeadline();
1410
+ if (next === wakeupAt) return;
1411
+ if (wakeupTimer) clearTimeout(wakeupTimer);
1412
+ wakeupTimer = null;
1413
+ wakeupAt = next;
1414
+ if (next === null) return;
1415
+ wakeupTimer = setTimeout(fireDue, Math.max(0, clock.toWall(next) - Date.now()));
1340
1416
  };
1341
- /** サーバー確定 state (楽観的更新のベース) */
1342
- let confirmedState = null;
1343
- /** 表示用 state (pending actions 適用済み) */
1344
- let displayState = null;
1345
- /** クライアント側の action 通番 */
1346
- let actionSeq = 0;
1347
- /**
1348
- * 送信済みだがサーバー未確認の action キュー。
1349
- * `time` は送信時に推定した値。再適用でも同じ値を使う (取り直すと表示がガタつく)。
1350
- */
1351
- const pendingActions = [];
1352
- /**
1353
- * 先読みで実行済みの events。
1354
- *
1355
- * IMPORTANT: pending action ごとではなくクライアント単位で持つ。二重実行は「自分の
1356
- * action の ack」だけでなく「他プレイヤーの action のブロードキャスト」でも起きるため
1357
- * (A と B が同時に同じ行送りを撃つと、B は自分の先読みで実行済みなのに A 由来の
1358
- * 配信でもう一度実行してしまう)。どの配信が来ても、まずここと突き合わせる。
1359
- *
1360
- * 記録には「どの action の先読みで実行したか」(seq) を持たせる。その action が ack
1361
- * された時点で引き当てられていない記録は「予測したが実際には起きなかった出来事」なので
1362
- * 捨てる。seq を持たせずに「pending が空になったら捨てる」だけにすると、別の action が
1363
- * 未確定な間ずっと外れた記録が生き残り、その後に本当に起きた同名イベントを 1 回
1364
- * 握り潰してしまう。
1365
- *
1366
- * サーバーは 1 本の接続へ処理順どおりに送るので、他プレイヤーの配信は自分の ack より
1367
- * 必ず先に届く = 引き当てのチャンスは記録が生きている間に必ず来る。
1368
- */
1369
- let firedPredictions = [];
1370
- /** 再適用時はイベントを発火しない (送信時に既に発火済み) */
1371
- const noopEmit = () => {};
1372
1417
  const dispatchEvents = (evts) => {
1373
1418
  for (const e of evts) {
1374
1419
  const handler = events?.[e.name]?.handler;
@@ -1376,63 +1421,250 @@ function createOptimisticActionClient(config) {
1376
1421
  callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1377
1422
  }
1378
1423
  };
1379
- /** events の同一性キー。name と data が一致すれば「同じ出来事」とみなす。 */
1380
- const eventKey = (e) => `${e.name}\u0000${JSON.stringify(e.data ?? {})}`;
1381
- /**
1382
- * サーバーの events から、先読みで実行済みのものを差し引く (多重集合の差)。
1383
- *
1384
- * - 予測が当たった → 差が空。二重に実行しない
1385
- * - 予測が外れた → サーバー側の正しい event が残り、確定として実行される
1386
- * - 先読みで実行していない (predict: false / serverActions 由来 / 他プレイヤー由来)
1387
- * → そのまま残って実行される
1388
- */
1389
- const subtractFired = (serverEvts) => {
1390
- if (firedPredictions.length === 0) return serverEvts;
1391
- const out = [];
1392
- for (const e of serverEvts) {
1393
- const k = eventKey(e);
1394
- const idx = firedPredictions.findIndex((f) => eventKey(f) === k);
1395
- if (idx >= 0) firedPredictions.splice(idx, 1);
1396
- else out.push(e);
1424
+ clock.restart();
1425
+ const setupTime = gameTime();
1426
+ const setupArgs = {
1427
+ players,
1428
+ ctx: {
1429
+ random,
1430
+ ...timeCtx(setupTime)
1397
1431
  }
1398
- return out;
1399
1432
  };
1400
- /**
1401
- * confirmedState をベースに pending actions を再適用して displayState を更新する。
1402
- * 再適用失敗した action はキューから除去する。
1403
- */
1404
- const reapplyPendingActions = () => {
1405
- if (confirmedState === null) return;
1406
- displayState = structuredClone(confirmedState);
1407
- let i = 0;
1408
- while (i < pendingActions.length) {
1409
- const { action, payload, time } = pendingActions[i];
1410
- const handler = logic.actions[action];
1411
- if (!handler || isServerOnlyAction(handler)) {
1412
- pendingActions.splice(i, 1);
1413
- continue;
1414
- }
1415
- const base = structuredClone(displayState);
1433
+ let state = withGameClock(setupTime, () => logic.setup(setupArgs));
1434
+ let tick = 0;
1435
+ const playerInputs = {};
1436
+ const dispatchAction = (type, payload) => {
1437
+ if (frozen()) {
1438
+ console.warn(`[SDK LocalServerAction] 緊急停止中は action を実行しません (action="${type}")`);
1439
+ return;
1440
+ }
1441
+ const plain = type in logic.actions ? logic.actions[type] : void 0;
1442
+ const legacyServerOnly = isServerOnlyAction(plain) ? plain : null;
1443
+ const server = logic.serverActions?.[type] ?? legacyServerOnly;
1444
+ if (!plain && !server) return;
1445
+ const plainEvents = [];
1446
+ const serverEvents = [];
1447
+ const plainEmit = (name, data) => plainEvents.push({
1448
+ name,
1449
+ data: data ?? {}
1450
+ });
1451
+ const serverEmit = (name, data) => serverEvents.push({
1452
+ name,
1453
+ data: data ?? {}
1454
+ });
1455
+ const time = gameTime();
1456
+ if (plain && !legacyServerOnly) {
1416
1457
  try {
1417
- runPredicted(action, () => handler({
1418
- state: displayState,
1419
- payload,
1420
- playerId,
1458
+ withGameClock(time, () => plain({
1459
+ state,
1460
+ payload: payload ?? {},
1461
+ playerId: myId,
1421
1462
  ctx: {
1422
- time,
1423
- after: (d) => plus(time, d),
1424
- emit: noopEmit
1463
+ ...timeCtx(time),
1464
+ emit: plainEmit
1425
1465
  }
1426
1466
  }));
1427
- i++;
1428
- } catch {
1429
- displayState = base;
1430
- pendingActions.splice(i, 1);
1467
+ } catch (err) {
1468
+ console.warn("[SDK LocalServerAction] Action error:", err);
1469
+ return;
1431
1470
  }
1471
+ dispatchEvents(plainEvents);
1472
+ syncWakeup();
1473
+ onState(state, myId);
1432
1474
  }
1433
- onState(displayState, playerId);
1434
- };
1435
- /**
1475
+ if (!server) return;
1476
+ (async () => {
1477
+ try {
1478
+ await server({
1479
+ state,
1480
+ payload: payload ?? {},
1481
+ playerId: myId,
1482
+ ctx: {
1483
+ tick,
1484
+ random,
1485
+ ...timeCtx(time),
1486
+ emit: serverEmit
1487
+ }
1488
+ });
1489
+ } catch (err) {
1490
+ console.warn("[SDK LocalServerAction] Action error:", err);
1491
+ return;
1492
+ }
1493
+ dispatchEvents(serverEvents);
1494
+ syncWakeup();
1495
+ onState(state, myId);
1496
+ })();
1497
+ };
1498
+ /**
1499
+ * ソロの停止 / 解除。サーバーが居ないので自分の時計を直接触る。
1500
+ *
1501
+ * `__uzu_dev.pauseTick()` とは別物。あちらは tick を止めるだけの開発者ツールで、
1502
+ * ゲーム内時計も action ゲートも持たない。流用しない。
1503
+ */
1504
+ const applyPause = (paused) => {
1505
+ if (!(paused ? clock.freeze("emergency-stop") : clock.unfreeze("emergency-stop"))) return;
1506
+ observeGameTime(gameTime());
1507
+ setPauseState(paused, paused ? myId : null);
1508
+ observeGameTime(gameTime());
1509
+ syncWakeup();
1510
+ };
1511
+ setPauseRequester(applyPause);
1512
+ inputs(dispatchAction);
1513
+ syncWakeup();
1514
+ onState(state, myId);
1515
+ if (tickRate > 0) setInterval(() => {
1516
+ if (frozen()) return;
1517
+ const tickEvents = [];
1518
+ const tickEmit = (name, data) => tickEvents.push({
1519
+ name,
1520
+ data: data ?? {}
1521
+ });
1522
+ const time = gameTime();
1523
+ try {
1524
+ withGameClock(time, () => logic.update({
1525
+ state,
1526
+ ctx: {
1527
+ random,
1528
+ tick,
1529
+ ...timeCtx(time),
1530
+ emit: tickEmit,
1531
+ playerInputs
1532
+ }
1533
+ }));
1534
+ } catch (err) {
1535
+ console.error(`[SDK LocalServerAction] tick error at tick=${tick}:`, err);
1536
+ tick++;
1537
+ return;
1538
+ }
1539
+ tick++;
1540
+ dispatchEvents(tickEvents);
1541
+ syncWakeup();
1542
+ onState(state, myId);
1543
+ }, 1e3 / tickRate);
1544
+ return {
1545
+ getRawState: () => state,
1546
+ setRawState: async (next) => {
1547
+ state = next;
1548
+ syncWakeup();
1549
+ onState(state, myId);
1550
+ },
1551
+ mergeRawState: async (patch) => {
1552
+ applyJsonMergePatch(state, patch);
1553
+ onState(state, myId);
1554
+ },
1555
+ patchRawState: async (ops) => {
1556
+ applyJsonPatch(state, ops);
1557
+ onState(state, myId);
1558
+ },
1559
+ sendAction: dispatchAction
1560
+ };
1561
+ };
1562
+ //#endregion
1563
+ //#region src/run/optimistic-action-client.ts
1564
+ const createOptimisticActionClient = (config) => {
1565
+ const { logic, playerId, events, sendAction } = config;
1566
+ /** Record の添字は型の上では常に値を返すので、存在確認を通してから引く。 */
1567
+ const actionHandler = (type) => type in logic.actions ? logic.actions[type] : void 0;
1568
+ /** 呼び出し箇所ごとに守らず、シナリオの描画へ出る唯一の口をここで塞ぐ。 */
1569
+ const onState = (state, id) => {
1570
+ callScenario("描画 (onState)", () => config.onState(state, id));
1571
+ };
1572
+ /** サーバー確定 state (楽観的更新のベース) */
1573
+ let confirmedState = null;
1574
+ /** 表示用 state (pending actions 適用済み) */
1575
+ let displayState = null;
1576
+ /** クライアント側の action 通番 */
1577
+ let actionSeq = 0;
1578
+ /**
1579
+ * 送信済みだがサーバー未確認の action キュー。
1580
+ * `time` は送信時に推定した値。再適用でも同じ値を使う (取り直すと表示がガタつく)。
1581
+ */
1582
+ const pendingActions = [];
1583
+ /**
1584
+ * 先読みで実行済みの events。
1585
+ *
1586
+ * IMPORTANT: pending action ごとではなくクライアント単位で持つ。二重実行は「自分の
1587
+ * action の ack」だけでなく「他プレイヤーの action のブロードキャスト」でも起きるため
1588
+ * (A と B が同時に同じ行送りを撃つと、B は自分の先読みで実行済みなのに A 由来の
1589
+ * 配信でもう一度実行してしまう)。どの配信が来ても、まずここと突き合わせる。
1590
+ *
1591
+ * 記録には「どの action の先読みで実行したか」(seq) を持たせる。その action が ack
1592
+ * された時点で引き当てられていない記録は「予測したが実際には起きなかった出来事」なので
1593
+ * 捨てる。seq を持たせずに「pending が空になったら捨てる」だけにすると、別の action が
1594
+ * 未確定な間ずっと外れた記録が生き残り、その後に本当に起きた同名イベントを 1 回
1595
+ * 握り潰してしまう。
1596
+ *
1597
+ * サーバーは 1 本の接続へ処理順どおりに送るので、他プレイヤーの配信は自分の ack より
1598
+ * 必ず先に届く = 引き当てのチャンスは記録が生きている間に必ず来る。
1599
+ */
1600
+ let firedPredictions = [];
1601
+ /** 再適用時はイベントを発火しない (送信時に既に発火済み) */
1602
+ const noopEmit = () => {};
1603
+ const dispatchEvents = (evts) => {
1604
+ for (const e of evts) {
1605
+ const handler = events?.[e.name]?.handler;
1606
+ if (!handler) continue;
1607
+ callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1608
+ }
1609
+ };
1610
+ /** events の同一性キー。name と data が一致すれば「同じ出来事」とみなす。 */
1611
+ const eventKey = (e) => `${e.name}\u0000${JSON.stringify(e.data)}`;
1612
+ /**
1613
+ * サーバーの events から、先読みで実行済みのものを差し引く (多重集合の差)。
1614
+ *
1615
+ * - 予測が当たった → 差が空。二重に実行しない
1616
+ * - 予測が外れた → サーバー側の正しい event が残り、確定として実行される
1617
+ * - 先読みで実行していない (predict: false / serverActions 由来 / 他プレイヤー由来)
1618
+ * → そのまま残って実行される
1619
+ */
1620
+ const subtractFired = (serverEvts) => {
1621
+ if (firedPredictions.length === 0) return serverEvts;
1622
+ const out = [];
1623
+ for (const e of serverEvts) {
1624
+ const k = eventKey(e);
1625
+ const idx = firedPredictions.findIndex((f) => eventKey(f) === k);
1626
+ if (idx >= 0) firedPredictions.splice(idx, 1);
1627
+ else out.push(e);
1628
+ }
1629
+ return out;
1630
+ };
1631
+ /**
1632
+ * confirmedState をベースに pending actions を再適用して displayState を更新する。
1633
+ * 再適用失敗した action はキューから除去する。
1634
+ */
1635
+ const reapplyPendingActions = () => {
1636
+ if (confirmedState === null) return;
1637
+ let display = structuredClone(confirmedState);
1638
+ let i = 0;
1639
+ while (i < pendingActions.length) {
1640
+ const { action, payload, time } = pendingActions[i];
1641
+ const handler = actionHandler(action);
1642
+ if (!handler || isServerOnlyAction(handler)) {
1643
+ pendingActions.splice(i, 1);
1644
+ continue;
1645
+ }
1646
+ const base = structuredClone(display);
1647
+ try {
1648
+ runPredicted(action, () => handler({
1649
+ state: display,
1650
+ payload,
1651
+ playerId,
1652
+ ctx: {
1653
+ time,
1654
+ after: (d) => plus(time, d),
1655
+ emit: noopEmit
1656
+ }
1657
+ }));
1658
+ i++;
1659
+ } catch {
1660
+ display = base;
1661
+ pendingActions.splice(i, 1);
1662
+ }
1663
+ }
1664
+ displayState = display;
1665
+ onState(display, playerId);
1666
+ };
1667
+ /**
1436
1668
  * サーバーからの配信を処理する。
1437
1669
  *
1438
1670
  * 送信元が誰であれ、まず先読みの実行記録と突き合わせる。自分の ack だけを見ていると、
@@ -1453,7 +1685,8 @@ function createOptimisticActionClient(config) {
1453
1685
  }
1454
1686
  actionSeq++;
1455
1687
  const seq = actionSeq;
1456
- const handler = logic.actions[type];
1688
+ const actionPayload = payload ?? {};
1689
+ const handler = actionHandler(type);
1457
1690
  if (!handler && !logic.serverActions?.[type]) console.error(`[uzu] unknown action: "${type}". logic.actions / logic.serverActions のどちらにも登録されていません。`);
1458
1691
  const target = displayState;
1459
1692
  const time = gameTime();
@@ -1471,7 +1704,7 @@ function createOptimisticActionClient(config) {
1471
1704
  if (handler && !isServerOnlyAction(handler) && target !== null) try {
1472
1705
  runPredicted(type, () => handler({
1473
1706
  state: target,
1474
- payload: payload ?? {},
1707
+ payload: actionPayload,
1475
1708
  playerId,
1476
1709
  ctx: {
1477
1710
  time,
@@ -1482,7 +1715,7 @@ function createOptimisticActionClient(config) {
1482
1715
  pendingActions.push({
1483
1716
  seq,
1484
1717
  action: type,
1485
- payload: payload ?? {},
1718
+ payload: actionPayload,
1486
1719
  time
1487
1720
  });
1488
1721
  onState(target, playerId);
@@ -1491,7 +1724,7 @@ function createOptimisticActionClient(config) {
1491
1724
  }
1492
1725
  sendAction({
1493
1726
  action: type,
1494
- payload: payload ?? {},
1727
+ payload: actionPayload,
1495
1728
  seq
1496
1729
  });
1497
1730
  },
@@ -1528,10 +1761,10 @@ function createOptimisticActionClient(config) {
1528
1761
  onState(displayState, playerId);
1529
1762
  }
1530
1763
  };
1531
- }
1764
+ };
1532
1765
  //#endregion
1533
1766
  //#region src/run/server-action.ts
1534
- function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, seatKind) {
1767
+ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, seatKind) => {
1535
1768
  const toWire = (p) => ({
1536
1769
  id: p.id,
1537
1770
  name: p.nickname,
@@ -1598,12 +1831,12 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1598
1831
  client.observeGameTime(parsed.gameTime);
1599
1832
  if (msgType === "__pause_state") {
1600
1833
  const frozen = parsed.frozen === true;
1601
- console.log(`[SDK ServerAction] ${frozen ? "⏸" : "▶️"} pause_state frozen=${frozen}`);
1834
+ console.log(`[SDK ServerAction] ${frozen ? "⏸" : "▶️"} pause_state frozen=${String(frozen)}`);
1602
1835
  setPauseState(frozen, parsed.by ?? null);
1603
1836
  return;
1604
1837
  }
1605
1838
  if (msgType === "__room_init") {
1606
- console.log(`[SDK ServerAction] ✅ Room init myId=${parsed.myId}`);
1839
+ console.log(`[SDK ServerAction] ✅ Room init myId=${String(parsed.myId)}`);
1607
1840
  return;
1608
1841
  }
1609
1842
  if (msgType === "__game_start") {
@@ -1671,7 +1904,7 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1671
1904
  }
1672
1905
  if (msgType === "__action_error") {
1673
1906
  const errorSeq = parsed.seq;
1674
- console.warn(`[SDK ServerAction] ⚠️ Action error: ${parsed.error} (seq=${errorSeq})`);
1907
+ console.warn(`[SDK ServerAction] ⚠️ Action error: ${String(parsed.error)} (seq=${errorSeq ?? "-"})`);
1675
1908
  if (errorSeq !== void 0) client.rollback(errorSeq);
1676
1909
  return;
1677
1910
  }
@@ -1699,244 +1932,7 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1699
1932
  console.error(`[SDK ServerAction] ❌ message 処理で例外 type=${String(msg.type)}`, err);
1700
1933
  }
1701
1934
  });
1702
- }
1703
- //#endregion
1704
- //#region src/run/local-server-action.ts
1705
- function runLocalServerAction(config) {
1706
- const { logic, inputs, events } = config;
1707
- const onState = (next, id) => {
1708
- observeGameTime(gameTime());
1709
- callScenario("描画 (onState)", () => config.onState(next, id, "player"));
1710
- };
1711
- const tickRate = logic.tickRate ?? 0;
1712
- const clock = new GameClock();
1713
- const gameTime = () => clock.now();
1714
- const frozen = () => clock.frozen;
1715
- /** ハンドラへ渡す ctx の時刻部分。1 回の呼び出し内で値が動かないよう束ねる。 */
1716
- const timeCtx = (t) => ({
1717
- time: t,
1718
- after: (d) => plus(t, d)
1719
- });
1720
- /** at() が壊れた値を返したと既に記録した締切。ログを 1 回に絞るため。 */
1721
- const warnedDeadlines = /* @__PURE__ */ new Set();
1722
- const random = new SeededRandomImpl(Math.floor(Math.random() * 4294967295));
1723
- const players = Array.from({ length: config.playerCount }, (_, i) => ({
1724
- id: `local_${i}`,
1725
- nickname: `Player ${i + 1}`,
1726
- iconUrl: DEFAULT_ICON_URLS[i % DEFAULT_ICON_URLS.length]
1727
- }));
1728
- const myId = players[0].id;
1729
- let wakeupTimer = null;
1730
- let wakeupAt = null;
1731
- const nextDeadline = () => {
1732
- if (!logic.deadlines) return null;
1733
- let earliest = null;
1734
- for (const [key, deadline] of Object.entries(logic.deadlines)) {
1735
- let raw;
1736
- try {
1737
- raw = deadline.at({ state });
1738
- } catch (err) {
1739
- console.error(`[Deadline] ❌ ${key}.at() で例外`, err);
1740
- continue;
1741
- }
1742
- const at = resolveDeadline(raw, key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
1743
- if (at === null) continue;
1744
- if (earliest === null || at < earliest) earliest = at;
1745
- }
1746
- return earliest;
1747
- };
1748
- const fireDue = () => {
1749
- wakeupTimer = null;
1750
- wakeupAt = null;
1751
- if (!logic.deadlines || frozen()) return;
1752
- const time = gameTime();
1753
- const evts = [];
1754
- const firedKeys = [];
1755
- for (const [key, deadline] of Object.entries(logic.deadlines)) {
1756
- const pending = [];
1757
- let snapshot = null;
1758
- try {
1759
- const at = resolveDeadline(deadline.at({ state }), key, warnedDeadlines, (m) => console.error(`[Deadline] ❌ ${m}`));
1760
- if (at === null || at > time) continue;
1761
- snapshot = structuredClone(state);
1762
- withGameClock(time, () => deadline.handler({
1763
- state,
1764
- ctx: {
1765
- ...timeCtx(time),
1766
- random,
1767
- emit: (name, data) => pending.push({
1768
- name,
1769
- data: data ?? {}
1770
- })
1771
- }
1772
- }));
1773
- evts.push(...pending);
1774
- firedKeys.push(key);
1775
- } catch (err) {
1776
- if (snapshot !== null) state = snapshot;
1777
- console.error(`[Deadline] ❌ ${key} で例外`, err);
1778
- }
1779
- }
1780
- syncWakeup();
1781
- if (firedKeys.length === 0) return;
1782
- console.log(`[Deadline] ⏰ ${firedKeys.length} 件発火: ${firedKeys.join(", ")}`);
1783
- dispatchEvents(evts);
1784
- onState(state, myId);
1785
- };
1786
- const syncWakeup = () => {
1787
- const next = frozen() ? null : nextDeadline();
1788
- if (next === wakeupAt) return;
1789
- if (wakeupTimer) clearTimeout(wakeupTimer);
1790
- wakeupTimer = null;
1791
- wakeupAt = next;
1792
- if (next === null) return;
1793
- wakeupTimer = setTimeout(fireDue, Math.max(0, clock.toWall(next) - Date.now()));
1794
- };
1795
- const dispatchEvents = (evts) => {
1796
- for (const e of evts) {
1797
- const handler = events?.[e.name]?.handler;
1798
- if (!handler) continue;
1799
- callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1800
- }
1801
- };
1802
- clock.restart();
1803
- const setupTime = gameTime();
1804
- const setupArgs = {
1805
- players,
1806
- ctx: {
1807
- random,
1808
- ...timeCtx(setupTime)
1809
- }
1810
- };
1811
- let state = withGameClock(setupTime, () => logic.setup(setupArgs));
1812
- let tick = 0;
1813
- const playerInputs = {};
1814
- const dispatchAction = (type, payload) => {
1815
- if (frozen()) {
1816
- console.warn(`[SDK LocalServerAction] 緊急停止中は action を実行しません (action="${type}")`);
1817
- return;
1818
- }
1819
- const plain = logic.actions[type];
1820
- const legacyServerOnly = isServerOnlyAction(plain) ? plain : null;
1821
- const server = logic.serverActions?.[type] ?? legacyServerOnly;
1822
- if (!plain && !server) return;
1823
- const plainEvents = [];
1824
- const serverEvents = [];
1825
- const plainEmit = (name, data) => plainEvents.push({
1826
- name,
1827
- data: data ?? {}
1828
- });
1829
- const serverEmit = (name, data) => serverEvents.push({
1830
- name,
1831
- data: data ?? {}
1832
- });
1833
- const time = gameTime();
1834
- if (plain && !legacyServerOnly) {
1835
- try {
1836
- withGameClock(time, () => plain({
1837
- state,
1838
- payload: payload ?? {},
1839
- playerId: myId,
1840
- ctx: {
1841
- ...timeCtx(time),
1842
- emit: plainEmit
1843
- }
1844
- }));
1845
- } catch (err) {
1846
- console.warn("[SDK LocalServerAction] Action error:", err);
1847
- return;
1848
- }
1849
- dispatchEvents(plainEvents);
1850
- syncWakeup();
1851
- onState(state, myId);
1852
- }
1853
- if (!server) return;
1854
- (async () => {
1855
- try {
1856
- await server({
1857
- state,
1858
- payload: payload ?? {},
1859
- playerId: myId,
1860
- ctx: {
1861
- tick,
1862
- random,
1863
- ...timeCtx(time),
1864
- emit: serverEmit
1865
- }
1866
- });
1867
- } catch (err) {
1868
- console.warn("[SDK LocalServerAction] Action error:", err);
1869
- return;
1870
- }
1871
- dispatchEvents(serverEvents);
1872
- syncWakeup();
1873
- onState(state, myId);
1874
- })();
1875
- };
1876
- /**
1877
- * ソロの停止 / 解除。サーバーが居ないので自分の時計を直接触る。
1878
- *
1879
- * `__uzu_dev.pauseTick()` とは別物。あちらは tick を止めるだけの開発者ツールで、
1880
- * ゲーム内時計も action ゲートも持たない。流用しない。
1881
- */
1882
- const applyPause = (paused) => {
1883
- if (!(paused ? clock.freeze("emergency-stop") : clock.unfreeze("emergency-stop"))) return;
1884
- observeGameTime(gameTime());
1885
- setPauseState(paused, paused ? myId : null);
1886
- observeGameTime(gameTime());
1887
- syncWakeup();
1888
- };
1889
- setPauseRequester(applyPause);
1890
- inputs(dispatchAction);
1891
- syncWakeup();
1892
- onState(state, myId);
1893
- if (tickRate > 0) setInterval(() => {
1894
- if (frozen()) return;
1895
- const tickEvents = [];
1896
- const tickEmit = (name, data) => tickEvents.push({
1897
- name,
1898
- data: data ?? {}
1899
- });
1900
- const time = gameTime();
1901
- try {
1902
- withGameClock(time, () => logic.update({
1903
- state,
1904
- ctx: {
1905
- random,
1906
- tick,
1907
- ...timeCtx(time),
1908
- emit: tickEmit,
1909
- playerInputs
1910
- }
1911
- }));
1912
- } catch (err) {
1913
- console.error(`[SDK LocalServerAction] tick error at tick=${tick}:`, err);
1914
- tick++;
1915
- return;
1916
- }
1917
- tick++;
1918
- dispatchEvents(tickEvents);
1919
- syncWakeup();
1920
- onState(state, myId);
1921
- }, 1e3 / tickRate);
1922
- return {
1923
- getRawState: () => state,
1924
- setRawState: async (next) => {
1925
- state = next;
1926
- syncWakeup();
1927
- onState(state, myId);
1928
- },
1929
- mergeRawState: async (patch) => {
1930
- applyJsonMergePatch(state, patch);
1931
- onState(state, myId);
1932
- },
1933
- patchRawState: async (ops) => {
1934
- applyJsonPatch(state, ops);
1935
- onState(state, myId);
1936
- },
1937
- sendAction: dispatchAction
1938
- };
1939
- }
1935
+ };
1940
1936
  //#endregion
1941
1937
  //#region src/index.ts
1942
1938
  const gameHandlers = /* @__PURE__ */ new Map();
@@ -1948,7 +1944,7 @@ let _usesRun = false;
1948
1944
  let _playerId = null;
1949
1945
  let _runHandle = null;
1950
1946
  const _snapshotListeners = /* @__PURE__ */ new Set();
1951
- function notifyDevSnapshot(snap) {
1947
+ const notifyDevSnapshot = (snap) => {
1952
1948
  _snapshotListeners.forEach((cb) => {
1953
1949
  try {
1954
1950
  cb(snap);
@@ -1956,10 +1952,10 @@ function notifyDevSnapshot(snap) {
1956
1952
  console.warn("[uzu_dev] snapshot listener threw:", err);
1957
1953
  }
1958
1954
  });
1959
- }
1960
- function attachDevHooksIfNotHosted() {
1955
+ };
1956
+ const attachDevHooksIfNotHosted = () => {
1961
1957
  if (window.FlutterHost) return;
1962
- attachDevHooks({
1958
+ const ctx = {
1963
1959
  getSnapshot: () => _lastStateSnap?.state ?? null,
1964
1960
  getRawState: _runHandle?.getRawState,
1965
1961
  setRawState: _runHandle?.setRawState,
@@ -1972,10 +1968,11 @@ function attachDevHooksIfNotHosted() {
1972
1968
  _snapshotListeners.delete(cb);
1973
1969
  };
1974
1970
  }
1975
- });
1976
- }
1971
+ };
1972
+ attachDevHooks(ctx);
1973
+ };
1977
1974
  const isHosted = typeof window !== "undefined" && (!!window.FlutterHost || window.parent !== window);
1978
- function init(opts) {
1975
+ const init = (_opts) => {
1979
1976
  const params = new URLSearchParams(window.location.search);
1980
1977
  _playerId = params.get("seatId");
1981
1978
  if (isHosted && !_playerId) console.warn("[UZU SDK] playerId is missing in the URL. Host-side sender filtering will fall back to legacy behavior.");
@@ -1993,13 +1990,10 @@ function init(opts) {
1993
1990
  handleMessage(msg);
1994
1991
  } };
1995
1992
  window.addEventListener("message", (event) => {
1996
- try {
1997
- const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
1998
- if (data && typeof data.channel === "string" && typeof data.type === "string") {
1999
- console.log(`[SDK] ⬅ recv postMessage channel=${data.channel} type=${data.type}`, JSON.stringify(data));
2000
- handleMessage(data);
2001
- }
2002
- } catch {}
1993
+ const data = parseBridgeEnvelope(event.data);
1994
+ if (data === null) return;
1995
+ console.log(`[SDK] ⬅ recv postMessage channel=${data.channel} type=${data.type}`, JSON.stringify(data));
1996
+ handleMessage(data);
2003
1997
  });
2004
1998
  applyInsets(params);
2005
1999
  onPauseChange((paused) => {
@@ -2012,31 +2006,49 @@ function init(opts) {
2012
2006
  _initialized = true;
2013
2007
  }
2014
2008
  if (!_usesRun) attachDevHooksIfNotHosted();
2015
- }
2009
+ };
2016
2010
  /** game channel でカスタムメッセージを送信する。 native コマンドには使用不可。 */
2017
- function send(type, payload) {
2011
+ const send = (type, payload) => {
2018
2012
  sendRaw("game", type, payload ?? {});
2019
- }
2013
+ };
2020
2014
  /** game channel のカスタムメッセージを受信する。 */
2021
- function on(type, handler) {
2015
+ const on = (type, handler) => {
2022
2016
  if (!gameHandlers.has(type)) gameHandlers.set(type, []);
2023
2017
  gameHandlers.get(type).push(handler);
2024
- }
2025
- function playSound(sound) {
2018
+ };
2019
+ const playSound = (sound) => {
2026
2020
  sendRaw("sdk", "playSound", { sound });
2027
- }
2028
- function playBgm(sound) {
2021
+ };
2022
+ const playBgm = (sound) => {
2029
2023
  sendRaw("sdk", "playBgm", { sound });
2030
- }
2031
- function stopBgm() {
2024
+ };
2025
+ const stopBgm = () => {
2032
2026
  sendRaw("sdk", "stopBgm", {});
2033
- }
2034
- function setMicEnabled(enabled) {
2027
+ };
2028
+ const setMicEnabled = (enabled) => {
2035
2029
  sendRaw("sdk", "setMicEnabled", { enabled });
2036
- }
2037
- function changeRoom(roomId) {
2030
+ };
2031
+ const changeRoom = (roomId) => {
2038
2032
  sendRaw("sdk", "changeRoom", { roomId });
2039
- }
2033
+ };
2034
+ const HAPTIC_FALLBACK_MS = {
2035
+ light: 10,
2036
+ medium: 20,
2037
+ heavy: 30
2038
+ };
2039
+ /**
2040
+ * 端末を短く振動させる。
2041
+ *
2042
+ * アプリの WebView では `navigator.vibrate` が効かない (iOS には Web から振動させる手段が
2043
+ * 無い) ので、ホストへ依頼する。端末の設定で触覚を切っている人には OS 側で効かない。
2044
+ */
2045
+ const haptic = (kind) => {
2046
+ if (!isHosted) {
2047
+ if ("vibrate" in navigator) navigator.vibrate(HAPTIC_FALLBACK_MS[kind]);
2048
+ return;
2049
+ }
2050
+ sendRaw("sdk", "haptic", { kind });
2051
+ };
2040
2052
  /**
2041
2053
  * splash 描画完了を Flutter ホストに通知する。
2042
2054
  *
@@ -2053,9 +2065,9 @@ function changeRoom(roomId) {
2053
2065
  * requestAnimationFrame(() => firstFrameReady());
2054
2066
  * ```
2055
2067
  */
2056
- function firstFrameReady() {
2068
+ const firstFrameReady = () => {
2057
2069
  sendRaw("sdk", "firstFrameReady", {});
2058
- }
2070
+ };
2059
2071
  /**
2060
2072
  * ユーザー操作受付可能を Flutter ホストに通知する。
2061
2073
  *
@@ -2072,9 +2084,9 @@ function firstFrameReady() {
2072
2084
  * gameReady();
2073
2085
  * ```
2074
2086
  */
2075
- function gameReady() {
2087
+ const gameReady = () => {
2076
2088
  sendRaw("sdk", "gameReady", {});
2077
- }
2089
+ };
2078
2090
  /**
2079
2091
  * ゲームを終えてプレイ画面を閉じるよう Flutter ホストに通知する。
2080
2092
  *
@@ -2095,13 +2107,13 @@ function gameReady() {
2095
2107
  * }
2096
2108
  * ```
2097
2109
  */
2098
- function finishGame() {
2110
+ const finishGame = () => {
2099
2111
  sendRaw("sdk", "finishGame", {});
2100
- }
2112
+ };
2101
2113
  /** Flutter からの playersChanged メッセージを受信するハンドラを登録する。 */
2102
- function onPlayersChanged(handler) {
2114
+ const onPlayersChanged = (handler) => {
2103
2115
  _playersChangedHandlers.push(handler);
2104
- }
2116
+ };
2105
2117
  /**
2106
2118
  * `A` に既定値が要る。 TypeScript は型引数の部分推論ができないので、 既定が無いと
2107
2119
  * 既存の `run<State>({…})` が「2 つ必要」で落ちる。 既定を置けば型引数を書かない
@@ -2111,7 +2123,7 @@ function onPlayersChanged(handler) {
2111
2123
  * `keyof (A & SA)` が `string` へ潰れ、action 名の検査が丸ごと消える。
2112
2124
  * `serverActions` を持たない logic でもキーが残るよう空 map を既定にする。
2113
2125
  */
2114
- function run(config) {
2126
+ const run = (config) => {
2115
2127
  _usesRun = true;
2116
2128
  const params = new URLSearchParams(window.location.search);
2117
2129
  if (!isHosted) return;
@@ -2137,7 +2149,7 @@ function run(config) {
2137
2149
  runOnlineServerAction(wrappedConfig, _gameEndpoint, roomId, seatId, players, seatKind);
2138
2150
  } 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.");
2139
2151
  attachDevHooksIfNotHosted();
2140
- }
2152
+ };
2141
2153
  /**
2142
2154
  * 自分の席種別を URL から取り出す。
2143
2155
  *
@@ -2147,17 +2159,17 @@ function run(config) {
2147
2159
  * この値は iframe URL 限定で、WebSocket には出さない。play-server は roster に居ない接続を
2148
2160
  * 通すだけで、その席が admin か spectator かを知る必要が無い。
2149
2161
  */
2150
- function resolveSeatKind(params) {
2162
+ const resolveSeatKind = (params) => {
2151
2163
  const raw = params.get("seatKind");
2152
2164
  return raw === "admin" || raw === "spectator" ? raw : "player";
2153
- }
2165
+ };
2154
2166
  /**
2155
2167
  * 自席の ID と roster を URL から取り出す。
2156
2168
  *
2157
2169
  * roster (`?players=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
2158
2170
  * 載らないまま接続してくる。
2159
2171
  */
2160
- function resolveSeatParams(params) {
2172
+ const resolveSeatParams = (params) => {
2161
2173
  const seatId = params.get("seatId");
2162
2174
  if (!seatId) throw new Error("[UZU SDK] seatId is required. Pass ?seatId=xxx in the URL.");
2163
2175
  const json = params.get("players");
@@ -2171,9 +2183,9 @@ function resolveSeatParams(params) {
2171
2183
  characterId: p.characterId
2172
2184
  }))
2173
2185
  };
2174
- }
2186
+ };
2175
2187
  /** 内部送信関数。 channel + type + payload のエンベロープで送信する。 */
2176
- function sendRaw(channel, type, payload) {
2188
+ const sendRaw = (channel, type, payload) => {
2177
2189
  const msg = {
2178
2190
  channel,
2179
2191
  type,
@@ -2183,8 +2195,24 @@ function sendRaw(channel, type, payload) {
2183
2195
  console.log(`[SDK] ➡ send channel=${channel} type=${type}`, JSON.stringify(msg));
2184
2196
  if (window.FlutterHost) window.FlutterHost.postMessage(JSON.stringify(msg));
2185
2197
  else if (window.parent !== window) window.parent.postMessage(JSON.stringify(msg), "*");
2186
- }
2187
- function handleMessage(msg) {
2198
+ };
2199
+ /** 親 frame から届く envelope。string でも object でも来るので、形を確かめてから通す。 */
2200
+ const parseBridgeEnvelope = (raw) => {
2201
+ let value = raw;
2202
+ if (typeof value === "string") try {
2203
+ value = JSON.parse(value);
2204
+ } catch {
2205
+ return null;
2206
+ }
2207
+ if (typeof value !== "object" || value === null) return null;
2208
+ const envelope = value;
2209
+ if (typeof envelope.channel !== "string" || typeof envelope.type !== "string") return null;
2210
+ return {
2211
+ channel: envelope.channel,
2212
+ type: envelope.type
2213
+ };
2214
+ };
2215
+ const handleMessage = (msg) => {
2188
2216
  const { channel, type, payload } = msg;
2189
2217
  if (channel === "sdk") {
2190
2218
  switch (type) {
@@ -2193,7 +2221,7 @@ function handleMessage(msg) {
2193
2221
  sendRaw("sdk", "stateResponse", { state: _lastStateSnap });
2194
2222
  return;
2195
2223
  case "playersChanged": {
2196
- const players = payload?.players ?? {};
2224
+ const players = payload.players ?? {};
2197
2225
  for (const fn of _playersChangedHandlers) callScenario("playersChanged ハンドラ", () => fn(players));
2198
2226
  return;
2199
2227
  }
@@ -2206,17 +2234,14 @@ function handleMessage(msg) {
2206
2234
  requestResume();
2207
2235
  return;
2208
2236
  case "insetsChanged":
2209
- handleInsetsChanged(payload ?? {});
2237
+ handleInsetsChanged(payload);
2210
2238
  return;
2211
2239
  }
2212
2240
  return;
2213
2241
  }
2214
- if (channel === "game") {
2215
- const h = gameHandlers.get(type) || [];
2216
- console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
2217
- for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload ?? {}));
2218
- return;
2219
- }
2220
- }
2242
+ const h = gameHandlers.get(type) ?? [];
2243
+ console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
2244
+ for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload));
2245
+ };
2221
2246
  //#endregion
2222
- export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, finishGame, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };
2247
+ export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, changeRoom, createDevHooks, finishGame, firstFrameReady, gameReady, gameTime, getInsets, getPredictionWarnings, haptic, init, isHosted, isPaused, isServerOnlyAction, minus, on, onInsetsChange, onPauseChange, onPlayersChanged, playBgm, playSound, plus, run, send, serverOnly, setMicEnabled, stopBgm, sub };