@uzuhq/code-sdk 0.8.12 → 0.9.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 +184 -76
- package/dist/dev-globals.d.ts +1 -1
- package/dist/{dev-hooks-7UDN8Xac.d.ts → dev-hooks-D30dnLtR.d.ts} +75 -17
- package/dist/index.d.ts +161 -12
- package/dist/index.js +573 -101
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -271,6 +271,26 @@ const serverOnly = (handler) => Object.assign(handler, { __serverOnly: true });
|
|
|
271
271
|
*/
|
|
272
272
|
const isServerOnlyAction = (handler) => typeof handler === "function" && "__serverOnly" in handler && handler.__serverOnly === true;
|
|
273
273
|
//#endregion
|
|
274
|
+
//#region ../engine-core/src/presence.ts
|
|
275
|
+
/** `voicePlan` を書かない作品の既定。全員が全体の部屋、マイクは本人の自由。 */
|
|
276
|
+
const DEFAULT_VOICE_PLAN = {
|
|
277
|
+
room: null,
|
|
278
|
+
mic: "free"
|
|
279
|
+
};
|
|
280
|
+
/** WebSocket で伝言を運ぶメッセージの type。`{ type: '__platform', message: PlatformMessage }`。 */
|
|
281
|
+
const PLATFORM_WIRE_TYPE = "__platform";
|
|
282
|
+
/** 伝言の type。 */
|
|
283
|
+
const PLATFORM_MESSAGE_TYPE = {
|
|
284
|
+
/** サーバー → 端末: presence (`PresenceSnapshot`)。 */
|
|
285
|
+
presence: "presence",
|
|
286
|
+
/** 端末 → サーバー: 自分の通話の申告 (`VoiceReport`)。 */
|
|
287
|
+
voiceReport: "voice.report",
|
|
288
|
+
/** 端末 → サーバー: 緊急一時停止。 */
|
|
289
|
+
pauseRequest: "pause.request",
|
|
290
|
+
/** 端末 → サーバー: 緊急一時停止の解除。 */
|
|
291
|
+
pauseResume: "pause.resume"
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
274
294
|
//#region src/server-clock.ts
|
|
275
295
|
/**
|
|
276
296
|
* @docs
|
|
@@ -329,65 +349,392 @@ const unfreezeGameTime = () => {
|
|
|
329
349
|
frozenAt = null;
|
|
330
350
|
};
|
|
331
351
|
//#endregion
|
|
332
|
-
//#region src/
|
|
352
|
+
//#region src/scenario-callback.ts
|
|
333
353
|
/**
|
|
334
354
|
* @docs
|
|
335
|
-
* -
|
|
355
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
336
356
|
*
|
|
337
|
-
*
|
|
357
|
+
* SDK からシナリオのコード (描画 / イベント購読 / bridge の購読) を呼ぶときの例外境界。
|
|
358
|
+
* シナリオのコールバックを呼ぶ箇所は、すべてここを通す。
|
|
338
359
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
360
|
+
* これらは state 配信やメッセージ受信のたびに同じ経路を通る。例外をそのまま上へ通すと
|
|
361
|
+
* 呼び出し元の state 適用 (confirmedState 更新 / ack 処理 / seq 前進) が途中で止まり、
|
|
362
|
+
* 以降の配信でも同じ経路で投げ続ける。WebSocket もホストアプリも生きたまま画面だけが
|
|
363
|
+
* 永久に更新されなくなる — 2026-09-09 の UZU TOKYO 公演で実際に起きた壊れ方がこれ。
|
|
342
364
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
365
|
+
* ここで断ち切れば state 適用は最後まで走り、次の配信で描画がやり直される
|
|
366
|
+
* (onState は毎回フル state から再実行されるので冪等)。同じ理由で、購読が複数ある
|
|
367
|
+
* ところでは 1 件の失敗を他の購読へ波及させない。
|
|
345
368
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (paused) freezeGameTime();
|
|
355
|
-
else unfreezeGameTime();
|
|
356
|
-
for (const cb of listeners$1) try {
|
|
357
|
-
cb(paused);
|
|
369
|
+
/**
|
|
370
|
+
* シナリオ側のコールバックを実行し、例外を境界で止める。
|
|
371
|
+
*
|
|
372
|
+
* 例外は握り潰さず `console.error` に出す。ホスト側のログ収集がこれを拾う。
|
|
373
|
+
*/
|
|
374
|
+
const callScenario = (label, fn) => {
|
|
375
|
+
try {
|
|
376
|
+
fn();
|
|
358
377
|
} catch (err) {
|
|
359
|
-
console.
|
|
378
|
+
console.error(`[uzu] シナリオの ${label} で例外が発生しました (処理は継続します)`, err);
|
|
360
379
|
}
|
|
361
380
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/presence.ts
|
|
383
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
384
|
+
const OPEN_MIC = { status: "open" };
|
|
385
|
+
const micOf = (value) => isRecord$2(value) && typeof value.status === "string" ? value : OPEN_MIC;
|
|
386
|
+
const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
387
|
+
const roomOf = (value) => typeof value === "string" ? value : null;
|
|
388
|
+
const meConnection = (connection) => connection.status === "online" ? { status: "online" } : {
|
|
389
|
+
status: connection.status,
|
|
390
|
+
since: connection.since
|
|
391
|
+
};
|
|
392
|
+
const meVoice = (input) => {
|
|
393
|
+
const self = input.voice?.self;
|
|
394
|
+
if (!self) return {
|
|
395
|
+
status: "connecting",
|
|
396
|
+
since: input.startedAt
|
|
397
|
+
};
|
|
398
|
+
const since = self.since ?? input.startedAt;
|
|
399
|
+
switch (self.session) {
|
|
400
|
+
case "off": return {
|
|
401
|
+
status: "off",
|
|
402
|
+
reason: self.reason === "table" ? "table" : "this-device"
|
|
403
|
+
};
|
|
404
|
+
case "reconnecting": return {
|
|
405
|
+
status: "reconnecting",
|
|
406
|
+
since
|
|
407
|
+
};
|
|
408
|
+
case "failed": return {
|
|
409
|
+
status: "failed",
|
|
410
|
+
reason: self.reason === "mic-permission" ? "mic-permission" : "network",
|
|
411
|
+
since
|
|
412
|
+
};
|
|
413
|
+
case "connected": return {
|
|
414
|
+
status: "here",
|
|
415
|
+
room: self.room,
|
|
416
|
+
mic: micOf(self.mic),
|
|
417
|
+
speaking: self.speaking,
|
|
418
|
+
quality: self.quality
|
|
419
|
+
};
|
|
420
|
+
default: return {
|
|
421
|
+
status: "connecting",
|
|
422
|
+
since
|
|
423
|
+
};
|
|
424
|
+
}
|
|
365
425
|
};
|
|
366
|
-
/**
|
|
367
|
-
const
|
|
368
|
-
|
|
426
|
+
/** アプリが聞こえていると言う相手。形が崩れていても既定の値で埋める。 */
|
|
427
|
+
const heardOf = (value) => {
|
|
428
|
+
const heard = isRecord$2(value) ? value : {};
|
|
429
|
+
return {
|
|
430
|
+
status: "here",
|
|
431
|
+
mic: micOf(heard.mic),
|
|
432
|
+
speaking: heard.speaking === true,
|
|
433
|
+
quality: heard.quality === "poor" ? "poor" : "good"
|
|
434
|
+
};
|
|
369
435
|
};
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
436
|
+
const otherVoice = (seatId, seat, me, input) => {
|
|
437
|
+
const voice = isRecord$2(seat) && isRecord$2(seat.voice) ? seat.voice : {};
|
|
438
|
+
const status = typeof voice.status === "string" ? voice.status : "unknown";
|
|
439
|
+
if (status === "lost") return {
|
|
440
|
+
status: "lost",
|
|
441
|
+
since: numberOr(voice.since, Date.now() - input.clockOffset) + input.clockOffset
|
|
442
|
+
};
|
|
443
|
+
if (me.status === "here") {
|
|
444
|
+
const heard = input.voice?.heard[seatId];
|
|
445
|
+
if (heard !== void 0) return heardOf(heard);
|
|
446
|
+
const missing = input.voice?.missing[seatId];
|
|
447
|
+
if (missing !== void 0) return {
|
|
448
|
+
status: "lost",
|
|
449
|
+
since: numberOr(isRecord$2(missing) ? missing.since : void 0, Date.now())
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
switch (status) {
|
|
453
|
+
case "off": return { status: "off" };
|
|
454
|
+
case "connected": {
|
|
455
|
+
if (me.status !== "here") return {
|
|
456
|
+
status: "unknown",
|
|
457
|
+
reason: "me-not-connected"
|
|
458
|
+
};
|
|
459
|
+
const room = roomOf(voice.room);
|
|
460
|
+
if (room !== me.room) return {
|
|
461
|
+
status: "elsewhere",
|
|
462
|
+
room
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
status: "here",
|
|
466
|
+
mic: OPEN_MIC,
|
|
467
|
+
speaking: false,
|
|
468
|
+
quality: "good"
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
default: return {
|
|
472
|
+
status: "unknown",
|
|
473
|
+
reason: "no-report"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
373
476
|
};
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
477
|
+
/** 3 つの入力から、シナリオに出す値を組み立てる。 */
|
|
478
|
+
const buildPresence = (input) => {
|
|
479
|
+
const me = {
|
|
480
|
+
connection: meConnection(input.connection),
|
|
481
|
+
voice: meVoice(input)
|
|
482
|
+
};
|
|
483
|
+
const others = {};
|
|
484
|
+
const seats = isRecord$2(input.snapshot?.seats) ? input.snapshot.seats : {};
|
|
485
|
+
for (const [seatId, seat] of Object.entries(seats)) {
|
|
486
|
+
if (seatId === input.mySeatId) continue;
|
|
487
|
+
others[seatId] = { voice: otherVoice(seatId, seat, me.voice, input) };
|
|
488
|
+
}
|
|
489
|
+
const heardAll = isRecord$2(input.voice?.heard) ? input.voice.heard : {};
|
|
490
|
+
const speaking = Object.entries(heardAll).filter(([, heard]) => isRecord$2(heard) && heard.speaking === true).filter(([id]) => id !== input.mySeatId && !(id in seats)).map(([id, heard]) => {
|
|
491
|
+
const nickname = isRecord$2(heard) ? heard.nickname : void 0;
|
|
492
|
+
return {
|
|
493
|
+
id,
|
|
494
|
+
nickname: typeof nickname === "string" ? nickname : id
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
const observers = input.snapshot?.observers;
|
|
498
|
+
return {
|
|
499
|
+
me,
|
|
500
|
+
others,
|
|
501
|
+
observers: {
|
|
502
|
+
count: numberOr(isRecord$2(observers) ? observers.count : void 0, 0),
|
|
503
|
+
speaking
|
|
504
|
+
}
|
|
505
|
+
};
|
|
377
506
|
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
507
|
+
/** 前回との差分。席が初めて現れたこと自体は変化として扱わない。 */
|
|
508
|
+
const diffPresence = (mySeatId, prev, next) => {
|
|
509
|
+
const changes = [];
|
|
510
|
+
const pushVoice = (seatId, before, after) => {
|
|
511
|
+
if (before && before.voice.status !== after.voice.status) changes.push({
|
|
512
|
+
kind: "voice",
|
|
513
|
+
seatId,
|
|
514
|
+
from: before.voice.status,
|
|
515
|
+
to: after.voice.status
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
if (prev.me.connection.status !== next.me.connection.status) changes.push({
|
|
519
|
+
kind: "connection",
|
|
520
|
+
seatId: mySeatId,
|
|
521
|
+
from: prev.me.connection.status,
|
|
522
|
+
to: next.me.connection.status
|
|
523
|
+
});
|
|
524
|
+
pushVoice(mySeatId, prev.me, next.me);
|
|
525
|
+
for (const [seatId, other] of Object.entries(next.others)) pushVoice(seatId, prev.others[seatId], other);
|
|
526
|
+
if (prev.observers.count !== next.observers.count) changes.push({
|
|
527
|
+
kind: "observers",
|
|
528
|
+
from: prev.observers.count,
|
|
529
|
+
to: next.observers.count
|
|
530
|
+
});
|
|
531
|
+
return changes;
|
|
532
|
+
};
|
|
533
|
+
const SESSIONS = [
|
|
534
|
+
"off",
|
|
535
|
+
"connecting",
|
|
536
|
+
"connected",
|
|
537
|
+
"reconnecting",
|
|
538
|
+
"failed"
|
|
539
|
+
];
|
|
540
|
+
/**
|
|
541
|
+
* アプリから届いた `voiceChanged` を検める。形が合わなければ null。
|
|
542
|
+
* 新しいアプリが項目を足しても落ちないよう、知っている項目だけを見る。
|
|
543
|
+
*
|
|
544
|
+
* 知らない通話の状態は connecting として受ける。アプリは SDK より先に新しくなるので、
|
|
545
|
+
* 捨てると聞こえている相手 (heard) まで止まってしまう。
|
|
546
|
+
*/
|
|
547
|
+
const parseVoiceChanged = (payload) => {
|
|
548
|
+
const { self, heard, missing } = payload;
|
|
549
|
+
if (!isRecord$2(self) || typeof self.session !== "string") return null;
|
|
550
|
+
const session = SESSIONS.includes(self.session) ? self.session : "connecting";
|
|
551
|
+
const room = typeof self.room === "string" ? self.room : null;
|
|
552
|
+
return {
|
|
553
|
+
self: {
|
|
554
|
+
session,
|
|
555
|
+
...typeof self.reason === "string" ? { reason: self.reason } : {},
|
|
556
|
+
...typeof self.since === "number" ? { since: self.since } : {},
|
|
557
|
+
room,
|
|
558
|
+
mic: micOf(self.mic),
|
|
559
|
+
speaking: self.speaking === true,
|
|
560
|
+
quality: self.quality === "poor" ? "poor" : "good"
|
|
561
|
+
},
|
|
562
|
+
heard: isRecord$2(heard) ? heard : {},
|
|
563
|
+
missing: isRecord$2(missing) ? missing : {}
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const initialInput = () => ({
|
|
567
|
+
mySeatId: "",
|
|
568
|
+
snapshot: null,
|
|
569
|
+
clockOffset: 0,
|
|
570
|
+
voice: null,
|
|
571
|
+
connection: {
|
|
572
|
+
status: "connecting",
|
|
573
|
+
since: Date.now()
|
|
574
|
+
},
|
|
575
|
+
startedAt: Date.now()
|
|
576
|
+
});
|
|
577
|
+
const initialState = () => ({
|
|
578
|
+
input: initialInput(),
|
|
579
|
+
fixed: null,
|
|
580
|
+
current: null,
|
|
581
|
+
pause: null,
|
|
582
|
+
pendingChanges: [],
|
|
583
|
+
flushScheduled: false,
|
|
584
|
+
lastNotified: ""
|
|
585
|
+
});
|
|
586
|
+
let state = initialState();
|
|
587
|
+
const presenceListeners = /* @__PURE__ */ new Set();
|
|
588
|
+
const pauseListeners = /* @__PURE__ */ new Set();
|
|
589
|
+
/** 発話は 1 秒に数回変わるので、描画のフレームごとにまとめて届ける。 */
|
|
590
|
+
const scheduleFlush = () => {
|
|
591
|
+
if (state.flushScheduled) return;
|
|
592
|
+
state.flushScheduled = true;
|
|
593
|
+
const run = () => {
|
|
594
|
+
state.flushScheduled = false;
|
|
595
|
+
flush();
|
|
596
|
+
};
|
|
597
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
598
|
+
else setTimeout(run, 0);
|
|
599
|
+
};
|
|
600
|
+
const flush = () => {
|
|
601
|
+
const presence = getPresence();
|
|
602
|
+
const serialized = JSON.stringify(presence);
|
|
603
|
+
if (serialized === state.lastNotified && state.pendingChanges.length === 0) return;
|
|
604
|
+
state.lastNotified = serialized;
|
|
605
|
+
const changes = state.pendingChanges;
|
|
606
|
+
state.pendingChanges = [];
|
|
607
|
+
for (const listener of presenceListeners) callScenario("onPresenceChange ハンドラ", () => listener(presence, changes));
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* 入力が変わったら組み立て直す。
|
|
611
|
+
*
|
|
612
|
+
* ここで例外を出すと、呼んだ側 (WebSocket の状態の処理) まで抜けて再接続の予約が消える。
|
|
613
|
+
* 組み立てに失敗したら直前の値を出し続ける。
|
|
614
|
+
*/
|
|
615
|
+
const recompute = () => {
|
|
616
|
+
try {
|
|
617
|
+
const prev = getPresence();
|
|
618
|
+
const next = state.fixed ?? buildPresence(state.input);
|
|
619
|
+
state.current = next;
|
|
620
|
+
state.pendingChanges.push(...diffPresence(state.input.mySeatId, prev, next));
|
|
621
|
+
scheduleFlush();
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error("[uzu] presence を組み立てられなかったので、直前の値を使い続けます", err);
|
|
382
624
|
}
|
|
383
|
-
requester(true);
|
|
384
625
|
};
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
626
|
+
const setPause = (pause) => {
|
|
627
|
+
if (JSON.stringify(pause) === JSON.stringify(state.pause)) return;
|
|
628
|
+
state.pause = pause;
|
|
629
|
+
for (const listener of pauseListeners) callScenario("onPauseChange ハンドラ", () => listener(pause));
|
|
630
|
+
};
|
|
631
|
+
const configurePresence = (mySeatId) => {
|
|
632
|
+
state.input = {
|
|
633
|
+
...state.input,
|
|
634
|
+
mySeatId
|
|
635
|
+
};
|
|
636
|
+
recompute();
|
|
637
|
+
};
|
|
638
|
+
/** ローカル実行。サーバーが無いので、自分はつながっていて通話は無いものとして固定する。 */
|
|
639
|
+
const useFixedPresence = (mySeatId) => {
|
|
640
|
+
state.input = {
|
|
641
|
+
...state.input,
|
|
642
|
+
mySeatId
|
|
643
|
+
};
|
|
644
|
+
state.fixed = {
|
|
645
|
+
me: {
|
|
646
|
+
connection: { status: "online" },
|
|
647
|
+
voice: {
|
|
648
|
+
status: "off",
|
|
649
|
+
reason: "this-device"
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
others: {},
|
|
653
|
+
observers: {
|
|
654
|
+
count: 0,
|
|
655
|
+
speaking: []
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
recompute();
|
|
659
|
+
};
|
|
660
|
+
/** ローカル実行の一時停止。サーバーが居ないので presence ではなく SDK 自身が決める。 */
|
|
661
|
+
const setLocalPause = (pause) => {
|
|
662
|
+
setPause(pause);
|
|
663
|
+
};
|
|
664
|
+
const applyPresenceSnapshot = (snapshot) => {
|
|
665
|
+
state.input = {
|
|
666
|
+
...state.input,
|
|
667
|
+
snapshot,
|
|
668
|
+
clockOffset: Date.now() - snapshot.serverNow
|
|
669
|
+
};
|
|
670
|
+
const pause = snapshot.pause;
|
|
671
|
+
setPause(isRecord$2(pause) ? pause : null);
|
|
672
|
+
recompute();
|
|
673
|
+
};
|
|
674
|
+
const applyVoiceChanged = (voice) => {
|
|
675
|
+
state.input = {
|
|
676
|
+
...state.input,
|
|
677
|
+
voice
|
|
678
|
+
};
|
|
679
|
+
recompute();
|
|
680
|
+
};
|
|
681
|
+
const setWireStatus = (status) => {
|
|
682
|
+
if (state.input.connection.status === status) return;
|
|
683
|
+
state.input = {
|
|
684
|
+
...state.input,
|
|
685
|
+
connection: {
|
|
686
|
+
status,
|
|
687
|
+
since: Date.now()
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
recompute();
|
|
691
|
+
};
|
|
692
|
+
const EMPTY_PRESENCE = {
|
|
693
|
+
me: {
|
|
694
|
+
connection: { status: "online" },
|
|
695
|
+
voice: {
|
|
696
|
+
status: "off",
|
|
697
|
+
reason: "this-device"
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
others: {},
|
|
701
|
+
observers: {
|
|
702
|
+
count: 0,
|
|
703
|
+
speaking: []
|
|
389
704
|
}
|
|
390
|
-
|
|
705
|
+
};
|
|
706
|
+
/** 今の接続と通話の状態。 */
|
|
707
|
+
const getPresence = () => {
|
|
708
|
+
if (state.current === null) try {
|
|
709
|
+
state.current = state.fixed ?? buildPresence(state.input);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error("[uzu] presence を組み立てられませんでした", err);
|
|
712
|
+
return EMPTY_PRESENCE;
|
|
713
|
+
}
|
|
714
|
+
return state.current;
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* 接続と通話の状態が変わったときに呼ばれる。値全体と、変わったところの一覧を渡す。
|
|
718
|
+
* 発話やミュートの変化では `changes` は空で、値だけが新しくなる。
|
|
719
|
+
* 戻り値を呼ぶと、呼ばれなくなる。
|
|
720
|
+
*/
|
|
721
|
+
const onPresenceChange = (listener) => {
|
|
722
|
+
presenceListeners.add(listener);
|
|
723
|
+
return () => {
|
|
724
|
+
presenceListeners.delete(listener);
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
/** 今止まっているか、止まっていればその理由。止まっていなければ null。 */
|
|
728
|
+
const getPause = () => state.pause;
|
|
729
|
+
/**
|
|
730
|
+
* 止まった・再開したときに呼ばれる。理由は今後増えることがあるので、知らない理由でも止まって
|
|
731
|
+
* いるものとして扱う。戻り値を呼ぶと、呼ばれなくなる。
|
|
732
|
+
*/
|
|
733
|
+
const onPauseChange = (listener) => {
|
|
734
|
+
pauseListeners.add(listener);
|
|
735
|
+
return () => {
|
|
736
|
+
pauseListeners.delete(listener);
|
|
737
|
+
};
|
|
391
738
|
};
|
|
392
739
|
//#endregion
|
|
393
740
|
//#region src/reconnectable-ws.ts
|
|
@@ -1093,7 +1440,7 @@ let hud = {
|
|
|
1093
1440
|
};
|
|
1094
1441
|
let current = null;
|
|
1095
1442
|
let envProbe = null;
|
|
1096
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1443
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
1097
1444
|
const zero = () => ({
|
|
1098
1445
|
top: 0,
|
|
1099
1446
|
right: 0,
|
|
@@ -1176,7 +1523,7 @@ const refresh = () => {
|
|
|
1176
1523
|
const next = compute();
|
|
1177
1524
|
if (current && same(current, next)) return;
|
|
1178
1525
|
current = next;
|
|
1179
|
-
for (const cb of listeners) try {
|
|
1526
|
+
for (const cb of listeners$1) try {
|
|
1180
1527
|
cb(next);
|
|
1181
1528
|
} catch (err) {
|
|
1182
1529
|
console.warn("[UZU SDK] onInsetsChange listener threw:", err);
|
|
@@ -1287,15 +1634,15 @@ const getInsets = () => {
|
|
|
1287
1634
|
* 登録時に即時呼び出しはしない。初期値は [getInsets] で読むこと。
|
|
1288
1635
|
*/
|
|
1289
1636
|
const onInsetsChange = (cb) => {
|
|
1290
|
-
listeners.add(cb);
|
|
1637
|
+
listeners$1.add(cb);
|
|
1291
1638
|
return () => {
|
|
1292
|
-
listeners.delete(cb);
|
|
1639
|
+
listeners$1.delete(cb);
|
|
1293
1640
|
};
|
|
1294
1641
|
};
|
|
1295
1642
|
//#endregion
|
|
1296
1643
|
//#region src/bridge-envelope.ts
|
|
1297
|
-
const isBridgeChannel = (value) => value === "sdk" || value === "game";
|
|
1298
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1644
|
+
const isBridgeChannel = (value) => value === "sdk" || value === "game" || value === "platform";
|
|
1645
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1299
1646
|
/**
|
|
1300
1647
|
* 親 frame から postMessage で届く envelope を BridgeMessage に復元する。
|
|
1301
1648
|
* string でも object でも来るので、形を確かめてから通す。
|
|
@@ -1311,45 +1658,119 @@ const parseBridgeEnvelope = (raw) => {
|
|
|
1311
1658
|
} catch {
|
|
1312
1659
|
return null;
|
|
1313
1660
|
}
|
|
1314
|
-
if (!isRecord(value)) return null;
|
|
1661
|
+
if (!isRecord$1(value)) return null;
|
|
1315
1662
|
const { channel, type, payload, playerId } = value;
|
|
1316
1663
|
if (!isBridgeChannel(channel) || typeof type !== "string") return null;
|
|
1317
1664
|
return {
|
|
1318
1665
|
channel,
|
|
1319
1666
|
type,
|
|
1320
|
-
payload: isRecord(payload) ? payload : {},
|
|
1667
|
+
payload: isRecord$1(payload) ? payload : {},
|
|
1321
1668
|
...typeof playerId === "string" ? { playerId } : {}
|
|
1322
1669
|
};
|
|
1323
1670
|
};
|
|
1324
1671
|
//#endregion
|
|
1325
|
-
//#region src/
|
|
1672
|
+
//#region src/host-bridge.ts
|
|
1673
|
+
let sender = null;
|
|
1674
|
+
const setHostSender = (fn) => {
|
|
1675
|
+
sender = fn;
|
|
1676
|
+
};
|
|
1677
|
+
const sendToHost = (channel, type, payload) => {
|
|
1678
|
+
sender?.(channel, type, payload);
|
|
1679
|
+
};
|
|
1680
|
+
//#endregion
|
|
1681
|
+
//#region src/pause-state.ts
|
|
1326
1682
|
/**
|
|
1327
1683
|
* @docs
|
|
1328
|
-
* -
|
|
1684
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
1329
1685
|
*
|
|
1330
|
-
*
|
|
1331
|
-
* シナリオのコールバックを呼ぶ箇所は、すべてここを通す。
|
|
1686
|
+
* サーバーが時計を止めているか (wire の `frozen`) を SDK 全体で 1 箇所に持つ。
|
|
1332
1687
|
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
* 永久に更新されなくなる — 2026-09-09 の UZU TOKYO 公演で実際に起きた壊れ方がこれ。
|
|
1688
|
+
* 止まった理由は見ない。理由 (緊急一時停止・自動中断) は presence の `pause` で届き、
|
|
1689
|
+
* シナリオには `getPause` / `onPauseChange` で出す。ここは理由を問わず時計と送信を
|
|
1690
|
+
* 止めるだけにしておき、理由が増えても公開済みのシナリオの時計が止まるようにする。
|
|
1337
1691
|
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
|
|
1692
|
+
* ゲーム内時計の凍結もここから駆動する。停止フラグと時計の凍結が別々に動くと、
|
|
1693
|
+
* 「止まっているのにカウントダウンだけ進む」がいつか必ず起きる。
|
|
1694
|
+
*/
|
|
1695
|
+
let paused = false;
|
|
1696
|
+
let pausedBy = null;
|
|
1697
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1698
|
+
/** transport から呼ぶ内部関数。停止状態が変わったときだけ通知する。 */
|
|
1699
|
+
const setPauseState = (next, by) => {
|
|
1700
|
+
pausedBy = next ? by : null;
|
|
1701
|
+
if (next === paused) return;
|
|
1702
|
+
paused = next;
|
|
1703
|
+
if (paused) freezeGameTime();
|
|
1704
|
+
else unfreezeGameTime();
|
|
1705
|
+
for (const cb of listeners) try {
|
|
1706
|
+
cb(paused);
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
console.warn("[uzu] frozen listener threw:", err);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
/** 時計が止まっているか。送信の先読みを止めるのに使う。 */
|
|
1712
|
+
const isPaused = () => {
|
|
1713
|
+
return paused;
|
|
1714
|
+
};
|
|
1715
|
+
/** 停止を要求した playerId。停止していなければ null。 */
|
|
1716
|
+
const getPausedBy = () => {
|
|
1717
|
+
return pausedBy;
|
|
1718
|
+
};
|
|
1719
|
+
/** 時計の停止の変化を購読する (ホストへの中継用)。 */
|
|
1720
|
+
const onFrozenChange = (cb) => {
|
|
1721
|
+
listeners.add(cb);
|
|
1722
|
+
};
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region src/platform.ts
|
|
1725
|
+
/**
|
|
1726
|
+
* @docs
|
|
1727
|
+
* - 接続と通話の状態 (Presence): docs/docs/uzu_code/presence.md
|
|
1728
|
+
* - ブリッジ仕様: docs/docs/uzu_code/bridge.md
|
|
1729
|
+
*
|
|
1730
|
+
* アプリとサーバーのあいだの伝言 (bridge の `platform` チャネル ⇄ WebSocket の `__platform`) を運ぶ。
|
|
1731
|
+
*
|
|
1732
|
+
* 中身は見ない。通話の申告や一時停止の要求の中身は、アプリとサーバーの更新だけで変えられる
|
|
1733
|
+
* ようにしておく。例外は presence で、SDK も読んでシナリオの `Presence` を組み立てる。
|
|
1341
1734
|
*/
|
|
1735
|
+
let serverSink = null;
|
|
1342
1736
|
/**
|
|
1343
|
-
*
|
|
1737
|
+
* 出口ができる前に届いた伝言。種類ごとに最新の 1 通だけ持つ。
|
|
1344
1738
|
*
|
|
1345
|
-
*
|
|
1739
|
+
* アプリは SDK の `ready` で通話の申告を送り直すが、`ready` は `run()` (出口を作る) より前に
|
|
1740
|
+
* 出る。捨てると、WebView を読み込み直したときに申告がサーバーへ届かないままになる
|
|
1741
|
+
* (ゲームの接続は online のまま残るので、アプリの送り直しも走らない)。どの種類も「今の状態」を
|
|
1742
|
+
* 伝えるものなので、溜めるのは最新の 1 通で足りる。
|
|
1346
1743
|
*/
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1744
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1745
|
+
/**
|
|
1746
|
+
* サーバーへの出口を登録する。オンラインは WebSocket、ローカル実行は SDK 自身
|
|
1747
|
+
* (サーバーが無いので、一時停止の要求だけをその場で処理する)。
|
|
1748
|
+
*/
|
|
1749
|
+
const setPlatformServerSink = (sink) => {
|
|
1750
|
+
serverSink = sink;
|
|
1751
|
+
if (!sink) return;
|
|
1752
|
+
const held = [...pending.values()];
|
|
1753
|
+
pending.clear();
|
|
1754
|
+
for (const message of held) sink(message);
|
|
1755
|
+
};
|
|
1756
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1757
|
+
/** アプリから届いた伝言をサーバーへ運ぶ。つながる前・切れている間は WebSocket 側が溜める。 */
|
|
1758
|
+
const forwardToServer = (message) => {
|
|
1759
|
+
if (!serverSink) {
|
|
1760
|
+
pending.delete(message.type);
|
|
1761
|
+
pending.set(message.type, message);
|
|
1762
|
+
return;
|
|
1352
1763
|
}
|
|
1764
|
+
serverSink(message);
|
|
1765
|
+
};
|
|
1766
|
+
/** 組み立てに要る最低限だけを見る。足りない項目は組み立てる側が既定の値で埋める。 */
|
|
1767
|
+
const isPresenceSnapshot = (value) => isRecord(value) && typeof value.serverNow === "number" && isRecord(value.seats);
|
|
1768
|
+
/** サーバーから届いた伝言をアプリへ運ぶ。 */
|
|
1769
|
+
const forwardToHost = (raw) => {
|
|
1770
|
+
if (!isRecord(raw) || typeof raw.type !== "string") return;
|
|
1771
|
+
const payload = isRecord(raw.payload) ? raw.payload : {};
|
|
1772
|
+
sendToHost("platform", raw.type, payload);
|
|
1773
|
+
if (raw.type === PLATFORM_MESSAGE_TYPE.presence && isPresenceSnapshot(raw.payload)) applyPresenceSnapshot(raw.payload);
|
|
1353
1774
|
};
|
|
1354
1775
|
//#endregion
|
|
1355
1776
|
//#region src/run/local-server-action.ts
|
|
@@ -1451,7 +1872,7 @@ const runLocalServerAction = (config) => {
|
|
|
1451
1872
|
}
|
|
1452
1873
|
};
|
|
1453
1874
|
clock.restart();
|
|
1454
|
-
const setupTime =
|
|
1875
|
+
const setupTime = GAME_START;
|
|
1455
1876
|
const setupArgs = {
|
|
1456
1877
|
players,
|
|
1457
1878
|
ctx: {
|
|
@@ -1534,10 +1955,18 @@ const runLocalServerAction = (config) => {
|
|
|
1534
1955
|
if (!(paused ? clock.freeze("emergency-stop") : clock.unfreeze("emergency-stop"))) return;
|
|
1535
1956
|
observeGameTime(gameTime());
|
|
1536
1957
|
setPauseState(paused, paused ? myId : null);
|
|
1958
|
+
setLocalPause(paused ? {
|
|
1959
|
+
reason: "emergency",
|
|
1960
|
+
by: myId
|
|
1961
|
+
} : null);
|
|
1537
1962
|
observeGameTime(gameTime());
|
|
1538
1963
|
syncWakeup();
|
|
1539
1964
|
};
|
|
1540
|
-
|
|
1965
|
+
setPlatformServerSink((message) => {
|
|
1966
|
+
if (message.type === PLATFORM_MESSAGE_TYPE.pauseRequest) applyPause(true);
|
|
1967
|
+
else if (message.type === PLATFORM_MESSAGE_TYPE.pauseResume) applyPause(false);
|
|
1968
|
+
});
|
|
1969
|
+
useFixedPresence(myId);
|
|
1541
1970
|
inputs(dispatchAction);
|
|
1542
1971
|
syncWakeup();
|
|
1543
1972
|
onState(state, myId);
|
|
@@ -1793,23 +2222,52 @@ const createOptimisticActionClient = (config) => {
|
|
|
1793
2222
|
};
|
|
1794
2223
|
//#endregion
|
|
1795
2224
|
//#region src/run/server-action.ts
|
|
1796
|
-
|
|
2225
|
+
/**
|
|
2226
|
+
* @docs
|
|
2227
|
+
* - play-server 仕様: docs/docs/uzu_code/play-server.md
|
|
2228
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
2229
|
+
*
|
|
2230
|
+
* ServerAction モードのオンライン接続 (WebSocket トランスポート)。
|
|
2231
|
+
*
|
|
2232
|
+
* ホスト/ゲストの区別なし。全クライアントがサーバーに対して同じ立場で action を送信し、
|
|
2233
|
+
* サーバーが reducer を実行して state を遷移させる。
|
|
2234
|
+
*
|
|
2235
|
+
* 楽観的更新ロジック (pending actions / reapply / ack 重複排除) は
|
|
2236
|
+
* `optimistic-action-client.ts` に集約。本ファイルは WebSocket 固有の責務
|
|
2237
|
+
* (接続管理 / メッセージ振り分け / delta seq の連続性チェック / フル state 再要求) のみ持つ。
|
|
2238
|
+
*/
|
|
2239
|
+
const WIRE_STATUS = {
|
|
2240
|
+
connecting: "connecting",
|
|
2241
|
+
connected: "online",
|
|
2242
|
+
reconnecting: "reconnecting",
|
|
2243
|
+
disconnected: "offline"
|
|
2244
|
+
};
|
|
2245
|
+
const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, seatKind, monitor) => {
|
|
2246
|
+
configurePresence(seatId);
|
|
1797
2247
|
const toWire = (p) => ({
|
|
1798
2248
|
id: p.id,
|
|
1799
2249
|
name: p.nickname,
|
|
1800
2250
|
iconUrl: p.iconUrl,
|
|
1801
2251
|
characterId: p.characterId
|
|
1802
2252
|
});
|
|
1803
|
-
const
|
|
2253
|
+
const query = new URLSearchParams({
|
|
1804
2254
|
seatId,
|
|
1805
2255
|
nickname: "Player",
|
|
1806
2256
|
players: JSON.stringify(players.map(toWire))
|
|
1807
|
-
})
|
|
2257
|
+
});
|
|
2258
|
+
if (monitor) query.set("monitor", "1");
|
|
2259
|
+
const wsUrl = `${gameEndpoint}/${roomId}?${query}`;
|
|
1808
2260
|
console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
|
|
1809
2261
|
const ws = new ReconnectableWebSocket(wsUrl, {
|
|
1810
2262
|
onConnectionStateChange: (state) => {
|
|
1811
2263
|
console.log(`[SDK ServerAction] 📡 Connection state: ${state}`);
|
|
1812
|
-
|
|
2264
|
+
try {
|
|
2265
|
+
const status = WIRE_STATUS[state];
|
|
2266
|
+
setWireStatus(status);
|
|
2267
|
+
sendToHost("sdk", "connectionChanged", { status });
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
console.error("[SDK ServerAction] 接続の状態を伝えられませんでした", err);
|
|
2270
|
+
}
|
|
1813
2271
|
},
|
|
1814
2272
|
shouldBuffer: (data) => {
|
|
1815
2273
|
try {
|
|
@@ -1820,9 +2278,11 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1820
2278
|
}
|
|
1821
2279
|
}
|
|
1822
2280
|
});
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2281
|
+
setPlatformServerSink((message) => {
|
|
2282
|
+
ws.send(JSON.stringify({
|
|
2283
|
+
type: PLATFORM_WIRE_TYPE,
|
|
2284
|
+
message
|
|
2285
|
+
}));
|
|
1826
2286
|
});
|
|
1827
2287
|
/** サーバー seq (delta の連続性チェック用) */
|
|
1828
2288
|
let serverSeq = 0;
|
|
@@ -1858,6 +2318,10 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1858
2318
|
const msgType = parsed.type;
|
|
1859
2319
|
console.log(`[SDK ServerAction] ⬅ recv type=${msgType}`);
|
|
1860
2320
|
client.observeGameTime(parsed.gameTime);
|
|
2321
|
+
if (msgType === "__platform") {
|
|
2322
|
+
forwardToHost(parsed.message);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
1861
2325
|
if (msgType === "__pause_state") {
|
|
1862
2326
|
const frozen = parsed.frozen === true;
|
|
1863
2327
|
console.log(`[SDK ServerAction] ${frozen ? "⏸" : "▶️"} pause_state frozen=${String(frozen)}`);
|
|
@@ -1965,7 +2429,6 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1965
2429
|
//#endregion
|
|
1966
2430
|
//#region src/index.ts
|
|
1967
2431
|
const gameHandlers = /* @__PURE__ */ new Map();
|
|
1968
|
-
const _playersChangedHandlers = [];
|
|
1969
2432
|
let _lastStateSnap = null;
|
|
1970
2433
|
let _gameEndpoint = null;
|
|
1971
2434
|
let _initialized = false;
|
|
@@ -2025,7 +2488,7 @@ const init = (_opts) => {
|
|
|
2025
2488
|
handleMessage(data);
|
|
2026
2489
|
});
|
|
2027
2490
|
applyInsets(params);
|
|
2028
|
-
|
|
2491
|
+
onFrozenChange((paused) => {
|
|
2029
2492
|
sendRaw("sdk", "pauseState", {
|
|
2030
2493
|
paused,
|
|
2031
2494
|
by: getPausedBy()
|
|
@@ -2054,11 +2517,23 @@ const playBgm = (sound) => {
|
|
|
2054
2517
|
const stopBgm = () => {
|
|
2055
2518
|
sendRaw("sdk", "stopBgm", {});
|
|
2056
2519
|
};
|
|
2057
|
-
|
|
2058
|
-
|
|
2520
|
+
/**
|
|
2521
|
+
* 観測席 (観戦・進行管理) が聴く通話の部屋を選ぶ。null は全体の部屋。
|
|
2522
|
+
*
|
|
2523
|
+
* プレイヤーの部屋はロジックの `voicePlan` が決めるので、プレイヤー席では何も起きない。
|
|
2524
|
+
*/
|
|
2525
|
+
const setListeningRoom = (room) => {
|
|
2526
|
+
sendRaw("sdk", "setListeningRoom", { room });
|
|
2059
2527
|
};
|
|
2060
|
-
|
|
2061
|
-
|
|
2528
|
+
/**
|
|
2529
|
+
* 本人のマイクをミュートする。
|
|
2530
|
+
*
|
|
2531
|
+
* 作品からできるのはミュートだけで、外すことはできない。外すのは本人が HUD のマイクボタンで
|
|
2532
|
+
* 行う。勝手にマイクを開くと、本人の知らないうちに声が他の人へ届いてしまうため。
|
|
2533
|
+
* 場面ごとに決まったミュートは `voicePlan` の `mic` で宣言する。
|
|
2534
|
+
*/
|
|
2535
|
+
const muteMic = () => {
|
|
2536
|
+
sendRaw("sdk", "setMicMuted", { muted: true });
|
|
2062
2537
|
};
|
|
2063
2538
|
const HAPTIC_FALLBACK_MS = {
|
|
2064
2539
|
light: 10,
|
|
@@ -2139,10 +2614,6 @@ const gameReady = () => {
|
|
|
2139
2614
|
const finishGame = () => {
|
|
2140
2615
|
sendRaw("sdk", "finishGame", {});
|
|
2141
2616
|
};
|
|
2142
|
-
/** Flutter からの playersChanged メッセージを受信するハンドラを登録する。 */
|
|
2143
|
-
const onPlayersChanged = (handler) => {
|
|
2144
|
-
_playersChangedHandlers.push(handler);
|
|
2145
|
-
};
|
|
2146
2617
|
/**
|
|
2147
2618
|
* `A` に既定値が要る。 TypeScript は型引数の部分推論ができないので、 既定が無いと
|
|
2148
2619
|
* 既存の `run<State>({…})` が「2 つ必要」で落ちる。 既定を置けば型引数を書かない
|
|
@@ -2175,7 +2646,8 @@ const run = (config) => {
|
|
|
2175
2646
|
else if (_gameEndpoint) {
|
|
2176
2647
|
const { seatId, players } = resolveSeatParams(params);
|
|
2177
2648
|
const seatKind = resolveSeatKind(params);
|
|
2178
|
-
|
|
2649
|
+
const monitor = params.get("monitor") === "1";
|
|
2650
|
+
runOnlineServerAction(wrappedConfig, _gameEndpoint, roomId, seatId, players, seatKind, monitor);
|
|
2179
2651
|
} 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.");
|
|
2180
2652
|
attachDevHooksIfNotHosted();
|
|
2181
2653
|
};
|
|
@@ -2225,6 +2697,7 @@ const sendRaw = (channel, type, payload) => {
|
|
|
2225
2697
|
if (window.FlutterHost) window.FlutterHost.postMessage(JSON.stringify(msg));
|
|
2226
2698
|
else if (window.parent !== window) window.parent.postMessage(JSON.stringify(msg), "*");
|
|
2227
2699
|
};
|
|
2700
|
+
setHostSender(sendRaw);
|
|
2228
2701
|
const handleMessage = (msg) => {
|
|
2229
2702
|
const { channel, type, payload } = msg;
|
|
2230
2703
|
if (channel === "sdk") {
|
|
@@ -2233,28 +2706,27 @@ const handleMessage = (msg) => {
|
|
|
2233
2706
|
console.log(`[SDK] 🔧 handleMessage sdk/getState → responding`);
|
|
2234
2707
|
sendRaw("sdk", "stateResponse", { state: _lastStateSnap });
|
|
2235
2708
|
return;
|
|
2236
|
-
case "
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2709
|
+
case "voiceChanged": {
|
|
2710
|
+
const voice = parseVoiceChanged(payload);
|
|
2711
|
+
if (voice) applyVoiceChanged(voice);
|
|
2239
2712
|
return;
|
|
2240
2713
|
}
|
|
2241
|
-
case "requestPause":
|
|
2242
|
-
console.log(`[SDK] ⏸ handleMessage sdk/requestPause`);
|
|
2243
|
-
requestPause();
|
|
2244
|
-
return;
|
|
2245
|
-
case "requestResume":
|
|
2246
|
-
console.log(`[SDK] ▶️ handleMessage sdk/requestResume`);
|
|
2247
|
-
requestResume();
|
|
2248
|
-
return;
|
|
2249
2714
|
case "insetsChanged":
|
|
2250
2715
|
handleInsetsChanged(payload);
|
|
2251
2716
|
return;
|
|
2252
2717
|
}
|
|
2253
2718
|
return;
|
|
2254
2719
|
}
|
|
2720
|
+
if (channel === "platform") {
|
|
2721
|
+
forwardToServer({
|
|
2722
|
+
type,
|
|
2723
|
+
payload
|
|
2724
|
+
});
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2255
2727
|
const h = gameHandlers.get(type) ?? [];
|
|
2256
2728
|
console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
|
|
2257
2729
|
for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload));
|
|
2258
2730
|
};
|
|
2259
2731
|
//#endregion
|
|
2260
|
-
export { DEFAULT_ICON_URLS, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks,
|
|
2732
|
+
export { DEFAULT_ICON_URLS, DEFAULT_VOICE_PLAN, GAME_START, ReconnectableWebSocket, SeededRandomImpl, applyJsonMergePatch, applyJsonPatch, attachDevHooks, createDevHooks, finishGame, firstFrameReady, gameReady, gameTime, getInsets, getPause, getPredictionWarnings, getPresence, haptic, init, isHosted, isServerOnlyAction, minus, muteMic, on, onInsetsChange, onPauseChange, onPresenceChange, playBgm, playSound, plus, run, send, serverOnly, setListeningRoom, stopBgm, sub };
|