Skip to content

feat(spectator): 最初の観戦者を観戦ホストにして複数の観戦者を中継する - #24

Draft
progre wants to merge 29 commits into
mainfrom
claude/fervent-bell-okj1ng
Draft

progre wants to merge 29 commits into
mainfrom
claude/fervent-bell-okj1ng

Conversation

@progre

@progre progre commented Sep 24, 2026

Copy link
Copy Markdown
Owner

概要

複数の観戦者を受け付けられるようにします。対戦者は最初の観戦者のみを受け付け、その観戦者を観戦ホストに任命します。以降の観戦者は観戦ホストが受け付け、入力を中継します。観戦ホストが離脱した場合、その配下の観戦者もすべて離脱します(仕様として許容)。

Note

このブランチは #23 の上に積んでいます。#23 のマージまではドラフトとし、差分には #23 の変更も含まれます。
このPR固有の差分: d329696...claude/fervent-bell-okj1ng

仕組み

  • 新しいメッセージ DelegateSpectatorHost で観戦ホストを任命します。予約部屋の場合は部屋名とキーも渡し、観戦ホストが予約部屋で観戦者を待ち受けます。Pure P2P の場合は観戦ホストが F1 で受け付けます
  • 対戦者は観戦ホストを任命した時点で受け付けを止めます。観戦ホスト(保留中を含む)がいなくなったら、Pure P2P で受け付けを再開します(予約部屋は観戦ホストの離脱時に削除されるため)
  • 観戦ホストは、入力と乱数シードを処理した箇所で、対戦者と同じ SpectatorHostState を呼び出して配下に送ります。feat(spectator): 観戦者が任意のタイミングで途中参加できるようにする #23 の同期ポイント・途中参加の仕組みも観戦ホスト自身の画面遷移から作られるため、配下の観戦者もそのまま途中参加できる想定です
  • SpectatorHostState::update は BattleSession の代わりに SpectatorMatchInfo(プレイヤー名・ゲーム設定)を受け取るように変更しました

注意点

  • 予約部屋の場合、部屋のキーが最初の観戦者に渡ります。観戦者がその部屋を削除することも技術的に可能になります
  • 対戦者の画面の観戦者数は、観戦ホストの 1 人分のみ表示されます
  • 観戦メッセージの形式を変更したため、旧バージョンとの組み合わせでは動作しません

確認状況

  • i686-pc-windows-msvc ターゲットで cargo clippy / fmt が通ることを確認済み(C 依存部分はスタブで代用。新しい警告なし)
  • 実機での動作は未確認です。特に以下の確認をお願いします
    1. 予約部屋・Pure P2P それぞれで、2 人目以降の観戦者が観戦ホスト経由で観戦できるか
    2. 観戦ホスト配下の観戦者が途中参加した場合に同期が取れるか
    3. 観戦ホストが離脱した後、対戦者が Pure P2P で受け付けを再開できるか

🤖 Generated with Claude Code

https://claude.ai/code/session_01EpMNqyMexaKkm8jCjwz9vS


Generated by Claude Code

これまで観戦者は難易度選択画面 (カード未選択時) でしか参加できなかった。
ホストが直近の同期ポイント (難易度選択画面・キャラクター選択画面に入った時点) の
状態と乱数シード、およびそれ以降の入力を記録しておき、途中参加した観戦者には
それらを送信して早送りで再生させることで、キャラクター選択中や対戦中でも
参加できるようにする。

- 観戦者の受け付けを画面に関わらず行う (F1 も対戦中に受け付ける)
- 観戦者側はホストからの初期化情報を非ブロッキングで待つ
- キャラクター選択画面から同期する場合は難易度を決定して画面遷移し、
  到達時にカード選択状態と乱数シードを再現する
- 対戦中に未処理の入力が溜まっている場合は早送りで追いつく

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
- sync_screen を bool を返す is_sync_point に変更
- SpectatorSelect の initializing_state を enum 化し、
  キャラクター選択画面経由の同期でのみ到達時に乱数シードを設定するよう整理
- WaitingForPureP2pSpectator の ready を実態に合わせて show_hint に改名
- pushed_f1 の戻り値の変数名を f1_pushed に改名
- SyncPoint のメッセージ蓄積量についてコメントを追記

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
- 同期ポイントへの移動処理を SpectatorSelect から SpectatorSync 状態に切り出し、
  SpectatorSelect を従来に近いシンプルな形に戻す
- 実質到達しない pending_sessions と関連表示を削除し、
  同期ポイントが無い場合は途中参加を拒否する
- 同期ポイント以降の記録量に上限 (約 10 分) を設け、超えたら次の同期ポイントまで
  途中参加を受け付けない
- 対戦中の F1 受け付けをやめ、ready の改名も元に戻す
- Screen::Game を削除して unimplemented!() によるパニック経路をなくす
- buffered_len を副作用がわかる poll_buffered_len に改名

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

@progre progre left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可読性・保守性の観点で気になった箇所を5件コメントしました。特に spectator_host.rs の pending_sessions 周りは動作に影響しうる懸念です。


Generated by Claude Code


fn recv_session(&mut self, pushed: bool, main_menu: Option<&MainMenu>, th19: &Th19) {
let Some(waiting) = &mut self.waiting else {
if self.sessions.is_empty() && self.pending_sessions.is_empty() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

委譲された観戦ホストセッションが pending_sessions に入ったまま切断すると検出できず、受け付け再開の判定が成立しないように見えます。

予約部屋で対戦者が最初の観戦者を観戦ホストに任命した直後、次の同期ポイント(キャラクター選択画面突入など)が来る前にそのホストが切断すると、セッションは pending_sessions に残ったままになります。pending_sessions は broadcast() で送信を試みられることがないため切断が検知されず、recv_session() の self.sessions.is_empty() && self.pending_sessions.is_empty() が真になりません。結果として対戦者は次の対戦(次の同期ポイント)が来るまで新しい観戦者を Pure P2P で一切受け付けられなくなります。PR本文にある「観戦ホストが離脱した後、対戦者が Pure P2P で受け付けを再開できるか」という未検証の懸念は、まさにこの経路に当たると思います。


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ご指摘の経路は実際には発生しないため、変更は見送ります。

pending_sessions に入るのは sync_point が None の間だけです。sync_point は一度作られると None に戻らず、以降の同期ポイントでは上書きされるだけです。そのため、2 回目以降の同期ポイント(キャラクター選択画面への突入など)の前に接続したセッションは、保留されずにその場で join され sessions に入ります。切断は broadcast() で検出されます。

pending_sessions に残る可能性があるのは、セッション開始後に一度も同期ポイントを通過していない間に接続した場合だけです。update() の初回呼び出しは難易度選択画面(カード未選択)またはキャラクター選択画面で行われます。どちらも同期ポイントであり、recv_session() より前に sync_point が作られます。したがって、この状態は実質的に起こりません。


Generated by Claude Code

}
}

/// 予約部屋の場合、観戦者を受信した後は再度 `new` で待ち受けを作り直す必要がある

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try_recv_session() は受信のみを行い、「予約部屋の場合は受信後に再度 new で待ち受けを作り直す必要がある」という義務をコメントのみで呼び出し側に転嫁しています。型で保護されていないため、現状唯一の呼び出し元 recv_session では正しく再生成されていますが、将来別の箇所から呼ばれてこの再生成を書き忘れると、WaitingForSpectatorInReservedRoom 内部の session_rx(oneshot::Receiver)は一度しか値を返せないため、以後その予約部屋は誰にも観戦者を受け付けなくなるにもかかわらずコンパイルは通ってしまいます。呼び出し側に契約を強制できる形(例えば受信と再生成をまとめて返す)にできないか検討の余地があると思います。


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再生成を呼び出し側に任せているのは意図的なため、現状のままとします。

対戦者は、最初の観戦者を受信したら予約部屋の待ち受けを作り直してはいけません。予約部屋の待ち受け(WaitingInRoom)は、破棄されると中断扱いになり、部屋を削除します。そのため、try_recv_session() 内で自動的に作り直すと、対戦者が委譲のためにそれを破棄した時点で、観戦ホストが引き継ぐはずの部屋が消えてしまいます。

このため、作り直すかどうかは状況を知っている呼び出し側(SpectatorHostState::recv_session)が判断し、rearm() にまとめています。6fc9740 で、委譲時に作り直さない理由を recv_session にもコメントとして残しました。


Generated by Claude Code

Comment thread junowen/src/state/battle_session/spectator_host.rs Outdated
Comment thread junowen/src/session/spectator.rs Outdated
Comment thread junowen/src/state/battle_session/battle_game.rs Outdated
claude and others added 19 commits September 24, 2026 04:35
入力ログを記録・再生する方式をやめ、観戦者が接続した時点の状態を送る方式にする。

- キャラクター選択画面では、カーソル位置・カード・各プレイヤーの進行段階
  (キャラクター選択/カード選択/準備完了) を送る。進行段階はメモリ上の値が
  未解析のため、ホストが決定キーとキャンセルキーの押下から推定する
- 観戦者は決定キーを自動で押して進行段階を再現してから、ホストの入力を再生する
- 対戦中など状態を送れない間に接続した観戦者は、次に状態を送れるまで待機させる
- 入力ログとその上限を削除
- 観戦用の接続のデータチャネルプロトコルを JUNOWEN/1.2 にし、
  旧バージョンの観戦者を接続時に拒否する (対戦は JUNOWEN/1.1 のまま)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
キャラクター選択とカード選択は同時に進み、準備完了後もカードを変更できるため、
キャラクター選択画面の途中状態を送る方式をやめる。

- ホストは観戦者の接続時にプレイヤー名と設定を送り、試合開始時 (読み込み画面に
  移ったとき) に難易度・キャラクター・カード・乱数シードを送る
- 観戦者は難易度選択画面またはキャラクター選択画面で待機し、試合開始時の状態を
  受け取ったら難易度・カーソル・カードを合わせ、一致を確認してから決定する
- 選択画面の入力は観戦者に送らず、試合中の入力のみを送る
- 対戦中に接続した観戦者は次の試合から観戦する
- SpectatorSync / SpectatorSelect を SpectatorStandby にまとめる

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
- 古い方式を前提にしたコメントを直す
- 試合開始時の状態が一致しない場合は観戦を終了し、状態を合わせられないまま
  一定フレームを超えた場合も諦める
- 観戦者の試合中に次の試合の開始を受け取ったら、今の試合を打ち切って
  次の試合から同期し直す
- 待機中もゲームパッドの PAUSE で中断できるようにする (自動で入力した PAUSE は除く)
- try_recv_init_game の副作用をなくし、待機状態に入るときに明示的に破棄する

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
ホストは進行中の試合の開始時の状態と、それ以降のラウンド初期化情報・入力を
記録する (1試合分で、試合ごとに作り直す)。試合中に接続した観戦者には
これらをまとめて送り、観戦者は試合の最初から早送りでホストに追いつく。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
- next_game_initial フィールドをやめ、試合中に受け取った InitGame は
  受信バッファの先頭に戻す
- 待機の上限を超えたときに RecvError を中断の合図として流用していることをコメントに明記

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
観戦者側で selection.card に書き込んでも、試合では常に初期位置のカードが
使われることがわかった。ゲームが実際に使うカードの値の場所を特定するため、
試合開始時にホストと観戦者の両方で候補の値をログに出力する。
原因がわかったら削除する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
カードは入力で選ばせる方式にするため、診断ログは不要になった。

This reverts commit e7ef63c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
selection.card への書き込みはキャラクター選択画面の表示にのみ影響し、
試合で使われるカードには反映されなかった。通常の操作と同じく
「低速移動を押しながら左右」の入力でカードを切り替える。

- 観戦者側で現在のカードを把握する。タイトル画面を経由すると初期位置に戻り、
  以降は前の試合の選択が引き継がれる
- キャラクター選択画面に入ってから少し待ち、目標との差の分だけ入力してから決定する

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
画面遷移中などに左右を押すと入力が無視され、カードが切り替わらなかった。
押す前の selection.card を覚えておき、値が変わったら入力が受け付けられたとみなして
次へ進む。一定フレーム変わらなければ押し直す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
selection.card は v1.10c で構造体の配置が変わった後、正しい値を指していない
可能性が高く、ホストが常に 0 を送っていたため観戦者はカードを切り替えなかった。

- ホストは前回の選択を保持している vs_mode のカードを送り、観戦者も同じ値で照合する
- selection.card の変化で入力の受け付けを確認する方式は、値があてにならないため
  回数を数えて入力する方式に戻し、開始前の待ち時間を 60 フレームに延ばす
- 原因を確かめるため、試合開始時に両方の候補の値をログに出す (一時的)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
vs_mode のカードに 37 という、カードの番号としてありえない値が入っていたため、
正しい値の場所を改めて調べる。試合開始時にホストと観戦者の両方で、
selection・vs_mode のカード周辺・キャラクター選択カーソルの生のバイト列をログに出力する。
原因がわかったら削除する。

This reverts commit 718716e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
v1.10c 対応で Player 構造体が大きくなった際、カードの位置が +90h のまま
残っており、常に別の値 (0 や 100) を読んでいた。診断ログで、カードの値が
+94h にあり vs_mode のカードと一致することを確認した。

カードの値を正しく読み書きできるようになったため、観戦者のカードは
入力ではなくキャラクター選択画面で直接書き込む方式に戻す。

- selection.card の位置を +94h に修正
- ホストは selection.card を送り、観戦者はキャラクター選択画面で書き込んで照合してから決定する
- 入力でカードを切り替える処理 (CardMover) と、観戦者側のカードの把握を削除
- 診断ログを削除

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
試合中に no wait を切り替えても反映されない可能性があり、読み込み画面で
no wait を強制的にオフにしていたため、試合全体が通常速度で進んでいた。
読み込み画面でも溜まった入力の量で判定し、溜まっていれば早送りで試合を始める。
早送り中も入力が届くまで待つので、追いついた後はホストの速度で進む。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
試合中に観戦を始めて10秒以上遅れても早送りが発生しなかった。
溜まった入力の量が条件を満たしていないのか、no wait を設定しても
ゲームに反映されていないのかを切り分けるため、約60フレームごとに
溜まった入力の数と no wait の設定前後の値をログに出力する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
診断ログで、溜まった入力が 964 フレーム分あり no wait を有効にしても、
試合中は早送りされないことがわかった。試合中は表示の垂直同期待ちで
60fps に制限されていると考え、早送り中は Present を8回に1回だけ行う。
早送りの要求が途絶えたら2フレームで通常の表示に戻す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
protocol は SignalingSocket が決めて渡す値のため、PeerConnection は保持しない。
データチャネルの作成と相手との照合で使う箇所だけ引数で受け取る。

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
早送り中は表示を間引く対応を入れたため、原因調査用の一時的な診断ログを削除する。

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from 6fc9740 to 3f5386a Compare October 11, 2026 03:14
Direct3D9 のラッパーは windows クレート以外に依存しないため、
features として app から分離する。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from 3f5386a to f17a4c7 Compare October 11, 2026 05:03
画面の種類は型引数で受け取る。
入力は MenuButtons で、効果音・キーボード・描画は MenuDevice と MenuRenderer で受け取る。
Th19 とのつなぎ込みは app 側の lobby/common_menu.rs に置く。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from f17a4c7 to 10c377e Compare October 11, 2026 06:54
progre and others added 2 commits October 11, 2026 18:12
…ュー定義を移動

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from 10c377e to 016b34e Compare October 11, 2026 09:35
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from 016b34e to d3ad067 Compare October 11, 2026 09:41
progre and others added 2 commits October 11, 2026 18:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
対戦者は最初の観戦者のみを受け付け、その観戦者を観戦ホストに任命して
以降の観戦者の受け付けと試合の進行の中継を委譲する。

- DelegateSpectatorHost メッセージで観戦ホストを任命し、専有ルームの場合は部屋名とキーを渡す
- 観戦ホストは自分が処理した試合開始・ラウンド初期化・入力を、処理した順に配下の観戦者へ中継する
- 観戦ホストが離脱した場合、配下の観戦者もすべて離脱し、対戦者は Pure P2P で受け付けを再開する
- SpectatorHostState を state 直下に移し、受け付け状態・試合記録・表示文言を別ファイルに分割する

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@progre
progre force-pushed the claude/fervent-bell-okj1ng branch from d3ad067 to 748dc12 Compare October 11, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants