diff --git a/Cargo.lock b/Cargo.lock index ce85850..cde6885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2021,7 +2021,9 @@ dependencies = [ "clipboard-win", "derive-new", "getset", + "junowen-direct3d", "junowen-lib", + "junowen-lobby", "reqwest", "rmp-serde", "rust-i18n", @@ -2042,6 +2044,14 @@ dependencies = [ "windows-core 0.59.0", ] +[[package]] +name = "junowen-direct3d" +version = "1.2.0" +dependencies = [ + "windows 0.59.0", + "windows-core 0.59.0", +] + [[package]] name = "junowen-lib" version = "1.2.0" @@ -2074,6 +2084,15 @@ dependencies = [ "windows 0.59.0", ] +[[package]] +name = "junowen-lobby" +version = "1.2.0" +dependencies = [ + "derive-new", + "getset", + "windows 0.59.0", +] + [[package]] name = "junowen-server" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 39ad324..4f8bf48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,14 @@ members = [ "archives/th19replayrecorder", "archives/th19savesettingsseparately", "archives/th19seed", - "junowen", - "junowen-lib", - "junowen-server", - "th19loader", + "crates/app/junowen", + "crates/app/junowen-server", + "crates/app/th19loader", + "crates/features/junowen-direct3d", + "crates/features/junowen-lib", + "crates/features/junowen-lobby", ] -default-members = ["junowen"] +default-members = ["crates/app/junowen"] resolver = "2" [workspace.package] @@ -26,7 +28,11 @@ anyhow = { version = "1.0.95", features = ["backtrace"] } async-trait = "0.1.85" bytes = "1.9.0" clipboard-win = "5.4.0" -junowen-lib = { path = "./junowen-lib" } +derive-new = "0.6.0" +getset = "0.1.2" +junowen-direct3d = { path = "./crates/features/junowen-direct3d" } +junowen-lib = { path = "./crates/features/junowen-lib" } +junowen-lobby = { path = "./crates/features/junowen-lobby" } rmp-serde = "1.3.0" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.135" diff --git a/junowen-server/Cargo.toml b/crates/app/junowen-server/Cargo.toml similarity index 100% rename from junowen-server/Cargo.toml rename to crates/app/junowen-server/Cargo.toml diff --git a/junowen-server/README.md b/crates/app/junowen-server/README.md similarity index 100% rename from junowen-server/README.md rename to crates/app/junowen-server/README.md diff --git a/junowen-server/docs/activity-diagrams.md b/crates/app/junowen-server/docs/activity-diagrams.md similarity index 100% rename from junowen-server/docs/activity-diagrams.md rename to crates/app/junowen-server/docs/activity-diagrams.md diff --git a/junowen-server/src/bin/access-stats/args.rs b/crates/app/junowen-server/src/bin/access-stats/args.rs similarity index 100% rename from junowen-server/src/bin/access-stats/args.rs rename to crates/app/junowen-server/src/bin/access-stats/args.rs diff --git a/junowen-server/src/bin/access-stats/cloudwatch.rs b/crates/app/junowen-server/src/bin/access-stats/cloudwatch.rs similarity index 100% rename from junowen-server/src/bin/access-stats/cloudwatch.rs rename to crates/app/junowen-server/src/bin/access-stats/cloudwatch.rs diff --git a/junowen-server/src/bin/access-stats/main.rs b/crates/app/junowen-server/src/bin/access-stats/main.rs similarity index 100% rename from junowen-server/src/bin/access-stats/main.rs rename to crates/app/junowen-server/src/bin/access-stats/main.rs diff --git a/junowen-server/src/bin/access-stats/output.rs b/crates/app/junowen-server/src/bin/access-stats/output.rs similarity index 100% rename from junowen-server/src/bin/access-stats/output.rs rename to crates/app/junowen-server/src/bin/access-stats/output.rs diff --git a/junowen-server/src/bin/access-stats/room_activity.rs b/crates/app/junowen-server/src/bin/access-stats/room_activity.rs similarity index 100% rename from junowen-server/src/bin/access-stats/room_activity.rs rename to crates/app/junowen-server/src/bin/access-stats/room_activity.rs diff --git a/junowen-server/src/database.rs b/crates/app/junowen-server/src/database.rs similarity index 100% rename from junowen-server/src/database.rs rename to crates/app/junowen-server/src/database.rs diff --git a/junowen-server/src/database/dynamodb.rs b/crates/app/junowen-server/src/database/dynamodb.rs similarity index 100% rename from junowen-server/src/database/dynamodb.rs rename to crates/app/junowen-server/src/database/dynamodb.rs diff --git a/junowen-server/src/database/dynamodb/reserved_room.rs b/crates/app/junowen-server/src/database/dynamodb/reserved_room.rs similarity index 100% rename from junowen-server/src/database/dynamodb/reserved_room.rs rename to crates/app/junowen-server/src/database/dynamodb/reserved_room.rs diff --git a/junowen-server/src/database/dynamodb/shared_room.rs b/crates/app/junowen-server/src/database/dynamodb/shared_room.rs similarity index 100% rename from junowen-server/src/database/dynamodb/shared_room.rs rename to crates/app/junowen-server/src/database/dynamodb/shared_room.rs diff --git a/junowen-server/src/database/file.rs b/crates/app/junowen-server/src/database/file.rs similarity index 100% rename from junowen-server/src/database/file.rs rename to crates/app/junowen-server/src/database/file.rs diff --git a/junowen-server/src/main.rs b/crates/app/junowen-server/src/main.rs similarity index 100% rename from junowen-server/src/main.rs rename to crates/app/junowen-server/src/main.rs diff --git a/junowen-server/src/routes.rs b/crates/app/junowen-server/src/routes.rs similarity index 100% rename from junowen-server/src/routes.rs rename to crates/app/junowen-server/src/routes.rs diff --git a/junowen-server/src/routes/custom.rs b/crates/app/junowen-server/src/routes/custom.rs similarity index 100% rename from junowen-server/src/routes/custom.rs rename to crates/app/junowen-server/src/routes/custom.rs diff --git a/junowen-server/src/routes/reserved_room.rs b/crates/app/junowen-server/src/routes/reserved_room.rs similarity index 100% rename from junowen-server/src/routes/reserved_room.rs rename to crates/app/junowen-server/src/routes/reserved_room.rs diff --git a/junowen-server/src/routes/reserved_room/create.rs b/crates/app/junowen-server/src/routes/reserved_room/create.rs similarity index 100% rename from junowen-server/src/routes/reserved_room/create.rs rename to crates/app/junowen-server/src/routes/reserved_room/create.rs diff --git a/junowen-server/src/routes/reserved_room/delete.rs b/crates/app/junowen-server/src/routes/reserved_room/delete.rs similarity index 100% rename from junowen-server/src/routes/reserved_room/delete.rs rename to crates/app/junowen-server/src/routes/reserved_room/delete.rs diff --git a/junowen-server/src/routes/reserved_room/read.rs b/crates/app/junowen-server/src/routes/reserved_room/read.rs similarity index 100% rename from junowen-server/src/routes/reserved_room/read.rs rename to crates/app/junowen-server/src/routes/reserved_room/read.rs diff --git a/junowen-server/src/routes/reserved_room/update.rs b/crates/app/junowen-server/src/routes/reserved_room/update.rs similarity index 100% rename from junowen-server/src/routes/reserved_room/update.rs rename to crates/app/junowen-server/src/routes/reserved_room/update.rs diff --git a/junowen-server/src/routes/room_utils.rs b/crates/app/junowen-server/src/routes/room_utils.rs similarity index 100% rename from junowen-server/src/routes/room_utils.rs rename to crates/app/junowen-server/src/routes/room_utils.rs diff --git a/junowen-server/src/tracing_helper.rs b/crates/app/junowen-server/src/tracing_helper.rs similarity index 100% rename from junowen-server/src/tracing_helper.rs rename to crates/app/junowen-server/src/tracing_helper.rs diff --git a/junowen/Cargo.toml b/crates/app/junowen/Cargo.toml similarity index 88% rename from junowen/Cargo.toml rename to crates/app/junowen/Cargo.toml index c2c61bd..affff3f 100644 --- a/junowen/Cargo.toml +++ b/crates/app/junowen/Cargo.toml @@ -20,9 +20,11 @@ anyhow.workspace = true async-trait.workspace = true bytes.workspace = true clipboard-win.workspace = true -derive-new = "0.6.0" -getset = "0.1.2" +derive-new.workspace = true +getset.workspace = true +junowen-direct3d.workspace = true junowen-lib.workspace = true +junowen-lobby.workspace = true reqwest = { version = "0.12.7", features = ["json"] } rmp-serde.workspace = true rust-i18n = "3.1.2" diff --git a/junowen/build.rs b/crates/app/junowen/build.rs similarity index 100% rename from junowen/build.rs rename to crates/app/junowen/build.rs diff --git a/junowen/locales/de.toml b/crates/app/junowen/locales/de.toml similarity index 100% rename from junowen/locales/de.toml rename to crates/app/junowen/locales/de.toml diff --git a/junowen/locales/en.toml b/crates/app/junowen/locales/en.toml similarity index 100% rename from junowen/locales/en.toml rename to crates/app/junowen/locales/en.toml diff --git a/junowen/locales/eo.toml b/crates/app/junowen/locales/eo.toml similarity index 100% rename from junowen/locales/eo.toml rename to crates/app/junowen/locales/eo.toml diff --git a/junowen/locales/es-ES.toml b/crates/app/junowen/locales/es-ES.toml similarity index 100% rename from junowen/locales/es-ES.toml rename to crates/app/junowen/locales/es-ES.toml diff --git a/junowen/locales/es.toml b/crates/app/junowen/locales/es.toml similarity index 100% rename from junowen/locales/es.toml rename to crates/app/junowen/locales/es.toml diff --git a/junowen/locales/fr.toml b/crates/app/junowen/locales/fr.toml similarity index 100% rename from junowen/locales/fr.toml rename to crates/app/junowen/locales/fr.toml diff --git a/junowen/locales/it.toml b/crates/app/junowen/locales/it.toml similarity index 100% rename from junowen/locales/it.toml rename to crates/app/junowen/locales/it.toml diff --git a/junowen/locales/ja.toml b/crates/app/junowen/locales/ja.toml similarity index 100% rename from junowen/locales/ja.toml rename to crates/app/junowen/locales/ja.toml diff --git a/junowen/locales/ko.toml b/crates/app/junowen/locales/ko.toml similarity index 100% rename from junowen/locales/ko.toml rename to crates/app/junowen/locales/ko.toml diff --git a/junowen/locales/pt-BR.toml b/crates/app/junowen/locales/pt-BR.toml similarity index 100% rename from junowen/locales/pt-BR.toml rename to crates/app/junowen/locales/pt-BR.toml diff --git a/junowen/locales/ru.toml b/crates/app/junowen/locales/ru.toml similarity index 100% rename from junowen/locales/ru.toml rename to crates/app/junowen/locales/ru.toml diff --git a/junowen/locales/zh-CN.toml b/crates/app/junowen/locales/zh-CN.toml similarity index 100% rename from junowen/locales/zh-CN.toml rename to crates/app/junowen/locales/zh-CN.toml diff --git a/junowen/locales/zh-TW.toml b/crates/app/junowen/locales/zh-TW.toml similarity index 100% rename from junowen/locales/zh-TW.toml rename to crates/app/junowen/locales/zh-TW.toml diff --git a/junowen/src/bin/junowen-standalone.rs b/crates/app/junowen/src/bin/junowen-standalone.rs similarity index 100% rename from junowen/src/bin/junowen-standalone.rs rename to crates/app/junowen/src/bin/junowen-standalone.rs diff --git a/junowen/src/bin/lang/ja.toml b/crates/app/junowen/src/bin/lang/ja.toml similarity index 100% rename from junowen/src/bin/lang/ja.toml rename to crates/app/junowen/src/bin/lang/ja.toml diff --git a/junowen/src/bin/lang/mod.rs b/crates/app/junowen/src/bin/lang/mod.rs similarity index 100% rename from junowen/src/bin/lang/mod.rs rename to crates/app/junowen/src/bin/lang/mod.rs diff --git a/junowen/src/file.rs b/crates/app/junowen/src/file.rs similarity index 100% rename from junowen/src/file.rs rename to crates/app/junowen/src/file.rs diff --git a/junowen/src/helper.rs b/crates/app/junowen/src/helper.rs similarity index 72% rename from junowen/src/helper.rs rename to crates/app/junowen/src/helper.rs index 70e9a99..1fe5e14 100644 --- a/junowen/src/helper.rs +++ b/crates/app/junowen/src/helper.rs @@ -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 +} diff --git a/junowen/src/junowen.rs b/crates/app/junowen/src/junowen.rs similarity index 95% rename from junowen/src/junowen.rs rename to crates/app/junowen/src/junowen.rs index 5de05ed..a17b7f9 100644 --- a/junowen/src/junowen.rs +++ b/crates/app/junowen/src/junowen.rs @@ -1,5 +1,6 @@ use std::ffi::c_void; +use junowen_direct3d::Direct3DDeviceEventListener; use junowen_lib::{ Th19, Th19EventListener, structs::{app::ScreenId, others::RenderingText}, @@ -7,7 +8,6 @@ use junowen_lib::{ use windows::Win32::Graphics::Direct3D9::IDirect3DDevice9; use crate::{ - custom_direct_3d::Direct3DDeviceEventListener, file::{Features, SettingsRepo}, lobby::{self, Lobby, TitleMenuModifier}, state::State, @@ -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; diff --git a/junowen/src/lib.rs b/crates/app/junowen/src/lib.rs similarity index 98% rename from junowen/src/lib.rs rename to crates/app/junowen/src/lib.rs index 996bc61..81908b8 100644 --- a/junowen/src/lib.rs +++ b/crates/app/junowen/src/lib.rs @@ -1,8 +1,8 @@ -mod custom_direct_3d; mod file; mod helper; mod junowen; mod lobby; +mod present_skipper; mod session; mod signaling; mod state; @@ -12,7 +12,7 @@ rust_i18n::i18n!("locales", fallback = "en"); use std::{cell::OnceCell, mem::take, path::Path, ptr::null_mut, slice, sync::LazyLock}; -use custom_direct_3d::CustomDirect3D9; +use junowen_direct3d::CustomDirect3D9; use junowen_lib::{ Th19, Th19EventDispatcher, hook_utils::{WELL_KNOWN_VERSION_HASHES, calc_th19_hash, show_warn_dialog}, diff --git a/junowen/src/lobby.rs b/crates/app/junowen/src/lobby.rs similarity index 94% rename from junowen/src/lobby.rs rename to crates/app/junowen/src/lobby.rs index e64aa20..6b11756 100644 --- a/junowen/src/lobby.rs +++ b/crates/app/junowen/src/lobby.rs @@ -1,5 +1,4 @@ mod common_menu; -mod helper; mod overlay; mod pure_p2p_guest; mod pure_p2p_offerer; @@ -7,13 +6,13 @@ mod pure_p2p_scenes; mod room; mod root; mod signaling_code; -mod text_layout; mod title_menu_modifier; use std::ffi::c_void; use getset::{Getters, MutGetters}; use junowen_lib::{Th19, structs::input_devices::InputValue}; +use junowen_lobby::overlay_description; use crate::{ file::SettingsRepo, @@ -24,8 +23,7 @@ use crate::{ }; use self::{ - common_menu::LobbyScene, - helper::overlay_description, + common_menu::{LobbyScene, MenuButtons, to_menu_buttons}, pure_p2p_scenes::PureP2pScenes, room::{reserved::ReservedRoom, shared::SharedRoom, tcp_signaling::TcpSignaling}, root::Root, @@ -44,7 +42,7 @@ pub struct Lobby { reserved_room: ReservedRoom, tcp_signaling: TcpSignaling, pure_p2p: PureP2pScenes, - prev_input: InputValue, + prev_input: MenuButtons, #[getset(get = "pub", get_mut = "pub")] waiting_for_match: Option, } @@ -61,17 +59,17 @@ impl Lobby { reserved_room: ReservedRoom::new(), tcp_signaling: TcpSignaling::new(), pure_p2p: PureP2pScenes::default(), - prev_input: InputValue::full(), + prev_input: to_menu_buttons(InputValue::full()), } } pub fn clear_input(&mut self) { - self.prev_input = InputValue::full(); + self.prev_input = to_menu_buttons(InputValue::full()); } pub fn on_input_menu(&mut self, th19: &mut Th19) { self.prev_scene = self.scene; - let current_input = th19.menu_input().current(); + let current_input = to_menu_buttons(th19.menu_input().current()); th19.menu_input_mut().set_current(InputValue::empty()); if let Some(scene) = match self.scene { @@ -134,7 +132,7 @@ impl Lobby { ), } { self.scene = scene; - self.prev_input = InputValue::full(); + self.prev_input = to_menu_buttons(InputValue::full()); } else { self.prev_input = current_input; } diff --git a/crates/app/junowen/src/lobby/common_menu.rs b/crates/app/junowen/src/lobby/common_menu.rs new file mode 100644 index 0000000..cef8e3d --- /dev/null +++ b/crates/app/junowen/src/lobby/common_menu.rs @@ -0,0 +1,68 @@ +use std::ffi::c_void; + +use junowen_lib::{ + Th19, + structs::{ + input_devices::{InputFlags, InputValue}, + others::RenderingText, + }, +}; +use junowen_lobby::{MenuDevice, MenuRenderer, MenuSound, MenuText}; + +pub use junowen_lobby::{ + CommonMenu, LobbyScene, Menu, MenuButtons, MenuItem, OnMenuInputResult, menu_item_color, + render_label_value, +}; + +pub fn to_menu_buttons(input: InputValue) -> MenuButtons { + let bits = |flags: &[InputFlags]| { + flags + .iter() + .enumerate() + .filter(|&(_, &flag)| input.0.contains(flag)) + .fold(0u8, |acc, (i, _)| acc | (1 << i)) + }; + MenuButtons { + up: input.0.contains(InputFlags::UP), + down: input.0.contains(InputFlags::DOWN), + decide: bits(&[InputFlags::SHOT, InputFlags::ENTER]), + cancel: bits(&[InputFlags::CHARGE, InputFlags::BOMB, InputFlags::PAUSE]), + } +} + +pub struct Th19MenuDevice<'a>(pub &'a Th19); + +impl MenuDevice for Th19MenuDevice<'_> { + fn play_sound(&self, sound: MenuSound) { + let id = match sound { + MenuSound::Cancel => 0x09, + MenuSound::Decide => 0x07, + MenuSound::Select => 0x0a, + }; + self.0.play_sound(self.0.sound_manager(), id, 0); + } + + fn keyboard_raw_keys(&self) -> &[u8; 256] { + self.0.input_devices().keyboard_input().raw_keys() + } +} + +pub struct Th19MenuRenderer<'a> { + pub th19: &'a Th19, + pub text_renderer: &'a c_void, +} + +impl MenuRenderer for Th19MenuRenderer<'_> { + fn render_text(&self, text: &MenuText) { + let mut rt = RenderingText::default(); + rt.set_text(text.text); + rt.set_x(text.x, self.th19.window_inner()); + rt.set_y(text.y, self.th19.window_inner()); + rt.color = text.color; + rt.font_type = text.font_type; + rt.drop_shadow = text.drop_shadow; + rt.horizontal_align = text.horizontal_align; + rt.vertical_align = text.vertical_align; + self.th19.render_text(self.text_renderer, &rt); + } +} diff --git a/junowen/src/lobby/overlay.rs b/crates/app/junowen/src/lobby/overlay.rs similarity index 93% rename from junowen/src/lobby/overlay.rs rename to crates/app/junowen/src/lobby/overlay.rs index 5159a41..9f605bc 100644 --- a/junowen/src/lobby/overlay.rs +++ b/crates/app/junowen/src/lobby/overlay.rs @@ -16,6 +16,8 @@ use windows_core::{HSTRING, w}; use super::Lobby; +pub use junowen_lobby::OverlayText; + /// 縁取りの太さと文字サイズの比 const OUTLINE_RATIO: f64 = 1.0 / 29.0; @@ -104,15 +106,6 @@ fn render_text_to_screen( unsafe { DrawTextW(hdc, &mut text, &mut rect, DT_LEFT) }; } -/// ゲーム内フォントでは英数字しか描画できないため、多言語のテキストは -/// GDI で画面に重ねて描画する。座標はゲーム内と同じ 1280x960 換算。 -pub struct OverlayText { - pub left: i32, - pub top: i32, - pub font_size: u32, - pub string: String, -} - pub struct Font { obj: HGDIOBJ, size: u32, diff --git a/junowen/src/lobby/pure_p2p_guest.rs b/crates/app/junowen/src/lobby/pure_p2p_guest.rs similarity index 86% rename from junowen/src/lobby/pure_p2p_guest.rs rename to crates/app/junowen/src/lobby/pure_p2p_guest.rs index 4c3d155..d1b8d66 100644 --- a/junowen/src/lobby/pure_p2p_guest.rs +++ b/crates/app/junowen/src/lobby/pure_p2p_guest.rs @@ -3,11 +3,12 @@ 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, }; +use junowen_lobby::{TextLayout, signaling_code_line_count}; use rust_i18n::t; use tokio::sync::mpsc; @@ -15,11 +16,12 @@ use crate::session::battle::BattleSession; use super::{ super::signaling::Signaling, - common_menu::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}, - helper::signaling_code_line_count, + common_menu::{ + CommonMenu, LobbyScene, Menu, MenuButtons, MenuItem, OnMenuInputResult, Th19MenuDevice, + Th19MenuRenderer, + }, overlay::OverlayText, signaling_code::paste_signaling_code, - text_layout::TextLayout, }; pub struct PureP2pGuest { @@ -45,7 +47,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, @@ -55,8 +61,8 @@ impl PureP2pGuest { pub fn on_input_menu( &mut self, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, session_rx: &mut Option>, ) -> Option { @@ -90,7 +96,7 @@ impl PureP2pGuest { } match self .common_menu - .on_input_menu(current_input, prev_input, th19) + .on_input_menu(current_input, prev_input, &Th19MenuDevice(th19)) { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => { @@ -165,8 +171,12 @@ impl PureP2pGuest { } pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { - self.common_menu.on_render_texts(th19, text_renderer); - self.layout().render_codes(th19, text_renderer); + let renderer = Th19MenuRenderer { + th19, + text_renderer, + }; + self.common_menu.on_render_texts(&renderer); + self.layout().render_codes(&renderer); } pub fn overlay_texts(&self) -> Vec { diff --git a/junowen/src/lobby/pure_p2p_offerer.rs b/crates/app/junowen/src/lobby/pure_p2p_offerer.rs similarity index 89% rename from junowen/src/lobby/pure_p2p_offerer.rs rename to crates/app/junowen/src/lobby/pure_p2p_offerer.rs index adb2e27..a0a2eef 100644 --- a/junowen/src/lobby/pure_p2p_offerer.rs +++ b/crates/app/junowen/src/lobby/pure_p2p_offerer.rs @@ -1,4 +1,3 @@ -mod menu; mod role; use std::ffi::c_void; @@ -10,23 +9,22 @@ use junowen_lib::{ DataChannel, PeerConnection, signaling::{SignalingCodeType, socket::async_read_write_socket::SignalingServerMessage}, }, - structs::input_devices::InputValue, }; +use junowen_lobby::pure_p2p_offerer_menu::{ + COPY_ACTION_ID, PASTE_ACTION_ID, REGENERATE_ACTION_ID, make_empty_menu, make_menu, +}; +use junowen_lobby::{TextLayout, signaling_code_line_count}; use rust_i18n::t; use tokio::sync::mpsc; use tracing::trace; use super::{ super::signaling::Signaling, - common_menu::{CommonMenu, LobbyScene, OnMenuInputResult}, - helper::signaling_code_line_count, + common_menu::{ + CommonMenu, LobbyScene, MenuButtons, OnMenuInputResult, Th19MenuDevice, Th19MenuRenderer, + }, overlay::OverlayText, signaling_code::paste_signaling_code, - text_layout::TextLayout, -}; - -use self::menu::{ - COPY_ACTION_ID, PASTE_ACTION_ID, REGENERATE_ACTION_ID, make_empty_menu, make_menu, }; pub use self::role::{Messages, pure_p2p_host, pure_p2p_spectator}; @@ -35,6 +33,7 @@ pub struct PureP2pOfferer { offer_type: SignalingCodeType, answer_type: SignalingCodeType, create_session: fn(PeerConnection, DataChannel) -> T, + protocol: &'static str, messages: Messages, common_menu: CommonMenu, signaling: Signaling, @@ -52,6 +51,7 @@ where offer_type: SignalingCodeType, answer_type: SignalingCodeType, create_session: fn(PeerConnection, DataChannel) -> T, + protocol: &'static str, label: &'static str, messages: Messages, ) -> Self { @@ -60,9 +60,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, @@ -71,8 +72,8 @@ where pub fn on_input_menu( &mut self, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, session_rx: &mut Option>, ) -> Option { @@ -89,7 +90,7 @@ where } match self .common_menu - .on_input_menu(current_input, prev_input, th19) + .on_input_menu(current_input, prev_input, &Th19MenuDevice(th19)) { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => { @@ -172,8 +173,12 @@ where } pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { - self.common_menu.on_render_texts(th19, text_renderer); - self.layout().render_codes(th19, text_renderer); + let renderer = Th19MenuRenderer { + th19, + text_renderer, + }; + self.common_menu.on_render_texts(&renderer); + self.layout().render_codes(&renderer); } pub fn overlay_texts(&self) -> Vec { @@ -185,6 +190,7 @@ where self.offer_type, self.answer_type, self.create_session, + self.protocol, self.common_menu.root_title(), self.messages, ); diff --git a/junowen/src/lobby/pure_p2p_offerer/role.rs b/crates/app/junowen/src/lobby/pure_p2p_offerer/role.rs similarity index 88% rename from junowen/src/lobby/pure_p2p_offerer/role.rs rename to crates/app/junowen/src/lobby/pure_p2p_offerer/role.rs index 10d902b..3b78649 100644 --- a/junowen/src/lobby/pure_p2p_offerer/role.rs +++ b/crates/app/junowen/src/lobby/pure_p2p_offerer/role.rs @@ -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}; @@ -17,6 +17,7 @@ pub fn pure_p2p_host() -> PureP2pOfferer { SignalingCodeType::BattleOffer, SignalingCodeType::BattleAnswer, |pc, dc| BattleSession::new(pc, dc, true), + BATTLE_PROTOCOL, "Connect as a Host", Messages { share: "pure_p2p.host_share", @@ -31,6 +32,7 @@ pub fn pure_p2p_spectator() -> PureP2pOfferer { SignalingCodeType::SpectatorOffer, SignalingCodeType::SpectatorAnswer, SpectatorSession::new, + SPECTATOR_PROTOCOL, "Connect as a Spectator", Messages { share: "pure_p2p.spectator_share", diff --git a/junowen/src/lobby/pure_p2p_scenes.rs b/crates/app/junowen/src/lobby/pure_p2p_scenes.rs similarity index 96% rename from junowen/src/lobby/pure_p2p_scenes.rs rename to crates/app/junowen/src/lobby/pure_p2p_scenes.rs index a42e54b..6b42683 100644 --- a/junowen/src/lobby/pure_p2p_scenes.rs +++ b/crates/app/junowen/src/lobby/pure_p2p_scenes.rs @@ -1,6 +1,6 @@ use std::ffi::c_void; -use junowen_lib::{Th19, structs::input_devices::InputValue}; +use junowen_lib::Th19; use crate::{ session::{battle::BattleSession, spectator::SpectatorSession}, @@ -10,7 +10,7 @@ use crate::{ }; use super::{ - common_menu::LobbyScene, + common_menu::{LobbyScene, MenuButtons}, overlay::OverlayText, pure_p2p_guest::PureP2pGuest, pure_p2p_offerer::{PureP2pOfferer, pure_p2p_host, pure_p2p_spectator}, @@ -28,8 +28,8 @@ impl PureP2pScenes { pub fn on_input_menu( &mut self, scene: LobbyScene, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, waiting_for_match: &mut Option, ) -> Option { diff --git a/junowen/src/lobby/room.rs b/crates/app/junowen/src/lobby/room.rs similarity index 91% rename from junowen/src/lobby/room.rs rename to crates/app/junowen/src/lobby/room.rs index cbf2025..17accba 100644 --- a/junowen/src/lobby/room.rs +++ b/crates/app/junowen/src/lobby/room.rs @@ -5,13 +5,11 @@ pub mod tcp_signaling; use std::{f64::consts::PI, ffi::c_void}; use junowen_lib::{Th19, structs::others::RenderingText}; +use junowen_lobby::render_text_line; use crate::signaling::waiting_for_match::WaitingInRoom; -use super::{ - common_menu::CommonMenu, - helper::{render_label_value, render_text_line}, -}; +use super::common_menu::{CommonMenu, Th19MenuRenderer, render_label_value}; fn progress_alphas(progress: f64) -> Vec { const LENGTH: f64 = 20.0; @@ -123,17 +121,21 @@ pub fn on_render_texts( th19: &Th19, text_renderer: &c_void, ) { - menu.on_render_texts(th19, text_renderer); + let renderer = Th19MenuRenderer { + th19, + text_renderer, + }; + menu.on_render_texts(&renderer); if let Some(value) = value { - render_label_value(th19, text_renderer, 240 - 56, 1, label, value); + render_label_value(&renderer, 240 - 56, 1, label, value); } if let Some(waiting) = waiting { let elapsed = waiting.elapsed(); render_progress(th19, text_renderer, elapsed.as_secs_f64() / 4.0); for (i, error) in waiting.errors().iter().rev().enumerate() { let error_msg = format!("Failed: {}", error); - render_text_line(th19, text_renderer, 13 + i as u32, error_msg.as_bytes()); + render_text_line(&renderer, 13 + i as u32, error_msg.as_bytes()); } } } diff --git a/junowen/src/lobby/room/reserved.rs b/crates/app/junowen/src/lobby/room/reserved.rs similarity index 93% rename from junowen/src/lobby/room/reserved.rs rename to crates/app/junowen/src/lobby/room/reserved.rs index 61b81aa..67a6ed6 100644 --- a/junowen/src/lobby/room/reserved.rs +++ b/crates/app/junowen/src/lobby/room/reserved.rs @@ -1,6 +1,6 @@ use std::ffi::c_void; -use junowen_lib::{Th19, structs::input_devices::InputValue}; +use junowen_lib::Th19; use crate::{ TOKIO_RUNTIME, @@ -12,7 +12,9 @@ use crate::{ }; use super::{ - super::common_menu::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}, + super::common_menu::{ + CommonMenu, LobbyScene, Menu, MenuButtons, MenuItem, OnMenuInputResult, Th19MenuDevice, + }, on_render_texts, }; @@ -70,8 +72,8 @@ impl ReservedRoom { pub fn on_input_menu( &mut self, settings_repo: &SettingsRepo, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, waiting: &mut Option, ) -> Option { @@ -96,7 +98,10 @@ impl ReservedRoom { } } - match self.menu.on_input_menu(current_input, prev_input, th19) { + match self + .menu + .on_input_menu(current_input, prev_input, &Th19MenuDevice(th19)) + { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => { self.enter = false; diff --git a/junowen/src/lobby/room/shared.rs b/crates/app/junowen/src/lobby/room/shared.rs similarity index 92% rename from junowen/src/lobby/room/shared.rs rename to crates/app/junowen/src/lobby/room/shared.rs index cf3f3f5..07ef256 100644 --- a/junowen/src/lobby/room/shared.rs +++ b/crates/app/junowen/src/lobby/room/shared.rs @@ -1,6 +1,6 @@ use std::ffi::c_void; -use junowen_lib::{Th19, structs::input_devices::InputValue}; +use junowen_lib::Th19; use rust_i18n::t; use crate::{ @@ -8,7 +8,9 @@ use crate::{ }; use super::{ - super::common_menu::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}, + super::common_menu::{ + CommonMenu, LobbyScene, Menu, MenuButtons, MenuItem, OnMenuInputResult, Th19MenuDevice, + }, on_render_texts, }; @@ -57,8 +59,8 @@ impl SharedRoom { pub fn on_input_menu( &mut self, settings_repo: &SettingsRepo, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, waiting: &mut Option, ) -> Option { @@ -76,7 +78,10 @@ impl SharedRoom { if let Some(waiting) = waiting { waiting.recv(); } - match self.menu.on_input_menu(current_input, prev_input, th19) { + match self + .menu + .on_input_menu(current_input, prev_input, &Th19MenuDevice(th19)) + { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => Some(LobbyScene::Root), OnMenuInputResult::SubScene(_) => unreachable!(), diff --git a/junowen/src/lobby/room/tcp_signaling.rs b/crates/app/junowen/src/lobby/room/tcp_signaling.rs similarity index 92% rename from junowen/src/lobby/room/tcp_signaling.rs rename to crates/app/junowen/src/lobby/room/tcp_signaling.rs index 2078420..549c48b 100644 --- a/junowen/src/lobby/room/tcp_signaling.rs +++ b/crates/app/junowen/src/lobby/room/tcp_signaling.rs @@ -1,8 +1,10 @@ -mod menu; - use std::ffi::c_void; -use junowen_lib::{Th19, structs::input_devices::InputValue}; +use junowen_lib::Th19; +use junowen_lobby::tcp_signaling_menu::{ + ADDRESS_INPUT_ACTION_ID, CHANGE_ADDRESS_ACTION_ID, GUEST_ACTION_ID, HOST_ACTION_ID, + OFFLINE_ACTION_ID, OFFLINE_ITEM_INDEX, TcpSignalingMenu, +}; use rust_i18n::t; use crate::{ @@ -10,13 +12,8 @@ use crate::{ signaling::waiting_for_match::WaitingForOpponentOverTcpSignaling, }; -use self::menu::{ - ADDRESS_INPUT_ACTION_ID, CHANGE_ADDRESS_ACTION_ID, GUEST_ACTION_ID, HOST_ACTION_ID, - OFFLINE_ACTION_ID, OFFLINE_ITEM_INDEX, TcpSignalingMenu, -}; - use super::{ - super::common_menu::{LobbyScene, MenuItem, OnMenuInputResult}, + super::common_menu::{LobbyScene, MenuButtons, MenuItem, OnMenuInputResult, Th19MenuDevice}, on_render_texts, }; @@ -40,8 +37,8 @@ impl TcpSignaling { pub fn on_input_menu( &mut self, settings_repo: &SettingsRepo, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &Th19, waiting: &mut Option, ) -> Option { @@ -59,11 +56,11 @@ impl TcpSignaling { waiting.recv(); } - match self - .menu - .common_menu_mut() - .on_input_menu(current_input, prev_input, th19) - { + match self.menu.common_menu_mut().on_input_menu( + current_input, + prev_input, + &Th19MenuDevice(th19), + ) { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => Some(LobbyScene::Root), OnMenuInputResult::SubScene(_) => unreachable!(), diff --git a/junowen/src/lobby/root.rs b/crates/app/junowen/src/lobby/root.rs similarity index 83% rename from junowen/src/lobby/root.rs rename to crates/app/junowen/src/lobby/root.rs index f1a42e2..31c60d5 100644 --- a/junowen/src/lobby/root.rs +++ b/crates/app/junowen/src/lobby/root.rs @@ -1,12 +1,12 @@ use std::ffi::c_void; -use junowen_lib::{ - Th19, - structs::input_devices::{InputFlags, InputValue}, -}; +use junowen_lib::{Th19, structs::input_devices::InputFlags}; use rust_i18n::t; -use super::common_menu::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}; +use super::common_menu::{ + CommonMenu, LobbyScene, Menu, MenuButtons, MenuItem, OnMenuInputResult, Th19MenuDevice, + Th19MenuRenderer, +}; pub struct Root { common_menu: CommonMenu, @@ -48,13 +48,13 @@ impl Root { pub fn on_input_menu( &mut self, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, th19: &mut Th19, ) -> Option { match self .common_menu - .on_input_menu(current_input, prev_input, th19) + .on_input_menu(current_input, prev_input, &Th19MenuDevice(th19)) { OnMenuInputResult::None => None, OnMenuInputResult::Cancel => { @@ -67,7 +67,10 @@ impl Root { } pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { - self.common_menu.on_render_texts(th19, text_renderer); + self.common_menu.on_render_texts(&Th19MenuRenderer { + th19, + text_renderer, + }); } pub fn text(&self) -> String { diff --git a/junowen/src/lobby/signaling_code.rs b/crates/app/junowen/src/lobby/signaling_code.rs similarity index 100% rename from junowen/src/lobby/signaling_code.rs rename to crates/app/junowen/src/lobby/signaling_code.rs diff --git a/junowen/src/lobby/title_menu_modifier.rs b/crates/app/junowen/src/lobby/title_menu_modifier.rs similarity index 99% rename from junowen/src/lobby/title_menu_modifier.rs rename to crates/app/junowen/src/lobby/title_menu_modifier.rs index 5d14491..3ac5d2c 100644 --- a/junowen/src/lobby/title_menu_modifier.rs +++ b/crates/app/junowen/src/lobby/title_menu_modifier.rs @@ -8,7 +8,7 @@ use junowen_lib::{ structs::others::RenderingText, }; -use super::helper::menu_item_color; +use super::common_menu::menu_item_color; fn direction(input: &Input, flag: InputFlags) -> bool { input.current().0 & InputValue::from(flag).0 != None diff --git a/crates/app/junowen/src/present_skipper.rs b/crates/app/junowen/src/present_skipper.rs new file mode 100644 index 0000000..2185342 --- /dev/null +++ b/crates/app/junowen/src/present_skipper.rs @@ -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) + } +} diff --git a/junowen/src/session.rs b/crates/app/junowen/src/session.rs similarity index 100% rename from junowen/src/session.rs rename to crates/app/junowen/src/session.rs diff --git a/junowen/src/session/battle.rs b/crates/app/junowen/src/session/battle.rs similarity index 100% rename from junowen/src/session/battle.rs rename to crates/app/junowen/src/session/battle.rs diff --git a/junowen/src/session/delayed_inputs.rs b/crates/app/junowen/src/session/delayed_inputs.rs similarity index 100% rename from junowen/src/session/delayed_inputs.rs rename to crates/app/junowen/src/session/delayed_inputs.rs diff --git a/junowen/src/session/session_message.rs b/crates/app/junowen/src/session/session_message.rs similarity index 91% rename from junowen/src/session/session_message.rs rename to crates/app/junowen/src/session/session_message.rs index baae8d4..616358e 100644 --- a/junowen/src/session/session_message.rs +++ b/crates/app/junowen/src/session/session_message.rs @@ -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, diff --git a/crates/app/junowen/src/session/spectator.rs b/crates/app/junowen/src/session/spectator.rs new file mode 100644 index 0000000..d9bc19f --- /dev/null +++ b/crates/app/junowen/src/session/spectator.rs @@ -0,0 +1,218 @@ +use std::{ + collections::VecDeque, + sync::mpsc::{RecvError, TryRecvError}, +}; + +use anyhow::Result; +use getset::{CopyGetters, Getters, Setters}; +use junowen_lib::connection::{DataChannel, PeerConnection}; +use tracing::{error, info, warn}; + +use super::{session_message::RoundInitial, to_channel}; + +mod message; +mod relay_outbox; + +pub use message::{ + GameInitial, PlayerGameInitial, SpectatorHostDelegation, SpectatorInitial, SpectatorRelayRoom, + SpectatorSessionMessage, +}; +pub use relay_outbox::RelayOutbox; + +#[derive(CopyGetters, Getters, Setters)] +pub struct SpectatorSession { + _conn: PeerConnection, + hook_incoming_rx: std::sync::mpsc::Receiver, + /// 受信済みで未処理のメッセージ。試合開始がホストより遅れた分を把握するために使う + buffer: VecDeque, + disconnected: bool, + spectator_initial: Option, + round_initial: Option, + relay_outbox: RelayOutbox, +} + +impl SpectatorSession { + pub fn new(conn: PeerConnection, data_channel: DataChannel) -> Self { + let (_hook_outgoing_tx, hook_incoming_rx) = + to_channel(data_channel, |input| rmp_serde::from_slice(input)); + Self { + _conn: conn, + hook_incoming_rx, + buffer: VecDeque::new(), + disconnected: false, + spectator_initial: None, + round_initial: None, + relay_outbox: RelayOutbox::default(), + } + } + + pub fn spectator_initial(&self) -> Option<&SpectatorInitial> { + self.spectator_initial.as_ref() + } + + pub fn relay_outbox_mut(&mut self) -> &mut RelayOutbox { + &mut self.relay_outbox + } + + fn fill_buffer(&mut self) { + loop { + match self.hook_incoming_rx.try_recv() { + Ok(msg) => { + if let Some(msg) = self.relay_outbox.filter_delegation(msg) { + self.buffer.push_back(msg); + } + } + Err(TryRecvError::Empty) => return, + Err(TryRecvError::Disconnected) => { + self.disconnected = true; + return; + } + } + } + } + + fn next_message(&mut self) -> Result { + let msg = self.pop_message()?; + self.relay_outbox.push_processed(&msg); + Ok(msg) + } + + fn pop_message(&mut self) -> Result { + if let Some(msg) = self.buffer.pop_front() { + return Ok(msg); + } + loop { + let msg = self.hook_incoming_rx.recv()?; + if let Some(msg) = self.relay_outbox.filter_delegation(msg) { + return Ok(msg); + } + } + } + + /// 受信済みで未処理のメッセージ数を返す + /// + /// 呼び出し時点で受信できるメッセージをすべてバッファへ取り込む + pub fn poll_buffered_len(&mut self) -> usize { + self.fill_buffer(); + self.buffer.len() + } + + /// ホスト側の準備が整うまで待つ必要があるため、ブロックせずに受信を試みる + /// + /// 受信できた場合は `true` を返す + pub fn try_recv_init_spectator(&mut self) -> Result { + self.fill_buffer(); + let Some(msg) = self.buffer.pop_front() else { + return if self.disconnected { + Err(RecvError) + } else { + Ok(false) + }; + }; + let init = match msg { + SpectatorSessionMessage::InitSpectator(init) => init, + msg => { + error!("unexpected message: {:?}", msg); + return Err(RecvError); + } + }; + self.spectator_initial = Some(init); + Ok(true) + } + + /// 観戦者の試合中に次の試合が始まったかどうか + /// + /// 観戦者の試合がホストとずれた場合に起こる。この場合は今の試合を打ち切り、次の試合から同期し直す + /// + /// 試合中に受け取った `InitGame` はバッファの先頭に戻しておき、次の試合の待機状態で受け取る + pub fn has_next_game_initial(&self) -> bool { + matches!( + self.buffer.front(), + Some(SpectatorSessionMessage::InitGame(_)) + ) + } + + /// 前の試合で残ったラウンドの初期化情報を破棄する。試合開始を待つ状態に入るときに呼ぶ + pub fn discard_round_initial(&mut self) { + self.round_initial = None; + } + + /// 試合開始時の状態をブロックせずに受信する + /// + /// 観戦者の試合がホストより早く終わった場合などに残った入力は読み捨てる + pub fn try_recv_init_game(&mut self) -> Result, RecvError> { + self.fill_buffer(); + while let Some(msg) = self.buffer.pop_front() { + match msg { + SpectatorSessionMessage::InitGame(init) => { + self.relay_outbox + .push(SpectatorSessionMessage::InitGame(init.clone())); + return Ok(Some(init)); + } + SpectatorSessionMessage::InitSpectator(init) => { + error!("unexpected init spectator message: {:?}", init); + return Err(RecvError); + } + msg => warn!("discard message: {:?}", msg), + } + } + if self.disconnected { + return Err(RecvError); + } + Ok(None) + } + + /// 次の試合が始まっている場合は `None` を返す + pub fn dequeue_init_round(&mut self) -> Result, RecvError> { + if let Some(round_initial) = self.round_initial.take() { + return Ok(Some(round_initial)); + } + loop { + match self.next_message()? { + SpectatorSessionMessage::InitSpectator(init) => { + error!("unexpected init spectator message: {:?}", init); + return Err(RecvError); + } + msg @ SpectatorSessionMessage::InitGame(_) => { + warn!("next game started during round over"); + self.buffer.push_front(msg); + return Ok(None); + } + SpectatorSessionMessage::InitRound(round_initial) => { + return Ok(Some(round_initial)); + } + SpectatorSessionMessage::Inputs(..) => continue, + SpectatorSessionMessage::DelegateSpectatorHost(..) => unreachable!(), + } + } + } + + pub fn dequeue_inputs(&mut self) -> Result<(u16, u16), RecvError> { + if self.round_initial.is_some() { + return Ok((0, 0)); + } + match self.next_message()? { + SpectatorSessionMessage::InitSpectator(init) => { + error!("unexpected init spectator message: {:?}", init); + Err(RecvError) + } + msg @ SpectatorSessionMessage::InitGame(_) => { + warn!("next game started during game"); + self.buffer.push_front(msg); + Ok((0, 0)) + } + SpectatorSessionMessage::InitRound(round_initial) => { + self.round_initial = Some(round_initial); + Ok((0, 0)) + } + SpectatorSessionMessage::Inputs(p1, p2) => Ok((p1, p2)), + SpectatorSessionMessage::DelegateSpectatorHost(..) => unreachable!(), + } + } +} + +impl Drop for SpectatorSession { + fn drop(&mut self) { + info!("spectator session guest closed"); + } +} diff --git a/crates/app/junowen/src/session/spectator/message.rs b/crates/app/junowen/src/session/spectator/message.rs new file mode 100644 index 0000000..84cd1f2 --- /dev/null +++ b/crates/app/junowen/src/session/spectator/message.rs @@ -0,0 +1,69 @@ +use derive_new::new; +use getset::{CopyGetters, Getters}; +use junowen_lib::structs::settings::GameSettings; +use serde::{Deserialize, Serialize}; + +use crate::session::RoundInitial; + +#[derive(new, Clone, Debug, Deserialize, Getters, Serialize)] +pub struct SpectatorInitial { + #[get = "pub"] + p1_name: String, + #[get = "pub"] + p2_name: String, + #[get = "pub"] + game_settings: GameSettings, +} + +#[derive(new, Clone, Copy, Debug, Deserialize, CopyGetters, PartialEq, Serialize)] +pub struct PlayerGameInitial { + #[get_copy = "pub"] + character: u8, + #[get_copy = "pub"] + card: u8, +} + +/// 試合開始時の状態。観戦者はこれに合わせてキャラクターとカードを決定する +#[derive(new, Clone, Debug, Deserialize, CopyGetters, Getters, Serialize)] +pub struct GameInitial { + #[get_copy = "pub"] + difficulty: u8, + #[get_copy = "pub"] + p1: PlayerGameInitial, + #[get_copy = "pub"] + p2: PlayerGameInitial, + #[get = "pub"] + round_initial: RoundInitial, +} + +/// 観戦ホストが観戦者を受け付ける専有ルームの名前とキーを表す。 +#[derive(new, Clone, Debug, Deserialize, Getters, Serialize)] +pub struct SpectatorRelayRoom { + #[get = "pub"] + room_name: String, + #[get = "pub"] + key: String, +} + +/// 観戦ホストへの任命 +#[derive(new, Clone, Debug, Deserialize, Serialize)] +pub struct SpectatorHostDelegation { + /// 観戦者を待ち受ける専有ルームの情報を持つ。ない場合は、Pure P2P で観戦者を待ち受ける。 + room: Option, +} + +impl SpectatorHostDelegation { + pub fn into_room(self) -> Option { + self.room + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum SpectatorSessionMessage { + InitSpectator(SpectatorInitial), + InitGame(GameInitial), + InitRound(RoundInitial), + Inputs(u16, u16), + /// 受信した観戦者を観戦ホストに任命し、以降の観戦者の受け付けと中継を委譲する + DelegateSpectatorHost(SpectatorHostDelegation), +} diff --git a/crates/app/junowen/src/session/spectator/relay_outbox.rs b/crates/app/junowen/src/session/spectator/relay_outbox.rs new file mode 100644 index 0000000..d4d8528 --- /dev/null +++ b/crates/app/junowen/src/session/spectator/relay_outbox.rs @@ -0,0 +1,60 @@ +use tracing::info; + +use super::{SpectatorHostDelegation, SpectatorSessionMessage}; + +/// 観戦ホストとして配下の観戦者に中継するメッセージの記録 +/// +/// 観戦ホストは、自分が処理したのと同じ順序で配下の観戦者に試合の進行を送る +#[derive(Default)] +pub struct RelayOutbox { + delegation: Option, + /// 観戦ホストに任命されているか + active: bool, + /// 処理済みのメッセージのうち、まだ中継していないもの + messages: Vec, +} + +impl RelayOutbox { + /// 観戦ホストへの任命を取り込み、それ以外のメッセージを返す + pub fn filter_delegation( + &mut self, + msg: SpectatorSessionMessage, + ) -> Option { + match msg { + SpectatorSessionMessage::DelegateSpectatorHost(delegation) => { + info!("delegated spectator host"); + self.delegation = Some(delegation); + self.active = true; + None + } + msg => Some(msg), + } + } + + /// 観戦ホストに任命された場合、その任命を返す + pub fn take_delegation(&mut self) -> Option { + self.delegation.take() + } + + pub fn push(&mut self, msg: SpectatorSessionMessage) { + if self.active { + self.messages.push(msg); + } + } + + /// 試合中に処理したメッセージを記録する + /// + /// `InitGame` はバッファに戻されることがあるため、試合の開始時に記録する + pub fn push_processed(&mut self, msg: &SpectatorSessionMessage) { + if matches!( + msg, + SpectatorSessionMessage::InitRound(_) | SpectatorSessionMessage::Inputs(..) + ) { + self.push(msg.clone()); + } + } + + pub fn take_messages(&mut self) -> Vec { + std::mem::take(&mut self.messages) + } +} diff --git a/junowen/src/session/spectator_host.rs b/crates/app/junowen/src/session/spectator_host.rs similarity index 69% rename from junowen/src/session/spectator_host.rs rename to crates/app/junowen/src/session/spectator_host.rs index 157a9af..0999576 100644 --- a/junowen/src/session/spectator_host.rs +++ b/crates/app/junowen/src/session/spectator_host.rs @@ -5,7 +5,10 @@ use tracing::info; use super::{ session_message::RoundInitial, - spectator::{SpectatorInitial, SpectatorSessionMessage}, + spectator::{ + GameInitial, SpectatorHostDelegation, SpectatorInitial, SpectatorRelayRoom, + SpectatorSessionMessage, + }, to_channel, }; @@ -25,12 +28,26 @@ impl SpectatorHostSession { } } + pub fn send_delegate_spectator_host(&self, room: Option) -> Result<()> { + Ok(self + .hook_outgoing_tx + .send(SpectatorSessionMessage::DelegateSpectatorHost( + SpectatorHostDelegation::new(room), + ))?) + } + pub fn send_init_spectator(&self, init: SpectatorInitial) -> Result<()> { Ok(self .hook_outgoing_tx .send(SpectatorSessionMessage::InitSpectator(init))?) } + pub fn send_init_game(&self, init: GameInitial) -> Result<()> { + Ok(self + .hook_outgoing_tx + .send(SpectatorSessionMessage::InitGame(init))?) + } + pub fn send_init_round(&self, init: RoundInitial) -> Result<()> { Ok(self .hook_outgoing_tx diff --git a/junowen/src/signaling.rs b/crates/app/junowen/src/signaling.rs similarity index 97% rename from junowen/src/signaling.rs rename to crates/app/junowen/src/signaling.rs index f34a120..971a8c1 100644 --- a/junowen/src/signaling.rs +++ b/crates/app/junowen/src/signaling.rs @@ -39,6 +39,7 @@ impl Signaling { pub fn new( session_tx: mpsc::Sender, create_session: fn(PeerConnection, DataChannel) -> T, + protocol: &'static str, ) -> Self where T: Send + 'static, @@ -49,7 +50,7 @@ impl Signaling { let (error_tx, error_rx) = oneshot::channel(); let (connected_tx, connected_rx) = oneshot::channel(); TOKIO_RUNTIME.spawn(async move { - let mut socket = ChannelSocket::new(offer_tx, answer_tx, msg_rx); + let mut socket = ChannelSocket::new(offer_tx, answer_tx, msg_rx, protocol); let (conn, dc, _host) = match socket.receive_signaling().await { Ok(ok) => ok, Err(err) => { diff --git a/junowen/src/signaling/waiting_for_match.rs b/crates/app/junowen/src/signaling/waiting_for_match.rs similarity index 80% rename from junowen/src/signaling/waiting_for_match.rs rename to crates/app/junowen/src/signaling/waiting_for_match.rs index 9b15048..8c64a23 100644 --- a/junowen/src/signaling/waiting_for_match.rs +++ b/crates/app/junowen/src/signaling/waiting_for_match.rs @@ -5,6 +5,7 @@ mod shared_room_opponent_socket; mod socket; mod tcp_signaling_guest_socket; mod tcp_signaling_host_socket; +mod waiting_for_pure_p2p_spectator; pub mod waiting_for_spectator; mod waiting_in_room; @@ -13,7 +14,8 @@ use tokio::sync::mpsc; use crate::session::{battle::BattleSession, spectator::SpectatorSession}; -pub use waiting_for_spectator::{WaitingForPureP2pSpectator, WaitingForSpectator}; +pub use waiting_for_pure_p2p_spectator::WaitingForPureP2pSpectator; +pub use waiting_for_spectator::WaitingForSpectator; pub use waiting_in_room::{ WaitingForOpponentInReservedRoom, WaitingForOpponentInSharedRoom, WaitingForOpponentOverTcpSignaling, WaitingForSpectatorHostInReservedRoom, WaitingInRoom, @@ -42,34 +44,19 @@ impl WaitingForOpponent { match self { Self::SharedRoom(waiting) => waiting .try_into_session() - .map(|session| { - ( - session, - WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()), - ) - }) + .map(|session| (session, WaitingForSpectator::new(None))) .map_err(WaitingForOpponent::SharedRoom), Self::ReservedRoom(waiting) => waiting .try_into_session_and_waiting_for_spectator() .map_err(WaitingForOpponent::ReservedRoom), Self::TcpSignaling(waiting) => waiting .try_into_session() - .map(|session| { - ( - session, - WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()), - ) - }) + .map(|session| (session, WaitingForSpectator::new(None))) .map_err(WaitingForOpponent::TcpSignaling), Self::PureP2p(mut waiting) => waiting .battle_session_rx .try_recv() - .map(|session| { - ( - session, - WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()), - ) - }) + .map(|session| (session, WaitingForSpectator::new(None))) .map_err(|_| Self::PureP2p(waiting)), } } diff --git a/junowen/src/signaling/waiting_for_match/reserved_room_opponent_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/reserved_room_opponent_socket.rs similarity index 100% rename from junowen/src/signaling/waiting_for_match/reserved_room_opponent_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/reserved_room_opponent_socket.rs diff --git a/junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs similarity index 92% rename from junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs index c52becb..4a67aa2 100644 --- a/junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs +++ b/crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs @@ -4,9 +4,12 @@ use anyhow::{Result, bail}; use async_trait::async_trait; use junowen_lib::{ - connection::signaling::{ - CompressedSdp, - socket::{OfferResponse, SignalingSocket}, + connection::{ + SPECTATOR_PROTOCOL, + signaling::{ + CompressedSdp, + socket::{OfferResponse, SignalingSocket}, + }, }, signaling_server::reserved_room::{ PostReservedRoomKeepRequestBody, PostReservedRoomKeepResponse, @@ -43,10 +46,6 @@ impl SignalingServerReservedRoomSpectatorHostSocket { } } - pub fn into_key(self) -> String { - self.key - } - async fn sleep_or_abort_and_delete_room(&mut self, retry_after: u32, key: &str) -> Result<()> { let url = &self.resource_url; sleep_or_abort_and_delete_room(retry_after, &mut self.abort_rx, &self.client, url, key) @@ -60,6 +59,10 @@ impl SignalingSocket for SignalingServerReservedRoomSpectatorHostSocket { Duration::from_secs(10) } + fn protocol(&self) -> &'static str { + SPECTATOR_PROTOCOL + } + async fn offer(&mut self, desc: CompressedSdp) -> Result { let key = self.key.clone(); let url = format!("{}/keep", self.resource_url); diff --git a/junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs similarity index 93% rename from junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs index 9914bca..eabad77 100644 --- a/junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs +++ b/crates/app/junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs @@ -3,9 +3,12 @@ use std::time::Duration; use anyhow::{Error, Result, bail}; use async_trait::async_trait; use junowen_lib::{ - connection::signaling::{ - CompressedSdp, - socket::{OfferResponse, SignalingSocket}, + connection::{ + SPECTATOR_PROTOCOL, + signaling::{ + CompressedSdp, + socket::{OfferResponse, SignalingSocket}, + }, }, signaling_server::reserved_room::{ GetReservedRoomResponse, PostReservedRoomSpectateRequestBody, @@ -52,6 +55,10 @@ impl SignalingSocket for SignalingServerReservedRoomSpectatorSocket { Duration::from_secs(10) } + fn protocol(&self) -> &'static str { + SPECTATOR_PROTOCOL + } + async fn offer(&mut self, _desc: CompressedSdp) -> Result { loop { info!("GET {}", self.resource_url); diff --git a/junowen/src/signaling/waiting_for_match/shared_room_opponent_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/shared_room_opponent_socket.rs similarity index 100% rename from junowen/src/signaling/waiting_for_match/shared_room_opponent_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/shared_room_opponent_socket.rs diff --git a/junowen/src/signaling/waiting_for_match/socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/socket.rs similarity index 100% rename from junowen/src/signaling/waiting_for_match/socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/socket.rs diff --git a/junowen/src/signaling/waiting_for_match/tcp_signaling_guest_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/tcp_signaling_guest_socket.rs similarity index 100% rename from junowen/src/signaling/waiting_for_match/tcp_signaling_guest_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/tcp_signaling_guest_socket.rs diff --git a/junowen/src/signaling/waiting_for_match/tcp_signaling_host_socket.rs b/crates/app/junowen/src/signaling/waiting_for_match/tcp_signaling_host_socket.rs similarity index 100% rename from junowen/src/signaling/waiting_for_match/tcp_signaling_host_socket.rs rename to crates/app/junowen/src/signaling/waiting_for_match/tcp_signaling_host_socket.rs diff --git a/junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs b/crates/app/junowen/src/signaling/waiting_for_match/waiting_for_pure_p2p_spectator.rs similarity index 67% rename from junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs rename to crates/app/junowen/src/signaling/waiting_for_match/waiting_for_pure_p2p_spectator.rs index 345b096..154aa5f 100644 --- a/junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs +++ b/crates/app/junowen/src/signaling/waiting_for_match/waiting_for_pure_p2p_spectator.rs @@ -4,18 +4,21 @@ use anyhow::Result; use clipboard_win::{get_clipboard_string, set_clipboard_string}; use junowen_lib::{ Th19, - connection::signaling::{ - SignalingCodeType, parse_signaling_code, - socket::async_read_write_socket::SignalingServerMessage, + connection::{ + SPECTATOR_PROTOCOL, + signaling::{ + SignalingCodeType, parse_signaling_code, + socket::async_read_write_socket::SignalingServerMessage, + }, }, - structs::app::{MainMenu, ScreenId}, + structs::app::MainMenu, }; -use tokio::sync::mpsc::{self, error::TryRecvError}; +use tokio::sync::mpsc; use tracing::info; use crate::session::spectator_host::SpectatorHostSession; -use super::{super::Signaling, waiting_in_room::WaitingForSpectatorInReservedRoom}; +use super::super::Signaling; fn try_start_signaling(th19: &Th19) -> Option { let Ok(ok) = get_clipboard_string() else { @@ -27,7 +30,7 @@ fn try_start_signaling(th19: &Th19) -> Option { return None; }; let (session_tx, session_rx) = mpsc::channel(1); - let mut signaling = Signaling::new(session_tx, SpectatorHostSession::new); + let mut signaling = Signaling::new(session_tx, SpectatorHostSession::new, SPECTATOR_PROTOCOL); signaling .msg_tx_mut() .take() @@ -91,13 +94,9 @@ impl WaitingForPureP2pSpectator { main_menu: Option<&MainMenu>, th19: &Th19, ) -> Result<()> { - let selection = th19.selection(); - self.set_ready( - main_menu.is_some() - && main_menu.unwrap().screen_id() == ScreenId::DifficultySelect - && selection.p1().card == 0 - && selection.p2().card == 0, - ); + // 観戦者の受け付けはメニュー画面 (難易度選択・キャラクター選択) で行う。 + // 観戦者は次の試合の開始 (`SpectatorHostState::start_game`) から合流する + self.set_ready(main_menu.is_some()); match self { Self::Standby { pushed, .. } => { @@ -169,47 +168,3 @@ impl WaitingForPureP2pSpectator { } } } - -pub enum WaitingForSpectator { - PureP2p(WaitingForPureP2pSpectator), - ReservedRoom(WaitingForSpectatorInReservedRoom), -} - -impl WaitingForSpectator { - pub fn try_recv_session( - &mut self, - pushed: bool, - menu: Option<&MainMenu>, - th19: &Th19, - ) -> Option { - match self { - Self::PureP2p(waiting) => { - waiting.update(pushed, menu, th19); - match waiting { - WaitingForPureP2pSpectator::Standby { .. } - | WaitingForPureP2pSpectator::SignalingCodeRecved { .. } => None, - WaitingForPureP2pSpectator::SignalingCodeSent { session_rx, .. } => { - match session_rx.try_recv() { - Err(TryRecvError::Empty) => None, - Err(TryRecvError::Disconnected) => { - *self = Self::PureP2p(WaitingForPureP2pSpectator::standby()); - None - } - Ok(session) => { - *self = Self::PureP2p(WaitingForPureP2pSpectator::standby()); - Some(session) - } - } - } - } - } - Self::ReservedRoom(waiting) => match waiting.try_session_and_waiting_for_spectator() { - Ok((session, waiting)) => { - *self = waiting; - Some(session) - } - Err(_) => None, - }, - } - } -} diff --git a/crates/app/junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs b/crates/app/junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs new file mode 100644 index 0000000..ff8c542 --- /dev/null +++ b/crates/app/junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs @@ -0,0 +1,72 @@ +use junowen_lib::{Th19, structs::app::MainMenu}; +use tokio::sync::mpsc::error::TryRecvError; + +use crate::session::{spectator::SpectatorRelayRoom, spectator_host::SpectatorHostSession}; + +use super::{ + waiting_for_pure_p2p_spectator::WaitingForPureP2pSpectator, + waiting_in_room::WaitingForSpectatorInReservedRoom, +}; + +pub enum WaitingForSpectator { + PureP2p(WaitingForPureP2pSpectator), + ReservedRoom { + room: SpectatorRelayRoom, + waiting: WaitingForSpectatorInReservedRoom, + }, +} + +impl WaitingForSpectator { + /// 専有ルームの情報があれば専有ルームで、無ければ Pure P2P で観戦者を待ち受ける + pub fn new(room: Option) -> Self { + match room { + Some(room) => { + let waiting = WaitingForSpectatorInReservedRoom::new( + room.room_name().clone(), + room.key().clone(), + ); + Self::ReservedRoom { room, waiting } + } + None => Self::PureP2p(WaitingForPureP2pSpectator::standby()), + } + } + + pub fn room(&self) -> Option<&SpectatorRelayRoom> { + match self { + Self::PureP2p(_) => None, + Self::ReservedRoom { room, .. } => Some(room), + } + } + + /// 専有ルームの場合、観戦者を受信した後は再度 `new` で待ち受けを作り直す必要がある + pub fn try_recv_session( + &mut self, + pushed: bool, + menu: Option<&MainMenu>, + th19: &Th19, + ) -> Option { + match self { + Self::PureP2p(waiting) => { + waiting.update(pushed, menu, th19); + match waiting { + WaitingForPureP2pSpectator::Standby { .. } + | WaitingForPureP2pSpectator::SignalingCodeRecved { .. } => None, + WaitingForPureP2pSpectator::SignalingCodeSent { session_rx, .. } => { + match session_rx.try_recv() { + Err(TryRecvError::Empty) => None, + Err(TryRecvError::Disconnected) => { + *self = Self::PureP2p(WaitingForPureP2pSpectator::standby()); + None + } + Ok(session) => { + *self = Self::PureP2p(WaitingForPureP2pSpectator::standby()); + Some(session) + } + } + } + } + } + Self::ReservedRoom { waiting, .. } => waiting.try_recv_session(), + } + } +} diff --git a/junowen/src/signaling/waiting_for_match/waiting_in_room.rs b/crates/app/junowen/src/signaling/waiting_for_match/waiting_in_room.rs similarity index 85% rename from junowen/src/signaling/waiting_for_match/waiting_in_room.rs rename to crates/app/junowen/src/signaling/waiting_for_match/waiting_in_room.rs index 4a00dd1..a8126b5 100644 --- a/junowen/src/signaling/waiting_for_match/waiting_in_room.rs +++ b/crates/app/junowen/src/signaling/waiting_for_match/waiting_in_room.rs @@ -6,8 +6,7 @@ use junowen_lib::connection::{DataChannel, PeerConnection, signaling::socket::Si use tokio::{ sync::{ mpsc::{self}, - oneshot::{self, error::TryRecvError}, - watch, + oneshot, watch, }, task::JoinHandle, time::sleep, @@ -17,7 +16,9 @@ use tracing::{Instrument, debug, debug_span, info}; use crate::{ TOKIO_RUNTIME, session::{ - battle::BattleSession, spectator::SpectatorSession, spectator_host::SpectatorHostSession, + battle::BattleSession, + spectator::{SpectatorRelayRoom, SpectatorSession}, + spectator_host::SpectatorHostSession, }, signaling::waiting_for_match::{ reserved_room_opponent_socket::SignalingServerReservedRoomOpponentSocket, @@ -25,7 +26,6 @@ use crate::{ shared_room_opponent_socket::SignalingServerSharedRoomOpponentSocket, tcp_signaling_guest_socket::TcpSignalingGuestSocket, tcp_signaling_host_socket::TcpSignalingHostSocket, - waiting_for_spectator::WaitingForPureP2pSpectator, }, }; @@ -50,7 +50,7 @@ pub type WaitingForOpponentInSharedRoom = WaitingInRoom; /// TCP シグナリングでの対戦の待機。`room_name` には接続先アドレスが入る pub type WaitingForOpponentOverTcpSignaling = WaitingInRoom; pub type WaitingForOpponentInReservedRoom = WaitingInRoom<(BattleSession, Option)>; -pub type WaitingForSpectatorInReservedRoom = WaitingInRoom<(SpectatorHostSession, RoomKey)>; +pub type WaitingForSpectatorInReservedRoom = WaitingInRoom; pub type WaitingForSpectatorHostInReservedRoom = WaitingInRoom; impl WaitingInRoom @@ -168,13 +168,8 @@ impl WaitingForOpponentInReservedRoom { let Ok((session, key)) = self.session_rx.try_recv() else { return Err(self); }; - let waiting = if let Some(key) = key { - let waiting = WaitingForSpectatorInReservedRoom::new(self.room_name.clone(), key.0); - WaitingForSpectator::ReservedRoom(waiting) - } else { - WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()) - }; - Ok((session, waiting)) + let room = key.map(|key| SpectatorRelayRoom::new(self.room_name.clone(), key.0)); + Ok((session, WaitingForSpectator::new(room))) } } @@ -186,23 +181,13 @@ impl WaitingForSpectatorInReservedRoom { origin, room_name, key, abort_rx, ) }, - |conn, dc, _host, socket| { - ( - SpectatorHostSession::new(conn, dc), - RoomKey(socket.into_key()), - ) - }, + |conn, dc, _host, _socket| SpectatorHostSession::new(conn, dc), room_name, ) } - pub fn try_session_and_waiting_for_spectator( - &mut self, - ) -> Result<(SpectatorHostSession, WaitingForSpectator), TryRecvError> { - let (session, key) = self.session_rx.try_recv()?; - let waiting = WaitingForSpectatorInReservedRoom::new(self.room_name.clone(), key.0); - let waiting = WaitingForSpectator::ReservedRoom(waiting); - Ok((session, waiting)) + pub fn try_recv_session(&mut self) -> Option { + self.session_rx.try_recv().ok() } } diff --git a/junowen/src/state.rs b/crates/app/junowen/src/state.rs similarity index 96% rename from junowen/src/state.rs rename to crates/app/junowen/src/state.rs index 36d3296..6276026 100644 --- a/junowen/src/state.rs +++ b/crates/app/junowen/src/state.rs @@ -2,6 +2,7 @@ mod battle_session; mod prepare; mod render_parts; mod session; +mod spectator_host; mod spectator_session; pub mod standby; @@ -104,6 +105,13 @@ impl State { } } + pub fn should_skip_present(&self) -> bool { + match self { + Self::Standby => false, + Self::Session(session) => session.should_skip_present(), + } + } + pub fn on_before_render_object( &self, title_menu_modifier: &TitleMenuModifier, diff --git a/junowen/src/state/battle_session.rs b/crates/app/junowen/src/state/battle_session.rs similarity index 95% rename from junowen/src/state/battle_session.rs rename to crates/app/junowen/src/state/battle_session.rs index e72ae37..5e2d44b 100644 --- a/junowen/src/state/battle_session.rs +++ b/crates/app/junowen/src/state/battle_session.rs @@ -1,7 +1,6 @@ mod battle_game; mod battle_select; mod in_session; -mod spectator_host; mod utils; use std::{ffi::c_void, sync::mpsc::RecvError}; @@ -20,9 +19,12 @@ use crate::{ use self::in_session::RenderingStatus; -use super::prepare::Prepare; +use super::{ + prepare::Prepare, + spectator_host::{SpectatorHostState, current_game_initial}, +}; -use {battle_game::BattleGame, battle_select::BattleSelect, spectator_host::SpectatorHostState}; +use {battle_game::BattleGame, battle_select::BattleSelect}; pub struct BattleSession { props: BattleSessionProps, @@ -79,6 +81,8 @@ impl BattleSession { let main_menu = th19.app().main_loop_tasks().find_main_menu().unwrap(); match main_menu.screen_id() { ScreenId::GameLoading => { + self.spectator_host_state + .start_game(current_game_initial(th19)); self.change_to_game_loading(); Some(Some(main_menu)) } @@ -100,6 +104,7 @@ impl BattleSession { if th19.round_frame().is_some() { return Some(None); } + self.spectator_host_state.end_game(); self.change_to_back_to_select(); Some(None) } diff --git a/junowen/src/state/battle_session/battle_game.rs b/crates/app/junowen/src/state/battle_session/battle_game.rs similarity index 78% rename from junowen/src/state/battle_session/battle_game.rs rename to crates/app/junowen/src/state/battle_session/battle_game.rs index 4502aa0..fd8ff07 100644 --- a/junowen/src/state/battle_session/battle_game.rs +++ b/crates/app/junowen/src/state/battle_session/battle_game.rs @@ -3,9 +3,13 @@ use std::sync::mpsc::RecvError; use anyhow::Result; use junowen_lib::{Th19, structs::input_devices::InputValue}; -use crate::{helper::inputed_number, session::battle::BattleSession}; +use crate::{ + helper::inputed_number, + session::battle::BattleSession, + state::spectator_host::{SpectatorHostState, current_round_initial}, +}; -use super::{spectator_host::SpectatorHostState, utils::init_round}; +use super::utils::init_round; pub struct BattleGame; @@ -42,7 +46,8 @@ impl BattleGame { .p2_input_mut() .set_current((p2 as u32).try_into().unwrap()); - spectator_host_state.update(false, None, th19, session, p1, p2); + spectator_host_state.update(false, None, th19, session); + spectator_host_state.send_inputs(p1, p2); Ok(()) } @@ -53,6 +58,8 @@ impl BattleGame { spectator_host_state: &mut SpectatorHostState, th19: &mut Th19, ) -> Result<(), RecvError> { - init_round(th19, session, spectator_host_state) + init_round(th19, session)?; + spectator_host_state.send_init_round(current_round_initial(th19)); + Ok(()) } } diff --git a/junowen/src/state/battle_session/battle_select.rs b/crates/app/junowen/src/state/battle_session/battle_select.rs similarity index 89% rename from junowen/src/state/battle_session/battle_select.rs rename to crates/app/junowen/src/state/battle_session/battle_select.rs index fb4afc9..0357d1c 100644 --- a/junowen/src/state/battle_session/battle_select.rs +++ b/crates/app/junowen/src/state/battle_session/battle_select.rs @@ -13,9 +13,10 @@ use tracing::trace; use crate::{ helper::{inputed_number, pushed_f1}, session::{MatchInitial, battle::BattleSession}, + state::spectator_host::SpectatorHostState, }; -use super::{spectator_host::SpectatorHostState, utils::init_round}; +use super::utils::init_round; fn init_match(th19: &mut Th19, battle_session: &mut BattleSession) -> Result<(), RecvError> { trace!("init_match"); @@ -59,13 +60,14 @@ impl BattleSelect { if session.match_initial().is_none() { init_match(th19, session)?; } - init_round(th19, session, spectator_host_state)?; + init_round(th19, session)?; } if main_menu.screen_id() == ScreenId::DifficultySelect { return Ok(()); } + let f1_pushed = pushed_f1(th19.input_devices()); let input_devices = th19.input_devices_mut(); let delay = if session.host() { inputed_number(input_devices) @@ -81,7 +83,7 @@ impl BattleSelect { .p2_input_mut() .set_current((p2 as u32).try_into().unwrap()); - spectator_host_state.update(false, Some(main_menu), th19, session, p1, p2); + spectator_host_state.update(f1_pushed, Some(main_menu), th19, session); Ok(()) } @@ -109,8 +111,8 @@ impl BattleSelect { let input = if p1 != 0 { p1 } else { p2 }; menu_input.set_current((input as u32).try_into().unwrap()); - let current_pushed = pushed_f1(input_devices); - spectator_host_state.update(current_pushed, Some(main_menu), th19, session, p1, p2); + let f1_pushed = pushed_f1(input_devices); + spectator_host_state.update(f1_pushed, Some(main_menu), th19, session); Ok(()) } diff --git a/crates/app/junowen/src/state/battle_session/in_session.rs b/crates/app/junowen/src/state/battle_session/in_session.rs new file mode 100644 index 0000000..e0d2dcd --- /dev/null +++ b/crates/app/junowen/src/state/battle_session/in_session.rs @@ -0,0 +1,32 @@ +use std::ffi::c_void; + +use junowen_lib::{Th19, structs::settings::GameSettings}; + +use crate::state::{ + render_parts::{render_footer, render_game_settings, render_names}, + spectator_host::{SpectatorHostState, status_text}, +}; + +pub struct RenderingStatus<'a> { + pub host: bool, + pub delay: u8, + pub p1_name: &'a str, + pub p2_name: &'a str, + pub game_settings: Option<&'a GameSettings>, + pub spectator_host_state: &'a SpectatorHostState, +} + +pub fn on_render_texts(th19: &Th19, text_renderer: &c_void, status: RenderingStatus) { + render_names(th19, text_renderer, status.p1_name, status.p2_name); + if let Some(game_settings) = status.game_settings { + render_game_settings(th19, text_renderer, game_settings); + } + + let (msg2_rear, msg2_front) = status_text(status.spectator_host_state); + + let delay_underline = if status.host { "_" } else { " " }; + let msg_front/* _ */= format!("Delay: {} {}", status.delay, msg2_front); + let msg_rear/* __ */= format!(" {} {}", delay_underline, msg2_rear); + + render_footer(th19, text_renderer, &msg_front, &msg_rear); +} diff --git a/junowen/src/state/battle_session/utils.rs b/crates/app/junowen/src/state/battle_session/utils.rs similarity index 74% rename from junowen/src/state/battle_session/utils.rs rename to crates/app/junowen/src/state/battle_session/utils.rs index 12bbfc1..0144152 100644 --- a/junowen/src/state/battle_session/utils.rs +++ b/crates/app/junowen/src/state/battle_session/utils.rs @@ -5,13 +5,7 @@ use junowen_lib::Th19; use crate::session::{RoundInitial, battle::BattleSession}; -use super::spectator_host::SpectatorHostState; - -pub fn init_round( - th19: &mut Th19, - battle_session: &mut BattleSession, - spectator_host_state: &mut SpectatorHostState, -) -> Result<(), RecvError> { +pub fn init_round(th19: &mut Th19, battle_session: &mut BattleSession) -> Result<(), RecvError> { if battle_session.host() { let opt = battle_session.init_round(Some(RoundInitial { seed1: th19.rand_seed1().unwrap(), @@ -27,6 +21,5 @@ pub fn init_round( th19.set_rand_seed3(init.seed3).unwrap(); th19.set_rand_seed4(init.seed4).unwrap(); } - spectator_host_state.send_init_round_if_connected(th19); Ok(()) } diff --git a/junowen/src/state/prepare.rs b/crates/app/junowen/src/state/prepare.rs similarity index 100% rename from junowen/src/state/prepare.rs rename to crates/app/junowen/src/state/prepare.rs diff --git a/junowen/src/state/render_parts.rs b/crates/app/junowen/src/state/render_parts.rs similarity index 100% rename from junowen/src/state/render_parts.rs rename to crates/app/junowen/src/state/render_parts.rs diff --git a/junowen/src/state/session.rs b/crates/app/junowen/src/state/session.rs similarity index 92% rename from junowen/src/state/session.rs rename to crates/app/junowen/src/state/session.rs index df0e3bd..7e2a0a8 100644 --- a/junowen/src/state/session.rs +++ b/crates/app/junowen/src/state/session.rs @@ -34,6 +34,13 @@ impl Session { } } + pub fn should_skip_present(&self) -> bool { + match self { + Self::BattleSession(_) => false, + Self::SpectatorSession(session_state) => session_state.should_skip_present(), + } + } + pub fn update_state(&mut self, th19: &Th19) -> Option> { match self { Self::BattleSession(session_state) => session_state.update_state(th19), diff --git a/crates/app/junowen/src/state/spectator_host.rs b/crates/app/junowen/src/state/spectator_host.rs new file mode 100644 index 0000000..4589048 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_host.rs @@ -0,0 +1,154 @@ +mod acceptance; +mod game_log; +mod initial; +mod status_text; + +use anyhow::Result; +use junowen_lib::{Th19, structs::app::MainMenu}; +use tracing::info; + +use crate::{ + session::{RoundInitial, spectator::GameInitial, spectator_host::SpectatorHostSession}, + signaling::waiting_for_match::WaitingForSpectator, +}; + +use self::{ + acceptance::Acceptance, + game_log::{GameLog, GameLogMessage}, +}; +pub use self::{ + initial::{SpectatorInitialSource, current_game_initial, current_round_initial}, + status_text::status_text, +}; + +/// 観戦者の管理 +/// +/// 観戦者は接続するとプレイヤー名と設定を受け取る。 +/// 選択画面で接続した場合は、次の試合の開始時にキャラクター・カード・乱数シードを受け取り、 +/// それに合わせて試合を始める。 +/// 試合中に接続した場合は、その試合の開始時の状態とそれまでの入力を受け取り、試合の最初から追いかける +/// +/// 対戦者は最初の観戦者のみを受け付け、その観戦者を観戦ホストに任命して以降の受け付けを委譲する。 +/// 観戦ホストは自分が処理した試合の進行を、同じ順序で配下の観戦者に中継する。 +/// 観戦ホストが離脱すると、配下の観戦者もすべて離脱する +pub struct SpectatorHostState { + acceptance: Acceptance, + /// 次の試合の開始を待っている観戦者 + standby_sessions: Vec, + /// 試合を観戦している観戦者 + sessions: Vec, + /// 試合中のみ `Some`。記録は1試合分で、試合ごとに作り直す + game_log: Option, +} + +impl SpectatorHostState { + /// 対戦者用。最初の観戦者を観戦ホストに任命する + pub fn new(waiting: WaitingForSpectator) -> Self { + Self::with_acceptance(Acceptance::WaitingForSpectatorHost(waiting)) + } + + /// 観戦ホスト用 + pub fn new_relay(waiting: WaitingForSpectator) -> Self { + Self::with_acceptance(Acceptance::Relaying(waiting)) + } + + fn with_acceptance(acceptance: Acceptance) -> Self { + Self { + acceptance, + standby_sessions: Vec::new(), + sessions: Vec::new(), + game_log: None, + } + } + + pub fn waiting(&self) -> Option<&WaitingForSpectator> { + self.acceptance.waiting() + } + + pub fn count_spectators(&self) -> usize { + self.standby_sessions.len() + self.sessions.len() + } + + /// 観戦者の接続を受け付ける + pub fn update( + &mut self, + pushed: bool, + main_menu: Option<&MainMenu>, + th19: &Th19, + initial_source: &impl SpectatorInitialSource, + ) { + let has_spectators = self.count_spectators() > 0; + let Some(session) = + self.acceptance + .try_recv_session(has_spectators, pushed, main_menu, th19) + else { + return; + }; + if let Err(err) = session.send_init_spectator(initial_source.spectator_initial(th19)) { + info!("initialize spectator failed: {:?}", err); + return; + } + let Some(game_log) = &self.game_log else { + info!("spectator joined. waiting for next game"); + self.standby_sessions.push(session); + return; + }; + if let Err(err) = game_log.send_to(&session) { + info!("initialize spectator failed: {:?}", err); + return; + } + info!( + "spectator joined during game. replaying {} messages", + game_log.message_count() + ); + self.sessions.push(session); + } + + fn broadcast( + &mut self, + msg: GameLogMessage, + send: impl Fn(&SpectatorHostSession) -> Result<()>, + ) { + self.sessions.retain(|session| { + if let Err(err) = send(session) { + info!("spectator host error: {:?}", err); + return false; + } + true + }); + if let Some(game_log) = &mut self.game_log { + game_log.push(msg); + } + } + + /// 試合開始時 (キャラクター選択画面から読み込み画面に移ったとき) に呼ぶ + pub fn start_game(&mut self, init: GameInitial) { + self.sessions.append(&mut self.standby_sessions); + self.sessions.retain(|session| { + if let Err(err) = session.send_init_game(init.clone()) { + info!("spectator host error: {:?}", err); + return false; + } + true + }); + self.game_log = Some(GameLog::new(init)); + } + + /// 試合終了時 (試合から選択画面に戻るとき) に呼ぶ + pub fn end_game(&mut self) { + self.game_log = None; + } + + pub fn send_init_round(&mut self, round_initial: RoundInitial) { + self.broadcast( + GameLogMessage::InitRound(round_initial.clone()), + |session| session.send_init_round(round_initial.clone()), + ); + } + + pub fn send_inputs(&mut self, p1_input: u16, p2_input: u16) { + self.broadcast(GameLogMessage::Inputs(p1_input, p2_input), |session| { + session.send_inputs(p1_input, p2_input) + }); + } +} diff --git a/crates/app/junowen/src/state/spectator_host/acceptance.rs b/crates/app/junowen/src/state/spectator_host/acceptance.rs new file mode 100644 index 0000000..ce3fd1a --- /dev/null +++ b/crates/app/junowen/src/state/spectator_host/acceptance.rs @@ -0,0 +1,74 @@ +use junowen_lib::{Th19, structs::app::MainMenu}; +use tracing::info; + +use crate::{ + session::{spectator::SpectatorRelayRoom, spectator_host::SpectatorHostSession}, + signaling::waiting_for_match::WaitingForSpectator, +}; + +/// 観戦者の受け付け状態 +pub enum Acceptance { + /// 対戦者: 最初の観戦者を待ち受け、観戦ホストに任命する + WaitingForSpectatorHost(WaitingForSpectator), + /// 対戦者: 観戦ホストに受け付けを委譲している + Delegated, + /// 観戦ホスト: 観戦者を待ち受け、受け取った試合の進行を中継する + Relaying(WaitingForSpectator), +} + +/// 専有ルームの待ち受けは一度しか受信できないため、受信後に作り直す +fn rearm(waiting: &mut WaitingForSpectator, room: Option) { + if room.is_some() { + *waiting = WaitingForSpectator::new(room); + } +} + +impl Acceptance { + pub fn waiting(&self) -> Option<&WaitingForSpectator> { + match self { + Self::WaitingForSpectatorHost(waiting) | Self::Relaying(waiting) => Some(waiting), + Self::Delegated => None, + } + } + + /// 観戦者の接続を受け付ける + /// + /// 対戦者が最初に受け付けた観戦者は観戦ホストに任命され、以降の受け付けは観戦ホストに委譲される。 + /// 観戦ホストが離脱して配下の観戦者もいなくなった場合は、Pure P2P で受け付けを再開する + pub fn try_recv_session( + &mut self, + has_spectators: bool, + pushed: bool, + main_menu: Option<&MainMenu>, + th19: &Th19, + ) -> Option { + let (waiting, delegates) = match &mut *self { + Self::WaitingForSpectatorHost(waiting) => (waiting, true), + Self::Relaying(waiting) => (waiting, false), + Self::Delegated => { + if !has_spectators { + // 専有ルームは観戦ホストの離脱時に削除されるため、Pure P2P で待ち受ける + info!("spectator host left. resume waiting for spectator"); + *self = Self::WaitingForSpectatorHost(WaitingForSpectator::new(None)); + } + return None; + } + }; + let session = waiting.try_recv_session(pushed, main_menu, th19)?; + let room = waiting.room().cloned(); + if !delegates { + rearm(waiting, room); + return Some(session); + } + if let Err(err) = session.send_delegate_spectator_host(room.clone()) { + info!("delegate spectator host failed: {:?}", err); + rearm(waiting, room); + return None; + } + // 専有ルームの待ち受けは観戦ホストが引き継ぐため、ここでは作り直さない + // (作り直した待ち受けを破棄すると部屋が削除されてしまう) + info!("delegated spectator host"); + *self = Self::Delegated; + Some(session) + } +} diff --git a/crates/app/junowen/src/state/spectator_host/game_log.rs b/crates/app/junowen/src/state/spectator_host/game_log.rs new file mode 100644 index 0000000..c0c0e12 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_host/game_log.rs @@ -0,0 +1,44 @@ +use anyhow::Result; + +use crate::session::{RoundInitial, spectator::GameInitial, spectator_host::SpectatorHostSession}; + +/// 進行中の試合の記録。試合の途中で接続した観戦者は、これを受け取って試合の最初から早送りで追いかける +pub struct GameLog { + init: GameInitial, + messages: Vec, +} + +pub enum GameLogMessage { + InitRound(RoundInitial), + Inputs(u16, u16), +} + +impl GameLog { + pub fn new(init: GameInitial) -> Self { + Self { + init, + messages: Vec::new(), + } + } + + pub fn message_count(&self) -> usize { + self.messages.len() + } + + pub fn push(&mut self, msg: GameLogMessage) { + self.messages.push(msg); + } + + pub fn send_to(&self, session: &SpectatorHostSession) -> Result<()> { + session.send_init_game(self.init.clone())?; + for msg in &self.messages { + match msg { + GameLogMessage::InitRound(round_initial) => { + session.send_init_round(round_initial.clone())? + } + GameLogMessage::Inputs(p1, p2) => session.send_inputs(*p1, *p2)?, + } + } + Ok(()) + } +} diff --git a/crates/app/junowen/src/state/spectator_host/initial.rs b/crates/app/junowen/src/state/spectator_host/initial.rs new file mode 100644 index 0000000..ccd43cc --- /dev/null +++ b/crates/app/junowen/src/state/spectator_host/initial.rs @@ -0,0 +1,57 @@ +use junowen_lib::{Th19, structs::selection::Player}; + +use crate::session::{ + RoundInitial, + battle::BattleSession, + spectator::{GameInitial, PlayerGameInitial, SpectatorInitial}, +}; + +pub fn current_round_initial(th19: &Th19) -> RoundInitial { + RoundInitial { + seed1: th19.rand_seed1().unwrap(), + seed2: th19.rand_seed2().unwrap(), + seed3: th19.rand_seed3().unwrap(), + seed4: th19.rand_seed4().unwrap(), + } +} + +pub fn current_game_initial(th19: &Th19) -> GameInitial { + let selection = th19.selection(); + let player = + |player: &Player| PlayerGameInitial::new(player.character as u8, player.card as u8); + GameInitial::new( + selection.difficulty as u8, + player(selection.p1()), + player(selection.p2()), + current_round_initial(th19), + ) +} + +/// 観戦者が接続した直後に、プレイヤー名と設定を観戦者へ渡す +pub trait SpectatorInitialSource { + fn spectator_initial(&self, th19: &Th19) -> SpectatorInitial; +} + +impl SpectatorInitialSource for BattleSession { + fn spectator_initial(&self, th19: &Th19) -> SpectatorInitial { + let local_player_name = th19.vs_mode().player_name().to_string(); + let remote_player_name = self.remote_player_name().clone(); + let (p1_name, p2_name) = if self.host() { + (local_player_name, remote_player_name) + } else { + (remote_player_name, local_player_name) + }; + SpectatorInitial::new( + p1_name, + p2_name, + self.match_initial().as_ref().unwrap().game_settings.clone(), + ) + } +} + +/// 観戦ホストは、対戦者から受け取った情報をそのまま配下の観戦者に送る +impl SpectatorInitialSource for SpectatorInitial { + fn spectator_initial(&self, _th19: &Th19) -> SpectatorInitial { + self.clone() + } +} diff --git a/crates/app/junowen/src/state/spectator_host/status_text.rs b/crates/app/junowen/src/state/spectator_host/status_text.rs new file mode 100644 index 0000000..059d2ff --- /dev/null +++ b/crates/app/junowen/src/state/spectator_host/status_text.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; + +use crate::signaling::waiting_for_match::{WaitingForPureP2pSpectator, WaitingForSpectator}; + +use super::SpectatorHostState; + +/// 観戦者の受け付け状況を表示するためのテキスト (下線, 本文) を返す +pub fn status_text(state: &SpectatorHostState) -> (&'static str, Cow<'static, str>) { + let spectators = state.count_spectators(); + if spectators > 0 { + return ( + " ", + Cow::Owned(format!("Spectator(s): {}", spectators)), + ); + } + let Some(WaitingForSpectator::PureP2p(waiting)) = state.waiting() else { + return ("", "".into()); + }; + match waiting { + WaitingForPureP2pSpectator::Standby { ready: false, .. } + | WaitingForPureP2pSpectator::SignalingCodeRecved { ready: false, .. } + | WaitingForPureP2pSpectator::SignalingCodeSent { ready: false, .. } => ("", "".into()), + WaitingForPureP2pSpectator::Standby { .. } => ( + " __ ", + "(Press F1 to accept spectator from clipboard)".into(), + ), + WaitingForPureP2pSpectator::SignalingCodeRecved { .. } => ( + " ", + "(Generating signaling code...)".into(), + ), + WaitingForPureP2pSpectator::SignalingCodeSent { .. } => ( + " ", + "(Your signaling code has been copied to the clipboard)".into(), + ), + } +} diff --git a/junowen/src/state/spectator_session.rs b/crates/app/junowen/src/state/spectator_session.rs similarity index 50% rename from junowen/src/state/spectator_session.rs rename to crates/app/junowen/src/state/spectator_session.rs index 42b0c75..f414482 100644 --- a/junowen/src/state/spectator_session.rs +++ b/crates/app/junowen/src/state/spectator_session.rs @@ -1,6 +1,8 @@ +mod game_initial; mod in_session; +mod relay; mod spectator_game; -mod spectator_select; +mod spectator_standby; use std::{ffi::c_void, sync::mpsc::RecvError}; @@ -13,22 +15,35 @@ use junowen_lib::{ input_devices::InputFlags, }, }; +use tracing::warn; -use crate::session::spectator::SpectatorSession as SpectatorSessionProps; +use crate::{ + helper::pushed_escape, + present_skipper::PresentSkipper, + session::{RoundInitial, spectator::SpectatorSession as SpectatorSessionProps}, +}; use super::prepare::Prepare; -use {spectator_game::SpectatorGame, spectator_select::SpectatorSelect}; +use { + game_initial::{set_rand_seeds, verify_game_initial}, + relay::Relay, + spectator_game::{SpectatorGame, update_no_wait_for_catch_up}, + spectator_standby::SpectatorStandby, +}; pub struct SpectatorSession { props: SpectatorSessionProps, + relay: Relay, + present_skipper: PresentSkipper, state: SpectatorSessionState, } enum SpectatorSessionState { Prepare(Prepare), - Select(SpectatorSelect), - GameLoading, + Standby(SpectatorStandby), + /// 最初のフレームで乱数シードを設定する + GameLoading(Option), Game(SpectatorGame), BackToSelect, } @@ -37,24 +52,36 @@ impl SpectatorSession { pub fn prepare(props: SpectatorSessionProps) -> Self { Self { props, + relay: Relay::default(), + present_skipper: PresentSkipper::default(), state: SpectatorSessionState::Prepare(Prepare::new()), } } + pub fn should_skip_present(&self) -> bool { + self.present_skipper.should_skip_present() + } + pub fn game_settings(&self) -> Option<&GameSettings> { self.props.spectator_initial().map(|x| x.game_settings()) } - pub fn change_to_select(&mut self) { - self.state = SpectatorSessionState::Select(SpectatorSelect::new()); + pub fn change_to_prepare(&mut self) { + self.relay.end_game(); + self.state = SpectatorSessionState::Prepare(Prepare::new()); + } + pub fn change_to_standby(&mut self, first_time: bool) { + self.props.discard_round_initial(); + self.state = SpectatorSessionState::Standby(SpectatorStandby::new(first_time)); } - pub fn change_to_game_loading(&mut self) { - self.state = SpectatorSessionState::GameLoading; + pub fn change_to_game_loading(&mut self, round_initial: Option) { + self.state = SpectatorSessionState::GameLoading(round_initial); } pub fn change_to_game(&mut self) { self.state = SpectatorSessionState::Game(SpectatorGame); } pub fn change_to_back_to_select(&mut self) { + self.relay.end_game(); self.state = SpectatorSessionState::BackToSelect; } @@ -65,28 +92,34 @@ impl SpectatorSession { return Some(None); }; if prepare.update_state(main_menu, th19) { - self.change_to_select(); + self.change_to_standby(true); } Some(Some(main_menu)) } - SpectatorSessionState::Select { .. } => { - let main_menu = th19.app().main_loop_tasks().find_main_menu().unwrap(); + SpectatorSessionState::Standby(standby) => { + // 自動で入力した PAUSE は観戦者の操作として扱わない + let pause = th19.input_devices().p1_input().current().0 & InputFlags::PAUSE != None; + if pushed_escape(th19.input_devices()) || (pause && !standby.pause_injected()) { + return None; + } + let main_menu = th19.app().main_loop_tasks().find_main_menu()?; match main_menu.screen_id() { ScreenId::PlayerMatchupSelect => None, - ScreenId::CharacterSelect => { - if th19.input_devices().p1_input().current().0 & InputFlags::PAUSE != None { + ScreenId::GameLoading => { + let init = standby.take_game_initial(); + if let Some(init) = &init + && !verify_game_initial(th19, init) + { + // ずれた試合を見せ続けないよう、観戦を終了する return None; } - Some(Some(main_menu)) - } - ScreenId::GameLoading => { - self.change_to_game_loading(); + self.change_to_game_loading(init.map(|x| x.round_initial().clone())); Some(Some(main_menu)) } _ => Some(Some(main_menu)), } } - SpectatorSessionState::GameLoading { .. } => { + SpectatorSessionState::GameLoading(_) => { let Some(round_frame) = th19.round_frame() else { return Some(None); }; @@ -100,20 +133,26 @@ impl SpectatorSession { if th19.input_devices().p1_input().current().0 & InputFlags::PAUSE != None { return None; } + if self.props.has_next_game_initial() { + // ホストの次の試合が始まったので、今の試合を打ち切って次の試合から同期し直す + warn!("spectator game is out of sync. resync from next game"); + self.change_to_prepare(); + return Some(None); + } if th19.round_frame().is_some() { return Some(None); } self.change_to_back_to_select(); Some(None) } - SpectatorSessionState::BackToSelect { .. } => { + SpectatorSessionState::BackToSelect => { let Some(main_menu) = th19.app().main_loop_tasks().find_main_menu() else { return Some(None); }; if main_menu.screen_id() != ScreenId::CharacterSelect { return Some(Some(main_menu)); } - self.change_to_select(); + self.change_to_standby(false); Some(Some(main_menu)) } } @@ -124,26 +163,34 @@ impl SpectatorSession { menu: Option<&MainMenu>, th19: &mut Th19, ) -> Result<(), RecvError> { + self.relay.start_if_delegated(&mut self.props); match &mut self.state { SpectatorSessionState::Prepare(prepare) => prepare.update_th19_on_input_players(th19), - SpectatorSessionState::Select(select) => { - select.update_th19_on_input_players(&mut self.props, menu.unwrap(), th19)? + SpectatorSessionState::Standby(standby) => { + standby.update_th19_on_input_players(&mut self.props, menu.unwrap(), th19)?; + self.relay.accept_spectators(&self.props, menu, th19); } - SpectatorSessionState::GameLoading { .. } => { - if th19.no_wait() { - th19.set_no_wait(false); + SpectatorSessionState::GameLoading(round_initial) => { + if let Some(round_initial) = round_initial.take() { + set_rand_seeds(th19, &round_initial); } + update_no_wait_for_catch_up(&mut self.props, &self.present_skipper, th19); } - SpectatorSessionState::Game(game) => game.update_th19(&mut self.props, th19)?, - SpectatorSessionState::BackToSelect { .. } => {} + SpectatorSessionState::Game(game) => { + game.update_th19(&mut self.props, &self.present_skipper, th19)?; + self.relay.accept_spectators(&self.props, None, th19); + } + SpectatorSessionState::BackToSelect => {} } + self.relay.forward(&mut self.props); Ok(()) } pub fn on_input_menu(&mut self, th19: &mut Th19) -> Result { + self.relay.start_if_delegated(&mut self.props); match &mut self.state { SpectatorSessionState::Prepare(prepare) => prepare.update_th19_on_input_menu(th19), - SpectatorSessionState::Select(select) => { + SpectatorSessionState::Standby(standby) => { let main_menu = th19 .app_mut() .main_loop_tasks_mut() @@ -154,17 +201,27 @@ impl SpectatorSession { { return Ok(false); } - select.update_th19_on_input_menu(&mut self.props, main_menu, th19)?; + standby.update_th19_on_input_menu(&mut self.props, main_menu, th19)?; + self.relay + .accept_spectators(&self.props, Some(&*main_menu), th19); } - SpectatorSessionState::GameLoading { .. } => {} + SpectatorSessionState::GameLoading(_) => {} SpectatorSessionState::Game { .. } => {} - SpectatorSessionState::BackToSelect { .. } => {} + SpectatorSessionState::BackToSelect => {} } + self.relay.forward(&mut self.props); Ok(true) } pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { + let waiting_for_host = matches!( + &self.state, + SpectatorSessionState::Standby(standby) if standby.is_waiting_for_host() + ); let Some(initial) = self.props.spectator_initial() else { + if waiting_for_host { + in_session::on_render_texts_waiting_for_host(th19, text_renderer); + } return; }; in_session::on_render_texts_spectator( @@ -172,6 +229,8 @@ impl SpectatorSession { text_renderer, initial.p1_name(), initial.p2_name(), + waiting_for_host, + self.relay.state(), ); } @@ -179,6 +238,8 @@ impl SpectatorSession { let SpectatorSessionState::Game(game) = &mut self.state else { return Ok(()); }; - game.on_round_over(&mut self.props, th19) + game.on_round_over(&mut self.props, th19)?; + self.relay.forward(&mut self.props); + Ok(()) } } diff --git a/crates/app/junowen/src/state/spectator_session/game_initial.rs b/crates/app/junowen/src/state/spectator_session/game_initial.rs new file mode 100644 index 0000000..6715ba8 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_session/game_initial.rs @@ -0,0 +1,38 @@ +use junowen_lib::Th19; +use tracing::warn; + +use crate::session::{RoundInitial, spectator::GameInitial}; + +pub fn set_rand_seeds(th19: &mut Th19, round_initial: &RoundInitial) { + th19.set_rand_seed1(round_initial.seed1).unwrap(); + th19.set_rand_seed2(round_initial.seed2).unwrap(); + th19.set_rand_seed3(round_initial.seed3).unwrap(); + th19.set_rand_seed4(round_initial.seed4).unwrap(); +} + +/// 観戦者の試合開始時の状態がホストと一致しているか確認する +pub fn verify_game_initial(th19: &Th19, init: &GameInitial) -> bool { + let selection = th19.selection(); + let actual = ( + selection.difficulty as u8, + selection.p1().character as u8, + selection.p1().card as u8, + selection.p2().character as u8, + selection.p2().card as u8, + ); + let expected = ( + init.difficulty(), + init.p1().character(), + init.p1().card(), + init.p2().character(), + init.p2().card(), + ); + if actual != expected { + warn!( + "game initial mismatch. expected={:?}, actual={:?}", + expected, actual + ); + return false; + } + true +} diff --git a/crates/app/junowen/src/state/spectator_session/in_session.rs b/crates/app/junowen/src/state/spectator_session/in_session.rs new file mode 100644 index 0000000..94a5da7 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_session/in_session.rs @@ -0,0 +1,32 @@ +use std::ffi::c_void; + +use junowen_lib::Th19; + +use crate::state::{ + render_parts::{render_footer, render_names}, + spectator_host::{SpectatorHostState, status_text}, +}; + +pub fn on_render_texts_spectator( + th19: &Th19, + text_renderer: &c_void, + p1_name: &str, + p2_name: &str, + waiting_for_next_game: bool, + relay: Option<&SpectatorHostState>, +) { + render_names(th19, text_renderer, p1_name, p2_name); + let spectating = if waiting_for_next_game { + "(Spectating: waiting for next game...)" + } else { + "(Spectating)" + }; + let (msg2_rear, msg2_front) = relay.map(status_text).unwrap_or_default(); + let msg_front/* _ */= format!("{} {}", spectating, msg2_front); + let msg_rear/* __ */= format!("{} {}", " ".repeat(spectating.len()), msg2_rear); + render_footer(th19, text_renderer, &msg_front, &msg_rear); +} + +pub fn on_render_texts_waiting_for_host(th19: &Th19, text_renderer: &c_void) { + render_footer(th19, text_renderer, "(Waiting for host...)", ""); +} diff --git a/crates/app/junowen/src/state/spectator_session/relay.rs b/crates/app/junowen/src/state/spectator_session/relay.rs new file mode 100644 index 0000000..9739528 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_session/relay.rs @@ -0,0 +1,61 @@ +use junowen_lib::{Th19, structs::app::MainMenu}; + +use crate::{ + helper::pushed_f1, + session::spectator::{SpectatorSession as SpectatorSessionProps, SpectatorSessionMessage}, + signaling::waiting_for_match::WaitingForSpectator, + state::spectator_host::SpectatorHostState, +}; + +/// 観戦ホストに任命された場合に、配下の観戦者の受け付けと試合の進行の中継を行う +#[derive(Default)] +pub struct Relay(Option); + +impl Relay { + pub fn state(&self) -> Option<&SpectatorHostState> { + self.0.as_ref() + } + + pub fn start_if_delegated(&mut self, props: &mut SpectatorSessionProps) { + if let Some(delegation) = props.relay_outbox_mut().take_delegation() { + let waiting = WaitingForSpectator::new(delegation.into_room()); + self.0 = Some(SpectatorHostState::new_relay(waiting)); + } + } + + /// 配下の観戦者の接続を受け付ける + pub fn accept_spectators( + &mut self, + props: &SpectatorSessionProps, + main_menu: Option<&MainMenu>, + th19: &Th19, + ) { + let (Some(state), Some(initial)) = (&mut self.0, props.spectator_initial()) else { + return; + }; + state.update(pushed_f1(th19.input_devices()), main_menu, th19, initial); + } + + /// 処理したメッセージを、処理した順に配下の観戦者へ送る + pub fn forward(&mut self, props: &mut SpectatorSessionProps) { + let messages = props.relay_outbox_mut().take_messages(); + let Some(state) = &mut self.0 else { + return; + }; + for msg in messages { + match msg { + SpectatorSessionMessage::InitGame(init) => state.start_game(init), + SpectatorSessionMessage::InitRound(init) => state.send_init_round(init), + SpectatorSessionMessage::Inputs(p1, p2) => state.send_inputs(p1, p2), + SpectatorSessionMessage::InitSpectator(_) + | SpectatorSessionMessage::DelegateSpectatorHost(_) => {} + } + } + } + + pub fn end_game(&mut self) { + if let Some(state) = &mut self.0 { + state.end_game(); + } + } +} diff --git a/crates/app/junowen/src/state/spectator_session/spectator_game.rs b/crates/app/junowen/src/state/spectator_session/spectator_game.rs new file mode 100644 index 0000000..5358eb8 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_session/spectator_game.rs @@ -0,0 +1,77 @@ +use std::sync::mpsc::RecvError; + +use anyhow::Result; +use junowen_lib::{Th19, structs::input_devices::InputValue}; + +use crate::{present_skipper::PresentSkipper, session::spectator::SpectatorSession}; + +use super::game_initial::set_rand_seeds; + +/// 未処理の入力がこのフレーム数を超えている場合、早送りして追いつく +const CATCH_UP_THRESHOLD: usize = 30; + +/// 溜まった入力がある場合は早送りしてホストに追いつく +/// +/// 観戦者の試合開始はホストより遅れる。試合中に観戦を始めた場合は、試合の最初から追いかける。 +/// NOTE: 試合中に no wait を切り替えても反映されない可能性があるため、読み込み画面でも判定する。 +/// 早送り中も入力が届くまで待つので、追いついた後はホストの速度で進む +pub fn update_no_wait_for_catch_up( + session: &mut SpectatorSession, + present_skipper: &PresentSkipper, + th19: &mut Th19, +) { + let buffered_len = session.poll_buffered_len(); + let no_wait = buffered_len > CATCH_UP_THRESHOLD; + let before = th19.no_wait(); + if before != no_wait { + th19.set_no_wait(no_wait); + } + if no_wait { + // 試合中は no wait が効かないため、表示を間引いて垂直同期の待ちをなくす + present_skipper.request_skip(); + } +} + +pub struct SpectatorGame; + +impl SpectatorGame { + pub fn update_th19( + &mut self, + session: &mut SpectatorSession, + present_skipper: &PresentSkipper, + th19: &mut Th19, + ) -> Result<(), RecvError> { + // -1フレーム目、0フレーム目は複数回呼ばれ、回数が不定なのでスキップする + if th19.round_frame().unwrap().frame < 1 { + let input_devices = th19.input_devices_mut(); + input_devices + .p1_input_mut() + .set_current(InputValue::empty()); + input_devices + .p2_input_mut() + .set_current(InputValue::empty()); + return Ok(()); + } + update_no_wait_for_catch_up(session, present_skipper, th19); + let input_devices = th19.input_devices_mut(); + let (p1, p2) = session.dequeue_inputs()?; + input_devices + .p1_input_mut() + .set_current((p1 as u32).try_into().unwrap()); + input_devices + .p2_input_mut() + .set_current((p2 as u32).try_into().unwrap()); + Ok(()) + } + + pub fn on_round_over( + &mut self, + session: &mut SpectatorSession, + th19: &mut Th19, + ) -> Result<(), RecvError> { + if let Some(init) = session.dequeue_init_round()? { + set_rand_seeds(th19, &init); + } + Ok(()) + } +} diff --git a/crates/app/junowen/src/state/spectator_session/spectator_standby.rs b/crates/app/junowen/src/state/spectator_session/spectator_standby.rs new file mode 100644 index 0000000..abba666 --- /dev/null +++ b/crates/app/junowen/src/state/spectator_session/spectator_standby.rs @@ -0,0 +1,198 @@ +use std::sync::mpsc::RecvError; + +use anyhow::Result; +use junowen_lib::{ + Th19, + structs::{ + app::{MainMenu, ScreenId}, + input_devices::{InputFlags, InputValue}, + }, + th19_helpers::{reset_cursors, shot_repeatedly}, +}; +use tracing::{trace, warn}; + +use crate::session::spectator::{GameInitial, SpectatorSession}; + +fn clear_player_inputs(th19: &mut Th19) { + let input_devices = th19.input_devices_mut(); + input_devices + .p1_input_mut() + .set_current(InputValue::empty()); + input_devices + .p2_input_mut() + .set_current(InputValue::empty()); +} + +/// キャラクター選択画面のカーソルとカードが試合開始時の状態と一致しているか +fn matches_character_select(main_menu: &MainMenu, th19: &Th19, init: &GameInitial) -> bool { + let menu = main_menu.menu(); + let selection = th19.selection(); + menu.p1_cursor().cursor == init.p1().character() as u32 + && menu.p2_cursor().cursor == init.p2().character() as u32 + && selection.p1().card == init.p1().card() as u32 + && selection.p2().card == init.p2().card() as u32 +} + +fn set_character_select(th19: &mut Th19, init: &GameInitial) { + let selection = th19.selection_mut(); + selection.p1_mut().card = init.p1().card() as u32; + selection.p2_mut().card = init.p2().card() as u32; + let menu = th19 + .app_mut() + .main_loop_tasks_mut() + .find_main_menu_mut() + .unwrap() + .menu_mut(); + let p1_cursor = menu.p1_cursor_mut(); + p1_cursor.cursor = init.p1().character() as u32; + p1_cursor.prev_cursor = p1_cursor.cursor; + let p2_cursor = menu.p2_cursor_mut(); + p2_cursor.cursor = init.p2().character() as u32; + p2_cursor.prev_cursor = p2_cursor.cursor; +} + +/// 試合開始時の状態を受け取ってから、試合開始までに許容するフレーム数 +/// +/// 状態を合わせられない場合 (カーソルの値とキャラクター番号の対応が想定と違う場合など) に諦める +const MAX_SYNC_FRAMES: u32 = 60 * 60; + +/// 難易度選択画面またはキャラクター選択画面で、ホストの試合開始を待つ +/// +/// 試合開始時の状態を受け取ったら、難易度・キャラクター・カードを合わせ、 +/// 一致していることを確認してから両プレイヤーを決定させる +pub struct SpectatorStandby { + cursors_reset: bool, + game_initial: Option, + sync_frames: u32, + /// 直前のフレームで PAUSE を自動で入力したか。観戦者自身の PAUSE と区別するために使う + pause_injected: bool, +} + +impl SpectatorStandby { + /// `first_time` は観戦を開始した直後かどうか + pub fn new(first_time: bool) -> Self { + Self { + cursors_reset: !first_time, + game_initial: None, + sync_frames: 0, + pause_injected: false, + } + } + + pub fn pause_injected(&self) -> bool { + self.pause_injected + } + + pub fn is_waiting_for_host(&self) -> bool { + self.game_initial.is_none() + } + + /// 試合開始時の状態を返す。読み込み画面に移ったときに取り出す + pub fn take_game_initial(&mut self) -> Option { + self.game_initial.take() + } + + /// 試合開始時の状態を受信済みなら `true` を返す + fn recv(&mut self, session: &mut SpectatorSession, th19: &mut Th19) -> Result { + if self.game_initial.is_some() { + return Ok(true); + } + if session.spectator_initial().is_none() && !session.try_recv_init_spectator()? { + return Ok(false); + } + self.game_initial = session.try_recv_init_game()?; + if let Some(init) = &self.game_initial { + trace!("game_initial: {:?}", init); + // 待機中は負荷を抑えるため通常速度にしていたので、追いつくために早送りする + th19.set_no_wait(true); + return Ok(true); + } + if th19.no_wait() { + th19.set_no_wait(false); + } + Ok(false) + } + + pub fn update_th19_on_input_players( + &mut self, + session: &mut SpectatorSession, + main_menu: &MainMenu, + th19: &mut Th19, + ) -> Result<(), RecvError> { + if !self.cursors_reset { + self.cursors_reset = true; + reset_cursors(th19); + } + self.pause_injected = false; + if !self.recv(session, th19)? { + clear_player_inputs(th19); + return Ok(()); + } + self.sync_frames += 1; + if self.sync_frames > MAX_SYNC_FRAMES { + warn!("failed to reproduce game initial"); + // NOTE: 呼び出し側はエラーでセッションを中断するため、`RecvError` を中断の合図として流用している + return Err(RecvError); + } + let init = self.game_initial.clone().unwrap(); + if main_menu.screen_id() != ScreenId::CharacterSelect { + clear_player_inputs(th19); + return Ok(()); + } + let input_devices = th19.input_devices(); + let (prev_p1, prev_p2) = ( + input_devices.p1_input().prev(), + input_devices.p2_input().prev(), + ); + let (p1, p2) = if th19.selection().difficulty as u8 != init.difficulty() { + // 難易度が異なるので難易度選択画面に戻る + let pause: InputValue = InputFlags::PAUSE.into(); + let p1 = if prev_p1 == pause { + InputValue::empty() + } else { + self.pause_injected = true; + pause + }; + (p1, InputValue::empty()) + } else if matches_character_select(main_menu, th19, &init) { + // 一致していることを確認できたので決定する。準備完了後もカードは変更できるため合わせ続ける + (shot_repeatedly(prev_p1), shot_repeatedly(prev_p2)) + } else { + set_character_select(th19, &init); + (InputValue::empty(), InputValue::empty()) + }; + let input_devices = th19.input_devices_mut(); + input_devices.p1_input_mut().set_current(p1); + input_devices.p2_input_mut().set_current(p2); + Ok(()) + } + + pub fn update_th19_on_input_menu( + &mut self, + session: &mut SpectatorSession, + main_menu: &mut MainMenu, + th19: &mut Th19, + ) -> Result<(), RecvError> { + if main_menu.screen_id() != ScreenId::DifficultySelect { + return Ok(()); + } + if !self.recv(session, th19)? { + th19.menu_input_mut().set_current(InputValue::empty()); + return Ok(()); + } + let init = self.game_initial.clone().unwrap(); + let menu = main_menu.menu_mut(); + if menu.cursor() != init.difficulty() as u32 { + menu.set_cursor(init.difficulty() as u32); + th19.menu_input_mut().set_current(InputValue::empty()); + return Ok(()); + } + // キャラクター選択画面の初期カーソル位置を合わせてから決定する + let selection = th19.selection_mut(); + selection.p1_mut().character = init.p1().character() as u32; + selection.p2_mut().character = init.p2().character() as u32; + let prev = th19.menu_input().prev(); + th19.menu_input_mut().set_current(shot_repeatedly(prev)); + Ok(()) + } +} diff --git a/junowen/src/state/standby.rs b/crates/app/junowen/src/state/standby.rs similarity index 100% rename from junowen/src/state/standby.rs rename to crates/app/junowen/src/state/standby.rs diff --git a/junowen/src/tracing_helper.rs b/crates/app/junowen/src/tracing_helper.rs similarity index 100% rename from junowen/src/tracing_helper.rs rename to crates/app/junowen/src/tracing_helper.rs diff --git a/th19loader/Cargo.toml b/crates/app/th19loader/Cargo.toml similarity index 89% rename from th19loader/Cargo.toml rename to crates/app/th19loader/Cargo.toml index 110ae2b..49cf1f0 100644 --- a/th19loader/Cargo.toml +++ b/crates/app/th19loader/Cargo.toml @@ -15,7 +15,7 @@ winres = "0.1" [dependencies] anyhow.workspace = true -junowen-lib = { path = "../junowen-lib" } +junowen-lib = { path = "../../features/junowen-lib" } windows.workspace = true [package.metadata.winres] diff --git a/th19loader/build.rs b/crates/app/th19loader/build.rs similarity index 100% rename from th19loader/build.rs rename to crates/app/th19loader/build.rs diff --git a/th19loader/src/lib.rs b/crates/app/th19loader/src/lib.rs similarity index 100% rename from th19loader/src/lib.rs rename to crates/app/th19loader/src/lib.rs diff --git a/crates/features/junowen-direct3d/Cargo.toml b/crates/features/junowen-direct3d/Cargo.toml new file mode 100644 index 0000000..f19afbf --- /dev/null +++ b/crates/features/junowen-direct3d/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "junowen-direct3d" +edition = "2024" +version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +windows.workspace = true +windows-core.workspace = true diff --git a/junowen/src/custom_direct_3d/custom_direct_3d_9.rs b/crates/features/junowen-direct3d/src/custom_direct_3d_9.rs similarity index 100% rename from junowen/src/custom_direct_3d/custom_direct_3d_9.rs rename to crates/features/junowen-direct3d/src/custom_direct_3d_9.rs diff --git a/junowen/src/custom_direct_3d/custom_direct_3d_device_9.rs b/crates/features/junowen-direct3d/src/custom_direct_3d_device_9.rs similarity index 99% rename from junowen/src/custom_direct_3d/custom_direct_3d_device_9.rs rename to crates/features/junowen-direct3d/src/custom_direct_3d_device_9.rs index d678998..fd75c3c 100644 --- a/junowen/src/custom_direct_3d/custom_direct_3d_device_9.rs +++ b/crates/features/junowen-direct3d/src/custom_direct_3d_device_9.rs @@ -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 diff --git a/junowen/src/custom_direct_3d/direct_3d_device_event_listener.rs b/crates/features/junowen-direct3d/src/direct_3d_device_event_listener.rs similarity index 62% rename from junowen/src/custom_direct_3d/direct_3d_device_event_listener.rs rename to crates/features/junowen-direct3d/src/direct_3d_device_event_listener.rs index c3a36b0..db470b0 100644 --- a/junowen/src/custom_direct_3d/direct_3d_device_event_listener.rs +++ b/crates/features/junowen-direct3d/src/direct_3d_device_event_listener.rs @@ -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); } diff --git a/junowen/src/custom_direct_3d.rs b/crates/features/junowen-direct3d/src/lib.rs similarity index 100% rename from junowen/src/custom_direct_3d.rs rename to crates/features/junowen-direct3d/src/lib.rs diff --git a/junowen-lib/Cargo.toml b/crates/features/junowen-lib/Cargo.toml similarity index 100% rename from junowen-lib/Cargo.toml rename to crates/features/junowen-lib/Cargo.toml diff --git a/junowen-lib/examples/webrtc_guest.rs b/crates/features/junowen-lib/examples/webrtc_guest.rs similarity index 100% rename from junowen-lib/examples/webrtc_guest.rs rename to crates/features/junowen-lib/examples/webrtc_guest.rs diff --git a/junowen-lib/examples/webrtc_host.rs b/crates/features/junowen-lib/examples/webrtc_host.rs similarity index 100% rename from junowen-lib/examples/webrtc_host.rs rename to crates/features/junowen-lib/examples/webrtc_host.rs diff --git a/crates/features/junowen-lib/src/connection.rs b/crates/features/junowen-lib/src/connection.rs new file mode 100644 index 0000000..b4dbfc7 --- /dev/null +++ b/crates/features/junowen-lib/src/connection.rs @@ -0,0 +1,8 @@ +mod data_channel; +mod peer_connection; +pub mod signaling; + +pub use self::{ + data_channel::DataChannel, + peer_connection::{BATTLE_PROTOCOL, PeerConnection, SPECTATOR_PROTOCOL}, +}; diff --git a/junowen-lib/src/connection/data_channel.rs b/crates/features/junowen-lib/src/connection/data_channel.rs similarity index 100% rename from junowen-lib/src/connection/data_channel.rs rename to crates/features/junowen-lib/src/connection/data_channel.rs diff --git a/junowen-lib/src/connection/peer_connection.rs b/crates/features/junowen-lib/src/connection/peer_connection.rs similarity index 90% rename from junowen-lib/src/connection/peer_connection.rs rename to crates/features/junowen-lib/src/connection/peer_connection.rs index ca188a7..0adc66a 100644 --- a/junowen-lib/src/connection/peer_connection.rs +++ b/crates/features/junowen-lib/src/connection/peer_connection.rs @@ -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) -> Result { @@ -130,13 +135,14 @@ impl PeerConnection { self.rtc.as_ref().unwrap() } - pub async fn start_as_offerer(&mut self) -> Result { + /// `protocol` はデータチャネルのプロトコル + pub async fn start_as_offerer(&mut self, protocol: &'static str) -> Result { let rtc_data_channel = self .rtc() .create_data_channel( "data", Some(RTCDataChannelInit { - protocol: Some(PROTOCOL.to_owned()), + protocol: Some(protocol.to_owned()), ..Default::default() }), ) @@ -197,11 +203,15 @@ impl PeerConnection { Ok(()) } - pub async fn wait_for_open_data_channel(&mut self) -> Result { + /// 相手のプロトコルが `protocol` と一致しない場合は接続を拒否する + pub async fn wait_for_open_data_channel( + &mut self, + protocol: &'static str, + ) -> Result { 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) diff --git a/junowen-lib/src/connection/signaling.rs b/crates/features/junowen-lib/src/connection/signaling.rs similarity index 100% rename from junowen-lib/src/connection/signaling.rs rename to crates/features/junowen-lib/src/connection/signaling.rs diff --git a/junowen-lib/src/connection/signaling/socket.rs b/crates/features/junowen-lib/src/connection/signaling/socket.rs similarity index 85% rename from junowen-lib/src/connection/signaling/socket.rs rename to crates/features/junowen-lib/src/connection/signaling/socket.rs index fc9c36b..6e854de 100644 --- a/junowen-lib/src/connection/signaling/socket.rs +++ b/crates/features/junowen-lib/src/connection/signaling/socket.rs @@ -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; @@ -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?; @@ -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)) } } diff --git a/junowen-lib/src/connection/signaling/socket/async_read_write_server_socket.rs b/crates/features/junowen-lib/src/connection/signaling/socket/async_read_write_server_socket.rs similarity index 100% rename from junowen-lib/src/connection/signaling/socket/async_read_write_server_socket.rs rename to crates/features/junowen-lib/src/connection/signaling/socket/async_read_write_server_socket.rs diff --git a/junowen-lib/src/connection/signaling/socket/async_read_write_socket.rs b/crates/features/junowen-lib/src/connection/signaling/socket/async_read_write_socket.rs similarity index 100% rename from junowen-lib/src/connection/signaling/socket/async_read_write_socket.rs rename to crates/features/junowen-lib/src/connection/signaling/socket/async_read_write_socket.rs diff --git a/junowen-lib/src/connection/signaling/socket/channel_socket.rs b/crates/features/junowen-lib/src/connection/signaling/socket/channel_socket.rs similarity index 91% rename from junowen-lib/src/connection/signaling/socket/channel_socket.rs rename to crates/features/junowen-lib/src/connection/signaling/socket/channel_socket.rs index 3191eb6..f85164d 100644 --- a/junowen-lib/src/connection/signaling/socket/channel_socket.rs +++ b/crates/features/junowen-lib/src/connection/signaling/socket/channel_socket.rs @@ -13,6 +13,7 @@ pub struct ChannelSocket { offer_sender: Option>, answer_sender: Option>, message_receiver: Option>, + protocol: &'static str, } impl ChannelSocket { @@ -20,11 +21,13 @@ impl ChannelSocket { offer_sender: oneshot::Sender, answer_sender: oneshot::Sender, message_receiver: oneshot::Receiver, + protocol: &'static str, ) -> Self { Self { offer_sender: Some(offer_sender), answer_sender: Some(answer_sender), message_receiver: Some(message_receiver), + protocol, } } } @@ -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 { self.offer_sender.take().unwrap().send(desc).unwrap(); Ok(match self.message_receiver.take().unwrap().await? { diff --git a/junowen-lib/src/connection/signaling/socket/frame.rs b/crates/features/junowen-lib/src/connection/signaling/socket/frame.rs similarity index 100% rename from junowen-lib/src/connection/signaling/socket/frame.rs rename to crates/features/junowen-lib/src/connection/signaling/socket/frame.rs diff --git a/junowen-lib/src/connection/signaling/stdio_signaling_interface.rs b/crates/features/junowen-lib/src/connection/signaling/stdio_signaling_interface.rs similarity index 100% rename from junowen-lib/src/connection/signaling/stdio_signaling_interface.rs rename to crates/features/junowen-lib/src/connection/signaling/stdio_signaling_interface.rs diff --git a/junowen-lib/src/find_process_id.rs b/crates/features/junowen-lib/src/find_process_id.rs similarity index 100% rename from junowen-lib/src/find_process_id.rs rename to crates/features/junowen-lib/src/find_process_id.rs diff --git a/junowen-lib/src/hook_utils.rs b/crates/features/junowen-lib/src/hook_utils.rs similarity index 100% rename from junowen-lib/src/hook_utils.rs rename to crates/features/junowen-lib/src/hook_utils.rs diff --git a/junowen-lib/src/hook_utils/dll_injection.rs b/crates/features/junowen-lib/src/hook_utils/dll_injection.rs similarity index 100% rename from junowen-lib/src/hook_utils/dll_injection.rs rename to crates/features/junowen-lib/src/hook_utils/dll_injection.rs diff --git a/junowen-lib/src/hook_utils/load_library_w_addr.rs b/crates/features/junowen-lib/src/hook_utils/load_library_w_addr.rs similarity index 100% rename from junowen-lib/src/hook_utils/load_library_w_addr.rs rename to crates/features/junowen-lib/src/hook_utils/load_library_w_addr.rs diff --git a/junowen-lib/src/lang.rs b/crates/features/junowen-lib/src/lang.rs similarity index 100% rename from junowen-lib/src/lang.rs rename to crates/features/junowen-lib/src/lang.rs diff --git a/junowen-lib/src/lib.rs b/crates/features/junowen-lib/src/lib.rs similarity index 100% rename from junowen-lib/src/lib.rs rename to crates/features/junowen-lib/src/lib.rs diff --git a/junowen-lib/src/macros.rs b/crates/features/junowen-lib/src/macros.rs similarity index 100% rename from junowen-lib/src/macros.rs rename to crates/features/junowen-lib/src/macros.rs diff --git a/junowen-lib/src/memory_accessors.rs b/crates/features/junowen-lib/src/memory_accessors.rs similarity index 100% rename from junowen-lib/src/memory_accessors.rs rename to crates/features/junowen-lib/src/memory_accessors.rs diff --git a/junowen-lib/src/memory_accessors/external_process.rs b/crates/features/junowen-lib/src/memory_accessors/external_process.rs similarity index 100% rename from junowen-lib/src/memory_accessors/external_process.rs rename to crates/features/junowen-lib/src/memory_accessors/external_process.rs diff --git a/junowen-lib/src/memory_accessors/hooked_process.rs b/crates/features/junowen-lib/src/memory_accessors/hooked_process.rs similarity index 100% rename from junowen-lib/src/memory_accessors/hooked_process.rs rename to crates/features/junowen-lib/src/memory_accessors/hooked_process.rs diff --git a/junowen-lib/src/signaling_server.rs b/crates/features/junowen-lib/src/signaling_server.rs similarity index 100% rename from junowen-lib/src/signaling_server.rs rename to crates/features/junowen-lib/src/signaling_server.rs diff --git a/junowen-lib/src/signaling_server/custom.rs b/crates/features/junowen-lib/src/signaling_server/custom.rs similarity index 100% rename from junowen-lib/src/signaling_server/custom.rs rename to crates/features/junowen-lib/src/signaling_server/custom.rs diff --git a/junowen-lib/src/signaling_server/reserved_room.rs b/crates/features/junowen-lib/src/signaling_server/reserved_room.rs similarity index 100% rename from junowen-lib/src/signaling_server/reserved_room.rs rename to crates/features/junowen-lib/src/signaling_server/reserved_room.rs diff --git a/junowen-lib/src/signaling_server/room.rs b/crates/features/junowen-lib/src/signaling_server/room.rs similarity index 100% rename from junowen-lib/src/signaling_server/room.rs rename to crates/features/junowen-lib/src/signaling_server/room.rs diff --git a/junowen-lib/src/signaling_server/room/delete_room.rs b/crates/features/junowen-lib/src/signaling_server/room/delete_room.rs similarity index 100% rename from junowen-lib/src/signaling_server/room/delete_room.rs rename to crates/features/junowen-lib/src/signaling_server/room/delete_room.rs diff --git a/junowen-lib/src/signaling_server/room/post_room_join.rs b/crates/features/junowen-lib/src/signaling_server/room/post_room_join.rs similarity index 100% rename from junowen-lib/src/signaling_server/room/post_room_join.rs rename to crates/features/junowen-lib/src/signaling_server/room/post_room_join.rs diff --git a/junowen-lib/src/signaling_server/room/post_room_keep.rs b/crates/features/junowen-lib/src/signaling_server/room/post_room_keep.rs similarity index 100% rename from junowen-lib/src/signaling_server/room/post_room_keep.rs rename to crates/features/junowen-lib/src/signaling_server/room/post_room_keep.rs diff --git a/junowen-lib/src/signaling_server/room/put_room.rs b/crates/features/junowen-lib/src/signaling_server/room/put_room.rs similarity index 100% rename from junowen-lib/src/signaling_server/room/put_room.rs rename to crates/features/junowen-lib/src/signaling_server/room/put_room.rs diff --git a/junowen-lib/src/th19.rs b/crates/features/junowen-lib/src/th19.rs similarity index 100% rename from junowen-lib/src/th19.rs rename to crates/features/junowen-lib/src/th19.rs diff --git a/junowen-lib/src/th19/structs.rs b/crates/features/junowen-lib/src/th19/structs.rs similarity index 100% rename from junowen-lib/src/th19/structs.rs rename to crates/features/junowen-lib/src/th19/structs.rs diff --git a/junowen-lib/src/th19/structs/app.rs b/crates/features/junowen-lib/src/th19/structs/app.rs similarity index 98% rename from junowen-lib/src/th19/structs/app.rs rename to crates/features/junowen-lib/src/th19/structs/app.rs index 25dd369..bc1f6c0 100644 --- a/junowen-lib/src/th19/structs/app.rs +++ b/crates/features/junowen-lib/src/th19/structs/app.rs @@ -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")] diff --git a/junowen-lib/src/th19/structs/input_devices.rs b/crates/features/junowen-lib/src/th19/structs/input_devices.rs similarity index 100% rename from junowen-lib/src/th19/structs/input_devices.rs rename to crates/features/junowen-lib/src/th19/structs/input_devices.rs diff --git a/junowen-lib/src/th19/structs/others.rs b/crates/features/junowen-lib/src/th19/structs/others.rs similarity index 100% rename from junowen-lib/src/th19/structs/others.rs rename to crates/features/junowen-lib/src/th19/structs/others.rs diff --git a/junowen-lib/src/th19/structs/selection.rs b/crates/features/junowen-lib/src/th19/structs/selection.rs similarity index 94% rename from junowen-lib/src/th19/structs/selection.rs rename to crates/features/junowen-lib/src/th19/structs/selection.rs index 638d11e..14b5423 100644 --- a/junowen-lib/src/th19/structs/selection.rs +++ b/crates/features/junowen-lib/src/th19/structs/selection.rs @@ -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) pub card: u32, - _unknown3: [u8; 0x34], + _unknown3: [u8; 0x30], } #[derive(Clone, Copy, PartialEq)] diff --git a/junowen-lib/src/th19/structs/settings.rs b/crates/features/junowen-lib/src/th19/structs/settings.rs similarity index 100% rename from junowen-lib/src/th19/structs/settings.rs rename to crates/features/junowen-lib/src/th19/structs/settings.rs diff --git a/junowen-lib/src/th19/th19_event_dispatcher.rs b/crates/features/junowen-lib/src/th19/th19_event_dispatcher.rs similarity index 100% rename from junowen-lib/src/th19/th19_event_dispatcher.rs rename to crates/features/junowen-lib/src/th19/th19_event_dispatcher.rs diff --git a/junowen-lib/src/th19/th19_helpers.rs b/crates/features/junowen-lib/src/th19/th19_helpers.rs similarity index 100% rename from junowen-lib/src/th19/th19_helpers.rs rename to crates/features/junowen-lib/src/th19/th19_helpers.rs diff --git a/junowen-lib/src/win_api_wrappers.rs b/crates/features/junowen-lib/src/win_api_wrappers.rs similarity index 100% rename from junowen-lib/src/win_api_wrappers.rs rename to crates/features/junowen-lib/src/win_api_wrappers.rs diff --git a/crates/features/junowen-lobby/Cargo.toml b/crates/features/junowen-lobby/Cargo.toml new file mode 100644 index 0000000..11b551d --- /dev/null +++ b/crates/features/junowen-lobby/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "junowen-lobby" +edition = "2024" +version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +derive-new.workspace = true +getset.workspace = true +windows.workspace = true + +[lints.clippy] +self_named_module_files = "warn" diff --git a/crates/features/junowen-lobby/src/helper.rs b/crates/features/junowen-lobby/src/helper.rs new file mode 100644 index 0000000..8beb58b --- /dev/null +++ b/crates/features/junowen-lobby/src/helper.rs @@ -0,0 +1,64 @@ +use crate::{MenuRenderer, MenuText, OverlayText}; + +const TEXT_LINE_LEFT: u32 = 32; +const TEXT_LINE_TOP: u32 = 160; +const TEXT_LINE_HEIGHT: u32 = 32; +const SIGNALING_CODE_CHUNK_LEN: usize = 100; + +pub fn render_text_line(renderer: &impl MenuRenderer, line: u32, text: &[u8]) { + let mut mt = MenuText { + text, + x: TEXT_LINE_LEFT, + y: TEXT_LINE_TOP + line * TEXT_LINE_HEIGHT, + color: 0xff000000, + font_type: 8, + ..Default::default() + }; + renderer.render_text(&mt); + + mt.color = 0xffffffff; + mt.font_type = 6; + renderer.render_text(&mt); +} + +/// シグナリングコードは 1 行に収まらないので、半分の行高に詰めて折り返す +pub fn render_signaling_code(renderer: &impl MenuRenderer, line: u32, code: &str) { + for (i, chunk) in code.as_bytes().chunks(SIGNALING_CODE_CHUNK_LEN).enumerate() { + renderer.render_text(&MenuText { + text: chunk, + x: TEXT_LINE_LEFT, + y: TEXT_LINE_TOP + (line * 2 + i as u32) * (TEXT_LINE_HEIGHT / 2), + font_type: 1, + ..Default::default() + }); + } +} + +/// render_signaling_code が消費する行数 +pub fn signaling_code_line_count(code: &str) -> u32 { + let chunks = code.as_bytes().chunks(SIGNALING_CODE_CHUNK_LEN).len(); + (chunks as f64 / 2.0).ceil() as u32 +} + +/// render_text_line と同じ位置へ多言語テキストを重ねる +pub fn overlay_text_line(line: u32, string: String) -> OverlayText { + OverlayText { + left: TEXT_LINE_LEFT as i32, + top: (TEXT_LINE_TOP + line * TEXT_LINE_HEIGHT) as i32, + font_size: TEXT_LINE_HEIGHT, + string, + } +} + +/// 画面下部にメニュー項目の説明文を重ねる +pub fn overlay_description(string: String) -> Vec { + if string.is_empty() { + return vec![]; + } + vec![OverlayText { + left: 96, + top: 640, + font_size: 58, + string, + }] +} diff --git a/crates/features/junowen-lobby/src/lib.rs b/crates/features/junowen-lobby/src/lib.rs new file mode 100644 index 0000000..1e12df0 --- /dev/null +++ b/crates/features/junowen-lobby/src/lib.rs @@ -0,0 +1,21 @@ +//! ロビー画面 +//! +//! 描画、効果音、キーボード入力はゲーム本体の機能で行う。 +//! このクレートはそれらの操作をトレイトで定義する。 +//! 実装は呼び出し元のクレートが用意する。 + +mod helper; +mod menu; +mod overlay_text; +pub mod pure_p2p_offerer_menu; +mod scene; +pub mod tcp_signaling_menu; +mod text_layout; + +pub use helper::{overlay_description, render_text_line, signaling_code_line_count}; +pub use menu::{ + MenuButtons, MenuDevice, MenuRenderer, MenuSound, MenuText, menu_item_color, render_label_value, +}; +pub use overlay_text::OverlayText; +pub use scene::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}; +pub use text_layout::TextLayout; diff --git a/crates/features/junowen-lobby/src/menu/action.rs b/crates/features/junowen-lobby/src/menu/action.rs new file mode 100644 index 0000000..55f57c6 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/action.rs @@ -0,0 +1,24 @@ +use derive_new::new; +use getset::CopyGetters; + +#[derive(Debug, CopyGetters, new)] +pub struct Action { + #[get_copy = "pub"] + id: u8, + #[get_copy = "pub"] + play_sound: bool, + value: Option, +} + +impl Action { + pub fn value(&self) -> Option<&str> { + self.value.as_deref() + } +} + +pub enum OnMenuInputResult { + None, + Cancel, + Action(Action), + SubScene(S), +} diff --git a/crates/features/junowen-lobby/src/menu/common_menu.rs b/crates/features/junowen-lobby/src/menu/common_menu.rs new file mode 100644 index 0000000..979b57d --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/common_menu.rs @@ -0,0 +1,140 @@ +use getset::{Getters, MutGetters}; + +use crate::menu::{ + action::{Action, OnMenuInputResult}, + device::{MenuDevice, MenuSound}, + input::{ + MenuButtons, MenuController, MenuControllerInputResult, MenuControllerUpdateDecideResult, + OnTextInputResult, + }, + render::{MenuRenderer, render_title}, + tree::{ + CurrentMenuSceneMutResult, CurrentMenuSceneResult, Menu, MenuItem, current_menu_scene, + current_menu_scene_mut, + }, +}; + +#[derive(Getters, MutGetters)] +pub struct CommonMenu { + #[getset(get = "pub", get_mut = "pub")] + menu: Menu, + instant_exit: bool, + base_height: u32, + #[getset(get_mut = "pub")] + controller: MenuController, +} + +impl CommonMenu { + pub fn new(instant_exit: bool, base_height: u32, menu: Menu) -> Self { + Self { + menu, + instant_exit, + base_height, + controller: MenuController::default(), + } + } + + pub fn root_title(&self) -> &'static str { + self.menu.title() + } + + fn apply_decide_count(&mut self) -> Option> { + match self.controller.update_decide() { + MenuControllerUpdateDecideResult::None => None, + MenuControllerUpdateDecideResult::Wait => Some(OnMenuInputResult::None), + MenuControllerUpdateDecideResult::Decide => { + if let Some(scene) = self.menu.dig() { + return Some(OnMenuInputResult::SubScene(scene)); + } + None + } + MenuControllerUpdateDecideResult::Cancel => { + if !self.menu.bury() { + return Some(OnMenuInputResult::Cancel); + } + None + } + } + } + + pub fn on_input_menu( + &mut self, + current_input: MenuButtons, + prev_input: MenuButtons, + device: &impl MenuDevice, + ) -> OnMenuInputResult { + if let Some(result) = self.apply_decide_count() { + return result; + } + + match current_menu_scene(&self.menu) { + CurrentMenuSceneResult::SubScene(scene) => OnMenuInputResult::SubScene(scene), + CurrentMenuSceneResult::Menu(menu) => { + let ignore_decide = menu.items().is_empty(); + let instant_decide = + ignore_decide || matches!(menu.selected_item(), MenuItem::Plain(_)); + let play_decide_sound = !ignore_decide + && menu + .selected_item() + .decided_action() + .map(|x| x.play_sound()) + .unwrap_or(true); + let root_cancel = !self.menu.decided() && self.instant_exit; + + let input_result = + self.controller + .input(current_input, prev_input, instant_decide, root_cancel); + + if root_cancel && let MenuControllerInputResult::Cancel = input_result { + return OnMenuInputResult::Cancel; + } + let CurrentMenuSceneMutResult::Menu(menu) = current_menu_scene_mut(&mut self.menu) + else { + unreachable!() + }; + if let Some(action) = + menu.input(input_result, ignore_decide, play_decide_sound, device) + { + OnMenuInputResult::Action(action) + } else { + OnMenuInputResult::None + } + } + CurrentMenuSceneResult::TextInput(_, _) => { + let CurrentMenuSceneMutResult::TextInput(text_input) = + current_menu_scene_mut(&mut self.menu) + else { + unreachable!() + }; + match text_input.on_input_menu(device) { + OnTextInputResult::None => OnMenuInputResult::None, + OnTextInputResult::Cancel => { + device.play_sound(MenuSound::Cancel); + self.controller.force_cancel(); + OnMenuInputResult::None + } + OnTextInputResult::Decide(changed_action, new_room_name) => { + device.play_sound(MenuSound::Decide); + self.controller.force_cancel(); + let action = Action::new(changed_action, false, Some(new_room_name)); + OnMenuInputResult::Action(action) + } + } + } + } + } + + pub fn on_render_texts(&self, renderer: &impl MenuRenderer) { + match current_menu_scene(&self.menu) { + CurrentMenuSceneResult::SubScene { .. } => unreachable!(), + CurrentMenuSceneResult::Menu(menu) => { + render_title(renderer, menu.title().as_bytes()); + menu.on_render_texts(self.base_height, renderer); + } + CurrentMenuSceneResult::TextInput(label, text_input) => { + render_title(renderer, label.as_bytes()); + text_input.on_render_texts(renderer); + } + } + } +} diff --git a/crates/features/junowen-lobby/src/menu/device.rs b/crates/features/junowen-lobby/src/menu/device.rs new file mode 100644 index 0000000..b9d879c --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/device.rs @@ -0,0 +1,13 @@ +#[derive(Clone, Copy, Debug)] +pub enum MenuSound { + Cancel, + Decide, + Select, +} + +/// メニュー操作に使うゲームの機能 +pub trait MenuDevice { + fn play_sound(&self, sound: MenuSound); + /// 仮想キーコードごとの押下状態。最上位ビットが立っていれば押されている + fn keyboard_raw_keys(&self) -> &[u8; 256]; +} diff --git a/crates/features/junowen-lobby/src/menu/input/menu_buttons.rs b/crates/features/junowen-lobby/src/menu/input/menu_buttons.rs new file mode 100644 index 0000000..0e89251 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/input/menu_buttons.rs @@ -0,0 +1,41 @@ +/// メニュー操作に使うボタンの押下状態 +/// +/// 決定とキャンセルは、対応するボタンのいずれかが新たに押された時点で反応する。 +/// そのため、ボタンごとに 1 ビットを使う +#[derive(Clone, Copy, Debug, Default)] +pub struct MenuButtons { + pub up: bool, + pub down: bool, + pub decide: u8, + pub cancel: u8, +} + +impl MenuButtons { + pub fn decide_pulsed(self, prev: Self) -> bool { + self.decide & !prev.decide != 0 + } + + pub fn cancel_pulsed(self, prev: Self) -> bool { + self.cancel & !prev.cancel != 0 + } +} + +#[cfg(test)] +mod tests { + use super::MenuButtons; + + fn cancel(bits: u8) -> MenuButtons { + MenuButtons { + cancel: bits, + ..Default::default() + } + } + + #[test] + fn test_cancel_pulsed() { + assert!(cancel(0b01).cancel_pulsed(cancel(0b00))); + assert!(!cancel(0b01).cancel_pulsed(cancel(0b01))); + assert!(cancel(0b11).cancel_pulsed(cancel(0b01))); + assert!(!cancel(0b01).cancel_pulsed(cancel(0b11))); + } +} diff --git a/junowen/src/lobby/common_menu/menu_controller.rs b/crates/features/junowen-lobby/src/menu/input/menu_controller.rs similarity index 68% rename from junowen/src/lobby/common_menu/menu_controller.rs rename to crates/features/junowen-lobby/src/menu/input/menu_controller.rs index f85e742..41525a9 100644 --- a/junowen/src/lobby/common_menu/menu_controller.rs +++ b/crates/features/junowen-lobby/src/menu/input/menu_controller.rs @@ -1,8 +1,4 @@ -use junowen_lib::structs::input_devices::{InputFlags, InputValue}; - -fn pulse(current: InputValue, prev: InputValue, flag: InputFlags) -> bool { - current.0 & flag != None && prev.0 & flag == None -} +use super::MenuButtons; pub enum MenuControllerUpdateDecideResult { None, @@ -47,11 +43,13 @@ impl MenuController { MenuControllerUpdateDecideResult::Wait } - fn cancel(&mut self, current_input: InputValue, prev_input: InputValue, instant: bool) -> bool { - if !pulse(current_input, prev_input, InputFlags::CHARGE) - && !pulse(current_input, prev_input, InputFlags::BOMB) - && !pulse(current_input, prev_input, InputFlags::PAUSE) - { + fn cancel( + &mut self, + current_input: MenuButtons, + prev_input: MenuButtons, + instant: bool, + ) -> bool { + if !current_input.cancel_pulsed(prev_input) { return false; } if !instant { @@ -64,10 +62,13 @@ impl MenuController { self.decide_count = -1; } - fn decide(&mut self, current_input: InputValue, prev_input: InputValue, instant: bool) -> bool { - if !pulse(current_input, prev_input, InputFlags::SHOT) - && !pulse(current_input, prev_input, InputFlags::ENTER) - { + fn decide( + &mut self, + current_input: MenuButtons, + prev_input: MenuButtons, + instant: bool, + ) -> bool { + if !current_input.decide_pulsed(prev_input) { return false; } if !instant { @@ -76,11 +77,9 @@ impl MenuController { true } - fn select(&mut self, current_input: InputValue, prev_input: InputValue) -> Option { + fn select(&mut self, current_input: MenuButtons, prev_input: MenuButtons) -> Option { let mut mv = None; - if current_input.0 & InputFlags::UP != None - && (prev_input.0 & InputFlags::UP == None || self.repeat_up > 0) - { + if current_input.up && (!prev_input.up || self.repeat_up > 0) { if [0, 25].contains(&self.repeat_up) { mv = Some(true); } @@ -91,9 +90,7 @@ impl MenuController { } else { self.repeat_up = 0; } - if current_input.0 & InputFlags::DOWN != None - && (prev_input.0 & InputFlags::DOWN == None || self.repeat_down > 0) - { + if current_input.down && (!prev_input.down || self.repeat_down > 0) { if [0, 25].contains(&self.repeat_down) { mv = Some(false); } @@ -109,8 +106,8 @@ impl MenuController { pub fn input( &mut self, - current_input: InputValue, - prev_input: InputValue, + current_input: MenuButtons, + prev_input: MenuButtons, instant_decide: bool, instant_cancel: bool, ) -> MenuControllerInputResult { diff --git a/crates/features/junowen-lobby/src/menu/input/mod.rs b/crates/features/junowen-lobby/src/menu/input/mod.rs new file mode 100644 index 0000000..8745e56 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/input/mod.rs @@ -0,0 +1,9 @@ +mod menu_buttons; +mod menu_controller; +mod text_input; + +pub use menu_buttons::MenuButtons; +pub use menu_controller::{ + MenuController, MenuControllerInputResult, MenuControllerUpdateDecideResult, +}; +pub use text_input::{OnTextInputResult, TextInput}; diff --git a/junowen/src/lobby/common_menu/text_input.rs b/crates/features/junowen-lobby/src/menu/input/text_input.rs similarity index 72% rename from junowen/src/lobby/common_menu/text_input.rs rename to crates/features/junowen-lobby/src/menu/input/text_input.rs index fa0f89f..923ccff 100644 --- a/junowen/src/lobby/common_menu/text_input.rs +++ b/crates/features/junowen-lobby/src/menu/input/text_input.rs @@ -1,10 +1,12 @@ -use std::{ffi::c_void, num::NonZeroU8}; +use std::num::NonZeroU8; use getset::Setters; -use junowen_lib::Th19; use windows::Win32::UI::Input::KeyboardAndMouse::{MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode}; -use crate::lobby::helper::render_label_value; +use crate::menu::{ + device::MenuDevice, + render::{MenuRenderer, render_label_value}, +}; fn to_ascii(vk: u32, current: &[u8; 256]) -> Option { let mut buf = [0u16; 2]; @@ -50,10 +52,10 @@ impl TextInputState { } } if current[self.current_vk as usize] & 0x80 != 0 { - if self.current_vk_count > 30 { - if let Some(ascii) = to_ascii(self.current_vk as u32, current) { - result.push(ascii.get()); - } + if self.current_vk_count > 30 + && let Some(ascii) = to_ascii(self.current_vk as u32, current) + { + result.push(ascii.get()); } self.current_vk_count += 1; } else { @@ -64,7 +66,7 @@ impl TextInputState { } } -pub enum OnMenuInputResult { +pub enum OnTextInputResult { None, Cancel, Decide(u8, String), @@ -97,33 +99,28 @@ impl TextInput { self.state.as_mut().unwrap() } - pub fn on_input_menu(&mut self, th19: &Th19) -> OnMenuInputResult { + pub fn on_input_menu(&mut self, device: &impl MenuDevice) -> OnTextInputResult { if self.state.is_none() { - self.state = Some(TextInputState::new( - th19.input_devices().keyboard_input().raw_keys(), - )); - return OnMenuInputResult::None; + self.state = Some(TextInputState::new(device.keyboard_raw_keys())); + return OnTextInputResult::None; } - for ascii in self - .state_mut() - .tick(th19.input_devices().keyboard_input().raw_keys()) - { + for ascii in self.state_mut().tick(device.keyboard_raw_keys()) { if (0x20..0x7f).contains(&ascii) { self.value.push(ascii as char); } else if ascii == 0x08 { self.value.pop(); } else if ascii == 0x0d { // CR - return OnMenuInputResult::Decide(self.changed_action, self.value.clone()); + return OnTextInputResult::Decide(self.changed_action, self.value.clone()); } else if ascii == 0x1b { // ESC - return OnMenuInputResult::Cancel; + return OnTextInputResult::Cancel; } } - OnMenuInputResult::None + OnTextInputResult::None } - pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { - render_label_value(th19, text_renderer, 480, 0, self.name, &self.value); + pub fn on_render_texts(&self, renderer: &impl MenuRenderer) { + render_label_value(renderer, 480, 0, self.name, &self.value); } } diff --git a/crates/features/junowen-lobby/src/menu/mod.rs b/crates/features/junowen-lobby/src/menu/mod.rs new file mode 100644 index 0000000..3badc00 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/mod.rs @@ -0,0 +1,15 @@ +//! ロビー画面のメニュー部品 + +mod action; +mod common_menu; +mod device; +mod input; +mod render; +mod tree; + +pub use action::OnMenuInputResult; +pub use common_menu::CommonMenu; +pub use device::{MenuDevice, MenuSound}; +pub use input::MenuButtons; +pub use render::{MenuRenderer, MenuText, menu_item_color, render_label_value}; +pub use tree::{Menu, MenuItem}; diff --git a/crates/features/junowen-lobby/src/menu/render.rs b/crates/features/junowen-lobby/src/menu/render.rs new file mode 100644 index 0000000..ec458d7 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/render.rs @@ -0,0 +1,123 @@ +/// ゲーム内フォントで描画するテキスト。座標はゲーム内と同じ 1280x960 換算 +#[derive(Clone, Copy, Debug)] +pub struct MenuText<'a> { + pub text: &'a [u8], + pub x: u32, + pub y: u32, + /// 0xaarrggbb + pub color: u32, + pub font_type: u32, + pub drop_shadow: bool, + /// 0: center, 1: left, 2: right + pub horizontal_align: u32, + /// 0: center, 1: top, 2: bottom + pub vertical_align: u32, +} + +impl Default for MenuText<'_> { + fn default() -> Self { + Self { + text: b"", + x: 0, + y: 0, + color: 0xffffffff, + font_type: 0, + drop_shadow: false, + horizontal_align: 1, + vertical_align: 1, + } + } +} + +pub trait MenuRenderer { + fn render_text(&self, text: &MenuText); +} + +pub fn render_title(renderer: &impl MenuRenderer, text: &[u8]) { + let mut mt = MenuText { + text, + x: 640, + y: 64, + color: 0xff000000, + font_type: 9, + drop_shadow: true, + horizontal_align: 0, + ..Default::default() + }; + renderer.render_text(&mt); + + mt.color = 0xffffffff; + mt.font_type = 7; + renderer.render_text(&mt); +} + +pub fn render_menu_item( + renderer: &impl MenuRenderer, + text: &[u8], + y: u32, + enabled: bool, + selected: bool, +) { + let mut mt = MenuText { + text, + x: 640, + y, + color: menu_item_color(9, enabled, selected), + font_type: 9, + horizontal_align: 0, + ..Default::default() + }; + renderer.render_text(&mt); + + mt.color = menu_item_color(7, enabled, selected); + mt.font_type = 7; + renderer.render_text(&mt); +} + +pub fn menu_item_color(font_type: u32, enabled: bool, selected: bool) -> u32 { + if !enabled { + match font_type { + 9 => 0x40ffffff, + 7 => 0xff808080, + _ => unreachable!(), + } + } else if selected { + match font_type { + 9 => 0xff000000, + 7 => 0xffffff80, + _ => unreachable!(), + } + } else { + match font_type { + 9 => 0xff404040, + 7 => 0xff808060, + _ => unreachable!(), + } + } +} + +pub fn render_label_value( + renderer: &impl MenuRenderer, + height: u32, + vertical_align: u32, + label: &str, + value: &str, +) { + let label = format!("{:<11}:", label); + let mut mt = MenuText { + text: label.as_bytes(), + x: 320, + y: height, + color: 0xffffffff, + font_type: 0, + horizontal_align: 1, + vertical_align, + ..Default::default() + }; + renderer.render_text(&mt); + + mt.text = value.as_bytes(); + mt.color = 0xffffffa0; + mt.x = 544; + renderer.render_text(&mt); +} diff --git a/crates/features/junowen-lobby/src/menu/tree/current_scene.rs b/crates/features/junowen-lobby/src/menu/tree/current_scene.rs new file mode 100644 index 0000000..c052069 --- /dev/null +++ b/crates/features/junowen-lobby/src/menu/tree/current_scene.rs @@ -0,0 +1,67 @@ +use crate::menu::input::TextInput; + +use super::{Menu, MenuItem}; + +#[derive(Debug)] +pub enum CurrentMenuSceneResult<'a, S> { + Menu(&'a Menu), + SubScene(S), + TextInput(&'static str, &'a TextInput), +} + +pub enum CurrentMenuSceneMutResult<'a, S> { + Menu(&'a mut Menu), + SubScene, + TextInput(&'a mut TextInput), +} + +/// 決定済みのサブメニューをたどり、操作対象の画面を返す +pub fn current_menu_scene(root: &Menu) -> CurrentMenuSceneResult<'_, S> { + if !root.decided() { + return CurrentMenuSceneResult::Menu(root); + } + let mut menu = root; + loop { + match menu.selected_item() { + MenuItem::Plain(_) => unreachable!(), + MenuItem::SubMenu(item) => { + if item.sub_menu().decided() { + menu = item.sub_menu(); + continue; + } + return CurrentMenuSceneResult::Menu(item.sub_menu()); + } + MenuItem::SubScene(item) => { + return CurrentMenuSceneResult::SubScene(item.sub_scene()); + } + MenuItem::TextInput(item) => { + return CurrentMenuSceneResult::TextInput(item.label(), item.text_input()); + } + } + } +} + +pub fn current_menu_scene_mut(root: &mut Menu) -> CurrentMenuSceneMutResult<'_, S> { + if !root.decided() { + return CurrentMenuSceneMutResult::Menu(root); + } + let mut menu = root; + loop { + match menu.selected_item_mut() { + MenuItem::Plain(_) => unreachable!(), + MenuItem::SubMenu(item) => { + if item.sub_menu().decided() { + menu = item.sub_menu_mut(); + continue; + } + return CurrentMenuSceneMutResult::Menu(item.sub_menu_mut()); + } + MenuItem::SubScene(_item) => { + return CurrentMenuSceneMutResult::SubScene; + } + MenuItem::TextInput(item) => { + return CurrentMenuSceneMutResult::TextInput(item.text_input_mut()); + } + } + } +} diff --git a/junowen/src/lobby/common_menu/menu_item.rs b/crates/features/junowen-lobby/src/menu/tree/menu_item.rs similarity index 87% rename from junowen/src/lobby/common_menu/menu_item.rs rename to crates/features/junowen-lobby/src/menu/tree/menu_item.rs index 913729c..8db4239 100644 --- a/junowen/src/lobby/common_menu/menu_item.rs +++ b/crates/features/junowen-lobby/src/menu/tree/menu_item.rs @@ -1,6 +1,8 @@ use getset::{CopyGetters, Getters, MutGetters, Setters}; -use super::{Action, LobbyScene, menu::Menu, text_input::TextInput}; +use crate::menu::{action::Action, input::TextInput}; + +use super::Menu; #[derive(Debug, Setters)] pub struct MenuPlainItem { @@ -12,13 +14,13 @@ pub struct MenuPlainItem { } #[derive(Debug, Getters, MutGetters, Setters)] -pub struct MenuSubMenuItem { +pub struct MenuSubMenuItem { #[set = "pub"] label: &'static str, enabled: bool, decided_action: Option, #[getset(get = "pub", get_mut = "pub")] - sub_menu: Menu, + sub_menu: Menu, } #[derive(CopyGetters, Debug, Getters, MutGetters, Setters)] @@ -32,23 +34,28 @@ pub struct MenuTextInputItem { } #[derive(CopyGetters, Debug, Setters)] -pub struct MenuSubSceneItem { +pub struct MenuSubSceneItem { #[getset(get_copy = "pub", set = "pub")] label: &'static str, enabled: bool, - #[get_copy = "pub"] - sub_scene: LobbyScene, + sub_scene: S, +} + +impl MenuSubSceneItem { + pub fn sub_scene(&self) -> S { + self.sub_scene + } } #[derive(Debug)] -pub enum MenuItem { +pub enum MenuItem { Plain(MenuPlainItem), - SubMenu(MenuSubMenuItem), + SubMenu(MenuSubMenuItem), TextInput(MenuTextInputItem), - SubScene(MenuSubSceneItem), + SubScene(MenuSubSceneItem), } -impl MenuItem { +impl MenuItem { pub fn plain(label: &'static str, decided_action: u8, play_sound: bool) -> Self { Self::Plain(MenuPlainItem { label, @@ -58,7 +65,7 @@ impl MenuItem { }) } - pub fn sub_menu(label: &'static str, decided_action: Option, sub_menu: Menu) -> Self { + pub fn sub_menu(label: &'static str, decided_action: Option, sub_menu: Menu) -> Self { Self::SubMenu(MenuSubMenuItem { label, enabled: true, @@ -67,7 +74,7 @@ impl MenuItem { }) } - pub fn sub_scene(label: &'static str, sub_scene: LobbyScene) -> Self { + pub fn sub_scene(label: &'static str, sub_scene: S) -> Self { Self::SubScene(MenuSubSceneItem { label, enabled: true, diff --git a/junowen/src/lobby/common_menu/menu.rs b/crates/features/junowen-lobby/src/menu/tree/mod.rs similarity index 75% rename from junowen/src/lobby/common_menu/menu.rs rename to crates/features/junowen-lobby/src/menu/tree/mod.rs index dc0d219..fb58ca2 100644 --- a/junowen/src/lobby/common_menu/menu.rs +++ b/crates/features/junowen-lobby/src/menu/tree/mod.rs @@ -1,30 +1,38 @@ -use std::ffi::c_void; +mod current_scene; +mod menu_item; use getset::{CopyGetters, Getters, MutGetters, Setters}; -use junowen_lib::Th19; -use crate::lobby::helper::render_menu_item; +use crate::menu::{ + action::Action, + device::{MenuDevice, MenuSound}, + input::MenuControllerInputResult, + render::{MenuRenderer, render_menu_item}, +}; -use super::{Action, LobbyScene, menu_controller::MenuControllerInputResult, menu_item::MenuItem}; +pub use current_scene::{ + CurrentMenuSceneMutResult, CurrentMenuSceneResult, current_menu_scene, current_menu_scene_mut, +}; +pub use menu_item::MenuItem; #[derive(CopyGetters, Debug, Getters, MutGetters, Setters)] -pub struct Menu { +pub struct Menu { #[get_copy = "pub"] title: &'static str, canceled_action: Option, #[getset(get = "pub", get_mut = "pub")] - items: Vec, + items: Vec>, #[get_copy = "pub"] cursor: usize, #[get_copy = "pub"] decided: bool, } -impl Menu { +impl Menu { pub fn new( title: &'static str, canceled_action: Option, - items: Vec, + items: Vec>, cursor: usize, ) -> Self { Self { @@ -36,15 +44,15 @@ impl Menu { } } - pub fn selected_item(&self) -> &MenuItem { + pub fn selected_item(&self) -> &MenuItem { &self.items[self.cursor] } - pub fn selected_item_mut(&mut self) -> &mut MenuItem { + pub fn selected_item_mut(&mut self) -> &mut MenuItem { &mut self.items[self.cursor] } - pub fn dig(&mut self) -> Option { + pub fn dig(&mut self) -> Option { if !self.decided { if let MenuItem::SubScene(scene) = self.selected_item() { return Some(scene.sub_scene()); @@ -73,18 +81,11 @@ impl Menu { sub_menu.sub_menu_mut().bury() } - pub fn on_render_texts(&self, base_height: u32, th19: &Th19, text_renderer: &c_void) { + pub fn on_render_texts(&self, base_height: u32, renderer: &impl MenuRenderer) { for (i, item) in self.items().iter().enumerate() { let label = item.label().as_bytes(); let height = base_height + 56 * i as u32; - render_menu_item( - th19, - text_renderer, - label, - height, - item.enabled(), - i == self.cursor(), - ); + render_menu_item(renderer, label, height, item.enabled(), i == self.cursor()); } } @@ -114,12 +115,12 @@ impl Menu { input_result: MenuControllerInputResult, ignore_decide: bool, play_decide_sound: bool, - th19: &Th19, + device: &impl MenuDevice, ) -> Option { match input_result { MenuControllerInputResult::None => None, MenuControllerInputResult::Cancel => { - th19.play_sound(th19.sound_manager(), 0x09, 0); + device.play_sound(MenuSound::Cancel); self.canceled_action.map(|id| Action::new(id, false, None)) } MenuControllerInputResult::Decide => { @@ -127,7 +128,7 @@ impl Menu { return None; } if play_decide_sound { - th19.play_sound(th19.sound_manager(), 0x07, 0); + device.play_sound(MenuSound::Decide); } self.selected_item().decided_action() } @@ -135,14 +136,14 @@ impl Menu { if !self.decrement_cursor() { return None; } - th19.play_sound(th19.sound_manager(), 0x0a, 0); + device.play_sound(MenuSound::Select); None } MenuControllerInputResult::Down => { if !self.increment_cursor() { return None; } - th19.play_sound(th19.sound_manager(), 0x0a, 0); + device.play_sound(MenuSound::Select); None } } diff --git a/crates/features/junowen-lobby/src/overlay_text.rs b/crates/features/junowen-lobby/src/overlay_text.rs new file mode 100644 index 0000000..c61d520 --- /dev/null +++ b/crates/features/junowen-lobby/src/overlay_text.rs @@ -0,0 +1,8 @@ +/// ゲーム内フォントでは英数字しか描画できないため、多言語のテキストは +/// GDI で画面に重ねて描画する。座標はゲーム内と同じ 1280x960 換算。 +pub struct OverlayText { + pub left: i32, + pub top: i32, + pub font_size: u32, + pub string: String, +} diff --git a/junowen/src/lobby/pure_p2p_offerer/menu.rs b/crates/features/junowen-lobby/src/pure_p2p_offerer_menu.rs similarity index 92% rename from junowen/src/lobby/pure_p2p_offerer/menu.rs rename to crates/features/junowen-lobby/src/pure_p2p_offerer_menu.rs index 3dec54c..eabf7e5 100644 --- a/junowen/src/lobby/pure_p2p_offerer/menu.rs +++ b/crates/features/junowen-lobby/src/pure_p2p_offerer_menu.rs @@ -1,4 +1,4 @@ -use crate::lobby::common_menu::{CommonMenu, Menu, MenuItem}; +use crate::{CommonMenu, Menu, MenuItem}; const BASE_HEIGHT: u32 = 720; diff --git a/crates/features/junowen-lobby/src/scene.rs b/crates/features/junowen-lobby/src/scene.rs new file mode 100644 index 0000000..96b69d8 --- /dev/null +++ b/crates/features/junowen-lobby/src/scene.rs @@ -0,0 +1,17 @@ +use crate::menu; + +#[derive(Clone, Copy, Debug)] +pub enum LobbyScene { + Root, + SharedRoom, + ReservedRoom, + TcpSignaling, + PureP2pHost, + PureP2pGuest, + PureP2pSpectator, +} + +pub type CommonMenu = menu::CommonMenu; +pub type Menu = menu::Menu; +pub type MenuItem = menu::MenuItem; +pub type OnMenuInputResult = menu::OnMenuInputResult; diff --git a/junowen/src/lobby/room/tcp_signaling/menu.rs b/crates/features/junowen-lobby/src/tcp_signaling_menu.rs similarity index 96% rename from junowen/src/lobby/room/tcp_signaling/menu.rs rename to crates/features/junowen-lobby/src/tcp_signaling_menu.rs index 2bb1d33..3b503af 100644 --- a/junowen/src/lobby/room/tcp_signaling/menu.rs +++ b/crates/features/junowen-lobby/src/tcp_signaling_menu.rs @@ -1,4 +1,4 @@ -use crate::lobby::common_menu::{CommonMenu, Menu, MenuItem}; +use crate::{CommonMenu, Menu, MenuItem}; const HOST_LABEL: &str = "Connect as a Host"; const GUEST_LABEL: &str = "Connect as a Guest"; @@ -47,6 +47,12 @@ pub struct TcpSignalingMenu { offline: Option, } +impl Default for TcpSignalingMenu { + fn default() -> Self { + Self::new() + } +} + impl TcpSignalingMenu { pub fn new() -> Self { Self { diff --git a/junowen/src/lobby/text_layout.rs b/crates/features/junowen-lobby/src/text_layout.rs similarity index 65% rename from junowen/src/lobby/text_layout.rs rename to crates/features/junowen-lobby/src/text_layout.rs index c04d108..69e6450 100644 --- a/junowen/src/lobby/text_layout.rs +++ b/crates/features/junowen-lobby/src/text_layout.rs @@ -1,15 +1,11 @@ -use std::ffi::c_void; - -use junowen_lib::Th19; - -use super::{ +use crate::{ + MenuRenderer, OverlayText, helper::{overlay_text_line, render_signaling_code}, - overlay::OverlayText, }; /// 画面に並ぶ要素を行番号で組み立てたもの。 -/// テキストは多言語を表示するため overlay で、シグナリングコードは -/// 英数字だけなのでゲーム内フォントで描画する。 +/// 多言語のテキストは overlay で描画する。 +/// シグナリングコードは英数字だけなので、ゲーム内フォントで描画する。 #[derive(Default)] pub struct TextLayout { texts: Vec<(u32, String)>, @@ -25,9 +21,9 @@ impl TextLayout { self.codes.push((line, code.into())); } - pub fn render_codes(&self, th19: &Th19, text_renderer: &c_void) { + pub fn render_codes(&self, renderer: &impl MenuRenderer) { for (line, code) in &self.codes { - render_signaling_code(th19, text_renderer, *line, code); + render_signaling_code(renderer, *line, code); } } diff --git a/junowen-lib/src/connection.rs b/junowen-lib/src/connection.rs deleted file mode 100644 index 374567d..0000000 --- a/junowen-lib/src/connection.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod data_channel; -mod peer_connection; -pub mod signaling; - -pub use self::{data_channel::DataChannel, peer_connection::PeerConnection}; diff --git a/junowen/src/lobby/common_menu.rs b/junowen/src/lobby/common_menu.rs deleted file mode 100644 index fd83592..0000000 --- a/junowen/src/lobby/common_menu.rs +++ /dev/null @@ -1,243 +0,0 @@ -mod menu; -mod menu_controller; -mod menu_item; -mod text_input; - -use std::ffi::c_void; - -use derive_new::new; -use getset::{CopyGetters, Getters, MutGetters}; -use junowen_lib::{Th19, structs::input_devices::InputValue}; - -use self::{ - menu_controller::{ - MenuController, MenuControllerInputResult, MenuControllerUpdateDecideResult, - }, - text_input::TextInput, -}; - -use super::helper::render_title; - -pub use {menu::Menu, menu_item::MenuItem}; - -#[derive(Debug, CopyGetters, new)] -pub struct Action { - #[get_copy = "pub"] - id: u8, - #[get_copy = "pub"] - play_sound: bool, - value: Option, -} - -impl Action { - pub fn value(&self) -> Option<&str> { - self.value.as_deref() - } -} - -#[derive(Clone, Copy, Debug)] -pub enum LobbyScene { - Root, - SharedRoom, - ReservedRoom, - TcpSignaling, - PureP2pHost, - PureP2pGuest, - PureP2pSpectator, -} - -pub enum OnMenuInputResult { - None, - Cancel, - Action(Action), - SubScene(LobbyScene), -} - -#[derive(Debug)] -enum CurrentMenuSceneResult<'a> { - Menu(&'a Menu), - SubScene(LobbyScene), - TextInput(&'static str, &'a TextInput), -} - -enum CurrentMenuSceneMutResult<'a> { - Menu(&'a mut Menu), - SubScene, - TextInput(&'a mut TextInput), -} - -#[derive(Getters, MutGetters)] -pub struct CommonMenu { - #[getset(get = "pub", get_mut = "pub")] - menu: Menu, - instant_exit: bool, - base_height: u32, - #[getset(get_mut = "pub")] - controller: MenuController, -} - -impl CommonMenu { - pub fn new(instant_exit: bool, base_height: u32, menu: Menu) -> Self { - Self { - menu, - instant_exit, - base_height, - controller: MenuController::default(), - } - } - - pub fn root_title(&self) -> &'static str { - self.menu.title() - } - - fn apply_decide_count(&mut self) -> Option { - match self.controller.update_decide() { - MenuControllerUpdateDecideResult::None => None, - MenuControllerUpdateDecideResult::Wait => Some(OnMenuInputResult::None), - MenuControllerUpdateDecideResult::Decide => { - if let Some(scene) = self.menu.dig() { - return Some(OnMenuInputResult::SubScene(scene)); - } - None - } - MenuControllerUpdateDecideResult::Cancel => { - if !self.menu.bury() { - return Some(OnMenuInputResult::Cancel); - } - None - } - } - } - - pub fn on_input_menu( - &mut self, - current_input: InputValue, - prev_input: InputValue, - th19: &Th19, - ) -> OnMenuInputResult { - if let Some(result) = self.apply_decide_count() { - return result; - } - - match self.current_menu_scene() { - CurrentMenuSceneResult::SubScene(scene) => OnMenuInputResult::SubScene(scene), - CurrentMenuSceneResult::Menu(menu) => { - let ignore_decide = menu.items().is_empty(); - let instant_decide = - ignore_decide || matches!(menu.selected_item(), MenuItem::Plain(_)); - let play_decide_sound = !ignore_decide - && menu - .selected_item() - .decided_action() - .map(|x| x.play_sound()) - .unwrap_or(true); - let root_cancel = !self.menu.decided() && self.instant_exit; - - let input_result = - self.controller - .input(current_input, prev_input, instant_decide, root_cancel); - - if root_cancel { - if let MenuControllerInputResult::Cancel = input_result { - return OnMenuInputResult::Cancel; - } - } - let CurrentMenuSceneMutResult::Menu(menu) = self.current_menu_scene_mut() else { - unreachable!() - }; - if let Some(action) = - menu.input(input_result, ignore_decide, play_decide_sound, th19) - { - OnMenuInputResult::Action(action) - } else { - OnMenuInputResult::None - } - } - CurrentMenuSceneResult::TextInput(_, _) => { - let CurrentMenuSceneMutResult::TextInput(text_input) = - self.current_menu_scene_mut() - else { - unreachable!() - }; - match text_input.on_input_menu(th19) { - text_input::OnMenuInputResult::None => OnMenuInputResult::None, - text_input::OnMenuInputResult::Cancel => { - th19.play_sound(th19.sound_manager(), 0x09, 0); - self.controller.force_cancel(); - OnMenuInputResult::None - } - text_input::OnMenuInputResult::Decide(changed_action, new_room_name) => { - th19.play_sound(th19.sound_manager(), 0x07, 0); - self.controller.force_cancel(); - let action = Action::new(changed_action, false, Some(new_room_name)); - OnMenuInputResult::Action(action) - } - } - } - } - } - - pub fn on_render_texts(&self, th19: &Th19, text_renderer: &c_void) { - match self.current_menu_scene() { - CurrentMenuSceneResult::SubScene { .. } => unreachable!(), - CurrentMenuSceneResult::Menu(menu) => { - render_title(th19, text_renderer, menu.title().as_bytes()); - menu.on_render_texts(self.base_height, th19, text_renderer); - } - CurrentMenuSceneResult::TextInput(label, text_input) => { - render_title(th19, text_renderer, label.as_bytes()); - text_input.on_render_texts(th19, text_renderer); - } - } - } - - fn current_menu_scene(&self) -> CurrentMenuSceneResult { - if !self.menu.decided() { - return CurrentMenuSceneResult::Menu(&self.menu); - } - let mut menu = &self.menu; - loop { - match menu.selected_item() { - MenuItem::Plain(_) => unreachable!(), - MenuItem::SubMenu(item) => { - if item.sub_menu().decided() { - menu = item.sub_menu(); - continue; - } - return CurrentMenuSceneResult::Menu(item.sub_menu()); - } - MenuItem::SubScene(item) => { - return CurrentMenuSceneResult::SubScene(item.sub_scene()); - } - MenuItem::TextInput(item) => { - return CurrentMenuSceneResult::TextInput(item.label(), item.text_input()); - } - } - } - } - - fn current_menu_scene_mut(&mut self) -> CurrentMenuSceneMutResult { - if !self.menu.decided() { - return CurrentMenuSceneMutResult::Menu(&mut self.menu); - } - let mut menu = &mut self.menu; - loop { - match menu.selected_item_mut() { - MenuItem::Plain(_) => unreachable!(), - MenuItem::SubMenu(item) => { - if item.sub_menu().decided() { - menu = item.sub_menu_mut(); - continue; - } - return CurrentMenuSceneMutResult::Menu(item.sub_menu_mut()); - } - MenuItem::SubScene(_item) => { - return CurrentMenuSceneMutResult::SubScene; - } - MenuItem::TextInput(item) => { - return CurrentMenuSceneMutResult::TextInput(item.text_input_mut()); - } - } - } - } -} diff --git a/junowen/src/lobby/helper.rs b/junowen/src/lobby/helper.rs deleted file mode 100644 index d2d8f30..0000000 --- a/junowen/src/lobby/helper.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::ffi::c_void; - -use junowen_lib::{Th19, structs::others::RenderingText}; - -use super::overlay::OverlayText; - -const TEXT_LINE_LEFT: u32 = 32; -const TEXT_LINE_TOP: u32 = 160; -const TEXT_LINE_HEIGHT: u32 = 32; -const SIGNALING_CODE_CHUNK_LEN: usize = 100; - -pub fn render_title(th19: &Th19, text_renderer: &c_void, text: &[u8]) { - let mut rt = RenderingText::default(); - rt.set_text(text); - rt.set_x(640, th19.window_inner()); - rt.set_y(64, th19.window_inner()); - rt.color = 0xff000000; - rt.font_type = 9; - rt.drop_shadow = true; - rt.horizontal_align = 0; - th19.render_text(text_renderer, &rt); - - rt.color = 0xffffffff; - rt.font_type = 7; - th19.render_text(text_renderer, &rt); -} - -pub fn render_menu_item( - th19: &Th19, - text_renderer: &c_void, - text: &[u8], - y: u32, - enabled: bool, - selected: bool, -) { - let mut rt = RenderingText::default(); - rt.set_text(text); - rt.set_x(640, th19.window_inner()); - rt.set_y(y, th19.window_inner()); - rt.color = menu_item_color(9, enabled, selected); - rt.font_type = 9; - rt.horizontal_align = 0; - th19.render_text(text_renderer, &rt); - - rt.color = menu_item_color(7, enabled, selected); - rt.font_type = 7; - th19.render_text(text_renderer, &rt); -} - -pub fn render_text_line(th19: &Th19, text_renderer: &c_void, line: u32, text: &[u8]) { - let mut rt = RenderingText::default(); - rt.set_text(text); - rt.set_x(TEXT_LINE_LEFT, th19.window_inner()); - rt.set_y(TEXT_LINE_TOP + line * TEXT_LINE_HEIGHT, th19.window_inner()); - rt.color = 0xff000000; - rt.font_type = 8; - th19.render_text(text_renderer, &rt); - - rt.color = 0xffffffff; - rt.font_type = 6; - th19.render_text(text_renderer, &rt); -} - -/// シグナリングコードは 1 行に収まらないので、半分の行高に詰めて折り返す -pub fn render_signaling_code(th19: &Th19, text_renderer: &c_void, line: u32, code: &str) { - for (i, chunk) in code.as_bytes().chunks(SIGNALING_CODE_CHUNK_LEN).enumerate() { - let mut rt = RenderingText::default(); - rt.set_text(chunk); - rt.set_x(TEXT_LINE_LEFT, th19.window_inner()); - rt.set_y( - TEXT_LINE_TOP + (line * 2 + i as u32) * (TEXT_LINE_HEIGHT / 2), - th19.window_inner(), - ); - rt.font_type = 1; - th19.render_text(text_renderer, &rt); - } -} - -/// render_signaling_code が消費する行数 -pub fn signaling_code_line_count(code: &str) -> u32 { - let chunks = code.as_bytes().chunks(SIGNALING_CODE_CHUNK_LEN).len(); - (chunks as f64 / 2.0).ceil() as u32 -} - -/// render_text_line と同じ位置へ多言語テキストを重ねる -pub fn overlay_text_line(line: u32, string: String) -> OverlayText { - OverlayText { - left: TEXT_LINE_LEFT as i32, - top: (TEXT_LINE_TOP + line * TEXT_LINE_HEIGHT) as i32, - font_size: TEXT_LINE_HEIGHT, - string, - } -} - -/// 画面下部にメニュー項目の説明文を重ねる -pub fn overlay_description(string: String) -> Vec { - if string.is_empty() { - return vec![]; - } - vec![OverlayText { - left: 96, - top: 640, - font_size: 58, - string, - }] -} - -pub fn menu_item_color(font_type: u32, enabled: bool, selected: bool) -> u32 { - if !enabled { - match font_type { - 9 => 0x40ffffff, - 7 => 0xff808080, - _ => unreachable!(), - } - } else if selected { - match font_type { - 9 => 0xff000000, - 7 => 0xffffff80, - _ => unreachable!(), - } - } else { - match font_type { - 9 => 0xff404040, - 7 => 0xff808060, - _ => unreachable!(), - } - } -} - -pub fn render_label_value( - th19: &Th19, - text_renderer: &c_void, - height: u32, - vertical_align: u32, - label: &str, - value: &str, -) { - let mut rt = RenderingText::default(); - rt.set_text(format!("{:<11}:", label).as_bytes()); - rt.set_x(320, th19.window_inner()); - rt.set_y(height, th19.window_inner()); - rt.color = 0xffffffff; - rt.font_type = 0; - rt.horizontal_align = 1; - rt.vertical_align = vertical_align; - th19.render_text(text_renderer, &rt); - - rt.set_text(value.as_bytes()); - rt.color = 0xffffffa0; - rt.set_x(544, th19.window_inner()); - th19.render_text(text_renderer, &rt); -} diff --git a/junowen/src/session/spectator.rs b/junowen/src/session/spectator.rs deleted file mode 100644 index 67cd8b5..0000000 --- a/junowen/src/session/spectator.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::sync::mpsc::RecvError; - -use anyhow::Result; -use derive_new::new; -use getset::{CopyGetters, Getters, Setters}; -use junowen_lib::{ - connection::{DataChannel, PeerConnection}, - structs::settings::GameSettings, -}; -use serde::{Deserialize, Serialize}; -use tracing::{error, info}; - -use super::{session_message::RoundInitial, to_channel}; - -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] -pub enum Screen { - DifficultySelect, - CharacterSelect, - Game, -} - -#[derive(new, Clone, Debug, Deserialize, CopyGetters, Serialize)] -pub struct InitialState { - #[get_copy = "pub"] - screen: Screen, - #[get_copy = "pub"] - difficulty: u8, - #[get_copy = "pub"] - p1_character: u8, - #[get_copy = "pub"] - p1_card: u8, - #[get_copy = "pub"] - p2_character: u8, - #[get_copy = "pub"] - p2_card: u8, -} - -#[derive(new, Clone, Debug, Deserialize, Getters, Serialize)] -pub struct SpectatorInitial { - #[get = "pub"] - p1_name: String, - #[get = "pub"] - p2_name: String, - #[get = "pub"] - game_settings: GameSettings, - #[get = "pub"] - initial_state: InitialState, -} - -#[derive(Debug, Deserialize, Serialize)] -pub enum SpectatorSessionMessage { - InitSpectator(SpectatorInitial), - InitRound(RoundInitial), - Inputs(u16, u16), -} - -#[derive(CopyGetters, Getters, Setters)] -pub struct SpectatorSession { - _conn: PeerConnection, - hook_incoming_rx: std::sync::mpsc::Receiver, - spectator_initial: Option, - round_initial: Option, -} - -impl SpectatorSession { - pub fn new(conn: PeerConnection, data_channel: DataChannel) -> Self { - let (_hook_outgoing_tx, hook_incoming_rx) = - to_channel(data_channel, |input| rmp_serde::from_slice(input)); - Self { - _conn: conn, - hook_incoming_rx, - spectator_initial: None, - round_initial: None, - } - } - - pub fn spectator_initial(&self) -> Option<&SpectatorInitial> { - self.spectator_initial.as_ref() - } - - pub fn recv_init_spectator(&mut self) -> Result<(), RecvError> { - let init = match self.hook_incoming_rx.recv()? { - SpectatorSessionMessage::InitSpectator(init) => init, - msg => { - error!("unexpected message: {:?}", msg); - return Err(RecvError); - } - }; - self.spectator_initial = Some(init); - Ok(()) - } - - pub fn dequeue_init_round(&mut self) -> Result { - if let Some(round_initial) = self.round_initial.take() { - return Ok(round_initial); - } - loop { - match self.hook_incoming_rx.recv()? { - SpectatorSessionMessage::InitSpectator(init) => { - error!("unexpected init spectator message: {:?}", init); - return Err(RecvError); - } - SpectatorSessionMessage::InitRound(round_initial) => return Ok(round_initial), - SpectatorSessionMessage::Inputs(..) => continue, - } - } - } - - pub fn dequeue_inputs(&mut self) -> Result<(u16, u16), RecvError> { - if self.round_initial.is_some() { - return Ok((0, 0)); - } - match self.hook_incoming_rx.recv()? { - SpectatorSessionMessage::InitSpectator(init) => { - error!("unexpected init spectator message: {:?}", init); - Err(RecvError) - } - SpectatorSessionMessage::InitRound(round_initial) => { - self.round_initial = Some(round_initial); - Ok((0, 0)) - } - SpectatorSessionMessage::Inputs(p1, p2) => Ok((p1, p2)), - } - } -} - -impl Drop for SpectatorSession { - fn drop(&mut self) { - info!("spectator session guest closed"); - } -} diff --git a/junowen/src/state/battle_session/in_session.rs b/junowen/src/state/battle_session/in_session.rs deleted file mode 100644 index ca2bde6..0000000 --- a/junowen/src/state/battle_session/in_session.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{borrow::Cow, ffi::c_void}; - -use junowen_lib::{Th19, structs::settings::GameSettings}; - -use crate::{ - signaling::waiting_for_match::{WaitingForPureP2pSpectator, WaitingForSpectator}, - state::render_parts::{render_footer, render_game_settings, render_names}, -}; - -use super::spectator_host::SpectatorHostState; - -pub struct RenderingStatus<'a> { - pub host: bool, - pub delay: u8, - pub p1_name: &'a str, - pub p2_name: &'a str, - pub game_settings: Option<&'a GameSettings>, - pub spectator_host_state: &'a SpectatorHostState, -} - -pub fn on_render_texts(th19: &Th19, text_renderer: &c_void, status: RenderingStatus) { - render_names(th19, text_renderer, status.p1_name, status.p2_name); - if let Some(game_settings) = status.game_settings { - render_game_settings(th19, text_renderer, game_settings); - } - - let (msg2_rear, msg2_front) = if status.spectator_host_state.count_spectators() > 0 { - ( - " ", - Cow::Owned(format!( - "Spectator(s): {}", - status.spectator_host_state.count_spectators() - )), - ) - } else { - match status.spectator_host_state.waiting() { - WaitingForSpectator::PureP2p(waiting) => match waiting { - WaitingForPureP2pSpectator::Standby { ready: false, .. } - | WaitingForPureP2pSpectator::SignalingCodeRecved { ready: false, .. } - | WaitingForPureP2pSpectator::SignalingCodeSent { ready: false, .. } => { - ("", "".into()) - } - WaitingForPureP2pSpectator::Standby { .. } => ( - " __ ", - "(Press F1 to accept spectator from clipboard)".into(), - ), - WaitingForPureP2pSpectator::SignalingCodeRecved { .. } => ( - " ", - "(Generating signaling code...)".into(), - ), - WaitingForPureP2pSpectator::SignalingCodeSent { .. } => ( - " ", - "(Your signaling code has been copied to the clipboard)".into(), - ), - }, - WaitingForSpectator::ReservedRoom(_) => ("", "".into()), - } - }; - - let delay_underline = if status.host { "_" } else { " " }; - let msg_front/* _ */= format!("Delay: {} {}", status.delay, msg2_front); - let msg_rear/* __ */= format!(" {} {}", delay_underline, msg2_rear); - - render_footer(th19, text_renderer, &msg_front, &msg_rear); -} diff --git a/junowen/src/state/battle_session/spectator_host.rs b/junowen/src/state/battle_session/spectator_host.rs deleted file mode 100644 index 7835082..0000000 --- a/junowen/src/state/battle_session/spectator_host.rs +++ /dev/null @@ -1,156 +0,0 @@ -use anyhow::{Result, bail}; -use getset::Getters; -use junowen_lib::{ - Th19, - structs::app::{MainMenu, ScreenId}, - structs::selection::Selection, -}; -use tracing::info; - -use crate::{ - session::{ - RoundInitial, - battle::BattleSession, - spectator::{self, InitialState, SpectatorInitial}, - spectator_host::SpectatorHostSession, - }, - signaling::waiting_for_match::WaitingForSpectator, -}; - -fn create_spectator_initial( - current_screen: ScreenId, - selection: &Selection, - battle_session: &BattleSession, - local_player_name: String, -) -> SpectatorInitial { - let p1_name = if battle_session.host() { - local_player_name.to_owned() - } else { - battle_session.remote_player_name().clone() - }; - let p2_name = if battle_session.host() { - battle_session.remote_player_name().clone() - } else { - local_player_name.to_owned() - }; - SpectatorInitial::new( - p1_name, - p2_name, - battle_session - .match_initial() - .as_ref() - .unwrap() - .game_settings - .clone(), - InitialState::new( - match current_screen { - ScreenId::DifficultySelect => spectator::Screen::DifficultySelect, - ScreenId::CharacterSelect => spectator::Screen::CharacterSelect, - _ => unreachable!(), - }, - selection.difficulty as u8, - selection.p1().character as u8, - selection.p1().card as u8, - selection.p2().character as u8, - selection.p2().card as u8, - ), - ) -} - -#[derive(Getters)] -pub struct SpectatorHostState { - #[get = "pub"] - waiting: WaitingForSpectator, - sessions: Vec, -} - -impl SpectatorHostState { - pub fn new(waiting: WaitingForSpectator) -> Self { - Self { - waiting, - sessions: Vec::new(), - } - } - - pub fn count_spectators(&self) -> usize { - self.sessions.len() - } - - pub fn send_init_round_if_connected(&mut self, th19: &Th19) { - self.sessions.retain(|session| { - if let Err(err) = session.send_init_round(RoundInitial { - seed1: th19.rand_seed1().unwrap(), - seed2: th19.rand_seed2().unwrap(), - seed3: th19.rand_seed3().unwrap(), - seed4: th19.rand_seed4().unwrap(), - }) { - info!("spectator host error: {:?}", err); - false - } else { - true - } - }); - } - - fn init_session( - &self, - session: &SpectatorHostSession, - battle_session: &BattleSession, - main_menu: Option<&MainMenu>, - th19: &Th19, - ) -> Result<()> { - let Some(main_menu) = main_menu else { - bail!("spectator not supported yet."); - }; - let vs_mode = th19.vs_mode(); - if main_menu.screen_id() != ScreenId::DifficultySelect - || vs_mode.p1_card() != 0 - || vs_mode.p2_card() != 0 - { - bail!( - "spectator not supported yet. screen={:?}, p1_card={}, p2_card={}", - main_menu.screen_id(), - vs_mode.p1_card(), - vs_mode.p2_card() - ); - } - session.send_init_spectator(create_spectator_initial( - main_menu.screen_id(), - th19.selection(), - battle_session, - th19.vs_mode().player_name().to_string(), - ))?; - session.send_init_round(RoundInitial { - seed1: th19.rand_seed1().unwrap(), - seed2: th19.rand_seed2().unwrap(), - seed3: th19.rand_seed3().unwrap(), - seed4: th19.rand_seed4().unwrap(), - })?; - Ok(()) - } - - pub fn update( - &mut self, - pushed: bool, - main_menu: Option<&MainMenu>, - th19: &Th19, - battle_session: &BattleSession, - p1_input: u16, - p2_input: u16, - ) { - if let Some(session) = self.waiting.try_recv_session(pushed, main_menu, th19) { - if let Err(err) = self.init_session(&session, battle_session, main_menu, th19) { - info!("initialize spectator failed: {:?}", err); - } else { - self.sessions.push(session); - } - } - self.sessions.retain(|session| { - if let Err(err) = session.send_inputs(p1_input, p2_input) { - info!("spectator host error: {:?}", err); - return false; - } - true - }); - } -} diff --git a/junowen/src/state/spectator_session/in_session.rs b/junowen/src/state/spectator_session/in_session.rs deleted file mode 100644 index 252aca9..0000000 --- a/junowen/src/state/spectator_session/in_session.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::ffi::c_void; - -use junowen_lib::Th19; - -use crate::state::render_parts::{render_footer, render_names}; - -pub fn on_render_texts_spectator( - th19: &Th19, - text_renderer: &c_void, - p1_name: &str, - p2_name: &str, -) { - render_names(th19, text_renderer, p1_name, p2_name); - render_footer(th19, text_renderer, "(Spectating)", ""); -} diff --git a/junowen/src/state/spectator_session/spectator_game.rs b/junowen/src/state/spectator_session/spectator_game.rs deleted file mode 100644 index d46e96e..0000000 --- a/junowen/src/state/spectator_session/spectator_game.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::sync::mpsc::RecvError; - -use anyhow::Result; -use junowen_lib::{Th19, structs::input_devices::InputValue}; - -use crate::session::spectator::SpectatorSession; - -pub struct SpectatorGame; - -impl SpectatorGame { - pub fn update_th19( - &mut self, - session: &mut SpectatorSession, - th19: &mut Th19, - ) -> Result<(), RecvError> { - // -1フレーム目、0フレーム目は複数回呼ばれ、回数が不定なのでスキップする - if th19.round_frame().unwrap().frame < 1 { - let input_devices = th19.input_devices_mut(); - input_devices - .p1_input_mut() - .set_current(InputValue::empty()); - input_devices - .p2_input_mut() - .set_current(InputValue::empty()); - return Ok(()); - } - let input_devices = th19.input_devices_mut(); - let (p1, p2) = session.dequeue_inputs()?; - input_devices - .p1_input_mut() - .set_current((p1 as u32).try_into().unwrap()); - input_devices - .p2_input_mut() - .set_current((p2 as u32).try_into().unwrap()); - Ok(()) - } - - pub fn on_round_over( - &mut self, - session: &mut SpectatorSession, - th19: &mut Th19, - ) -> Result<(), RecvError> { - let init = session.dequeue_init_round()?; - th19.set_rand_seed1(init.seed1).unwrap(); - th19.set_rand_seed2(init.seed2).unwrap(); - th19.set_rand_seed3(init.seed3).unwrap(); - th19.set_rand_seed4(init.seed4).unwrap(); - Ok(()) - } -} diff --git a/junowen/src/state/spectator_session/spectator_select.rs b/junowen/src/state/spectator_session/spectator_select.rs deleted file mode 100644 index c2dd359..0000000 --- a/junowen/src/state/spectator_session/spectator_select.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::sync::mpsc::RecvError; - -use anyhow::Result; -use derive_new::new; -use getset::{Getters, MutGetters}; -use junowen_lib::{ - Th19, - structs::{ - app::{MainMenu, ScreenId}, - input_devices::InputValue, - }, - th19_helpers::reset_cursors, -}; -use tracing::trace; - -use crate::session::spectator::{self, SpectatorSession}; - -#[derive(new, Getters, MutGetters)] -pub struct SpectatorSelect { - #[new(value = "0")] - initializing_state: u8, -} - -impl SpectatorSelect { - pub fn update_th19_on_input_players( - &mut self, - session: &mut SpectatorSession, - main_menu: &MainMenu, - th19: &mut Th19, - ) -> Result<(), RecvError> { - if self.initializing_state == 0 { - if session.spectator_initial().is_none() { - self.initializing_state = 1; - reset_cursors(th19); - session.recv_init_spectator()?; - } else { - self.initializing_state = 2; - } - let round_initial = session.dequeue_init_round()?; - th19.set_rand_seed1(round_initial.seed1).unwrap(); - th19.set_rand_seed2(round_initial.seed2).unwrap(); - th19.set_rand_seed3(round_initial.seed3).unwrap(); - th19.set_rand_seed4(round_initial.seed4).unwrap(); - } - if main_menu.screen_id() == ScreenId::DifficultySelect { - return Ok(()); - } - if self.initializing_state == 1 { - let init = session.spectator_initial().unwrap(); - match init.initial_state().screen() { - spectator::Screen::DifficultySelect => { - return Ok(()); - } - spectator::Screen::CharacterSelect => unimplemented!(), - spectator::Screen::Game => unimplemented!(), - } - } - if !th19.no_wait() { - th19.set_no_wait(true); - } - - let (p1, p2) = session.dequeue_inputs()?; - let input_devices = th19.input_devices_mut(); - input_devices - .p1_input_mut() - .set_current((p1 as u32).try_into().unwrap()); - input_devices - .p2_input_mut() - .set_current((p2 as u32).try_into().unwrap()); - - Ok(()) - } - - pub fn update_th19_on_input_menu( - &mut self, - session: &mut SpectatorSession, - main_menu: &mut MainMenu, - th19: &mut Th19, - ) -> Result<(), RecvError> { - if main_menu.screen_id() != ScreenId::DifficultySelect { - return Ok(()); - } - let menu = main_menu.menu_mut(); - if self.initializing_state == 1 { - let init = session.spectator_initial().unwrap(); - trace!("spectator_initial: {:?}", init); - let initial_state = init.initial_state(); - match initial_state.screen() { - spectator::Screen::DifficultySelect => { - if menu.cursor() != initial_state.difficulty() as u32 { - menu.set_cursor(initial_state.difficulty() as u32); - th19.menu_input_mut().set_current(InputValue::empty()); - return Ok(()); - } - let selection = th19.selection_mut(); - selection.p1_mut().character = initial_state.p1_character() as u32; - selection.p2_mut().character = initial_state.p2_character() as u32; - th19.set_no_wait(false); - self.initializing_state = 2; - } - spectator::Screen::CharacterSelect => unimplemented!(), - spectator::Screen::Game => unimplemented!(), - } - } - - let (p1, p2) = session.dequeue_inputs()?; - let input = if p1 != 0 { p1 } else { p2 }; - th19.menu_input_mut() - .set_current((input as u32).try_into().unwrap()); - Ok(()) - } -}