Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4945000
feat(spectator): 観戦者が任意のタイミングで途中参加できるようにする
claude Sep 23, 2026
d329696
refactor(spectator): レビュー指摘を反映して可読性を改善
claude Sep 24, 2026
f8b7908
refactor(spectator): 設計レビューを反映して整理
claude Sep 24, 2026
7491044
feat(spectator): 接続時点の状態を送る方式に変更し、観戦用プロトコルを分ける
claude Sep 24, 2026
95f9cd2
feat(spectator): 試合開始時の状態に合わせて観戦を始める方式に変更
claude Sep 24, 2026
0fe6dbd
fix(spectator): 再レビューの指摘を反映
claude Sep 24, 2026
197c120
feat(spectator): 試合中に接続した観戦者は試合の最初から早送りで追いかける
claude Sep 24, 2026
fd0cf94
refactor(spectator): 次の試合の InitGame をバッファの先頭に戻して保持する
claude Sep 24, 2026
e7ef63c
chore(spectator): カードの選択状態の保持場所を調べる一時的な診断ログを追加
claude Sep 27, 2026
718716e
Revert "chore(spectator): カードの選択状態の保持場所を調べる一時的な診断ログを追加"
claude Sep 27, 2026
85e0399
fix(spectator): カードを入力で選ばせて同期する
claude Sep 27, 2026
29947ec
fix(spectator): カードの切り替えが受け付けられたことを確認しながら入力する
claude Sep 27, 2026
e6374f3
fix(spectator): ホストが送るカードを vs_mode の値にする
claude Sep 27, 2026
ed85689
chore(spectator): カードの値の場所を調べる診断ログを再度追加
claude Sep 28, 2026
d62918d
fix(junowen-lib): Player のカードの位置を +94h に直す
claude Sep 28, 2026
b40fe10
fix(spectator): 試合中に観戦を始めたときに早送りで追いかける
claude Sep 28, 2026
ed432d8
chore(spectator): 早送りが効かない原因を調べる一時的な診断ログを追加
claude Sep 28, 2026
56fc4c2
feat(spectator): 早送り中は画面の表示を間引いて追いつく
claude Sep 28, 2026
a87f850
refactor(junowen-lib): PeerConnection の protocol を引数で受け取る形に変更
progre Oct 8, 2026
2c34946
chore(spectator): 早送りの原因調査用の診断ログを削除
progre Oct 8, 2026
50a2987
refactor(spectator): present_skipper のグローバル変数を観戦セッションの状態へ移動
progre Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion junowen-lib/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ mod data_channel;
mod peer_connection;
pub mod signaling;

pub use self::{data_channel::DataChannel, peer_connection::PeerConnection};
pub use self::{
data_channel::DataChannel,
peer_connection::{BATTLE_PROTOCOL, PeerConnection, SPECTATOR_PROTOCOL},
};
20 changes: 15 additions & 5 deletions junowen-lib/src/connection/peer_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ impl Drop for PeerConnection {
}
}

const PROTOCOL: &str = "JUNOWEN/1.1";
/// 対戦用の接続のプロトコル。互換性のない変更をしたら上げる
pub const BATTLE_PROTOCOL: &str = "JUNOWEN/1.1";
/// 観戦用の接続のプロトコル。互換性のない変更をしたら上げる
///
/// 1.2: 試合開始時の状態を送り、いつ接続しても次の試合から観戦できるようにした
pub const SPECTATOR_PROTOCOL: &str = "JUNOWEN/1.2";

impl PeerConnection {
pub async fn new(timeout: Duration, ice_server_urls: Vec<String>) -> Result<Self> {
Expand Down Expand Up @@ -130,13 +135,14 @@ impl PeerConnection {
self.rtc.as_ref().unwrap()
}

pub async fn start_as_offerer(&mut self) -> Result<CompressedSdp> {
/// `protocol` はデータチャネルのプロトコル
pub async fn start_as_offerer(&mut self, protocol: &'static str) -> Result<CompressedSdp> {
let rtc_data_channel = self
.rtc()
.create_data_channel(
"data",
Some(RTCDataChannelInit {
protocol: Some(PROTOCOL.to_owned()),
protocol: Some(protocol.to_owned()),
..Default::default()
}),
)
Expand Down Expand Up @@ -197,11 +203,15 @@ impl PeerConnection {
Ok(())
}

pub async fn wait_for_open_data_channel(&mut self) -> Result<DataChannel> {
/// 相手のプロトコルが `protocol` と一致しない場合は接続を拒否する
pub async fn wait_for_open_data_channel(
&mut self,
protocol: &'static str,
) -> Result<DataChannel> {
let data_channel_task = async {
let mut data_channel = self.data_channel_rx.take().unwrap().await.unwrap();
data_channel.wait_for_open_data_channel().await;
if data_channel.protocol() != PROTOCOL {
if data_channel.protocol() != protocol {
bail!("unexpected protocol: {}", data_channel.protocol());
}
Ok(data_channel)
Expand Down
11 changes: 8 additions & 3 deletions junowen-lib/src/connection/signaling/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use async_trait::async_trait;

use crate::connection::data_channel::DataChannel;

use super::super::peer_connection::PeerConnection;
use super::super::peer_connection::{BATTLE_PROTOCOL, PeerConnection};

use super::CompressedSdp;

Expand All @@ -36,10 +36,15 @@ pub trait SignalingSocket {
vec![DEFAULT_STUN_SERVER_URL.to_owned()]
}

/// データチャネルのプロトコル。観戦用の接続では `SPECTATOR_PROTOCOL` を返す
fn protocol(&self) -> &'static str {
BATTLE_PROTOCOL
}

async fn receive_signaling(&mut self) -> Result<(PeerConnection, DataChannel, bool)> {
let mut conn = PeerConnection::new(Self::timeout(), self.ice_server_urls()).await?;
let offer_desc = conn
.start_as_offerer()
.start_as_offerer(self.protocol())
.await
.context("Failed to start as host")?;
let answer_desc = self.offer(offer_desc).await?;
Expand All @@ -60,7 +65,7 @@ pub trait SignalingSocket {
(conn, false)
}
};
let data_channel = conn.wait_for_open_data_channel().await?;
let data_channel = conn.wait_for_open_data_channel(self.protocol()).await?;
Ok((conn, data_channel, host))
}
}
7 changes: 7 additions & 0 deletions junowen-lib/src/connection/signaling/socket/channel_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ pub struct ChannelSocket {
offer_sender: Option<oneshot::Sender<CompressedSdp>>,
answer_sender: Option<oneshot::Sender<CompressedSdp>>,
message_receiver: Option<oneshot::Receiver<SignalingServerMessage>>,
protocol: &'static str,
Comment thread
progre marked this conversation as resolved.
}

impl ChannelSocket {
pub fn new(
offer_sender: oneshot::Sender<CompressedSdp>,
answer_sender: oneshot::Sender<CompressedSdp>,
message_receiver: oneshot::Receiver<SignalingServerMessage>,
protocol: &'static str,
) -> Self {
Self {
offer_sender: Some(offer_sender),
answer_sender: Some(answer_sender),
message_receiver: Some(message_receiver),
protocol,
}
}
}
Expand All @@ -35,6 +38,10 @@ impl SignalingSocket for ChannelSocket {
Duration::from_secs(20 * 60)
}

fn protocol(&self) -> &'static str {
self.protocol
}

async fn offer(&mut self, desc: CompressedSdp) -> Result<OfferResponse> {
self.offer_sender.take().unwrap().send(desc).unwrap();
Ok(match self.message_receiver.take().unwrap().await? {
Expand Down
2 changes: 1 addition & 1 deletion junowen-lib/src/th19/structs/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub struct CharacterCursor {
_unknown1: [u8; 0xd0],
}

#[derive(CopyGetters, Debug, MutGetters, Setters)]
#[derive(CopyGetters, Debug, Getters, MutGetters, Setters)]
#[repr(C)]
pub struct Menu {
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
Expand Down
8 changes: 4 additions & 4 deletions junowen-lib/src/th19/structs/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ use std::mem::transmute;
use anyhow::{Result, bail};
use getset::{Getters, MutGetters};

/// length=c0
/// length=c8
#[repr(C)]
pub struct Player {
_unknown1: [u8; 0x0c],
/// NOT available on player select screen
pub character: u32,
_unknown2: [u8; 0x80],
/// Available on player select screen
_unknown2: [u8; 0x84],
/// Available on player select screen (+94h)

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.

何を根拠に位置を変えたのですか?(+94h は自明なので書かないでください)

pub card: u32,
_unknown3: [u8; 0x34],
_unknown3: [u8; 0x30],
}

#[derive(Clone, Copy, PartialEq)]
Expand Down
3 changes: 3 additions & 0 deletions junowen/src/custom_direct_3d/custom_direct_3d_device_9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ where
hdestwindowoverride: HWND,
pdirtyregion: *const RGNDATA,
) -> windows::core::Result<()> {
if self.delegate.should_skip_present() {
return Ok(());
}
self.delegate.on_before_present(&self.instance);
unsafe {
self.instance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use windows::Win32::Graphics::Direct3D9::IDirect3DDevice9;

pub trait Direct3DDeviceEventListener {
/// 表示を間引く場合は `true` を返す
fn should_skip_present(&self) -> bool;
fn on_before_present(&self, device: &IDirect3DDevice9);
}
5 changes: 5 additions & 0 deletions junowen/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ pub fn pushed_f1(input_devices: &InputDevices) -> bool {
let raw_keys = input_devices.keyboard_input().raw_keys();
raw_keys[0x70] & 0x80 != 0
}

pub fn pushed_escape(input_devices: &InputDevices) -> bool {
let raw_keys = input_devices.keyboard_input().raw_keys();
raw_keys[0x1b] & 0x80 != 0
}
4 changes: 4 additions & 0 deletions junowen/src/junowen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ impl Th19EventListener for Junowen {
}

impl Direct3DDeviceEventListener for &Junowen {
fn should_skip_present(&self) -> bool {
self.state.should_skip_present()
}

fn on_before_present(&self, device: &IDirect3DDevice9) {
if !self.title_menu_modifier.selected_junowen() {
return;
Expand Down
1 change: 1 addition & 0 deletions junowen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod file;
mod helper;
mod junowen;
mod lobby;
mod present_skipper;
mod session;
mod signaling;
mod state;
Expand Down
11 changes: 8 additions & 3 deletions junowen/src/lobby/pure_p2p_guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::ffi::c_void;
use clipboard_win::set_clipboard_string;
use junowen_lib::{
Th19,
connection::signaling::{
SignalingCodeType, socket::async_read_write_socket::SignalingServerMessage,
connection::{
BATTLE_PROTOCOL,
signaling::{SignalingCodeType, socket::async_read_write_socket::SignalingServerMessage},
},
structs::input_devices::InputValue,
};
Expand Down Expand Up @@ -45,7 +46,11 @@ impl PureP2pGuest {
0,
),
),
signaling: Signaling::new(session_tx, |conn, dc| BattleSession::new(conn, dc, false)),
signaling: Signaling::new(
session_tx,
|conn, dc| BattleSession::new(conn, dc, false),
BATTLE_PROTOCOL,
),
session_rx: Some(session_rx),
offer: None,
answer_generated: false,
Expand Down
6 changes: 5 additions & 1 deletion junowen/src/lobby/pure_p2p_offerer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub struct PureP2pOfferer<T> {
offer_type: SignalingCodeType,
answer_type: SignalingCodeType,
create_session: fn(PeerConnection, DataChannel) -> T,
protocol: &'static str,
messages: Messages,
common_menu: CommonMenu,
signaling: Signaling,
Expand All @@ -52,6 +53,7 @@ where
offer_type: SignalingCodeType,
answer_type: SignalingCodeType,
create_session: fn(PeerConnection, DataChannel) -> T,
protocol: &'static str,
label: &'static str,
messages: Messages,
) -> Self {
Expand All @@ -60,9 +62,10 @@ where
offer_type,
answer_type,
create_session,
protocol,
messages,
common_menu: make_menu(label),
signaling: Signaling::new(session_tx, create_session),
signaling: Signaling::new(session_tx, create_session, protocol),
session_rx: Some(session_rx),
answer: None,
copy_state: 0,
Expand Down Expand Up @@ -185,6 +188,7 @@ where
self.offer_type,
self.answer_type,
self.create_session,
self.protocol,
self.common_menu.root_title(),
self.messages,
);
Expand Down
4 changes: 3 additions & 1 deletion junowen/src/lobby/pure_p2p_offerer/role.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use junowen_lib::connection::signaling::SignalingCodeType;
use junowen_lib::connection::{BATTLE_PROTOCOL, SPECTATOR_PROTOCOL, signaling::SignalingCodeType};

use crate::session::{battle::BattleSession, spectator::SpectatorSession};

Expand All @@ -17,6 +17,7 @@ pub fn pure_p2p_host() -> PureP2pOfferer<BattleSession> {
SignalingCodeType::BattleOffer,
SignalingCodeType::BattleAnswer,
|pc, dc| BattleSession::new(pc, dc, true),
BATTLE_PROTOCOL,
"Connect as a Host",
Messages {
share: "pure_p2p.host_share",
Expand All @@ -31,6 +32,7 @@ pub fn pure_p2p_spectator() -> PureP2pOfferer<SpectatorSession> {
SignalingCodeType::SpectatorOffer,
SignalingCodeType::SpectatorAnswer,
SpectatorSession::new,
SPECTATOR_PROTOCOL,
"Connect as a Spectator",
Messages {
share: "pure_p2p.spectator_share",
Expand Down
42 changes: 42 additions & 0 deletions junowen/src/present_skipper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! 観戦者がホストに追いつくまでの早送り中は、画面の表示 (`Present`) を間引く
//!
//! 試合中は、ゲームの no wait 設定を有効にしても処理が速くならない。
//! 表示のたびに垂直同期を待つため、1 秒あたり 60 フレームに抑えられていると考えられる。
//! そこで表示を間引き、この待ちを減らす。

use std::sync::atomic::{AtomicU32, Ordering};

/// 早送りを要求されてから、表示を間引き続けるフレーム数。
/// 要求が途絶えたら (セッションが終了した場合など) すぐに通常の表示に戻す
const SKIP_FRAMES_PER_REQUEST: u32 = 2;
/// 間引いている間も、このフレーム数ごとに1回は表示する
const PRESENT_INTERVAL: u32 = 8;

#[derive(Default)]
pub struct PresentSkipper {
remaining_frames: AtomicU32,
counter: AtomicU32,
}

impl PresentSkipper {
/// 早送り中は毎フレーム呼ぶ
pub fn request_skip(&self) {
self.remaining_frames
.store(SKIP_FRAMES_PER_REQUEST, Ordering::Relaxed);
}

/// 表示を間引く場合は `true` を返す
pub fn should_skip_present(&self) -> bool {
let requested = self
.remaining_frames
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |x| x.checked_sub(1))
.is_ok();
if !requested {
return false;
}
!self
.counter
.fetch_add(1, Ordering::Relaxed)
.is_multiple_of(PRESENT_INTERVAL)
}
}
2 changes: 1 addition & 1 deletion junowen/src/session/session_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub struct MatchInitial {
pub game_settings: GameSettings,
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RoundInitial {
pub seed1: u32,
pub seed2: u32,
Expand Down
Loading
Loading