@uzuhq/code-sdk 0.8.11 → 0.9.0
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 +598 -113
- 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: []
|
|
704
|
+
}
|
|
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;
|
|
389
713
|
}
|
|
390
|
-
|
|
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,40 +1634,143 @@ 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
|
-
//#region src/
|
|
1643
|
+
//#region src/bridge-envelope.ts
|
|
1644
|
+
const isBridgeChannel = (value) => value === "sdk" || value === "game" || value === "platform";
|
|
1645
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1646
|
+
/**
|
|
1647
|
+
* 親 frame から postMessage で届く envelope を BridgeMessage に復元する。
|
|
1648
|
+
* string でも object でも来るので、形を確かめてから通す。
|
|
1649
|
+
*
|
|
1650
|
+
* payload を欠いた envelope は `{}` で埋める。受け手の handleMessage は
|
|
1651
|
+
* `payload.players` のように payload を直に読むので、ここで落とすと
|
|
1652
|
+
* 例外やハンドラへの `undefined` 渡しになる。
|
|
1653
|
+
*/
|
|
1654
|
+
const parseBridgeEnvelope = (raw) => {
|
|
1655
|
+
let value = raw;
|
|
1656
|
+
if (typeof value === "string") try {
|
|
1657
|
+
value = JSON.parse(value);
|
|
1658
|
+
} catch {
|
|
1659
|
+
return null;
|
|
1660
|
+
}
|
|
1661
|
+
if (!isRecord$1(value)) return null;
|
|
1662
|
+
const { channel, type, payload, playerId } = value;
|
|
1663
|
+
if (!isBridgeChannel(channel) || typeof type !== "string") return null;
|
|
1664
|
+
return {
|
|
1665
|
+
channel,
|
|
1666
|
+
type,
|
|
1667
|
+
payload: isRecord$1(payload) ? payload : {},
|
|
1668
|
+
...typeof playerId === "string" ? { playerId } : {}
|
|
1669
|
+
};
|
|
1670
|
+
};
|
|
1671
|
+
//#endregion
|
|
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
|
|
1297
1682
|
/**
|
|
1298
1683
|
* @docs
|
|
1299
|
-
* -
|
|
1684
|
+
* - 緊急停止とゲーム内時計: docs/docs/uzu_code/emergency-stop.md
|
|
1300
1685
|
*
|
|
1301
|
-
*
|
|
1302
|
-
* シナリオのコールバックを呼ぶ箇所は、すべてここを通す。
|
|
1686
|
+
* サーバーが時計を止めているか (wire の `frozen`) を SDK 全体で 1 箇所に持つ。
|
|
1303
1687
|
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
* 永久に更新されなくなる — 2026-09-09 の UZU TOKYO 公演で実際に起きた壊れ方がこれ。
|
|
1688
|
+
* 止まった理由は見ない。理由 (緊急一時停止・自動中断) は presence の `pause` で届き、
|
|
1689
|
+
* シナリオには `getPause` / `onPauseChange` で出す。ここは理由を問わず時計と送信を
|
|
1690
|
+
* 止めるだけにしておき、理由が増えても公開済みのシナリオの時計が止まるようにする。
|
|
1308
1691
|
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
* ところでは 1 件の失敗を他の購読へ波及させない。
|
|
1692
|
+
* ゲーム内時計の凍結もここから駆動する。停止フラグと時計の凍結が別々に動くと、
|
|
1693
|
+
* 「止まっているのにカウントダウンだけ進む」がいつか必ず起きる。
|
|
1312
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
|
|
1313
1725
|
/**
|
|
1314
|
-
*
|
|
1726
|
+
* @docs
|
|
1727
|
+
* - 接続と通話の状態 (Presence): docs/docs/uzu_code/presence.md
|
|
1728
|
+
* - ブリッジ仕様: docs/docs/uzu_code/bridge.md
|
|
1315
1729
|
*
|
|
1316
|
-
*
|
|
1730
|
+
* アプリとサーバーのあいだの伝言 (bridge の `platform` チャネル ⇄ WebSocket の `__platform`) を運ぶ。
|
|
1731
|
+
*
|
|
1732
|
+
* 中身は見ない。通話の申告や一時停止の要求の中身は、アプリとサーバーの更新だけで変えられる
|
|
1733
|
+
* ようにしておく。例外は presence で、SDK も読んでシナリオの `Presence` を組み立てる。
|
|
1317
1734
|
*/
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1735
|
+
let serverSink = null;
|
|
1736
|
+
/**
|
|
1737
|
+
* 出口ができる前に届いた伝言。種類ごとに最新の 1 通だけ持つ。
|
|
1738
|
+
*
|
|
1739
|
+
* アプリは SDK の `ready` で通話の申告を送り直すが、`ready` は `run()` (出口を作る) より前に
|
|
1740
|
+
* 出る。捨てると、WebView を読み込み直したときに申告がサーバーへ届かないままになる
|
|
1741
|
+
* (ゲームの接続は online のまま残るので、アプリの送り直しも走らない)。どの種類も「今の状態」を
|
|
1742
|
+
* 伝えるものなので、溜めるのは最新の 1 通で足りる。
|
|
1743
|
+
*/
|
|
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;
|
|
1323
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);
|
|
1324
1774
|
};
|
|
1325
1775
|
//#endregion
|
|
1326
1776
|
//#region src/run/local-server-action.ts
|
|
@@ -1422,7 +1872,7 @@ const runLocalServerAction = (config) => {
|
|
|
1422
1872
|
}
|
|
1423
1873
|
};
|
|
1424
1874
|
clock.restart();
|
|
1425
|
-
const setupTime =
|
|
1875
|
+
const setupTime = GAME_START;
|
|
1426
1876
|
const setupArgs = {
|
|
1427
1877
|
players,
|
|
1428
1878
|
ctx: {
|
|
@@ -1505,10 +1955,18 @@ const runLocalServerAction = (config) => {
|
|
|
1505
1955
|
if (!(paused ? clock.freeze("emergency-stop") : clock.unfreeze("emergency-stop"))) return;
|
|
1506
1956
|
observeGameTime(gameTime());
|
|
1507
1957
|
setPauseState(paused, paused ? myId : null);
|
|
1958
|
+
setLocalPause(paused ? {
|
|
1959
|
+
reason: "emergency",
|
|
1960
|
+
by: myId
|
|
1961
|
+
} : null);
|
|
1508
1962
|
observeGameTime(gameTime());
|
|
1509
1963
|
syncWakeup();
|
|
1510
1964
|
};
|
|
1511
|
-
|
|
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);
|
|
1512
1970
|
inputs(dispatchAction);
|
|
1513
1971
|
syncWakeup();
|
|
1514
1972
|
onState(state, myId);
|
|
@@ -1764,23 +2222,52 @@ const createOptimisticActionClient = (config) => {
|
|
|
1764
2222
|
};
|
|
1765
2223
|
//#endregion
|
|
1766
2224
|
//#region src/run/server-action.ts
|
|
1767
|
-
|
|
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);
|
|
1768
2247
|
const toWire = (p) => ({
|
|
1769
2248
|
id: p.id,
|
|
1770
2249
|
name: p.nickname,
|
|
1771
2250
|
iconUrl: p.iconUrl,
|
|
1772
2251
|
characterId: p.characterId
|
|
1773
2252
|
});
|
|
1774
|
-
const
|
|
2253
|
+
const query = new URLSearchParams({
|
|
1775
2254
|
seatId,
|
|
1776
2255
|
nickname: "Player",
|
|
1777
2256
|
players: JSON.stringify(players.map(toWire))
|
|
1778
|
-
})
|
|
2257
|
+
});
|
|
2258
|
+
if (monitor) query.set("monitor", "1");
|
|
2259
|
+
const wsUrl = `${gameEndpoint}/${roomId}?${query}`;
|
|
1779
2260
|
console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
|
|
1780
2261
|
const ws = new ReconnectableWebSocket(wsUrl, {
|
|
1781
2262
|
onConnectionStateChange: (state) => {
|
|
1782
2263
|
console.log(`[SDK ServerAction] 📡 Connection state: ${state}`);
|
|
1783
|
-
|
|
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
|
+
}
|
|
1784
2271
|
},
|
|
1785
2272
|
shouldBuffer: (data) => {
|
|
1786
2273
|
try {
|
|
@@ -1791,9 +2278,11 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1791
2278
|
}
|
|
1792
2279
|
}
|
|
1793
2280
|
});
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2281
|
+
setPlatformServerSink((message) => {
|
|
2282
|
+
ws.send(JSON.stringify({
|
|
2283
|
+
type: PLATFORM_WIRE_TYPE,
|
|
2284
|
+
message
|
|
2285
|
+
}));
|
|
1797
2286
|
});
|
|
1798
2287
|
/** サーバー seq (delta の連続性チェック用) */
|
|
1799
2288
|
let serverSeq = 0;
|
|
@@ -1829,6 +2318,10 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1829
2318
|
const msgType = parsed.type;
|
|
1830
2319
|
console.log(`[SDK ServerAction] ⬅ recv type=${msgType}`);
|
|
1831
2320
|
client.observeGameTime(parsed.gameTime);
|
|
2321
|
+
if (msgType === "__platform") {
|
|
2322
|
+
forwardToHost(parsed.message);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
1832
2325
|
if (msgType === "__pause_state") {
|
|
1833
2326
|
const frozen = parsed.frozen === true;
|
|
1834
2327
|
console.log(`[SDK ServerAction] ${frozen ? "⏸" : "▶️"} pause_state frozen=${String(frozen)}`);
|
|
@@ -1936,7 +2429,6 @@ const runOnlineServerAction = (config, gameEndpoint, roomId, seatId, players, se
|
|
|
1936
2429
|
//#endregion
|
|
1937
2430
|
//#region src/index.ts
|
|
1938
2431
|
const gameHandlers = /* @__PURE__ */ new Map();
|
|
1939
|
-
const _playersChangedHandlers = [];
|
|
1940
2432
|
let _lastStateSnap = null;
|
|
1941
2433
|
let _gameEndpoint = null;
|
|
1942
2434
|
let _initialized = false;
|
|
@@ -1996,7 +2488,7 @@ const init = (_opts) => {
|
|
|
1996
2488
|
handleMessage(data);
|
|
1997
2489
|
});
|
|
1998
2490
|
applyInsets(params);
|
|
1999
|
-
|
|
2491
|
+
onFrozenChange((paused) => {
|
|
2000
2492
|
sendRaw("sdk", "pauseState", {
|
|
2001
2493
|
paused,
|
|
2002
2494
|
by: getPausedBy()
|
|
@@ -2025,11 +2517,23 @@ const playBgm = (sound) => {
|
|
|
2025
2517
|
const stopBgm = () => {
|
|
2026
2518
|
sendRaw("sdk", "stopBgm", {});
|
|
2027
2519
|
};
|
|
2028
|
-
|
|
2029
|
-
|
|
2520
|
+
/**
|
|
2521
|
+
* 観測席 (観戦・進行管理) が聴く通話の部屋を選ぶ。null は全体の部屋。
|
|
2522
|
+
*
|
|
2523
|
+
* プレイヤーの部屋はロジックの `voicePlan` が決めるので、プレイヤー席では何も起きない。
|
|
2524
|
+
*/
|
|
2525
|
+
const setListeningRoom = (room) => {
|
|
2526
|
+
sendRaw("sdk", "setListeningRoom", { room });
|
|
2030
2527
|
};
|
|
2031
|
-
|
|
2032
|
-
|
|
2528
|
+
/**
|
|
2529
|
+
* 本人のマイクをミュートする。
|
|
2530
|
+
*
|
|
2531
|
+
* 作品からできるのはミュートだけで、外すことはできない。外すのは本人が HUD のマイクボタンで
|
|
2532
|
+
* 行う。勝手にマイクを開くと、本人の知らないうちに声が他の人へ届いてしまうため。
|
|
2533
|
+
* 場面ごとに決まったミュートは `voicePlan` の `mic` で宣言する。
|
|
2534
|
+
*/
|
|
2535
|
+
const muteMic = () => {
|
|
2536
|
+
sendRaw("sdk", "setMicMuted", { muted: true });
|
|
2033
2537
|
};
|
|
2034
2538
|
const HAPTIC_FALLBACK_MS = {
|
|
2035
2539
|
light: 10,
|
|
@@ -2110,10 +2614,6 @@ const gameReady = () => {
|
|
|
2110
2614
|
const finishGame = () => {
|
|
2111
2615
|
sendRaw("sdk", "finishGame", {});
|
|
2112
2616
|
};
|
|
2113
|
-
/** Flutter からの playersChanged メッセージを受信するハンドラを登録する。 */
|
|
2114
|
-
const onPlayersChanged = (handler) => {
|
|
2115
|
-
_playersChangedHandlers.push(handler);
|
|
2116
|
-
};
|
|
2117
2617
|
/**
|
|
2118
2618
|
* `A` に既定値が要る。 TypeScript は型引数の部分推論ができないので、 既定が無いと
|
|
2119
2619
|
* 既存の `run<State>({…})` が「2 つ必要」で落ちる。 既定を置けば型引数を書かない
|
|
@@ -2146,7 +2646,8 @@ const run = (config) => {
|
|
|
2146
2646
|
else if (_gameEndpoint) {
|
|
2147
2647
|
const { seatId, players } = resolveSeatParams(params);
|
|
2148
2648
|
const seatKind = resolveSeatKind(params);
|
|
2149
|
-
|
|
2649
|
+
const monitor = params.get("monitor") === "1";
|
|
2650
|
+
runOnlineServerAction(wrappedConfig, _gameEndpoint, roomId, seatId, players, seatKind, monitor);
|
|
2150
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.");
|
|
2151
2652
|
attachDevHooksIfNotHosted();
|
|
2152
2653
|
};
|
|
@@ -2196,22 +2697,7 @@ const sendRaw = (channel, type, payload) => {
|
|
|
2196
2697
|
if (window.FlutterHost) window.FlutterHost.postMessage(JSON.stringify(msg));
|
|
2197
2698
|
else if (window.parent !== window) window.parent.postMessage(JSON.stringify(msg), "*");
|
|
2198
2699
|
};
|
|
2199
|
-
|
|
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
|
-
};
|
|
2700
|
+
setHostSender(sendRaw);
|
|
2215
2701
|
const handleMessage = (msg) => {
|
|
2216
2702
|
const { channel, type, payload } = msg;
|
|
2217
2703
|
if (channel === "sdk") {
|
|
@@ -2220,28 +2706,27 @@ const handleMessage = (msg) => {
|
|
|
2220
2706
|
console.log(`[SDK] 🔧 handleMessage sdk/getState → responding`);
|
|
2221
2707
|
sendRaw("sdk", "stateResponse", { state: _lastStateSnap });
|
|
2222
2708
|
return;
|
|
2223
|
-
case "
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2709
|
+
case "voiceChanged": {
|
|
2710
|
+
const voice = parseVoiceChanged(payload);
|
|
2711
|
+
if (voice) applyVoiceChanged(voice);
|
|
2226
2712
|
return;
|
|
2227
2713
|
}
|
|
2228
|
-
case "requestPause":
|
|
2229
|
-
console.log(`[SDK] ⏸ handleMessage sdk/requestPause`);
|
|
2230
|
-
requestPause();
|
|
2231
|
-
return;
|
|
2232
|
-
case "requestResume":
|
|
2233
|
-
console.log(`[SDK] ▶️ handleMessage sdk/requestResume`);
|
|
2234
|
-
requestResume();
|
|
2235
|
-
return;
|
|
2236
2714
|
case "insetsChanged":
|
|
2237
2715
|
handleInsetsChanged(payload);
|
|
2238
2716
|
return;
|
|
2239
2717
|
}
|
|
2240
2718
|
return;
|
|
2241
2719
|
}
|
|
2720
|
+
if (channel === "platform") {
|
|
2721
|
+
forwardToServer({
|
|
2722
|
+
type,
|
|
2723
|
+
payload
|
|
2724
|
+
});
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2242
2727
|
const h = gameHandlers.get(type) ?? [];
|
|
2243
2728
|
console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
|
|
2244
2729
|
for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload));
|
|
2245
2730
|
};
|
|
2246
2731
|
//#endregion
|
|
2247
|
-
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 };
|