├── .cargo └── config.toml ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.ja.md ├── README.md ├── archives ├── th19netdelayemulate │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── th19onlinevsfix │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── th19padlight │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── th19replayplayer-lib │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── th19replayplayer │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── th19replayrecorder │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── th19savesettingsseparately │ ├── Cargo.toml │ └── src │ │ ├── character_selecter.rs │ │ ├── file.rs │ │ ├── lib.rs │ │ └── settings_editor.rs └── th19seed │ ├── Cargo.toml │ └── src │ └── main.rs ├── junowen-lib ├── Cargo.toml ├── examples │ ├── webrtc_guest.rs │ └── webrtc_host.rs └── src │ ├── connection.rs │ ├── connection │ ├── data_channel.rs │ ├── peer_connection.rs │ ├── signaling.rs │ └── signaling │ │ ├── socket.rs │ │ ├── socket │ │ ├── async_read_write_socket.rs │ │ └── channel_socket.rs │ │ └── stdio_signaling_interface.rs │ ├── find_process_id.rs │ ├── hook_utils.rs │ ├── hook_utils │ ├── dll_injection.rs │ └── load_library_w_addr.rs │ ├── lang.rs │ ├── lib.rs │ ├── macros.rs │ ├── memory_accessors.rs │ ├── memory_accessors │ ├── external_process.rs │ └── hooked_process.rs │ ├── signaling_server.rs │ ├── signaling_server │ ├── custom.rs │ ├── reserved_room.rs │ ├── room.rs │ └── room │ │ ├── delete_room.rs │ │ ├── post_room_join.rs │ │ ├── post_room_keep.rs │ │ └── put_room.rs │ ├── th19.rs │ ├── th19 │ ├── structs.rs │ ├── structs │ │ ├── app.rs │ │ ├── input_devices.rs │ │ ├── others.rs │ │ ├── selection.rs │ │ └── settings.rs │ └── th19_helpers.rs │ └── win_api_wrappers.rs ├── junowen-server ├── Cargo.toml ├── README.md ├── docs │ └── activity-diagrams.md └── src │ ├── database.rs │ ├── database │ ├── dynamodb.rs │ ├── dynamodb │ │ ├── reserved_room.rs │ │ └── shared_room.rs │ └── file.rs │ ├── main.rs │ ├── routes.rs │ ├── routes │ ├── custom.rs │ ├── reserved_room.rs │ ├── reserved_room │ │ ├── create.rs │ │ ├── delete.rs │ │ ├── read.rs │ │ └── update.rs │ └── room_utils.rs │ └── tracing_helper.rs ├── junowen ├── Cargo.toml ├── build.rs └── src │ ├── bin │ ├── junowen-standalone.rs │ └── lang │ │ ├── ja.toml │ │ └── mod.rs │ ├── file.rs │ ├── helper.rs │ ├── in_game_lobby.rs │ ├── in_game_lobby │ ├── common_menu.rs │ ├── common_menu │ │ ├── menu.rs │ │ ├── menu_controller.rs │ │ ├── menu_item.rs │ │ └── text_input.rs │ ├── helper.rs │ ├── lobby.rs │ ├── pure_p2p_guest.rs │ ├── pure_p2p_offerer.rs │ ├── room.rs │ ├── room │ │ ├── reserved.rs │ │ └── shared.rs │ └── title_menu_modifier.rs │ ├── lib.rs │ ├── session.rs │ ├── session │ ├── battle.rs │ ├── delayed_inputs.rs │ ├── session_message.rs │ ├── spectator.rs │ └── spectator_host.rs │ ├── signaling.rs │ ├── signaling │ ├── waiting_for_match.rs │ └── waiting_for_match │ │ ├── reserved_room_opponent_socket.rs │ │ ├── reserved_room_spectator_host_socket.rs │ │ ├── reserved_room_spectator_socket.rs │ │ ├── shared_room_opponent_socket.rs │ │ ├── socket.rs │ │ ├── waiting_for_spectator.rs │ │ └── waiting_in_room.rs │ ├── state.rs │ ├── state │ ├── battle_session_state.rs │ ├── battle_session_state │ │ ├── battle_game.rs │ │ ├── battle_select.rs │ │ ├── in_session.rs │ │ ├── spectator_host.rs │ │ └── utils.rs │ ├── junowen_state.rs │ ├── junowen_state │ │ ├── on_rewrite_controller_assignments.rs │ │ └── standby.rs │ ├── prepare.rs │ ├── render_parts.rs │ ├── spectator_session_state.rs │ └── spectator_session_state │ │ ├── in_session.rs │ │ ├── spectator_game.rs │ │ └── spectator_select.rs │ └── tracing_helper.rs └── th19loader ├── Cargo.toml ├── build.rs └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "i686-pc-windows-msvc" 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [progre] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /store.json 3 | 4 | *.log 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "archives/th19netdelayemulate", 4 | "archives/th19onlinevsfix", 5 | "archives/th19padlight", 6 | "archives/th19replayplayer", 7 | "archives/th19replayplayer-lib", 8 | "archives/th19replayrecorder", 9 | "archives/th19savesettingsseparately", 10 | "archives/th19seed", 11 | "junowen", 12 | "junowen-lib", 13 | "junowen-server", 14 | "th19loader", 15 | ] 16 | default-members = ["junowen"] 17 | resolver = "2" 18 | 19 | [workspace.package] 20 | version = "1.1.0-beta.1" 21 | authors = ["Progre"] 22 | license = "GPL-3.0" 23 | 24 | [workspace.dependencies] 25 | anyhow = { version = "1.0.86", features = ["backtrace"] } 26 | async-trait = "0.1.81" 27 | bytes = "1.7.1" 28 | clipboard-win = "5.4.0" 29 | junowen-lib = { path = "./junowen-lib" } 30 | rmp-serde = "1.3.0" 31 | serde = { version = "1.0.208", features = ["derive"] } 32 | serde_json = "1.0.125" 33 | static_vcruntime = "2.0" 34 | thiserror = "1.0.63" 35 | tokio = { version = "1.39.3", features = [ 36 | "rt", 37 | "macros", 38 | "rt-multi-thread", 39 | "time" 40 | ] } 41 | toml = "0.8.19" 42 | tracing = "0.1.40" 43 | tracing-subscriber = { version = "0.3.18", features = [ 44 | "env-filter", 45 | "local-time" 46 | ] } 47 | windows = { version = "0.58.0", features = [ 48 | "Win32_Foundation", 49 | "Win32_Graphics_Direct3D9", 50 | "Win32_Graphics_Gdi", 51 | "Win32_Security", 52 | "Win32_Storage_FileSystem", 53 | "Win32_System_Console", 54 | "Win32_System_Diagnostics_Debug", 55 | "Win32_System_Diagnostics_ToolHelp", 56 | "Win32_System_LibraryLoader", 57 | "Win32_System_Memory", 58 | "Win32_System_ProcessStatus", 59 | "Win32_System_SystemInformation", 60 | "Win32_System_SystemServices", 61 | "Win32_System_Threading", 62 | "Win32_UI_Input_KeyboardAndMouse", 63 | "Win32_UI_Shell", 64 | "Win32_UI_WindowsAndMessaging", 65 | ] } 66 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # Ju.N.Owen 2 | 3 | 東方獣王園の非公式オンライン対戦ツールです。 4 | 5 | 非公式のツールです。**自己責任で使用してください。** 6 | 7 | 公式のオンライン対戦のマッチングや同期機構とは異なる、独自の仕組みでオンライン対戦を実現します。 8 | adonis や th075caster と同じような仕組みで動作します。 9 | 10 | ## 特徴 11 | 12 | - 公式のオンライン対戦よりもずれにくい 13 | - ゲーム中にディレイを変更できる 14 | - サーバーなしでも接続できる 15 | - 観戦ができる 16 | 17 | ## インストール方法 18 | 19 | 1. zip ファイルを展開し、d3d9.dll と、modules フォルダーの中に th19_junowen.dll があることを確認します 20 | 2. 獣王園のインストールフォルダーを開きます 21 | 3. 獣王園のインストールフォルダーに d3d9.dll と modules フォルダーを移動します 22 | 4. 獣王園を起動します 23 | 5. うまくいけば獣王園のタイトル画面の項目に「Ju.N.Owen」が追加されます 24 | 25 | ## 使い方 26 | 27 | 現在3つの接続方法をサポートしています。 28 | 29 | ### Shared Room (共用ルーム) 30 | 31 | 設定したルーム名と一致するユーザーと接続する方式です。 32 | 接続待ちの間に他の機能を使用できます。 33 | 34 | ※ルーム名は「Online VS Mode」で設定してください。 35 | 36 | 接続待ち画面でショットボタンを押すと接続待ちが中断され、キャンセルボタンを押すと他の機能を使用できます。 37 | 38 | ### Reserved Room (専有ルーム) 39 | 40 | 設定したルーム名と一致するユーザーと接続する方式です。 41 | 対戦を他のプレイヤーに観戦してもらうことができます。 42 | 43 | ※ルーム名は「Online VS Mode」で設定してください。 44 | 45 | ### Pure P2P (サーバーを介さない接続) 46 | 47 | 接続サーバーを使わず、チャットなどで対戦相手と接続情報を交換する方式です。 48 | 49 | #### Pure P2P での対戦の仕方 50 | 51 | 1. 「Ju.N.Owen」→「Pure P2P」を選択します 52 | 2. ホストとして接続を待ち受ける場合は「Connect as a Host」を、 53 | ゲストとして接続する場合は「Connect as a Guset」を選択します 54 | - ホスト 55 | 1. `********` という長い文字列が表示され、自動的にクリップボードにコピーされるので、 56 | この文字列を Discord 等を使って対戦相手に送信してください 57 | 「Copy your code」を選択すると再度クリップボードにコピーされます 58 | 2. 対戦相手から `********` という文字列を受け取り、 59 | クリップボードにコピーしてください 60 | 3. 「Paste guest's code」を選択してください 61 | 4. うまくいけば難易度選択に遷移し、対戦が開始されます 62 | - ゲスト 63 | 1. 対戦相手から `********` という文字列を受け取り、クリップボードにコピーしてください 64 | 2. ショットボタンを押すと、クリップボードの内容が入力されます 65 | 3. `********` という長い文字列が表示され、自動的にクリップボードにコピーされるので、 66 | この文字列を Discord 等を使って対戦相手に送信してください 67 | ショットボタンを押すと再度クリップボードにコピーされます 68 | 4. うまくいけば難易度選択に遷移し、対戦が開始されます 69 | 70 | #### Pure P2P での観戦の仕方 71 | 72 | - 観戦者 73 | 1. 「Ju.N.Owen」→「Pure P2P」→「Connect as a Spectator」を選択します 74 | 2. `********` という長い文字列が表示され、自動的にクリップボードにコピーされるので、 75 | この文字列を Discord 等を使ってプレイヤーのどちらかに送信してください 76 | 「Copy your code」を選択すると再度クリップボードにコピーされます 77 | 3. プレイヤーから `********` という文字列を受け取り、 78 | クリップボードにコピーしてください 79 | 4. 「Paste guest's code」を選択してください 80 | 5. うまくいけば観戦が開始されます 81 | 6. ポーズボタンを押すと観戦を中止します 82 | - プレイヤー 83 | 1. Ju.N.Owen の対戦機能で対戦相手と接続し、難易度選択で待機します 84 | 2. 観戦者から `********` という文字列を受け取り、クリップボードにコピーしてください 85 | 3. F1 キーを押すと、クリップボードの内容が入力されます 86 | 4. `********` という長い文字列が表示され、自動的にクリップボードにコピーされるので、 87 | この文字列を Discord 等を使って対戦相手に送信してください 88 | 5. うまくいけば観戦が開始されます 89 | 90 | ### 接続後 91 | 92 | - 接続中はお互いの名前が画面上部に表示され、切断されると表示が消えます 93 | - ホストはゲーム中に数字キーの0-9でディレイ値を変更できます 94 | 95 | ## 補足 96 | 97 | - ポート開放は必要ありません 98 | - ポートを開放しもそのポートを指定することはできません 99 | 100 | ## 現在の制約 101 | 102 | - 「Online VS Mode」が解放されていないと正しく動作しません 103 | - 観戦者の追加はプレイヤーの接続直後のみ可能です 104 | - 通信が遅延したり良くないことが起きるとゲームがフレーズすることがあります 105 | 106 | ## 作者と配布元 107 | 108 | [ぷろぐれ](https://bsky.app/profile/progre.me) 109 | 110 | 111 | -------------------------------------------------------------------------------- /archives/th19netdelayemulate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19netdelayemulate" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | name = "th19netdelayemulate_hook" 10 | crate-type = ['cdylib'] 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | bytes = "1.5.0" 15 | interprocess = "1.2.1" 16 | junowen-lib.workspace = true 17 | windows = "0.51.1" 18 | -------------------------------------------------------------------------------- /archives/th19netdelayemulate/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, ffi::OsStr, io::Read, sync::mpsc, thread::spawn}; 2 | 3 | use bytes::{Buf, BufMut, BytesMut}; 4 | use interprocess::os::windows::named_pipe::{ByteReaderPipeStream, PipeListenerOptions, PipeMode}; 5 | use windows::Win32::{ 6 | Foundation::HINSTANCE, 7 | System::{Console::AllocConsole, SystemServices::DLL_PROCESS_ATTACH}, 8 | }; 9 | 10 | use junowen_lib::{FnOfHookAssembly, Th19}; 11 | 12 | static mut PROPS: Option = None; 13 | static mut STATE: Option = None; 14 | 15 | struct Props { 16 | original_fn_from_0aba30_00fb: Option, 17 | new_delay_receiver: mpsc::Receiver, 18 | } 19 | 20 | struct State { 21 | th19: Th19, 22 | p1_buffer: BytesMut, 23 | p2_buffer: BytesMut, 24 | } 25 | 26 | impl State { 27 | fn new(th19: Th19) -> Self { 28 | Self { 29 | th19, 30 | p1_buffer: BytesMut::new(), 31 | p2_buffer: BytesMut::new(), 32 | } 33 | } 34 | } 35 | 36 | fn props() -> &'static Props { 37 | unsafe { PROPS.as_ref().unwrap() } 38 | } 39 | 40 | fn state_mut() -> &'static mut State { 41 | unsafe { STATE.as_mut().unwrap() } 42 | } 43 | 44 | extern "fastcall" fn hook_0abb2b() { 45 | let th19 = &mut state_mut().th19; 46 | let state = state_mut(); 47 | 48 | let input_devices = th19.input_devices_mut(); 49 | 50 | let new_delay_receiver = &props().new_delay_receiver; 51 | if let Ok(delay) = new_delay_receiver.try_recv() { 52 | let old_delay = state.p1_buffer.len() / 4; 53 | println!("old delay: {}, new delay: {}", old_delay, delay); 54 | let delay = delay as usize; 55 | match delay.cmp(&old_delay) { 56 | Ordering::Less => { 57 | let skip = (old_delay - delay) * 4; 58 | state.p1_buffer.advance(skip); 59 | state.p2_buffer.advance(skip); 60 | } 61 | Ordering::Greater => { 62 | for _ in 0..(delay - old_delay) { 63 | state.p1_buffer.put_u32(0); 64 | state.p2_buffer.put_u32(0); 65 | } 66 | } 67 | Ordering::Equal => (), 68 | } 69 | } 70 | 71 | if !state.p1_buffer.is_empty() { 72 | let old_p1 = state.p1_buffer.get_u32().try_into().unwrap(); 73 | let p1 = input_devices.p1_input().current(); 74 | input_devices.p1_input_mut().set_current(old_p1); 75 | state.p1_buffer.put_u32(p1.bits()); 76 | 77 | let old_p2 = state.p2_buffer.get_u32().try_into().unwrap(); 78 | let p2 = input_devices.p2_input().current(); 79 | input_devices.p2_input_mut().set_current(old_p2); 80 | state.p2_buffer.put_u32(p2.bits()); 81 | } 82 | 83 | if let Some(func) = props().original_fn_from_0aba30_00fb { 84 | func() 85 | } 86 | } 87 | 88 | fn init_interprecess(tx: mpsc::Sender) { 89 | let pipe = PipeListenerOptions::new() 90 | .name(OsStr::new("th19netdelayemulate")) 91 | .mode(PipeMode::Bytes) 92 | .create() 93 | .unwrap(); 94 | 95 | let mut buf = [0; 1]; 96 | spawn(move || loop { 97 | let mut reader: ByteReaderPipeStream = pipe.accept().unwrap(); 98 | reader.read_exact(&mut buf).unwrap(); 99 | println!("pipe received {}", buf[0]); 100 | tx.send(buf[0] as i8).unwrap(); 101 | }); 102 | } 103 | 104 | #[no_mangle] 105 | pub extern "stdcall" fn DllMain(_inst_dll: HINSTANCE, reason: u32, _reserved: u32) -> bool { 106 | if reason == DLL_PROCESS_ATTACH { 107 | if cfg!(debug_assertions) { 108 | let _ = unsafe { AllocConsole() }; 109 | } 110 | let th19 = Th19::new_hooked_process("th19.exe").unwrap(); 111 | let (original_fn_from_0aba30_00fb, apply) = th19.hook_on_input_players(hook_0abb2b); 112 | let (tx, rx) = mpsc::channel(); 113 | init_interprecess(tx); 114 | unsafe { 115 | PROPS = Some(Props { 116 | original_fn_from_0aba30_00fb, 117 | new_delay_receiver: rx, 118 | }); 119 | STATE = Some(State::new(th19)); 120 | } 121 | apply(&mut state_mut().th19); 122 | } 123 | true 124 | } 125 | -------------------------------------------------------------------------------- /archives/th19netdelayemulate/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{self, current_exe}, 3 | ffi::OsStr, 4 | io::Write, 5 | }; 6 | 7 | use anyhow::Result; 8 | use interprocess::os::windows::named_pipe::ByteWriterPipeStream; 9 | use junowen_lib::hook_utils::do_dll_injection; 10 | 11 | fn main() -> Result<()> { 12 | let name = OsStr::new("th19netdelayemulate"); 13 | let mut pipe = if let Ok(pipe) = ByteWriterPipeStream::connect(name) { 14 | println!("フック済みのDLLに接続しました"); 15 | pipe 16 | } else { 17 | let dll_path = current_exe()? 18 | .as_path() 19 | .parent() 20 | .unwrap() 21 | .join(concat!(env!("CARGO_PKG_NAME"), "_hook.dll")); 22 | 23 | do_dll_injection("th19.exe", &dll_path)?; 24 | 25 | let name = OsStr::new("th19netdelayemulate"); 26 | ByteWriterPipeStream::connect(name).unwrap() 27 | }; 28 | 29 | let buf = [env::args().nth(1).unwrap().parse::().unwrap(); 1]; 30 | pipe.write_all(&buf).unwrap(); 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /archives/th19onlinevsfix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19onlinevsfix" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | crate-type = ['cdylib'] 10 | name = "th19_onlinevsfix" 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | junowen-lib.workspace = true 15 | windows = "0.51.1" 16 | -------------------------------------------------------------------------------- /archives/th19onlinevsfix/src/lib.rs: -------------------------------------------------------------------------------- 1 | use junowen_lib::{ 2 | hook_utils::WELL_KNOWN_VERSION_HASHES, th19_helpers::reset_cursors, FnOfHookAssembly, Th19, 3 | }; 4 | use windows::Win32::{ 5 | Foundation::{HINSTANCE, HMODULE}, 6 | Graphics::Direct3D9::IDirect3D9, 7 | System::{ 8 | Console::AllocConsole, 9 | SystemServices::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH}, 10 | }, 11 | }; 12 | 13 | static mut MODULE: HMODULE = HMODULE(0); 14 | static mut PROPS: Option = None; 15 | static mut STATE: Option = None; 16 | 17 | struct Props { 18 | old_on_waiting_online_vs_connection: Option, 19 | } 20 | 21 | struct State { 22 | th19: Th19, 23 | } 24 | 25 | fn props() -> &'static Props { 26 | unsafe { PROPS.as_ref().unwrap() } 27 | } 28 | 29 | fn state_mut() -> &'static mut State { 30 | unsafe { STATE.as_mut().unwrap() } 31 | } 32 | 33 | #[no_mangle] 34 | pub extern "stdcall" fn DllMain(inst_dll: HINSTANCE, reason: u32, _reserved: u32) -> bool { 35 | match reason { 36 | DLL_PROCESS_ATTACH => { 37 | unsafe { MODULE = inst_dll.into() }; 38 | } 39 | DLL_PROCESS_DETACH => {} 40 | _ => {} 41 | } 42 | true 43 | } 44 | 45 | #[allow(non_snake_case)] 46 | #[no_mangle] 47 | pub extern "C" fn CheckVersion(hash: *const u8, length: usize) -> bool { 48 | let valid_hash = &WELL_KNOWN_VERSION_HASHES.v110c_steam; 49 | if length != valid_hash.len() { 50 | return false; 51 | } 52 | for (i, &valid_hash_byte) in valid_hash.iter().enumerate() { 53 | if unsafe { *(hash.wrapping_add(i)) } != valid_hash_byte { 54 | return false; 55 | } 56 | } 57 | true 58 | } 59 | 60 | extern "fastcall" fn on_waiting_online_vs_connection() { 61 | reset_cursors(&mut state_mut().th19); 62 | 63 | if let Some(func) = props().old_on_waiting_online_vs_connection { 64 | func() 65 | } 66 | } 67 | 68 | #[allow(non_snake_case)] 69 | #[no_mangle] 70 | pub extern "C" fn Initialize(_direct_3d: *const IDirect3D9) -> bool { 71 | if cfg!(debug_assertions) { 72 | let _ = unsafe { AllocConsole() }; 73 | std::env::set_var("RUST_BACKTRACE", "1"); 74 | } 75 | 76 | let th19 = Th19::new_hooked_process("th19.exe").unwrap(); 77 | let (old_on_waiting_online_vs_connection, apply) = 78 | th19.hook_on_waiting_online_vs_connection(on_waiting_online_vs_connection); 79 | unsafe { 80 | PROPS = Some(Props { 81 | old_on_waiting_online_vs_connection, 82 | }); 83 | STATE = Some(State { th19 }); 84 | } 85 | apply(&mut state_mut().th19); 86 | 87 | true 88 | } 89 | -------------------------------------------------------------------------------- /archives/th19padlight/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19padlight" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | name = "th19padlight_hook" 10 | crate-type = ['cdylib'] 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | junowen-lib.workspace = true 15 | windows = { workspace = true, features = [ 16 | "Win32_Foundation", 17 | "Win32_Graphics_Direct3D9", 18 | "Win32_Graphics_Gdi", 19 | "Win32_System_Console", 20 | "Win32_System_Diagnostics_Debug", 21 | "Win32_System_Diagnostics_ToolHelp", 22 | "Win32_System_LibraryLoader", 23 | "Win32_System_Memory", 24 | "Win32_System_ProcessStatus", 25 | "Win32_System_SystemInformation", 26 | "Win32_System_SystemServices", 27 | "Win32_System_Threading", 28 | "Win32_UI_WindowsAndMessaging", 29 | ] } 30 | -------------------------------------------------------------------------------- /archives/th19padlight/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_exe; 2 | 3 | use anyhow::Result; 4 | use junowen_lib::hook_utils::do_dll_injection; 5 | 6 | fn main() -> Result<()> { 7 | let dll_path = current_exe()? 8 | .as_path() 9 | .parent() 10 | .unwrap() 11 | .join(concat!(env!("CARGO_PKG_NAME"), "_hook.dll")); 12 | 13 | do_dll_injection("th19.exe", &dll_path)?; 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /archives/th19replayplayer-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19replayplayer-lib" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | bytes = "1.5.0" 11 | interprocess = "1.2.1" 12 | junowen-lib.workspace = true 13 | windows = "0.51.1" 14 | -------------------------------------------------------------------------------- /archives/th19replayplayer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19replayplayer" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | name = "th19replayplayer_hook" 10 | crate-type = ['cdylib'] 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | bytes = "1.5.0" 15 | interprocess = "1.2.1" 16 | junowen-lib.workspace = true 17 | th19replayplayer-lib = { path = "../th19replayplayer-lib" } 18 | windows = "0.51.1" 19 | -------------------------------------------------------------------------------- /archives/th19replayplayer/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{self, current_exe}, 3 | ffi::OsStr, 4 | io::Write, 5 | thread::sleep, 6 | time::Duration, 7 | }; 8 | 9 | use anyhow::Result; 10 | use bytes::{BufMut, BytesMut}; 11 | use interprocess::os::windows::named_pipe::ByteWriterPipeStream; 12 | use junowen_lib::hook_utils::do_dll_injection; 13 | 14 | fn main() -> Result<()> { 15 | let replay_file = env::args().nth(1).unwrap(); 16 | 17 | let pkg_name = env!("CARGO_PKG_NAME"); 18 | let name = OsStr::new(pkg_name); 19 | let mut pipe = if let Ok(pipe) = ByteWriterPipeStream::connect(name) { 20 | println!("フック済みのDLLに接続しました"); 21 | pipe 22 | } else { 23 | let dll_path = current_exe()? 24 | .as_path() 25 | .parent() 26 | .unwrap() 27 | .join(concat!(env!("CARGO_PKG_NAME"), "_hook.dll")); 28 | 29 | do_dll_injection("th19.exe", &dll_path)?; 30 | 31 | let name = OsStr::new(pkg_name); 32 | loop { 33 | if let Ok(pipe) = ByteWriterPipeStream::connect(name) { 34 | break pipe; 35 | } 36 | println!("waiting for pipe..."); 37 | sleep(Duration::from_secs(3)); 38 | } 39 | }; 40 | 41 | let replay_file_bytes = replay_file.as_bytes(); 42 | let mut buf = BytesMut::with_capacity(4); 43 | buf.put_u32_le(replay_file_bytes.len() as u32); 44 | pipe.write_all(&buf).unwrap(); 45 | pipe.write_all(replay_file_bytes).unwrap(); 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /archives/th19replayrecorder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19replayrecorder" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | crate-type = ['cdylib'] 10 | name = "th19_replayrecorder" 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | bytes = "1.5.0" 15 | chrono = "0.4.30" 16 | junowen-lib.workspace = true 17 | th19replayplayer-lib = { path = "../th19replayplayer-lib" } 18 | windows = { version = "0.51.1", features = [ 19 | "Win32_Foundation", 20 | "Win32_Graphics_Direct3D9", 21 | "Win32_System_Console", 22 | "Win32_System_Diagnostics_Debug", 23 | "Win32_System_Diagnostics_ToolHelp", 24 | "Win32_System_LibraryLoader", 25 | "Win32_System_Memory", 26 | "Win32_System_ProcessStatus", 27 | "Win32_System_SystemInformation", 28 | "Win32_System_SystemServices", 29 | "Win32_System_Threading", 30 | ] } 31 | -------------------------------------------------------------------------------- /archives/th19savesettingsseparately/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19savesettingsseparately" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | crate-type = ['cdylib'] 10 | name = "th19_savesettingsseparately" 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | junowen-lib.workspace = true 15 | windows = { version = "0.51.1", features = [ 16 | "Win32_Foundation", 17 | "Win32_Graphics_Direct3D9", 18 | "Win32_System_Console", 19 | "Win32_System_Diagnostics_Debug", 20 | "Win32_System_Diagnostics_ToolHelp", 21 | "Win32_System_LibraryLoader", 22 | "Win32_System_Memory", 23 | "Win32_System_ProcessStatus", 24 | "Win32_System_SystemInformation", 25 | "Win32_System_SystemServices", 26 | "Win32_System_Threading", 27 | ] } 28 | -------------------------------------------------------------------------------- /archives/th19savesettingsseparately/src/character_selecter.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::th19_helpers::is_network_mode; 4 | 5 | use crate::{file::read_from_file, props, state_mut}; 6 | 7 | pub extern "thiscall" fn post_read_battle_settings_from_menu_to_game( 8 | this: *const c_void, 9 | arg1: u32, 10 | ) -> u32 { 11 | let prop = props(); 12 | let th19 = &mut state_mut().th19; 13 | let func = prop.original_fn_from_13f9d0_0446; 14 | if is_network_mode(th19) { 15 | return func(this, arg1); 16 | } 17 | 18 | // ファイルから読み込んだ設定を適用 19 | let battle_settings = read_from_file(&prop.settings_path) 20 | .or_else(|_| th19.game_settings_in_menu()) 21 | .unwrap(); 22 | th19.put_game_settings_in_game(&battle_settings).unwrap(); 23 | 24 | func(this, arg1) 25 | } 26 | -------------------------------------------------------------------------------- /archives/th19savesettingsseparately/src/file.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | 3 | use anyhow::{bail, Result}; 4 | use junowen_lib::structs::settings::GameSettings; 5 | 6 | pub fn read_from_file(settings_path: &str) -> Result { 7 | let vec = std::fs::read(settings_path)?; 8 | if vec.len() != 12 { 9 | bail!("Invalid file size"); 10 | } 11 | let mut bytes = [0u8; 12]; 12 | bytes.copy_from_slice(&vec); 13 | Ok(unsafe { transmute::<[u8; 12], GameSettings>(bytes) }) 14 | } 15 | 16 | pub fn write_to_file(settings_path: &str, battle_settings: &GameSettings) -> Result<()> { 17 | let contents: &[u8; 12] = unsafe { transmute(battle_settings) }; 18 | Ok(std::fs::write(settings_path, contents)?) 19 | } 20 | -------------------------------------------------------------------------------- /archives/th19savesettingsseparately/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod character_selecter; 2 | mod file; 3 | mod settings_editor; 4 | 5 | use std::path::Path; 6 | 7 | use junowen_lib::{ 8 | hook_utils::WELL_KNOWN_VERSION_HASHES, structs::settings::GameSettings, Fn002530, Fn009fa0, 9 | Fn012480, Th19, 10 | }; 11 | use settings_editor::{on_close_settings_editor, on_open_settings_editor}; 12 | use windows::{ 13 | core::PCWSTR, 14 | Win32::{ 15 | Foundation::{HINSTANCE, HMODULE, MAX_PATH}, 16 | Graphics::Direct3D9::IDirect3D9, 17 | System::{ 18 | LibraryLoader::GetModuleFileNameW, 19 | SystemServices::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH}, 20 | }, 21 | }, 22 | }; 23 | 24 | use character_selecter::post_read_battle_settings_from_menu_to_game; 25 | 26 | static mut MODULE: HMODULE = HMODULE(0); 27 | static mut PROPS: Option = None; 28 | static mut STATE: Option = None; 29 | 30 | struct Props { 31 | settings_path: String, 32 | original_fn_from_13f9d0_0446: Fn009fa0, 33 | original_fn_from_107540_0046: Fn012480, 34 | original_fn_from_107540_0937: Fn002530, 35 | } 36 | 37 | impl Props { 38 | fn new( 39 | original_fn_from_13f9d0_0446: Fn009fa0, 40 | original_fn_from_107540_0046: Fn012480, 41 | original_fn_from_107540_0937: Fn002530, 42 | ) -> Self { 43 | let dll_path = { 44 | let mut buf = [0u16; MAX_PATH as usize]; 45 | if unsafe { GetModuleFileNameW(MODULE, &mut buf) } == 0 { 46 | panic!(); 47 | } 48 | unsafe { PCWSTR::from_raw(buf.as_ptr()).to_string() }.unwrap() 49 | }; 50 | 51 | Self { 52 | settings_path: Path::new(&dll_path) 53 | .with_extension("cfg") 54 | .to_string_lossy() 55 | .to_string(), 56 | original_fn_from_13f9d0_0446, 57 | original_fn_from_107540_0046, 58 | original_fn_from_107540_0937, 59 | } 60 | } 61 | } 62 | 63 | struct State { 64 | th19: Th19, 65 | tmp_battle_settings: GameSettings, 66 | } 67 | 68 | fn props() -> &'static Props { 69 | unsafe { PROPS.as_ref().unwrap() } 70 | } 71 | 72 | fn state() -> &'static State { 73 | unsafe { STATE.as_ref().unwrap() } 74 | } 75 | fn state_mut() -> &'static mut State { 76 | unsafe { STATE.as_mut().unwrap() } 77 | } 78 | 79 | #[no_mangle] 80 | pub extern "stdcall" fn DllMain(inst_dll: HINSTANCE, reason: u32, _reserved: u32) -> bool { 81 | match reason { 82 | DLL_PROCESS_ATTACH => { 83 | unsafe { MODULE = inst_dll.into() }; 84 | } 85 | DLL_PROCESS_DETACH => {} 86 | _ => {} 87 | } 88 | true 89 | } 90 | 91 | #[allow(non_snake_case)] 92 | #[no_mangle] 93 | pub extern "C" fn CheckVersion(hash: *const u8, length: usize) -> bool { 94 | let valid_hash = &WELL_KNOWN_VERSION_HASHES.v110c_steam; 95 | if length != valid_hash.len() { 96 | return false; 97 | } 98 | for (i, &valid_hash_byte) in valid_hash.iter().enumerate() { 99 | if unsafe { *(hash.wrapping_add(i)) } != valid_hash_byte { 100 | return false; 101 | } 102 | } 103 | true 104 | } 105 | 106 | #[allow(non_snake_case)] 107 | #[no_mangle] 108 | pub extern "C" fn Initialize(_direct_3d: *const IDirect3D9) -> bool { 109 | let th19 = Th19::new_hooked_process("th19.exe").unwrap(); 110 | let (original_fn_from_13f9d0_0446, apply_hook_13f9d0_0446) = 111 | th19.hook_13f9d0_0446(post_read_battle_settings_from_menu_to_game); 112 | let (original_fn_from_107540_0046, apply_hook_107540_0046) = 113 | th19.hook_107540_0046(on_open_settings_editor); 114 | let (original_fn_from_107540_0937, apply_hook_107540_0937) = 115 | th19.hook_107540_0937(on_close_settings_editor); 116 | unsafe { 117 | PROPS = Some(Props::new( 118 | original_fn_from_13f9d0_0446, 119 | original_fn_from_107540_0046, 120 | original_fn_from_107540_0937, 121 | )); 122 | STATE = Some(State { 123 | th19, 124 | tmp_battle_settings: Default::default(), 125 | }); 126 | } 127 | let th19 = &mut state_mut().th19; 128 | apply_hook_13f9d0_0446(th19); 129 | apply_hook_107540_0046(th19); 130 | apply_hook_107540_0937(th19); 131 | 132 | true 133 | } 134 | -------------------------------------------------------------------------------- /archives/th19savesettingsseparately/src/settings_editor.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::th19_helpers::is_network_mode; 4 | 5 | use crate::{ 6 | file::{read_from_file, write_to_file}, 7 | props, state, state_mut, 8 | }; 9 | 10 | // 1. 画面を開くときに本来の値をメモリーから退避し、ファイルの値をメモリーに適用する 11 | // 2. 画面を閉じるときにメモリーの値をファイルに書き出し、本来の値をメモリーに戻す 12 | // 既知の不具合: 編集中に正規の手段で終了すると値が保存されてしまう 13 | 14 | pub extern "thiscall" fn on_open_settings_editor(this: *const c_void, arg1: u32) -> u32 { 15 | let props = props(); 16 | let th19 = &mut state_mut().th19; 17 | let func = props.original_fn_from_107540_0046; 18 | if is_network_mode(th19) { 19 | return func(this, arg1); 20 | } 21 | 22 | // ファイルから読み込んだ設定を適用 23 | state_mut().tmp_battle_settings = th19.game_settings_in_menu().unwrap(); 24 | let settings_of_file = read_from_file(&props.settings_path) 25 | .or_else(|_| th19.game_settings_in_menu()) 26 | .unwrap(); 27 | th19.put_game_settings_in_menu(&settings_of_file).unwrap(); 28 | 29 | func(this, arg1) 30 | } 31 | 32 | pub extern "thiscall" fn on_close_settings_editor(this: *const c_void) { 33 | let props = props(); 34 | let th19 = &mut state_mut().th19; 35 | let func = props.original_fn_from_107540_0937; 36 | if is_network_mode(th19) { 37 | return func(this); 38 | } 39 | 40 | // ファイルに書き出し 41 | let current = th19.game_settings_in_menu().unwrap(); 42 | write_to_file(&props.settings_path, ¤t).unwrap(); 43 | th19.put_game_settings_in_menu(&state().tmp_battle_settings) 44 | .unwrap(); 45 | 46 | func(this) 47 | } 48 | -------------------------------------------------------------------------------- /archives/th19seed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19seed" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | junowen-lib.workspace = true 11 | -------------------------------------------------------------------------------- /archives/th19seed/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | use anyhow::Result; 4 | 5 | use junowen_lib::Th19; 6 | 7 | fn main() -> Result<()> { 8 | let mut args = args(); 9 | args.next(); 10 | let seed1 = args.next().and_then(|x| x.parse::().ok()); 11 | let seed2 = args.next().and_then(|x| x.parse::().ok()); 12 | let mut th19 = Th19::new_external_process("th19.exe")?; 13 | if let (Some(seed1), Some(seed2)) = (seed1, seed2) { 14 | th19.set_rand_seed1(seed1)?; 15 | th19.set_rand_seed2(seed2)?; 16 | Ok(()) 17 | } else { 18 | let seed1 = th19.rand_seed1()?; 19 | let seed2 = th19.rand_seed2()?; 20 | println!("seed: {} {}", seed1, seed2); 21 | Ok(()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /junowen-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "junowen-lib" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | async-trait.workspace = true 11 | base64 = "0.22.1" 12 | bytes.workspace = true 13 | clipboard-win.workspace = true 14 | derivative = "2.2.0" 15 | derive-new = "0.6.0" 16 | flagset = "0.4.6" 17 | flate2 = "1.0.32" 18 | getset = "0.1.2" 19 | hex-literal = "0.4.1" 20 | http = "1.1.0" 21 | num_enum = "0.7.3" 22 | regex = "1.10.6" 23 | rmp-serde.workspace = true 24 | serde.workspace = true 25 | serde_json.workspace = true 26 | sha3 = "0.10.8" 27 | sys-locale = "0.3.1" 28 | thiserror.workspace = true 29 | tokio.workspace = true 30 | toml.workspace = true 31 | tracing.workspace = true 32 | uuid = "1.10.0" 33 | webrtc = "0.11.0" 34 | windows.workspace = true 35 | -------------------------------------------------------------------------------- /junowen-lib/examples/webrtc_guest.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bytes::Bytes; 3 | use tokio::{net::windows::named_pipe, spawn}; 4 | 5 | use junowen_lib::{ 6 | connection::signaling::{ 7 | socket::{AsyncReadWriteSocket, SignalingSocket}, 8 | stdio_signaling_interface::connect_as_answerer, 9 | }, 10 | lang::Lang, 11 | }; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<()> { 15 | let name = &format!(r"\\.\pipe\{}", env!("CARGO_PKG_NAME")); 16 | let server_pipe = named_pipe::ServerOptions::new().create(name).unwrap(); 17 | let mut client_pipe = named_pipe::ClientOptions::new().open(name).unwrap(); 18 | server_pipe.connect().await?; 19 | 20 | let task = spawn(async move { 21 | let mut socket = AsyncReadWriteSocket::new(server_pipe); 22 | socket.receive_signaling().await.unwrap() 23 | }); 24 | connect_as_answerer(&mut client_pipe, &Lang::resolve()) 25 | .await 26 | .unwrap(); 27 | let (_conn, mut dc, _host) = task.await.unwrap(); 28 | 29 | let msg = dc.recv().await.unwrap(); 30 | println!("msg: {:?}", msg); 31 | dc.message_sender 32 | .send(Bytes::from_iter(b"pong".iter().copied())) 33 | .await?; 34 | let msg = dc.recv().await.unwrap(); 35 | println!("msg: {:?}", msg); 36 | dc.message_sender 37 | .send(Bytes::from_iter(b"pong".iter().copied())) 38 | .await?; 39 | let msg = dc.recv().await; 40 | println!("msg: {:?}", msg); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /junowen-lib/examples/webrtc_host.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bytes::Bytes; 3 | use tokio::{net::windows::named_pipe, spawn}; 4 | 5 | use junowen_lib::{ 6 | connection::signaling::{ 7 | socket::{AsyncReadWriteSocket, SignalingSocket}, 8 | stdio_signaling_interface::connect_as_offerer, 9 | }, 10 | lang::Lang, 11 | }; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<()> { 15 | let name = &format!(r"\\.\pipe\{}", env!("CARGO_PKG_NAME")); 16 | let server_pipe = named_pipe::ServerOptions::new().create(name).unwrap(); 17 | let mut client_pipe = named_pipe::ClientOptions::new().open(name).unwrap(); 18 | server_pipe.connect().await?; 19 | 20 | let task = spawn(async move { 21 | let mut socket = AsyncReadWriteSocket::new(server_pipe); 22 | socket.receive_signaling().await.unwrap() 23 | }); 24 | connect_as_offerer(&mut client_pipe, &Lang::resolve()) 25 | .await 26 | .unwrap(); 27 | let (_conn, mut dc, _host) = task.await.unwrap(); 28 | 29 | dc.message_sender 30 | .send(Bytes::from_iter(b"ping".iter().copied())) 31 | .await?; 32 | let msg = dc.recv().await.unwrap(); 33 | println!("msg: {:?}", msg); 34 | dc.message_sender 35 | .send(Bytes::from_iter(b"ping".iter().copied())) 36 | .await?; 37 | let msg = dc.recv().await.unwrap(); 38 | println!("msg: {:?}", msg); 39 | dc.message_sender 40 | .send(Bytes::from_iter(b"bye".iter().copied())) 41 | .await?; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /junowen-lib/src/connection.rs: -------------------------------------------------------------------------------- 1 | mod data_channel; 2 | mod peer_connection; 3 | pub mod signaling; 4 | 5 | pub use self::{data_channel::DataChannel, peer_connection::PeerConnection}; 6 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/data_channel.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use bytes::Bytes; 4 | use tokio::{ 5 | spawn, 6 | sync::{broadcast, mpsc, oneshot}, 7 | }; 8 | use tracing::{error, warn}; 9 | use webrtc::data_channel::RTCDataChannel; 10 | 11 | pub struct DataChannel { 12 | rtc: Arc, 13 | open_rx: Option>, 14 | close_rx: mpsc::Receiver<()>, 15 | pc_disconnected_rx: broadcast::Receiver<()>, 16 | pub message_sender: mpsc::Sender, 17 | incoming_message_rx: mpsc::Receiver, 18 | } 19 | 20 | impl DataChannel { 21 | pub async fn new( 22 | rtc: Arc, 23 | pc_disconnected_rx: broadcast::Receiver<()>, 24 | ) -> Self { 25 | let (open_tx, open_rx) = oneshot::channel(); 26 | let mut open_tx = Some(open_tx); 27 | let (message_sender, mut outgoing_message_receiver) = mpsc::channel(1); 28 | let (incoming_message_tx, incoming_message_rx) = mpsc::channel(1); 29 | let (close_sender, close_rx) = mpsc::channel(1); 30 | rtc.on_open(Box::new(move || { 31 | let open_sender = open_tx.take().unwrap(); 32 | Box::pin(async move { open_sender.send(()).unwrap() }) 33 | })); 34 | rtc.on_message(Box::new(move |msg| { 35 | let incoming_message_tx = incoming_message_tx.clone(); 36 | Box::pin(async move { 37 | let _ = incoming_message_tx.send(msg.data).await; 38 | }) 39 | })); 40 | rtc.on_error(Box::new(|err| { 41 | warn!("{}", err); 42 | Box::pin(async {}) 43 | })); 44 | rtc.on_close(Box::new(move || { 45 | let close_sender = close_sender.clone(); 46 | Box::pin(async move { 47 | let _ = close_sender.send(()).await; 48 | }) 49 | })); 50 | rtc.on_buffered_amount_low(Box::new(|| Box::pin(async {}))) 51 | .await; 52 | 53 | { 54 | // NOTE: To make it possible to have separate references for receiving and sending, 55 | // sending is implemented with a channel and a task. 56 | // Or, it would be nice to have something like tokio::io::{ReadHalf, WriteHalf}. 57 | let rtc = rtc.clone(); 58 | spawn(async move { 59 | while let Some(data) = outgoing_message_receiver.recv().await { 60 | match rtc.send(&data).await { 61 | Ok(_) => {} 62 | Err(webrtc::Error::ErrClosedPipe) => return, 63 | Err(err) => { 64 | error!("outgoing_message_receiver.recv() failed: {}", err); 65 | } 66 | } 67 | } 68 | }); 69 | } 70 | 71 | Self { 72 | rtc, 73 | open_rx: Some(open_rx), 74 | close_rx, 75 | pc_disconnected_rx, 76 | message_sender, 77 | incoming_message_rx, 78 | } 79 | } 80 | 81 | pub fn protocol(&self) -> &str { 82 | self.rtc.protocol() 83 | } 84 | 85 | pub async fn wait_for_open_data_channel(&mut self) { 86 | self.open_rx.take().unwrap().await.unwrap() 87 | } 88 | 89 | /// This method returns `None` if either `incoming_message_rx`, 90 | /// `RTCDataChannel`, or `RTCPeerConnection` is closed. 91 | pub async fn recv(&mut self) -> Option { 92 | tokio::select! { 93 | result = self.incoming_message_rx.recv() => result, 94 | _ = self.close_rx.recv() => None, 95 | _ = self.pc_disconnected_rx.recv() => None, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/signaling.rs: -------------------------------------------------------------------------------- 1 | pub mod socket; 2 | #[cfg(target_os = "windows")] 3 | pub mod stdio_signaling_interface; 4 | 5 | use std::io::Write; 6 | 7 | use anyhow::{anyhow, bail, Result}; 8 | use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; 9 | use flate2::{ 10 | write::{DeflateDecoder, DeflateEncoder}, 11 | Compression, 12 | }; 13 | use regex::Regex; 14 | use serde::{Deserialize, Serialize}; 15 | use webrtc::peer_connection::sdp::{ 16 | sdp_type::RTCSdpType, session_description::RTCSessionDescription, 17 | }; 18 | 19 | #[derive(Clone, Copy, PartialEq)] 20 | pub enum SignalingCodeType { 21 | BattleOffer, 22 | BattleAnswer, 23 | SpectatorOffer, 24 | SpectatorAnswer, 25 | } 26 | 27 | impl SignalingCodeType { 28 | pub fn to_string(&self, desc: &CompressedSdp) -> String { 29 | let tag = match self { 30 | Self::BattleOffer => "offer", 31 | Self::BattleAnswer => "answer", 32 | Self::SpectatorOffer => "s-offer", 33 | Self::SpectatorAnswer => "s-answer", 34 | }; 35 | format!("<{}>{}", tag, desc.0, tag,) 36 | } 37 | } 38 | 39 | #[derive(Clone, Debug, Deserialize, Serialize)] 40 | pub struct CompressedSdp(String); 41 | 42 | impl CompressedSdp { 43 | pub fn into_inner(self) -> String { 44 | self.0 45 | } 46 | 47 | pub fn compress(desc: &RTCSessionDescription) -> Self { 48 | let mut e = DeflateEncoder::new(Vec::new(), Compression::best()); 49 | e.write_all(desc.sdp.as_bytes()).unwrap(); 50 | let compressed_bytes = e.finish().unwrap(); 51 | Self(BASE64_STANDARD_NO_PAD.encode(compressed_bytes)) 52 | } 53 | } 54 | 55 | pub fn parse_signaling_code(code: &str) -> Result<(SignalingCodeType, CompressedSdp)> { 56 | let code = Regex::new(r"\s").unwrap().replace_all(code, ""); 57 | let captures = Regex::new(r#"<(.+?)>(.+?)"#) 58 | .unwrap() 59 | .captures(&code) 60 | .ok_or_else(|| anyhow!("Failed to parse"))?; 61 | let tag = &captures[1]; 62 | let tag_end = &captures[3]; 63 | let desc = &captures[2]; 64 | if tag != tag_end { 65 | bail!("unmatched tag: <{}>", tag, tag_end); 66 | } 67 | let sct = match tag { 68 | "offer" => SignalingCodeType::BattleOffer, 69 | "answer" => SignalingCodeType::BattleAnswer, 70 | "s-offer" => SignalingCodeType::SpectatorOffer, 71 | "s-answer" => SignalingCodeType::SpectatorAnswer, 72 | _ => bail!("unknown tag: {}", tag), 73 | }; 74 | Ok((sct, CompressedSdp(desc.to_owned()))) 75 | } 76 | 77 | pub fn decompress_session_description( 78 | sdp_type: RTCSdpType, 79 | csdp: CompressedSdp, 80 | ) -> Result { 81 | let compressed_bytes = BASE64_STANDARD_NO_PAD.decode(csdp.0)?; 82 | let mut d = DeflateDecoder::new(Vec::new()); 83 | d.write_all(&compressed_bytes)?; 84 | let sdp = String::from_utf8_lossy(&d.finish()?).to_string(); 85 | Ok(match sdp_type { 86 | RTCSdpType::Offer => RTCSessionDescription::offer(sdp)?, 87 | RTCSdpType::Answer => RTCSessionDescription::answer(sdp)?, 88 | RTCSdpType::Pranswer | RTCSdpType::Unspecified | RTCSdpType::Rollback => unreachable!(), 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/signaling/socket.rs: -------------------------------------------------------------------------------- 1 | pub mod async_read_write_socket; 2 | pub mod channel_socket; 3 | 4 | use std::time::Duration; 5 | 6 | use anyhow::{Context, Result}; 7 | use async_trait::async_trait; 8 | 9 | use crate::connection::data_channel::DataChannel; 10 | 11 | use super::super::peer_connection::PeerConnection; 12 | 13 | use super::CompressedSdp; 14 | 15 | pub use async_read_write_socket::AsyncReadWriteSocket; 16 | 17 | pub enum OfferResponse { 18 | Offer(CompressedSdp), 19 | Answer(CompressedSdp), 20 | } 21 | 22 | #[async_trait] 23 | pub trait SignalingSocket { 24 | fn timeout() -> Duration; 25 | async fn offer(&mut self, desc: CompressedSdp) -> Result; 26 | async fn answer(&mut self, desc: CompressedSdp) -> Result<()>; 27 | 28 | async fn receive_signaling(&mut self) -> Result<(PeerConnection, DataChannel, bool)> { 29 | let mut conn = PeerConnection::new(Self::timeout()).await?; 30 | let offer_desc = conn 31 | .start_as_offerer() 32 | .await 33 | .context("Failed to start as host")?; 34 | let answer_desc = self.offer(offer_desc).await?; 35 | let (mut conn, host) = match answer_desc { 36 | OfferResponse::Answer(answer_desc) => { 37 | conn.set_answer_desc(answer_desc) 38 | .await 39 | .context("Failed to set answer desc")?; 40 | (conn, true) 41 | } 42 | OfferResponse::Offer(offer_desc) => { 43 | let mut conn = PeerConnection::new(Self::timeout()).await?; 44 | let answer_desc = conn 45 | .start_as_answerer(offer_desc) 46 | .await 47 | .context("Failed to start as guest")?; 48 | self.answer(answer_desc).await?; 49 | (conn, false) 50 | } 51 | }; 52 | let data_channel = conn.wait_for_open_data_channel().await?; 53 | Ok((conn, data_channel, host)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/signaling/socket/async_read_write_socket.rs: -------------------------------------------------------------------------------- 1 | use std::{io, time::Duration}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use async_trait::async_trait; 5 | use serde::{Deserialize, Serialize}; 6 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; 7 | 8 | use super::{super::CompressedSdp, OfferResponse, SignalingSocket}; 9 | 10 | #[derive(Debug, Deserialize, Serialize)] 11 | pub enum SignalingServerMessage { 12 | RequestAnswer(CompressedSdp), 13 | SetAnswerDesc(CompressedSdp), 14 | } 15 | 16 | #[derive(Deserialize, Serialize)] 17 | pub enum SignalingClientMessage { 18 | OfferDesc(CompressedSdp), 19 | AnswerDesc(CompressedSdp), 20 | } 21 | 22 | pub struct AsyncReadWriteSocket 23 | where 24 | T: AsyncRead + AsyncWrite + Unpin + Send + Sync, 25 | { 26 | read_write: T, 27 | } 28 | 29 | impl AsyncReadWriteSocket 30 | where 31 | T: AsyncRead + AsyncWrite + Unpin + Send + Sync, 32 | { 33 | pub fn new(read_write: T) -> Self { 34 | Self { read_write } 35 | } 36 | 37 | async fn send(&mut self, msg: SignalingClientMessage) -> Result<(), io::Error> { 38 | self.read_write 39 | .write_all(&rmp_serde::to_vec(&msg).unwrap()) 40 | .await 41 | } 42 | 43 | async fn recv(&mut self) -> Result { 44 | let mut buf = [0u8; 4 * 1024]; 45 | let len = self.read_write.read(&mut buf).await?; 46 | rmp_serde::from_slice(&buf[..len]) 47 | .map_err(|err| anyhow!("parse failed (len={}): {}", len, err)) 48 | } 49 | 50 | pub fn into_inner(self) -> T { 51 | self.read_write 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl SignalingSocket for AsyncReadWriteSocket 57 | where 58 | T: AsyncRead + AsyncWrite + Unpin + Send + Sync, 59 | { 60 | fn timeout() -> Duration { 61 | Duration::from_secs(20 * 60) 62 | } 63 | 64 | async fn offer(&mut self, desc: CompressedSdp) -> Result { 65 | self.send(SignalingClientMessage::OfferDesc(desc)).await?; 66 | Ok(match self.recv().await? { 67 | SignalingServerMessage::SetAnswerDesc(answer_desc) => { 68 | OfferResponse::Answer(answer_desc) 69 | } 70 | SignalingServerMessage::RequestAnswer(offer_desc) => OfferResponse::Offer(offer_desc), 71 | }) 72 | } 73 | 74 | async fn answer(&mut self, desc: CompressedSdp) -> Result<()> { 75 | Ok(self.send(SignalingClientMessage::AnswerDesc(desc)).await?) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/signaling/socket/channel_socket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use tokio::sync::oneshot; 6 | 7 | use super::{ 8 | super::CompressedSdp, async_read_write_socket::SignalingServerMessage, OfferResponse, 9 | SignalingSocket, 10 | }; 11 | 12 | pub struct ChannelSocket { 13 | offer_sender: Option>, 14 | answer_sender: Option>, 15 | message_receiver: Option>, 16 | } 17 | 18 | impl ChannelSocket { 19 | pub fn new( 20 | offer_sender: oneshot::Sender, 21 | answer_sender: oneshot::Sender, 22 | message_receiver: oneshot::Receiver, 23 | ) -> Self { 24 | Self { 25 | offer_sender: Some(offer_sender), 26 | answer_sender: Some(answer_sender), 27 | message_receiver: Some(message_receiver), 28 | } 29 | } 30 | } 31 | 32 | #[async_trait] 33 | impl SignalingSocket for ChannelSocket { 34 | fn timeout() -> Duration { 35 | Duration::from_secs(20 * 60) 36 | } 37 | 38 | async fn offer(&mut self, desc: CompressedSdp) -> Result { 39 | self.offer_sender.take().unwrap().send(desc).unwrap(); 40 | Ok(match self.message_receiver.take().unwrap().await? { 41 | SignalingServerMessage::SetAnswerDesc(answer_desc) => { 42 | OfferResponse::Answer(answer_desc) 43 | } 44 | SignalingServerMessage::RequestAnswer(offer_desc) => OfferResponse::Offer(offer_desc), 45 | }) 46 | } 47 | 48 | async fn answer(&mut self, desc: CompressedSdp) -> Result<()> { 49 | self.answer_sender.take().unwrap().send(desc).unwrap(); 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /junowen-lib/src/connection/signaling/stdio_signaling_interface.rs: -------------------------------------------------------------------------------- 1 | use std::{io, process::exit}; 2 | 3 | use anyhow::Result; 4 | use clipboard_win::set_clipboard_string; 5 | use tokio::{ 6 | io::{AsyncReadExt, AsyncWriteExt}, 7 | net::windows::named_pipe::NamedPipeClient, 8 | }; 9 | 10 | use crate::{connection::signaling::SignalingCodeType, lang::Lang}; 11 | 12 | use super::{ 13 | socket::async_read_write_socket::{SignalingClientMessage, SignalingServerMessage}, 14 | CompressedSdp, 15 | }; 16 | 17 | fn read_line() -> String { 18 | let mut buf = String::new(); 19 | io::stdin().read_line(&mut buf).unwrap_or_else(|_| exit(1)); 20 | buf.trim().to_owned() 21 | } 22 | 23 | fn read_line_loop(lang: &Lang, msg: &str) -> String { 24 | loop { 25 | lang.println(msg); 26 | let buf = read_line(); 27 | if !buf.trim().is_empty() { 28 | break buf; 29 | } 30 | } 31 | } 32 | 33 | fn offer_desc(lang: &Lang) -> CompressedSdp { 34 | CompressedSdp(read_line_loop(lang, "Input host's signaling code:")) 35 | } 36 | 37 | fn print_offer_desc_and_get_answer_desc(lang: &Lang, offer_desc: CompressedSdp) -> CompressedSdp { 38 | lang.println("Your signaling code:"); 39 | let offer_str = SignalingCodeType::BattleOffer.to_string(&offer_desc); 40 | println!(); 41 | println!("{}", offer_str); 42 | println!(); 43 | set_clipboard_string(&offer_str).unwrap(); 44 | lang.println("It was copied to your clipboard. Share your signaling code with your guest."); 45 | println!(); 46 | let answer_desc = CompressedSdp(read_line_loop(lang, "Input guest's signaling code:")); 47 | lang.println("Waiting for guest to connect..."); 48 | answer_desc 49 | } 50 | 51 | fn print_answer_desc(lang: &Lang, answer_desc: CompressedSdp) { 52 | lang.println("Your signaling code:"); 53 | let answer_str = SignalingCodeType::BattleAnswer.to_string(&answer_desc); 54 | println!(); 55 | println!("{}", answer_str); 56 | println!(); 57 | set_clipboard_string(&answer_str).unwrap(); 58 | lang.println("It was copied to your clipboard. Share your signaling code with your host."); 59 | println!(); 60 | lang.println("Waiting for host to connect..."); 61 | } 62 | 63 | async fn send(pipe: &mut NamedPipeClient, msg: SignalingServerMessage) -> Result<(), io::Error> { 64 | pipe.write_all(&rmp_serde::to_vec(&msg).unwrap()).await 65 | } 66 | 67 | async fn recv(pipe: &mut NamedPipeClient) -> Result { 68 | let mut buf = [0u8; 4 * 1024]; 69 | let len = pipe.read(&mut buf).await?; 70 | Ok(rmp_serde::from_slice(&buf[..len]).unwrap()) 71 | } 72 | 73 | pub async fn connect_as_offerer( 74 | client_pipe: &mut NamedPipeClient, 75 | lang: &Lang, 76 | ) -> Result<(), io::Error> { 77 | let SignalingClientMessage::OfferDesc(offer_desc) = recv(client_pipe).await? else { 78 | panic!("unexpected message"); 79 | }; 80 | 81 | let answer_desc = print_offer_desc_and_get_answer_desc(lang, offer_desc); 82 | send( 83 | client_pipe, 84 | SignalingServerMessage::SetAnswerDesc(answer_desc), 85 | ) 86 | .await?; 87 | Ok(()) 88 | } 89 | 90 | pub async fn connect_as_answerer( 91 | client_pipe: &mut NamedPipeClient, 92 | lang: &Lang, 93 | ) -> Result<(), io::Error> { 94 | let SignalingClientMessage::OfferDesc(_) = recv(client_pipe).await? else { 95 | panic!("unexpected message"); 96 | }; 97 | let offer_desc = offer_desc(lang); 98 | send( 99 | client_pipe, 100 | SignalingServerMessage::RequestAnswer(offer_desc), 101 | ) 102 | .await?; 103 | let SignalingClientMessage::AnswerDesc(answer_desc) = recv(client_pipe).await? else { 104 | panic!("unexpected message"); 105 | }; 106 | print_answer_desc(lang, answer_desc); 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /junowen-lib/src/find_process_id.rs: -------------------------------------------------------------------------------- 1 | use std::mem::{size_of, transmute}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use windows::Win32::System::Diagnostics::ToolHelp::{ 5 | CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS, 6 | }; 7 | 8 | use crate::win_api_wrappers::SafeHandle; 9 | 10 | fn find_process_id_in_snapshot(snapshot: SafeHandle, exe_file: &str) -> Option { 11 | let mut pe = PROCESSENTRY32 { 12 | dwSize: size_of::() as u32, 13 | cntUsage: 0, 14 | th32ProcessID: 0, 15 | th32DefaultHeapID: 0, 16 | th32ModuleID: 0, 17 | cntThreads: 0, 18 | th32ParentProcessID: 0, 19 | pcPriClassBase: 0, 20 | dwFlags: 0, 21 | szExeFile: [0; 260], 22 | }; 23 | if unsafe { Process32First(snapshot.0, &mut pe) }.is_err() { 24 | return None; 25 | } 26 | loop { 27 | let current = 28 | String::from_utf8_lossy(unsafe { transmute::<&[i8], &[u8]>(&pe.szExeFile[..]) }); 29 | if current.contains(exe_file) { 30 | return Some(pe.th32ProcessID); 31 | } 32 | 33 | if unsafe { Process32Next(snapshot.0, &mut pe) }.is_err() { 34 | return None; 35 | } 36 | } 37 | } 38 | 39 | pub fn find_process_id(exe_file: &str) -> Result { 40 | let snapshot = SafeHandle(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?); 41 | 42 | let process_id = find_process_id_in_snapshot(snapshot, exe_file) 43 | .ok_or_else(|| anyhow!("process not found"))?; 44 | 45 | Ok(process_id) 46 | } 47 | -------------------------------------------------------------------------------- /junowen-lib/src/hook_utils.rs: -------------------------------------------------------------------------------- 1 | mod dll_injection; 2 | mod load_library_w_addr; 3 | 4 | use std::{fs::File, io::Read}; 5 | 6 | use hex_literal::hex; 7 | use sha3::digest::Digest; // using for Sha3_224::new() 8 | use sha3::{digest::generic_array::GenericArray, Sha3_224}; 9 | use windows::{ 10 | core::{HSTRING, PCWSTR}, 11 | Win32::{ 12 | Foundation::{HWND, MAX_PATH}, 13 | System::LibraryLoader::GetModuleFileNameW, 14 | UI::WindowsAndMessaging::{MessageBoxW, MB_ICONWARNING, MB_OK}, 15 | }, 16 | }; 17 | 18 | pub use dll_injection::{do_dll_injection, DllInjectionError}; 19 | 20 | pub fn show_warn_dialog(msg: &str) { 21 | unsafe { 22 | MessageBoxW( 23 | HWND::default(), 24 | &HSTRING::from(msg), 25 | &HSTRING::from(env!("CARGO_PKG_NAME")), 26 | MB_ICONWARNING | MB_OK, 27 | ) 28 | }; 29 | } 30 | 31 | pub fn calc_th19_hash() -> Vec { 32 | let mut buf = [0u16; MAX_PATH as usize]; 33 | if unsafe { GetModuleFileNameW(None, &mut buf) } == 0 { 34 | panic!(); 35 | } 36 | let exe_file_path = unsafe { PCWSTR::from_raw(buf.as_ptr()).to_string() }.unwrap(); 37 | let mut file = File::open(exe_file_path).unwrap(); 38 | let mut buffer = Vec::new(); 39 | file.read_to_end(&mut buffer).unwrap(); 40 | let mut hasher: Sha3_224 = Sha3_224::new(); 41 | hasher.update(&buffer); 42 | let hash: GenericArray<_, _> = hasher.finalize(); 43 | hash.to_vec() 44 | } 45 | 46 | pub struct WellKnownVersionHashes { 47 | pub v110c: [u8; 28], 48 | pub v110c_steam: [u8; 28], 49 | } 50 | 51 | impl WellKnownVersionHashes { 52 | pub fn all_v110c(&self) -> [&[u8; 28]; 2] { 53 | [&self.v110c, &self.v110c_steam] 54 | } 55 | } 56 | 57 | pub const WELL_KNOWN_VERSION_HASHES: WellKnownVersionHashes = WellKnownVersionHashes { 58 | v110c: hex!("f7cfd5dc38a4cab6efd91646264b09f21cd79409d568f23b7cbfd359"), 59 | v110c_steam: hex!("a2bbb4ff6c7ee5bd1126b536416762f2bea3b83ebf351f24cb66af64"), 60 | }; 61 | -------------------------------------------------------------------------------- /junowen-lib/src/hook_utils/dll_injection.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem::{size_of_val, transmute}, 3 | os::raw::c_void, 4 | path::Path, 5 | }; 6 | 7 | use anyhow::{Error, Result}; 8 | use windows::{ 9 | core::HSTRING, 10 | Win32::{ 11 | Foundation::FALSE, 12 | System::{ 13 | Diagnostics::Debug::WriteProcessMemory, 14 | Memory::{VirtualAllocEx, VirtualFreeEx, MEM_COMMIT, MEM_RELEASE, PAGE_READWRITE}, 15 | Threading::{ 16 | CreateRemoteThread, OpenProcess, WaitForSingleObject, LPTHREAD_START_ROUTINE, 17 | PROCESS_ALL_ACCESS, 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | use crate::{find_process_id::find_process_id, win_api_wrappers::SafeHandle}; 24 | 25 | use super::load_library_w_addr::load_library_w_addr; 26 | 27 | struct VirtualAllocatedMem<'a> { 28 | process: &'a SafeHandle, 29 | pub addr: *mut c_void, 30 | } 31 | 32 | impl<'a> VirtualAllocatedMem<'a> { 33 | pub fn new(process: &'a SafeHandle, size: usize) -> Self { 34 | Self { 35 | process, 36 | addr: unsafe { VirtualAllocEx(process.0, None, size, MEM_COMMIT, PAGE_READWRITE) }, 37 | } 38 | } 39 | } 40 | 41 | impl<'a> Drop for VirtualAllocatedMem<'a> { 42 | fn drop(&mut self) { 43 | unsafe { VirtualFreeEx(self.process.0, self.addr, 0, MEM_RELEASE) }.unwrap(); 44 | } 45 | } 46 | 47 | #[derive(Debug, thiserror::Error)] 48 | pub enum DllInjectionError { 49 | #[error("DLL not found")] 50 | DllNotFound, 51 | #[error("Process not found: {}", .0)] 52 | ProcessNotFound(Error), 53 | } 54 | 55 | pub fn do_dll_injection(exe_file: &str, dll_path: &Path) -> Result<(), DllInjectionError> { 56 | if !dll_path.exists() { 57 | return Err(DllInjectionError::DllNotFound); 58 | } 59 | let process_id = find_process_id(exe_file).map_err(DllInjectionError::ProcessNotFound)?; 60 | let process = SafeHandle( 61 | unsafe { OpenProcess(PROCESS_ALL_ACCESS, FALSE, process_id) } 62 | .map_err(|err| DllInjectionError::ProcessNotFound(Error::new(err)))?, 63 | ); 64 | let dll_path_hstr = HSTRING::from(dll_path); 65 | let dll_path_hstr_size = size_of_val(dll_path_hstr.as_wide()); 66 | let remote_dll_path_wstr = VirtualAllocatedMem::new(&process, dll_path_hstr_size); 67 | 68 | unsafe { 69 | WriteProcessMemory( 70 | process.0, 71 | remote_dll_path_wstr.addr, 72 | dll_path_hstr.as_ptr() as _, 73 | dll_path_hstr_size, 74 | None, 75 | ) 76 | } 77 | .unwrap(); 78 | let load_library_w_addr = load_library_w_addr(process_id).unwrap(); 79 | let thread = SafeHandle( 80 | unsafe { 81 | CreateRemoteThread( 82 | process.0, 83 | None, 84 | 0, 85 | transmute::(load_library_w_addr), 86 | Some(remote_dll_path_wstr.addr), 87 | 0, 88 | None, 89 | ) 90 | } 91 | .unwrap(), 92 | ); 93 | 94 | unsafe { WaitForSingleObject(thread.0, u32::MAX) }; // wait thread 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /junowen-lib/src/lang.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs}; 2 | 3 | use sys_locale::get_locales; 4 | 5 | #[derive(derive_new::new)] 6 | pub struct Lang { 7 | lang: HashMap, 8 | } 9 | 10 | impl Lang { 11 | pub fn resolve() -> Self { 12 | let lang = get_locales() 13 | .flat_map(|tag| { 14 | let primary_lang = tag.split('-').next().unwrap_or(&tag).to_owned(); 15 | [tag, primary_lang] 16 | }) 17 | .filter_map(|tag| fs::read_to_string(format!("lang/{}.toml", tag)).ok()) 18 | .find_map(|file| toml::from_str(&file).ok()) 19 | .unwrap_or_default(); 20 | Self { lang } 21 | } 22 | 23 | pub fn print(&self, msg: &str) { 24 | print!("{}", self.lang.get(msg).map(|s| s.as_str()).unwrap_or(msg)); 25 | } 26 | 27 | pub fn println(&self, msg: &str) { 28 | println!("{}", self.lang.get(msg).map(|s| s.as_str()).unwrap_or(msg)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /junowen-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | #[cfg(target_os = "windows")] 3 | mod find_process_id; 4 | #[cfg(target_os = "windows")] 5 | pub mod hook_utils; 6 | #[cfg(target_os = "windows")] 7 | pub mod lang; 8 | #[cfg(target_os = "windows")] 9 | mod macros; 10 | #[cfg(target_os = "windows")] 11 | mod memory_accessors; 12 | pub mod signaling_server; 13 | #[cfg(target_os = "windows")] 14 | mod th19; 15 | #[cfg(target_os = "windows")] 16 | mod win_api_wrappers; 17 | #[cfg(target_os = "windows")] 18 | pub use crate::th19::*; 19 | -------------------------------------------------------------------------------- /junowen-lib/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! hook { 3 | ($addr:expr, $hook:ident, $type:ty) => { 4 | pub fn $hook(&self, target: $type) -> ($type, ApplyFn) { 5 | let addr = $addr; 6 | unsafe { 7 | transmute::<(usize, ApplyFn), ($type, ApplyFn)>(self.hook_call(addr, target as _)) 8 | } 9 | } 10 | }; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! hook_todo { 15 | ($addr:expr, $hook:ident, $type:ty) => { 16 | pub fn $hook(&self, _target: $type) -> ($type, ApplyFn) { 17 | todo!() 18 | } 19 | }; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! u16_prop { 24 | ($addr:expr, $getter:ident) => { 25 | pub fn $getter(&self) -> Result { 26 | self.memory_accessor.read_u16($addr) 27 | } 28 | }; 29 | 30 | ($addr:expr, $getter:ident, $setter:ident) => { 31 | $crate::u16_prop!($addr, $getter); 32 | pub fn $setter(&mut self, value: u16) -> Result<()> { 33 | self.memory_accessor.write_u16($addr, value) 34 | } 35 | }; 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! u32_prop { 40 | ($addr:expr, $getter:ident) => { 41 | pub fn $getter(&self) -> Result { 42 | self.memory_accessor.read_u32($addr) 43 | } 44 | }; 45 | 46 | ($addr:expr, $getter:ident, $setter:ident) => { 47 | $crate::u32_prop!($addr, $getter); 48 | pub fn $setter(&mut self, value: u32) -> Result<()> { 49 | self.memory_accessor.write_u32($addr, value) 50 | } 51 | }; 52 | } 53 | 54 | #[macro_export] 55 | macro_rules! u32_prop_todo { 56 | ($addr:expr, $getter:ident) => { 57 | pub fn $getter(&self) -> Result { 58 | todo!("u32_prop"); 59 | } 60 | }; 61 | 62 | ($addr:expr, $getter:ident, $setter:ident) => { 63 | $crate::u32_prop_todo!($addr, $getter); 64 | pub fn $setter(&mut self, value: u32) -> Result<()> { 65 | todo!("u32_prop"); 66 | } 67 | }; 68 | } 69 | 70 | #[macro_export] 71 | macro_rules! pointer { 72 | ($addr:expr, $getter:ident, $type:ty) => { 73 | pub fn $getter(&self) -> &'static $type { 74 | self.pointer($addr).unwrap() 75 | } 76 | }; 77 | ($addr:expr, $getter:ident, $getter_mut:ident, $type:ty) => { 78 | pointer!($addr, $getter, $type); 79 | pub fn $getter_mut(&mut self) -> &'static mut $type { 80 | self.pointer_mut($addr).unwrap() 81 | } 82 | }; 83 | } 84 | 85 | #[macro_export] 86 | macro_rules! ptr_opt { 87 | ($addr:expr, $getter:ident, $type:ty) => { 88 | pub fn $getter(&self) -> Option<&'static $type> { 89 | self.pointer($addr) 90 | } 91 | }; 92 | ($addr:expr, $getter:ident, $getter_mut:ident, $type:ty) => { 93 | ptr_opt!($addr, $getter, $type); 94 | pub fn $getter_mut(&mut self) -> Option<&'static mut $type> { 95 | self.pointer_mut($addr) 96 | } 97 | }; 98 | } 99 | 100 | #[macro_export] 101 | macro_rules! value_ref { 102 | ($addr:expr, $getter:ident, $type:ty) => { 103 | pub fn $getter(&self) -> &'static $type { 104 | self.value_ref($addr) 105 | } 106 | }; 107 | ($addr:expr, $getter:ident, $getter_mut:ident, $type:ty) => { 108 | value_ref!($addr, $getter, $type); 109 | pub fn $getter_mut(&mut self) -> &'static mut $type { 110 | self.value_mut($addr) 111 | } 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /junowen-lib/src/memory_accessors.rs: -------------------------------------------------------------------------------- 1 | mod external_process; 2 | mod hooked_process; 3 | 4 | use anyhow::Result; 5 | 6 | pub use external_process::ExternalProcess; 7 | pub use hooked_process::FnOfHookAssembly; 8 | pub use hooked_process::HookedProcess; 9 | 10 | pub enum MemoryAccessor { 11 | ExternalProcess(ExternalProcess), 12 | HookedProcess(HookedProcess), 13 | } 14 | 15 | impl MemoryAccessor { 16 | pub fn read_u32(&self, addr: usize) -> Result { 17 | let mut buffer = [0; 4]; 18 | self.read(addr, &mut buffer)?; 19 | Ok(u32::from_le_bytes(buffer)) 20 | } 21 | 22 | #[allow(unused)] 23 | pub fn write_u32(&mut self, addr: usize, value: u32) -> Result<()> { 24 | self.write(addr, &value.to_le_bytes()) 25 | } 26 | 27 | pub fn read(&self, addr: usize, buffer: &mut [u8]) -> Result<()> { 28 | match self { 29 | MemoryAccessor::ExternalProcess(accessor) => accessor.read(addr, buffer), 30 | MemoryAccessor::HookedProcess(accessor) => { 31 | accessor.read(addr, buffer); 32 | Ok(()) 33 | } 34 | } 35 | } 36 | 37 | pub fn write(&mut self, addr: usize, buffer: &[u8]) -> Result<()> { 38 | match self { 39 | MemoryAccessor::ExternalProcess(accessor) => accessor.write(addr, buffer), 40 | MemoryAccessor::HookedProcess(accessor) => { 41 | accessor.write(addr, buffer); 42 | Ok(()) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /junowen-lib/src/memory_accessors/external_process.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, mem::size_of}; 2 | 3 | use anyhow::{anyhow, bail, Result}; 4 | use windows::Win32::{ 5 | Foundation::{CloseHandle, FALSE, HANDLE, HMODULE, MAX_PATH}, 6 | System::{ 7 | Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory}, 8 | ProcessStatus::{EnumProcessModules, GetModuleBaseNameA}, 9 | Threading::{ 10 | OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, 11 | PROCESS_VM_WRITE, 12 | }, 13 | }, 14 | }; 15 | 16 | use crate::find_process_id::find_process_id; 17 | 18 | fn find_base_module(process: HANDLE, exe_file: &str) -> Result { 19 | let mut modules = [HMODULE::default(); 1024]; 20 | let mut cb_needed = 0; 21 | unsafe { 22 | EnumProcessModules( 23 | process, 24 | modules.as_mut_ptr(), 25 | size_of::<[HMODULE; 1024]>() as u32, 26 | &mut cb_needed, 27 | ) 28 | }?; 29 | let num_modules = cb_needed as usize / size_of::(); 30 | 31 | modules[0..num_modules] 32 | .iter() 33 | .filter(|&&module| { 34 | let mut base_name = [0u8; MAX_PATH as usize]; 35 | let len = unsafe { GetModuleBaseNameA(process, module, &mut base_name) }; 36 | len > 0 && String::from_utf8_lossy(&base_name[0..len as usize]) == exe_file 37 | }) 38 | .copied() 39 | .next() 40 | .ok_or(anyhow!("module not found")) 41 | } 42 | 43 | pub struct ExternalProcess { 44 | process: HANDLE, 45 | base_module: HMODULE, 46 | } 47 | 48 | impl ExternalProcess { 49 | pub fn new(exe_file: &str) -> Result { 50 | let process_id = find_process_id(exe_file)?; 51 | let process = unsafe { 52 | OpenProcess( 53 | PROCESS_QUERY_INFORMATION 54 | | PROCESS_VM_OPERATION 55 | | PROCESS_VM_READ 56 | | PROCESS_VM_WRITE, 57 | FALSE, 58 | process_id, 59 | ) 60 | }?; 61 | let base_module = find_base_module(process, exe_file)?; 62 | 63 | Ok(Self { 64 | process, 65 | base_module, 66 | }) 67 | } 68 | 69 | pub fn read(&self, addr: usize, buffer: &mut [u8]) -> Result<()> { 70 | let mut number_of_bytes_read: usize = 0; 71 | unsafe { 72 | ReadProcessMemory( 73 | self.process, 74 | (self.base_module.0 as usize + addr) as *const c_void, 75 | buffer.as_mut_ptr() as *mut c_void, 76 | buffer.len(), 77 | Some(&mut number_of_bytes_read), 78 | ) 79 | }?; 80 | if number_of_bytes_read != buffer.len() { 81 | bail!("ReadProcessMemory failed"); 82 | } 83 | Ok(()) 84 | } 85 | 86 | pub fn write(&mut self, addr: usize, buffer: &[u8]) -> Result<()> { 87 | let mut number_of_bytes_written: usize = 0; 88 | unsafe { 89 | WriteProcessMemory( 90 | self.process, 91 | (self.base_module.0 as usize + addr) as *const c_void, 92 | buffer.as_ptr() as *const c_void, 93 | buffer.len(), 94 | Some(&mut number_of_bytes_written), 95 | ) 96 | }?; 97 | if number_of_bytes_written != buffer.len() { 98 | bail!("WriteProcessMemory failed"); 99 | } 100 | Ok(()) 101 | } 102 | } 103 | 104 | impl Drop for ExternalProcess { 105 | fn drop(&mut self) { 106 | unsafe { CloseHandle(self.process) }.unwrap(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server.rs: -------------------------------------------------------------------------------- 1 | pub mod custom; 2 | pub mod reserved_room; 3 | pub mod room; 4 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/custom.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use getset::Getters; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::connection::signaling::CompressedSdp; 6 | 7 | use super::room::{PostRoomKeepResponse, PutRoomResponse, PutRoomResponseAnswerBody}; 8 | 9 | #[derive(Debug, Deserialize, Serialize, new)] 10 | pub struct PutSharedRoomResponseConflictBody { 11 | offer: CompressedSdp, 12 | } 13 | 14 | impl PutSharedRoomResponseConflictBody { 15 | pub fn into_offer(self) -> CompressedSdp { 16 | self.offer 17 | } 18 | } 19 | 20 | pub type PutSharedRoomResponse = PutRoomResponse; 21 | 22 | #[derive(Deserialize, Serialize, Getters, new)] 23 | pub struct PostSharedRoomKeepRequestBody { 24 | key: String, 25 | } 26 | 27 | impl PostSharedRoomKeepRequestBody { 28 | pub fn into_key(self) -> String { 29 | self.key 30 | } 31 | } 32 | 33 | pub type PostSharedRoomKeepResponse = PostRoomKeepResponse; 34 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/reserved_room.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use anyhow::Result; 3 | use derive_new::new; 4 | use getset::Getters; 5 | use http::StatusCode; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::connection::signaling::CompressedSdp; 10 | 11 | use super::room::PostRoomKeepResponse; 12 | use super::room::PutRoomResponse; 13 | 14 | // PUT /reserved-room/{name} 15 | 16 | #[derive(Debug, Deserialize, Serialize, new)] 17 | pub struct PutReservedRoomResponseConflictBody { 18 | opponent_offer: Option, 19 | } 20 | 21 | impl PutReservedRoomResponseConflictBody { 22 | pub fn into_offer(self) -> Option { 23 | self.opponent_offer 24 | } 25 | } 26 | 27 | pub type PutReservedRoomResponse = PutRoomResponse; 28 | 29 | // GET /reserved-room/{name} 30 | 31 | #[derive(Debug, Deserialize, Serialize, new)] 32 | pub struct GetReservedRoomResponseOkBody { 33 | opponent_offer: Option, 34 | spectator_offer: Option, 35 | } 36 | 37 | impl GetReservedRoomResponseOkBody { 38 | pub fn opponent_offer(&self) -> Option<&CompressedSdp> { 39 | self.opponent_offer.as_ref() 40 | } 41 | 42 | pub fn into_spectator_offer(self) -> Option { 43 | self.spectator_offer 44 | } 45 | } 46 | 47 | pub enum GetReservedRoomResponse { 48 | Ok(GetReservedRoomResponseOkBody), 49 | NotFound, 50 | } 51 | 52 | impl GetReservedRoomResponse { 53 | pub fn parse(status: StatusCode, text: Option<&str>) -> Result { 54 | match (status, text) { 55 | (StatusCode::OK, Some(text)) => { 56 | if let Ok(body) = serde_json::from_str::(text) { 57 | return Ok(Self::Ok(body)); 58 | } 59 | } 60 | (StatusCode::NOT_FOUND, _) => return Ok(Self::NotFound), 61 | _ => {} 62 | } 63 | bail!("invalid response") 64 | } 65 | 66 | pub fn status_code(&self) -> StatusCode { 67 | match self { 68 | Self::Ok(_) => StatusCode::OK, 69 | Self::NotFound => StatusCode::NOT_FOUND, 70 | } 71 | } 72 | 73 | pub fn to_body(&self) -> Option { 74 | match self { 75 | Self::Ok(body) => Some(serde_json::to_string(&body).unwrap()), 76 | Self::NotFound => None, 77 | } 78 | } 79 | } 80 | 81 | // POST /reserved-room/{name}/keep 82 | 83 | #[derive(Deserialize, Serialize, Getters, new)] 84 | pub struct PostReservedRoomKeepRequestBody { 85 | key: String, 86 | spectator_offer: Option, 87 | } 88 | 89 | impl PostReservedRoomKeepRequestBody { 90 | pub fn into_inner(self) -> (String, Option) { 91 | (self.key, self.spectator_offer) 92 | } 93 | } 94 | 95 | #[derive(Debug, Deserialize, Serialize, new)] 96 | pub struct PostReservedRoomKeepResponseOkOpponentAnswerBody { 97 | opponent_answer: CompressedSdp, 98 | } 99 | 100 | impl PostReservedRoomKeepResponseOkOpponentAnswerBody { 101 | pub fn into_opponent_answer(self) -> CompressedSdp { 102 | self.opponent_answer 103 | } 104 | } 105 | 106 | #[derive(Debug, Deserialize, Serialize, new)] 107 | pub struct PostReservedRoomKeepResponseOkSpectatorAnswerBody { 108 | spectator_answer: CompressedSdp, 109 | } 110 | 111 | impl PostReservedRoomKeepResponseOkSpectatorAnswerBody { 112 | pub fn into_spectator_answer(self) -> CompressedSdp { 113 | self.spectator_answer 114 | } 115 | } 116 | 117 | #[derive(Debug, Deserialize, Serialize)] 118 | pub enum PostReservedRoomKeepResponseOkBody { 119 | OpponentAnswer(PostReservedRoomKeepResponseOkOpponentAnswerBody), 120 | SpectatorAnswer(PostReservedRoomKeepResponseOkSpectatorAnswerBody), 121 | } 122 | 123 | impl From for PostReservedRoomKeepResponseOkBody { 124 | fn from(body: PostReservedRoomKeepResponseOkOpponentAnswerBody) -> Self { 125 | Self::OpponentAnswer(body) 126 | } 127 | } 128 | 129 | impl From 130 | for PostReservedRoomKeepResponseOkBody 131 | { 132 | fn from(body: PostReservedRoomKeepResponseOkSpectatorAnswerBody) -> Self { 133 | Self::SpectatorAnswer(body) 134 | } 135 | } 136 | 137 | pub type PostReservedRoomKeepResponse = PostRoomKeepResponse; 138 | 139 | impl From for PostReservedRoomKeepResponse { 140 | fn from(body: PostReservedRoomKeepResponseOkBody) -> Self { 141 | Self::Ok(body) 142 | } 143 | } 144 | 145 | // POST /reserved-room/{name}/join 146 | 147 | pub use super::room::PostRoomJoinRequestBody as PostReservedRoomSpectateRequestBody; 148 | pub use super::room::PostRoomJoinResponse as PostReservedRoomSpectateResponse; 149 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/room.rs: -------------------------------------------------------------------------------- 1 | mod delete_room; 2 | mod post_room_join; 3 | mod post_room_keep; 4 | mod put_room; 5 | 6 | pub use put_room::RequestBody as PutRoomRequestBody; 7 | pub use put_room::Response as PutRoomResponse; 8 | pub use put_room::ResponseAnswerBody as PutRoomResponseAnswerBody; 9 | pub use put_room::ResponseWaitingBody as PutRoomResponseWaitingBody; 10 | 11 | pub use post_room_keep::Response as PostRoomKeepResponse; 12 | 13 | pub use delete_room::RequestBody as DeleteRoomRequestBody; 14 | pub use delete_room::Response as DeleteRoomResponse; 15 | 16 | pub use post_room_join::RequestBody as PostRoomJoinRequestBody; 17 | pub use post_room_join::Response as PostRoomJoinResponse; 18 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/room/delete_room.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use derive_new::new; 3 | use getset::Getters; 4 | use http::StatusCode; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Deserialize, Serialize, Getters, new)] 8 | pub struct RequestBody { 9 | key: String, 10 | } 11 | 12 | impl RequestBody { 13 | pub fn into_key(self) -> String { 14 | self.key 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub enum Response { 20 | BadRequest, 21 | NoContent, 22 | } 23 | 24 | impl Response { 25 | pub fn parse(status: StatusCode) -> Result { 26 | match status { 27 | StatusCode::BAD_REQUEST => Ok(Self::BadRequest), 28 | StatusCode::NO_CONTENT => Ok(Self::NoContent), 29 | _ => bail!("invalid response"), 30 | } 31 | } 32 | 33 | pub fn status_code(&self) -> StatusCode { 34 | match self { 35 | Self::BadRequest => StatusCode::BAD_REQUEST, 36 | Self::NoContent => StatusCode::NO_CONTENT, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/room/post_room_join.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use derive_new::new; 3 | use http::StatusCode; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::connection::signaling::CompressedSdp; 7 | 8 | #[derive(Deserialize, Serialize, new)] 9 | pub struct RequestBody { 10 | answer: CompressedSdp, 11 | } 12 | 13 | impl RequestBody { 14 | pub fn into_answer(self) -> CompressedSdp { 15 | self.answer 16 | } 17 | } 18 | 19 | pub enum Response { 20 | Ok, 21 | Conflict, 22 | } 23 | 24 | impl Response { 25 | pub fn parse(status: StatusCode) -> Result { 26 | match status { 27 | StatusCode::OK => Ok(Self::Ok), 28 | StatusCode::CREATED => Ok(Self::Ok), 29 | StatusCode::CONFLICT => Ok(Self::Conflict), 30 | _ => bail!("invalid response"), 31 | } 32 | } 33 | 34 | pub fn status_code_old(&self) -> StatusCode { 35 | match self { 36 | Response::Ok => StatusCode::CREATED, 37 | Response::Conflict => StatusCode::CONFLICT, 38 | } 39 | } 40 | 41 | pub fn status_code(&self) -> StatusCode { 42 | match self { 43 | Response::Ok => StatusCode::OK, 44 | Response::Conflict => StatusCode::CONFLICT, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/room/post_room_keep.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use http::StatusCode; 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug)] 6 | pub enum Response { 7 | BadRequest, 8 | NoContent { retry_after: u32 }, 9 | Ok(T), 10 | } 11 | 12 | impl<'a, T> Response 13 | where 14 | T: Deserialize<'a>, 15 | { 16 | pub fn parse( 17 | status: StatusCode, 18 | retry_after: Option, 19 | text: Option<&'a str>, 20 | ) -> Result { 21 | match status { 22 | StatusCode::BAD_REQUEST => Ok(Self::BadRequest), 23 | StatusCode::NO_CONTENT => Ok(Self::NoContent { 24 | retry_after: retry_after.ok_or_else(|| anyhow!("invalid response"))?, 25 | }), 26 | StatusCode::OK => Ok(Self::Ok(serde_json::from_str( 27 | text.ok_or_else(|| anyhow!("invalid response"))?, 28 | )?)), 29 | _ => bail!("invalid response"), 30 | } 31 | } 32 | 33 | pub fn retry_after(&self) -> Option { 34 | match self { 35 | Response::BadRequest => None, 36 | Response::NoContent { retry_after } => Some(*retry_after), 37 | Response::Ok(_) => None, 38 | } 39 | } 40 | 41 | pub fn status_code(&self) -> StatusCode { 42 | match self { 43 | Response::BadRequest => StatusCode::BAD_REQUEST, 44 | Response::NoContent { .. } => StatusCode::NO_CONTENT, 45 | Response::Ok(_) => StatusCode::OK, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /junowen-lib/src/signaling_server/room/put_room.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use derive_new::new; 3 | use getset::Getters; 4 | use http::StatusCode; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::connection::signaling::CompressedSdp; 8 | 9 | #[derive(Deserialize, Serialize, Getters, new)] 10 | pub struct RequestBody { 11 | #[get = "pub"] 12 | offer: CompressedSdp, 13 | } 14 | 15 | #[derive(Debug, Deserialize, Serialize, new)] 16 | pub struct ResponseWaitingBody { 17 | key: String, 18 | } 19 | 20 | impl ResponseWaitingBody { 21 | pub fn into_key(self) -> String { 22 | self.key 23 | } 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize, new)] 27 | pub struct ResponseAnswerBody { 28 | answer: CompressedSdp, 29 | } 30 | 31 | impl ResponseAnswerBody { 32 | pub fn into_answer(self) -> CompressedSdp { 33 | self.answer 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum Response { 39 | CreatedWithKey { 40 | retry_after: u32, 41 | body: ResponseWaitingBody, 42 | }, 43 | CreatedWithAnswer { 44 | retry_after: u32, 45 | body: ResponseAnswerBody, 46 | }, 47 | Conflict { 48 | retry_after: u32, 49 | body: T, 50 | }, 51 | } 52 | 53 | impl<'a, T> Response 54 | where 55 | T: Deserialize<'a>, 56 | { 57 | pub fn created_with_key(retry_after: u32, body: ResponseWaitingBody) -> Self { 58 | Self::CreatedWithKey { retry_after, body } 59 | } 60 | pub fn created_with_answer(retry_after: u32, body: ResponseAnswerBody) -> Self { 61 | Self::CreatedWithAnswer { retry_after, body } 62 | } 63 | pub fn conflict(retry_after: u32, body: T) -> Self { 64 | Self::Conflict { retry_after, body } 65 | } 66 | 67 | pub fn parse(status: StatusCode, retry_after: Option, text: &'a str) -> Result { 68 | match status { 69 | StatusCode::CREATED => { 70 | if let Ok(res) = serde_json::from_str::(text) { 71 | return Ok(Self::CreatedWithKey { 72 | retry_after: retry_after.ok_or_else(|| anyhow!("invalid response"))?, 73 | body: res, 74 | }); 75 | } 76 | if let Ok(res) = serde_json::from_str::(text) { 77 | return Ok(Self::CreatedWithAnswer { 78 | retry_after: retry_after.ok_or_else(|| anyhow!("invalid response"))?, 79 | body: res, 80 | }); 81 | } 82 | } 83 | StatusCode::CONFLICT => { 84 | if let Ok(res) = serde_json::from_str(text) { 85 | return Ok(Self::Conflict { 86 | retry_after: retry_after.ok_or_else(|| anyhow!("invalid response"))?, 87 | body: res, 88 | }); 89 | } 90 | } 91 | _ => {} 92 | } 93 | bail!("invalid response") 94 | } 95 | 96 | pub fn status_code(&self) -> StatusCode { 97 | match self { 98 | Response::CreatedWithKey { .. } => StatusCode::CREATED, 99 | Response::CreatedWithAnswer { .. } => StatusCode::CREATED, 100 | Response::Conflict { .. } => StatusCode::CONFLICT, 101 | } 102 | } 103 | 104 | pub fn retry_after(&self) -> u32 { 105 | match self { 106 | Response::CreatedWithKey { retry_after, .. } => *retry_after, 107 | Response::CreatedWithAnswer { retry_after, .. } => *retry_after, 108 | Response::Conflict { retry_after, .. } => *retry_after, 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /junowen-lib/src/th19/structs.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod input_devices; 3 | pub mod others; 4 | pub mod selection; 5 | pub mod settings; 6 | -------------------------------------------------------------------------------- /junowen-lib/src/th19/structs/input_devices.rs: -------------------------------------------------------------------------------- 1 | use flagset::{flags, FlagSet, InvalidBits}; 2 | use getset::{CopyGetters, Getters, MutGetters, Setters}; 3 | 4 | flags! { 5 | pub enum InputFlags: u32 { 6 | SHOT, 7 | CHARGE, 8 | BOMB, 9 | SLOW, 10 | UP, 11 | DOWN, 12 | LEFT, 13 | RIGHT, 14 | PAUSE, 15 | _UNKNOWN1, 16 | _UNKNOWN2, 17 | _UNKNOWN3, 18 | _UNKNOWN4, 19 | _UNKNOWN5, 20 | _UNKNOWN6, 21 | _UNKNOWN7, 22 | _UNKNOWN8, 23 | _UNKNOWN9, 24 | _UNKNOWN10, 25 | ENTER, 26 | } 27 | } 28 | 29 | #[derive(Copy, Clone, Debug, PartialEq)] 30 | pub struct InputValue(pub FlagSet); 31 | 32 | impl InputValue { 33 | pub fn full() -> Self { 34 | Self(FlagSet::full()) 35 | } 36 | 37 | pub fn empty() -> Self { 38 | Self(None.into()) 39 | } 40 | 41 | pub fn bits(&self) -> u32 { 42 | self.0.bits() 43 | } 44 | } 45 | 46 | impl TryFrom for InputValue { 47 | type Error = InvalidBits; 48 | 49 | fn try_from(value: u32) -> Result { 50 | Ok(Self(FlagSet::::new(value)?)) 51 | } 52 | } 53 | 54 | impl From for InputValue { 55 | fn from(flag: InputFlags) -> Self { 56 | Self(flag.into()) 57 | } 58 | } 59 | 60 | #[derive(CopyGetters, Setters)] 61 | #[repr(C)] 62 | pub struct Input { 63 | #[getset(get_copy = "pub", set = "pub")] 64 | current: InputValue, 65 | #[getset(get_copy = "pub")] 66 | prev: InputValue, 67 | #[getset(get_copy = "pub")] 68 | repeat: InputValue, 69 | _repeat2: InputValue, 70 | _unknown: [u8; 0x18], 71 | #[getset(get_copy = "pub")] 72 | up_repeat_count: u32, 73 | #[getset(get_copy = "pub")] 74 | down_repeat_count: u32, 75 | #[getset(get_copy = "pub")] 76 | left_repeat_count: u32, 77 | #[getset(get_copy = "pub")] 78 | right_repeat_count: u32, 79 | _unknown1: [u8; 0x278], 80 | _unknown2: [u8; 0x010], 81 | } 82 | 83 | impl Input { 84 | pub fn decide(&self) -> bool { 85 | [InputFlags::SHOT, InputFlags::ENTER] 86 | .into_iter() 87 | .any(|flag| self.prev.0 & flag == None && self.current.0 & flag != None) 88 | } 89 | } 90 | 91 | /// 0x3d4 92 | #[derive(Getters, MutGetters)] 93 | #[repr(C)] 94 | pub struct InputDevice { 95 | _unknown1: [u8; 0x010], 96 | #[getset(get = "pub", get_mut = "pub")] 97 | input: Input, 98 | #[getset(get = "pub")] 99 | raw_keys: [u8; 0x100], 100 | _unknown2: [u8; 0x04], 101 | } 102 | 103 | #[derive(CopyGetters, Getters, Setters)] 104 | #[repr(C)] 105 | pub struct InputDevices { 106 | _unknown1: [u8; 0x20], 107 | input_device_array: [InputDevice; 3 + 9], 108 | _unknown2: [u8; 0x14], 109 | #[getset(get_copy = "pub", set = "pub")] 110 | p1_idx: u32, 111 | p2_idx: u32, 112 | // unknown remains... 113 | } 114 | 115 | impl InputDevices { 116 | pub fn keyboard_input(&self) -> &InputDevice { 117 | &self.input_device_array[0] 118 | } 119 | 120 | pub fn p1_input(&self) -> &Input { 121 | &self.input_device_array[self.p1_idx as usize].input 122 | } 123 | pub fn p1_input_mut(&mut self) -> &mut Input { 124 | &mut self.input_device_array[self.p1_idx as usize].input 125 | } 126 | 127 | pub fn p2_input(&self) -> &Input { 128 | &self.input_device_array[self.p2_idx as usize].input 129 | } 130 | pub fn p2_input_mut(&mut self) -> &mut Input { 131 | &mut self.input_device_array[self.p2_idx as usize].input 132 | } 133 | 134 | pub fn is_conflict_input_device(&self) -> bool { 135 | self.p1_idx == self.p2_idx 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /junowen-lib/src/th19/structs/others.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{CStr, FromBytesUntilNulError}, 3 | fmt, 4 | }; 5 | 6 | use anyhow::Result; 7 | use derivative::Derivative; 8 | use getset::CopyGetters; 9 | 10 | #[derive(Debug)] 11 | #[repr(C)] 12 | pub struct RoundFrame { 13 | _unknown: [u8; 0x10], 14 | pub pre_frame: u32, 15 | pub frame: u32, 16 | } 17 | 18 | impl RoundFrame { 19 | pub fn is_first_frame(&self) -> bool { 20 | self.pre_frame == 0xffffffff && self.frame == 0 21 | } 22 | } 23 | 24 | #[derive(CopyGetters)] 25 | #[repr(C)] 26 | pub struct VSMode { 27 | _unknown1: [u8; 0x02E868], 28 | _unknown2: [u8; 0x08], 29 | _unknown3: [u8; 0x58], 30 | player_name: [u8; 0x22], 31 | room_name: [u8; 0x22], 32 | _unknown4: [u8; 0x0108], 33 | /// Readonly 34 | #[get_copy = "pub"] 35 | p1_card: u8, // +2ea14h 36 | /// Readonly 37 | #[get_copy = "pub"] 38 | p2_card: u8, 39 | // unknown remains... 40 | } 41 | 42 | impl VSMode { 43 | pub fn player_name(&self) -> &str { 44 | CStr::from_bytes_until_nul(&self.player_name) 45 | .unwrap_or_default() 46 | .to_str() 47 | .unwrap() 48 | } 49 | 50 | pub fn room_name(&self) -> &str { 51 | CStr::from_bytes_until_nul(&self.room_name) 52 | .unwrap_or_default() 53 | .to_str() 54 | .unwrap() 55 | } 56 | } 57 | 58 | #[derive(CopyGetters)] 59 | pub struct WindowInner { 60 | #[get_copy = "pub"] 61 | width: u32, 62 | #[get_copy = "pub"] 63 | height: u32, 64 | } 65 | 66 | #[derive(Derivative)] 67 | #[derivative(Default)] 68 | #[repr(C)] 69 | pub struct RenderingText { 70 | #[derivative(Default(value = "[0u8; 256]"))] 71 | raw_text: [u8; 256], 72 | x: f32, 73 | y: f32, 74 | pub _unknown1: u32, 75 | /// 0xaarrggbb 76 | #[derivative(Default(value = "0xffffffff"))] 77 | pub color: u32, 78 | #[derivative(Default(value = "1.0"))] 79 | pub scale_x: f32, 80 | #[derivative(Default(value = "1.0"))] 81 | pub scale_y: f32, 82 | /// radian 83 | pub rotate: f32, 84 | pub _unknown2: [u8; 0x08], 85 | pub font_type: u32, 86 | pub drop_shadow: bool, 87 | pub _padding_drop_shadow: [u8; 0x03], 88 | pub _unknown3: u32, 89 | pub hide: u32, 90 | /// 0: center, 1: left, 2: right 91 | #[derivative(Default(value = "1"))] 92 | pub horizontal_align: u32, 93 | /// 0: center, 1: top, 2: bottom 94 | #[derivative(Default(value = "1"))] 95 | pub vertical_align: u32, 96 | } 97 | 98 | impl fmt::Debug for RenderingText { 99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { 100 | f.debug_struct("RenderingText") 101 | .field("text", &CStr::from_bytes_until_nul(&self.raw_text)) 102 | .field("x", &self.x) 103 | .field("y", &self.y) 104 | .field("_unknown1", &self._unknown1) 105 | .field("color", &format!("{:x}", self.color)) 106 | .field("scale_x", &self.scale_x) 107 | .field("scale_y", &self.scale_y) 108 | .field("rotate", &self.rotate) 109 | .field("_unknown2", &self._unknown2) 110 | .field("font_type", &self.font_type) 111 | .field("drop_shadow", &self.drop_shadow) 112 | .field("_padding_drop_shadow", &self._padding_drop_shadow) 113 | .field("_unknown3", &self._unknown3) 114 | .field("hide", &self.hide) 115 | .field("horizontal_align", &self.horizontal_align) 116 | .field("vertical_align", &self.vertical_align) 117 | .finish() 118 | } 119 | } 120 | 121 | impl RenderingText { 122 | pub fn text(&self) -> Result<&CStr, FromBytesUntilNulError> { 123 | CStr::from_bytes_until_nul(&self.raw_text) 124 | } 125 | 126 | pub fn set_text(&mut self, text: &[u8]) { 127 | let mut raw_text = [0u8; 256]; 128 | raw_text[0..(text.len())].copy_from_slice(text); 129 | self.raw_text = raw_text; 130 | } 131 | 132 | pub fn set_x(&mut self, x: u32, window_inner: &WindowInner) { 133 | self.x = (x * window_inner.width() / 1280) as f32; 134 | } 135 | 136 | pub fn set_y(&mut self, y: u32, window_inner: &WindowInner) { 137 | self.y = (y * window_inner.height() / 960) as f32; 138 | } 139 | 140 | pub fn sub_y(&mut self, y: u32, window_inner: &WindowInner) { 141 | self.y -= (y * window_inner.height() / 960) as f32; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /junowen-lib/src/th19/structs/selection.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | 3 | use anyhow::{bail, Result}; 4 | use getset::{Getters, MutGetters}; 5 | 6 | /// length=c0 7 | #[repr(C)] 8 | pub struct Player { 9 | _unknown1: [u8; 0x0c], 10 | /// NOT available on player select screen 11 | pub character: u32, 12 | _unknown2: [u8; 0x80], 13 | /// Available on player select screen 14 | pub card: u32, 15 | _unknown3: [u8; 0x34], 16 | } 17 | 18 | #[derive(Clone, Copy, PartialEq)] 19 | #[repr(u32)] 20 | pub enum Difficulty { 21 | Easy, 22 | Normal, 23 | Hard, 24 | Lunatic, 25 | } 26 | 27 | impl Default for Difficulty { 28 | fn default() -> Self { 29 | Self::Normal 30 | } 31 | } 32 | 33 | impl TryFrom for Difficulty { 34 | type Error = anyhow::Error; 35 | fn try_from(value: u32) -> Result { 36 | if !(0..4).contains(&value) { 37 | bail!("Invalid Difficulty: {}", value); 38 | } 39 | Ok(unsafe { transmute::(value) }) 40 | } 41 | } 42 | 43 | #[derive(Clone, Copy, PartialEq)] 44 | #[repr(u32)] 45 | pub enum GameMode { 46 | Story, 47 | Unused, 48 | Versus, 49 | } 50 | 51 | impl TryFrom for GameMode { 52 | type Error = anyhow::Error; 53 | fn try_from(value: u32) -> Result { 54 | if !(0..3).contains(&value) { 55 | bail!("Invalid GameMode: {}", value); 56 | } 57 | Ok(unsafe { transmute::(value) }) 58 | } 59 | } 60 | 61 | #[derive(Clone, Copy, PartialEq)] 62 | #[repr(u32)] 63 | pub enum PlayerMatchup { 64 | HumanVsHuman, 65 | HumanVsCpu, 66 | CpuVsCpu, 67 | YoukaiVsYoukai, 68 | } 69 | 70 | impl Default for PlayerMatchup { 71 | fn default() -> Self { 72 | Self::HumanVsHuman 73 | } 74 | } 75 | 76 | impl TryFrom for PlayerMatchup { 77 | type Error = anyhow::Error; 78 | fn try_from(value: u32) -> Result { 79 | if !(0..4).contains(&value) { 80 | bail!("Invalid PlayerMatchup: {}", value); 81 | } 82 | Ok(unsafe { transmute::(value) }) 83 | } 84 | } 85 | 86 | #[derive(Getters, MutGetters)] 87 | #[repr(C)] 88 | pub struct Selection { 89 | #[getset(get = "pub", get_mut = "pub")] 90 | p1: Player, 91 | #[getset(get = "pub", get_mut = "pub")] 92 | p2: Player, 93 | pub difficulty: Difficulty, 94 | pub game_mode: GameMode, 95 | pub player_matchup: PlayerMatchup, 96 | } 97 | -------------------------------------------------------------------------------- /junowen-lib/src/win_api_wrappers.rs: -------------------------------------------------------------------------------- 1 | use windows::Win32::Foundation::{CloseHandle, HANDLE}; 2 | 3 | pub struct SafeHandle(pub HANDLE); 4 | 5 | impl Drop for SafeHandle { 6 | fn drop(&mut self) { 7 | unsafe { CloseHandle(self.0) }.unwrap(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /junowen-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "junowen-server" 3 | edition = "2021" 4 | version = "0.9.0" 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | [dependencies] 10 | anyhow.workspace = true 11 | async-trait.workspace = true 12 | aws-config = "*" 13 | aws-sdk-dynamodb = "*" 14 | base_custom = "0.2.0" 15 | chrono = "0.4.31" 16 | derive-new = "0.6.0" 17 | getset = "0.1.2" 18 | junowen-lib.workspace = true 19 | lambda_http = "0.11.1" 20 | once_cell = "1.18.0" 21 | regex = "1.10.2" 22 | serde.workspace = true 23 | serde_dynamo = { version = "4.2.8", features = ["aws-sdk-dynamodb+0_34"] } 24 | serde_json.workspace = true 25 | time = "0.3.29" 26 | tokio.workspace = true 27 | tracing.workspace = true 28 | tracing-subscriber.workspace = true 29 | urlencoding = "2.1.3" 30 | uuid = "1.5.0" 31 | 32 | [target.x86_64-unknown-linux-gnu.dependencies] 33 | openssl = { version = "0.10", features = ["vendored"] } 34 | -------------------------------------------------------------------------------- /junowen-server/README.md: -------------------------------------------------------------------------------- 1 | # junowen-server 2 | 3 | ## create 4 | 5 | ```sh 6 | cargo lambda deploy \ 7 | --binary-name junowen-server \ 8 | --enable-function-url \ 9 | --env-var ENV=prod \ 10 | --profile $PROFILE \ 11 | junowen-server 12 | ``` 13 | 14 | ## Dynamo DB definition 15 | 16 | * env = dev | prod 17 | * table_name = Offer | Answer | ReservedRoom | ReservedRoomOpponentAnswer | ReservedRoomSpectatorAnswer 18 | 19 | ### {env}.{table_name} 20 | 21 | * Partition Key = { name: String } 22 | * Capacity mode = ondemand 23 | * delete protection 24 | * TTL = ttl_sec 25 | -------------------------------------------------------------------------------- /junowen-server/src/database.rs: -------------------------------------------------------------------------------- 1 | mod dynamodb; 2 | mod file; 3 | 4 | use async_trait::async_trait; 5 | use derive_new::new; 6 | pub use dynamodb::DynamoDB; 7 | pub use file::File; 8 | 9 | use anyhow::Result; 10 | use getset::{Getters, Setters}; 11 | use junowen_lib::connection::signaling::CompressedSdp; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Clone, Debug, Deserialize, Getters, Serialize, new)] 15 | pub struct SharedRoom { 16 | /// primary 17 | #[get = "pub"] 18 | name: String, 19 | /// ルームの所有者であることを証明する為のキー 20 | #[get = "pub"] 21 | key: String, 22 | #[get = "pub"] 23 | sdp: CompressedSdp, 24 | ttl_sec: u64, 25 | } 26 | 27 | impl SharedRoom { 28 | pub fn into_sdp(self) -> CompressedSdp { 29 | self.sdp 30 | } 31 | 32 | pub fn is_expired(&self, now_sec: u64) -> bool { 33 | now_sec > self.ttl_sec 34 | } 35 | } 36 | 37 | #[derive(Serialize, Getters, Deserialize, new)] 38 | pub struct Answer { 39 | /// primary 40 | #[get = "pub"] 41 | name: String, 42 | #[get = "pub"] 43 | sdp: CompressedSdp, 44 | ttl_sec: u64, 45 | } 46 | 47 | impl Answer { 48 | pub fn into_sdp(self) -> CompressedSdp { 49 | self.sdp 50 | } 51 | } 52 | 53 | pub type SharedRoomOpponentAnswer = Answer; 54 | 55 | #[derive(Debug)] 56 | pub enum PutError { 57 | Conflict, 58 | Unknown(anyhow::Error), 59 | } 60 | 61 | #[async_trait] 62 | pub trait SharedRoomTables: Send + Sync + 'static { 63 | async fn put_room(&self, offer: SharedRoom) -> Result<(), PutError>; 64 | async fn find_room(&self, name: String) -> Result>; 65 | async fn keep_room(&self, name: String, key: String, ttl_sec: u64) -> Result; 66 | async fn remove_room(&self, name: String, key: Option) -> Result; 67 | 68 | async fn put_room_opponent_answer( 69 | &self, 70 | answer: SharedRoomOpponentAnswer, 71 | ) -> Result<(), PutError>; 72 | async fn remove_room_opponent_answer( 73 | &self, 74 | name: String, 75 | ) -> Result>; 76 | } 77 | 78 | #[derive(Clone, Debug, Deserialize, Getters, Setters, Serialize, new)] 79 | pub struct ReservedRoom { 80 | /// primary 81 | #[get = "pub"] 82 | name: String, 83 | /// ルームの所有者であることを証明する為のキー 84 | #[get = "pub"] 85 | key: String, 86 | #[getset(get = "pub", set = "pub")] 87 | opponent_offer_sdp: Option, 88 | #[get = "pub"] 89 | spectator_offer_sdp: Option, 90 | ttl_sec: u64, 91 | } 92 | 93 | impl ReservedRoom { 94 | pub fn into_opponent_offer_sdp(self) -> Option { 95 | self.opponent_offer_sdp 96 | } 97 | 98 | pub fn into_opponent_offer_sdp_spectator_offer_sdp( 99 | self, 100 | ) -> (Option, Option) { 101 | (self.opponent_offer_sdp, self.spectator_offer_sdp) 102 | } 103 | 104 | pub fn is_expired(&self, now_sec: u64) -> bool { 105 | now_sec > self.ttl_sec 106 | } 107 | } 108 | 109 | #[derive(Serialize, Deserialize)] 110 | pub struct ReservedRoomOpponentAnswer(pub Answer); 111 | #[derive(Serialize, Deserialize)] 112 | pub struct ReservedRoomSpectatorAnswer(pub Answer); 113 | 114 | #[async_trait] 115 | pub trait ReservedRoomTables: Send + Sync + 'static { 116 | async fn put_room(&self, offer: ReservedRoom) -> Result<(), PutError>; 117 | async fn find_room(&self, name: String) -> Result>; 118 | async fn keep_room( 119 | &self, 120 | name: String, 121 | key: String, 122 | spectator_offer_sdp: Option, 123 | ttl_sec: u64, 124 | ) -> Result>; 125 | async fn remove_opponent_offer_sdp_in_room(&self, name: String) -> Result; 126 | async fn remove_spectator_offer_sdp_in_room(&self, name: String) -> Result; 127 | async fn remove_room(&self, name: String, key: Option) -> Result; 128 | 129 | async fn put_room_opponent_answer( 130 | &self, 131 | answer: ReservedRoomOpponentAnswer, 132 | ) -> Result<(), PutError>; 133 | async fn remove_room_opponent_answer( 134 | &self, 135 | name: String, 136 | ) -> Result>; 137 | 138 | async fn put_room_spectator_answer( 139 | &self, 140 | answer: ReservedRoomSpectatorAnswer, 141 | ) -> Result<(), PutError>; 142 | async fn remove_room_spectator_answer( 143 | &self, 144 | name: String, 145 | ) -> Result>; 146 | } 147 | 148 | pub trait Database: SharedRoomTables + ReservedRoomTables {} 149 | -------------------------------------------------------------------------------- /junowen-server/src/database/dynamodb/shared_room.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use aws_sdk_dynamodb::{error::SdkError, types::AttributeValue}; 4 | 5 | use crate::database::{PutError, SharedRoom, SharedRoomOpponentAnswer, SharedRoomTables}; 6 | 7 | use super::DynamoDB; 8 | 9 | #[async_trait] 10 | impl SharedRoomTables for DynamoDB { 11 | async fn put_room(&self, room: SharedRoom) -> Result<(), PutError> { 12 | self.put_item(&self.table_name_shared_room, room).await 13 | } 14 | 15 | async fn find_room(&self, name: String) -> Result> { 16 | self.find_item_by_name(&self.table_name_shared_room, name) 17 | .await 18 | } 19 | 20 | async fn keep_room(&self, name: String, key: String, ttl_sec: u64) -> Result { 21 | let result = self 22 | .client 23 | .update_item() 24 | .table_name(&self.table_name_shared_room) 25 | .key("name", AttributeValue::S(name)) 26 | .condition_expression("#key = :key") 27 | .update_expression("SET #ttl_sec = :ttl_sec") 28 | .expression_attribute_names("#key", "key") 29 | .expression_attribute_values(":key", AttributeValue::S(key)) 30 | .expression_attribute_names("#ttl_sec", "ttl_sec") 31 | .expression_attribute_values(":ttl_sec", AttributeValue::N((ttl_sec).to_string())) 32 | .send() 33 | .await; 34 | if let Err(err) = result { 35 | if let SdkError::ServiceError(service_error) = &err { 36 | if service_error.err().is_conditional_check_failed_exception() { 37 | return Ok(false); 38 | } 39 | } 40 | return Err(err.into()); 41 | } 42 | Ok(true) 43 | } 44 | 45 | async fn remove_room(&self, name: String, key: Option) -> Result { 46 | self.remove_item(&self.table_name_shared_room, name, key) 47 | .await 48 | } 49 | 50 | async fn put_room_opponent_answer( 51 | &self, 52 | answer: SharedRoomOpponentAnswer, 53 | ) -> Result<(), PutError> { 54 | self.put_item(&self.table_name_shared_room_opponent_answer, answer) 55 | .await 56 | } 57 | 58 | async fn remove_room_opponent_answer( 59 | &self, 60 | name: String, 61 | ) -> Result> { 62 | self.remove_item_and_get_old(&self.table_name_shared_room_opponent_answer, name) 63 | .await 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /junowen-server/src/main.rs: -------------------------------------------------------------------------------- 1 | mod database; 2 | mod routes; 3 | mod tracing_helper; 4 | 5 | mod local { 6 | use std::env::args; 7 | 8 | use lambda_http::{ 9 | http::{request::Builder, Method}, 10 | Body, IntoResponse, Request, 11 | }; 12 | use tracing::trace; 13 | 14 | use crate::{database, routes::routes, tracing_helper}; 15 | 16 | async fn func(req: Request) -> Result { 17 | let db = database::File; 18 | routes(&req, &db).await 19 | } 20 | 21 | #[allow(unused)] 22 | pub async fn main() -> anyhow::Result<()> { 23 | tracing_helper::init_local_tracing(); 24 | 25 | let mut args = args(); 26 | let method = args.nth(1).unwrap(); 27 | let uri = args.next().unwrap(); 28 | let body = args.next().unwrap(); 29 | 30 | let req: Request = Builder::new() 31 | .method(Method::from_bytes(method.as_bytes()).unwrap()) 32 | .uri(uri) 33 | .body(Body::Text(body)) 34 | .unwrap(); 35 | let res = func(req).await?; 36 | trace!("{:?}", res.into_response().await); 37 | Ok(()) 38 | } 39 | } 40 | 41 | mod lambda { 42 | use lambda_http::{service_fn, IntoResponse, Request}; 43 | use tracing::error; 44 | 45 | use crate::{database, routes::routes, tracing_helper}; 46 | 47 | async fn func(req: Request) -> Result { 48 | let db = database::DynamoDB::new().await; 49 | routes(&req, &db).await.map_err(|err| { 50 | error!("Fatal error: {:?}", err); 51 | err 52 | }) 53 | } 54 | 55 | #[allow(unused)] 56 | pub async fn main() -> Result<(), lambda_http::Error> { 57 | tracing_helper::init_server_tracing(); 58 | 59 | lambda_http::run(service_fn(func)).await 60 | } 61 | } 62 | 63 | #[cfg(not(target_os = "linux"))] 64 | #[tokio::main] 65 | async fn main() -> anyhow::Result<()> { 66 | local::main().await 67 | } 68 | 69 | #[cfg(target_os = "linux")] 70 | #[tokio::main] 71 | async fn main() -> Result<(), lambda_http::Error> { 72 | lambda::main().await 73 | } 74 | -------------------------------------------------------------------------------- /junowen-server/src/routes.rs: -------------------------------------------------------------------------------- 1 | mod custom; 2 | mod reserved_room; 3 | mod room_utils; 4 | 5 | use std::{ 6 | collections::hash_map::DefaultHasher, 7 | hash::{Hash, Hasher}, 8 | }; 9 | 10 | use anyhow::{bail, Result}; 11 | use base_custom::BaseCustom; 12 | use lambda_http::{ 13 | http::{header::RETRY_AFTER, StatusCode}, 14 | Body, IntoResponse, Request, Response, 15 | }; 16 | use once_cell::sync::Lazy; 17 | use serde::Deserialize; 18 | use tracing::{info_span, trace, Instrument}; 19 | 20 | use crate::{database::Database, routes::room_utils::RETRY_AFTER_INTERVAL_SEC}; 21 | 22 | static BASE_YOTEICHI_MOD: Lazy> = Lazy::new(|| { 23 | const CHARS: &str = concat!( 24 | "ー", 25 | "あいうえお", 26 | "かがきぎくぐけげこご", 27 | "さざしじすずせぜそぞ", 28 | "ただちぢつづてでとど", 29 | "なにぬねの", 30 | "はばぱひびぴふぶぷへべぺほぼぽ", 31 | "まみむめも", 32 | "やゆよ", 33 | "らりるれろ", 34 | "わゐゑをん", 35 | "ゔ", 36 | ); 37 | base_custom::BaseCustom::::new(CHARS.chars().collect()) 38 | }); 39 | 40 | fn base_yoteichi_mod(input_val: u64) -> String { 41 | BASE_YOTEICHI_MOD 42 | .gen(input_val) 43 | .chars() 44 | .collect::>() 45 | .chunks(4) 46 | .map(|c| c.iter().collect::()) 47 | .collect::>() 48 | .join(" ") 49 | } 50 | 51 | fn try_parse<'a, T>(body: &'a Body) -> anyhow::Result 52 | where 53 | T: Deserialize<'a>, 54 | { 55 | let Body::Text(body) = body else { 56 | bail!("Not text"); 57 | }; 58 | serde_json::from_str(body.as_str()).map_err(|err| err.into()) 59 | } 60 | 61 | fn to_response(status_code: StatusCode, body: impl Into) -> Response { 62 | Response::builder() 63 | .status(status_code) 64 | .header(RETRY_AFTER, RETRY_AFTER_INTERVAL_SEC) 65 | .body(body.into()) 66 | .unwrap() 67 | } 68 | 69 | fn ip_hash(req: &Request) -> u64 { 70 | let ip = req 71 | .headers() 72 | .get("x-forwarded-for") 73 | .and_then(|x| x.to_str().ok()) 74 | .unwrap_or_default(); 75 | let mut s = DefaultHasher::new(); 76 | ip.hash(&mut s); 77 | s.finish() 78 | } 79 | 80 | pub async fn routes(req: &Request, db: &impl Database) -> Result { 81 | trace!("{:?}", req); 82 | 83 | if let Some(relative_uri) = req.uri().path().strip_prefix("/custom/") { 84 | return custom::route(relative_uri, req, db) 85 | .instrument(info_span!("req", ip_hash = base_yoteichi_mod(ip_hash(req)))) 86 | .await; 87 | } 88 | if let Some(relative_uri) = req.uri().path().strip_prefix("/reserved-room/") { 89 | return reserved_room::route(relative_uri, req, db) 90 | .instrument(info_span!("req", ip_hash = base_yoteichi_mod(ip_hash(req)))) 91 | .await; 92 | } 93 | Ok(to_response(StatusCode::NOT_FOUND, Body::Empty)) 94 | } 95 | -------------------------------------------------------------------------------- /junowen-server/src/routes/reserved_room/create.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use junowen_lib::signaling_server::{ 3 | reserved_room::{PutReservedRoomResponse, PutReservedRoomResponseConflictBody}, 4 | room::{PutRoomRequestBody, PutRoomResponseAnswerBody, PutRoomResponseWaitingBody}, 5 | }; 6 | use tracing::info; 7 | use uuid::Uuid; 8 | 9 | use crate::{ 10 | database::{PutError, ReservedRoom, ReservedRoomTables}, 11 | routes::{ 12 | reserved_room::{read::find_valid_room, update::find_opponent}, 13 | room_utils::{now_sec, ttl_sec, RETRY_AFTER_INTERVAL_SEC}, 14 | }, 15 | }; 16 | 17 | pub async fn put_room( 18 | db: &impl ReservedRoomTables, 19 | name: &str, 20 | body: PutRoomRequestBody, 21 | ) -> Result { 22 | let now_sec = now_sec(); 23 | let key = Uuid::new_v4().to_string(); 24 | let room = ReservedRoom::new( 25 | name.to_owned(), 26 | key.clone(), 27 | Some(body.offer().clone()), 28 | None, 29 | ttl_sec(now_sec), 30 | ); 31 | for retry in 0.. { 32 | if let Some(room) = find_valid_room(db, now_sec, name.to_owned()).await? { 33 | let body = PutReservedRoomResponseConflictBody::new(room.into_opponent_offer_sdp()); 34 | let response = PutReservedRoomResponse::conflict(RETRY_AFTER_INTERVAL_SEC, body); 35 | return Ok(response); 36 | } 37 | match db.put_room(room.clone()).await { 38 | Ok(()) => break, 39 | Err(PutError::Conflict) => { 40 | if retry >= 2 { 41 | panic!(); 42 | } 43 | continue; 44 | } 45 | Err(PutError::Unknown(err)) => bail!("{:?}", err), 46 | } 47 | } 48 | info!("[Reserved Room] Created: {}", name); 49 | Ok( 50 | if let Some(answer) = find_opponent(db, name.to_owned()).await? { 51 | let body = PutRoomResponseAnswerBody::new(answer.0.into_sdp()); 52 | PutReservedRoomResponse::created_with_answer(RETRY_AFTER_INTERVAL_SEC, body) 53 | } else { 54 | let body = PutRoomResponseWaitingBody::new(key); 55 | PutReservedRoomResponse::created_with_key(RETRY_AFTER_INTERVAL_SEC, body) 56 | }, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /junowen-server/src/routes/reserved_room/delete.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use junowen_lib::signaling_server::room::{DeleteRoomRequestBody, DeleteRoomResponse}; 3 | use tracing::info; 4 | 5 | use crate::database::ReservedRoomTables; 6 | 7 | pub async fn delete_room( 8 | db: &impl ReservedRoomTables, 9 | name: &str, 10 | body: DeleteRoomRequestBody, 11 | ) -> Result { 12 | if !db 13 | .remove_room(name.to_owned(), Some(body.into_key())) 14 | .await? 15 | { 16 | Ok(DeleteRoomResponse::BadRequest) 17 | } else { 18 | db.remove_room_opponent_answer(name.to_owned()).await?; 19 | info!("[Reserved Room] Removed: {}", name); 20 | Ok(DeleteRoomResponse::NoContent) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /junowen-server/src/routes/reserved_room/read.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use junowen_lib::signaling_server::reserved_room::{ 3 | GetReservedRoomResponse, GetReservedRoomResponseOkBody, 4 | }; 5 | 6 | use crate::{ 7 | database::{ReservedRoom, ReservedRoomTables}, 8 | routes::room_utils::now_sec, 9 | }; 10 | 11 | pub async fn find_valid_room( 12 | db: &impl ReservedRoomTables, 13 | now_sec: u64, 14 | name: String, 15 | ) -> Result> { 16 | let Some(offer) = db.find_room(name.to_owned()).await? else { 17 | return Ok(None); 18 | }; 19 | if !offer.is_expired(now_sec) { 20 | return Ok(Some(offer)); 21 | } 22 | db.remove_room(offer.name().clone(), None).await?; 23 | db.remove_room_opponent_answer(offer.name().clone()).await?; 24 | db.remove_room_spectator_answer(offer.name().clone()) 25 | .await?; 26 | Ok(None) 27 | } 28 | 29 | pub async fn get_room(db: &impl ReservedRoomTables, name: &str) -> Result { 30 | let now_sec = now_sec(); 31 | let Some(room) = find_valid_room(db, now_sec, name.to_owned()).await? else { 32 | return Ok(GetReservedRoomResponse::NotFound); 33 | }; 34 | let (opponent_offer_sdp, spectator_offer_sdp) = 35 | room.into_opponent_offer_sdp_spectator_offer_sdp(); 36 | let body = GetReservedRoomResponseOkBody::new(opponent_offer_sdp, spectator_offer_sdp); 37 | Ok(GetReservedRoomResponse::Ok(body)) 38 | } 39 | -------------------------------------------------------------------------------- /junowen-server/src/routes/reserved_room/update.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use junowen_lib::signaling_server::{ 3 | reserved_room::{ 4 | PostReservedRoomKeepRequestBody, PostReservedRoomKeepResponse, 5 | PostReservedRoomKeepResponseOkBody, PostReservedRoomKeepResponseOkOpponentAnswerBody, 6 | PostReservedRoomKeepResponseOkSpectatorAnswerBody, PostReservedRoomSpectateRequestBody, 7 | PostReservedRoomSpectateResponse, 8 | }, 9 | room::{PostRoomJoinRequestBody, PostRoomJoinResponse}, 10 | }; 11 | use tracing::info; 12 | use uuid::Uuid; 13 | 14 | use crate::{ 15 | database::{ 16 | Answer, PutError, ReservedRoomOpponentAnswer, ReservedRoomSpectatorAnswer, 17 | ReservedRoomTables, 18 | }, 19 | routes::room_utils::{now_sec, ttl_sec, RETRY_AFTER_INTERVAL_SEC}, 20 | }; 21 | 22 | pub async fn find_opponent( 23 | db: &impl ReservedRoomTables, 24 | name: String, 25 | ) -> Result> { 26 | let Some(answer) = db.remove_room_opponent_answer(name.clone()).await? else { 27 | return Ok(None); 28 | }; 29 | db.remove_opponent_offer_sdp_in_room(name).await?; 30 | Ok(Some(answer)) 31 | } 32 | 33 | pub async fn find_spectator( 34 | db: &impl ReservedRoomTables, 35 | name: String, 36 | ) -> Result> { 37 | let Some(answer) = db.remove_room_spectator_answer(name.clone()).await? else { 38 | return Ok(None); 39 | }; 40 | db.remove_spectator_offer_sdp_in_room(name).await?; 41 | Ok(Some(answer)) 42 | } 43 | 44 | pub async fn post_room_keep( 45 | db: &impl ReservedRoomTables, 46 | name: &str, 47 | body: PostReservedRoomKeepRequestBody, 48 | ) -> Result { 49 | let (key, spectator_offer) = body.into_inner(); 50 | if Uuid::parse_str(&key).is_err() { 51 | return Ok(PostReservedRoomKeepResponse::BadRequest); 52 | } 53 | let room = db 54 | .keep_room(name.to_owned(), key, spectator_offer, ttl_sec(now_sec())) 55 | .await?; 56 | let Some(room) = room else { 57 | return Ok(PostReservedRoomKeepResponse::BadRequest); 58 | }; 59 | if room.opponent_offer_sdp().is_some() { 60 | return Ok( 61 | if let Some(answer) = find_opponent(db, name.to_owned()).await? { 62 | PostReservedRoomKeepResponseOkBody::from( 63 | PostReservedRoomKeepResponseOkOpponentAnswerBody::new(answer.0.into_sdp()), 64 | ) 65 | .into() 66 | } else { 67 | let retry_after = RETRY_AFTER_INTERVAL_SEC; 68 | PostReservedRoomKeepResponse::NoContent { retry_after } 69 | }, 70 | ); 71 | } 72 | if room.spectator_offer_sdp().is_some() { 73 | return Ok( 74 | if let Some(answer) = find_spectator(db, name.to_owned()).await? { 75 | PostReservedRoomKeepResponseOkBody::from( 76 | PostReservedRoomKeepResponseOkSpectatorAnswerBody::new(answer.0.into_sdp()), 77 | ) 78 | .into() 79 | } else { 80 | let retry_after = RETRY_AFTER_INTERVAL_SEC; 81 | PostReservedRoomKeepResponse::NoContent { retry_after } 82 | }, 83 | ); 84 | } 85 | let retry_after = RETRY_AFTER_INTERVAL_SEC; 86 | Ok(PostReservedRoomKeepResponse::NoContent { retry_after }) 87 | } 88 | 89 | pub async fn post_room_join( 90 | db: &impl ReservedRoomTables, 91 | name: &str, 92 | body: PostRoomJoinRequestBody, 93 | ) -> Result { 94 | let answer = ReservedRoomOpponentAnswer(Answer::new( 95 | name.to_owned(), 96 | body.into_answer(), 97 | ttl_sec(now_sec()), 98 | )); 99 | match db.put_room_opponent_answer(answer).await { 100 | Ok(()) => { 101 | info!("[Reserved Room] Join: {}", name); 102 | Ok(PostRoomJoinResponse::Ok) 103 | } 104 | Err(PutError::Conflict) => Ok(PostRoomJoinResponse::Conflict), 105 | Err(PutError::Unknown(err)) => Err(err), 106 | } 107 | } 108 | 109 | pub async fn post_room_spectate( 110 | db: &impl ReservedRoomTables, 111 | name: &str, 112 | body: PostReservedRoomSpectateRequestBody, 113 | ) -> Result { 114 | let answer = ReservedRoomSpectatorAnswer(Answer::new( 115 | name.to_owned(), 116 | body.into_answer(), 117 | ttl_sec(now_sec()), 118 | )); 119 | match db.put_room_spectator_answer(answer).await { 120 | Ok(()) => { 121 | info!("[Reserved Room] Spectate: {}", name); 122 | Ok(PostRoomJoinResponse::Ok) 123 | } 124 | Err(PutError::Conflict) => Ok(PostRoomJoinResponse::Conflict), 125 | Err(PutError::Unknown(err)) => Err(err), 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /junowen-server/src/routes/room_utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | string::FromUtf8Error, 3 | time::{SystemTime, UNIX_EPOCH}, 4 | }; 5 | 6 | use junowen_lib::signaling_server::room::{PostRoomKeepResponse, PutRoomResponse}; 7 | use lambda_http::{Body, Response}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use super::to_response; 11 | 12 | const OFFER_TTL_DURATION_SEC: u64 = 10; 13 | pub const RETRY_AFTER_INTERVAL_SEC: u32 = 3; 14 | 15 | pub fn now_sec() -> u64 { 16 | let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); 17 | now.as_secs() 18 | } 19 | 20 | pub fn ttl_sec(now_sec: u64) -> u64 { 21 | now_sec + OFFER_TTL_DURATION_SEC 22 | } 23 | 24 | pub fn from_put_room_response<'a, T>(value: PutRoomResponse) -> Response 25 | where 26 | T: Deserialize<'a> + Serialize, 27 | { 28 | let status_code = value.status_code(); 29 | let body = match value { 30 | PutRoomResponse::CreatedWithKey { body, .. } => { 31 | Body::Text(serde_json::to_string(&body).unwrap()) 32 | } 33 | PutRoomResponse::CreatedWithAnswer { body, .. } => { 34 | Body::Text(serde_json::to_string(&body).unwrap()) 35 | } 36 | PutRoomResponse::Conflict { body, .. } => Body::Text(serde_json::to_string(&body).unwrap()), 37 | }; 38 | to_response(status_code, body) 39 | } 40 | 41 | pub fn from_post_room_keep_response<'a, T>(value: PostRoomKeepResponse) -> Response 42 | where 43 | T: Deserialize<'a> + Serialize, 44 | { 45 | let status_code = value.status_code(); 46 | let body = match value { 47 | PostRoomKeepResponse::BadRequest => Body::Empty, 48 | PostRoomKeepResponse::NoContent { .. } => Body::Empty, 49 | PostRoomKeepResponse::Ok(body) => Body::Text(serde_json::to_string(&body).unwrap()), 50 | }; 51 | to_response(status_code, body) 52 | } 53 | 54 | pub fn decode_room_name(encoded_room_name: &str) -> Result { 55 | urlencoding::decode(&encoded_room_name.replace('+', "%20")).map(|x| x.to_string()) 56 | } 57 | -------------------------------------------------------------------------------- /junowen-server/src/tracing_helper.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroU8, panic}; 2 | 3 | use time::format_description::well_known::{ 4 | iso8601::{self, EncodedConfig}, 5 | Iso8601, 6 | }; 7 | use tracing::error; 8 | use tracing_subscriber::{ 9 | fmt::{ 10 | self, 11 | format::{Compact, DefaultFields, Format}, 12 | time::{FormatTime, LocalTime, SystemTime}, 13 | }, 14 | prelude::__tracing_subscriber_SubscriberExt, 15 | EnvFilter, Layer, Registry, 16 | }; 17 | 18 | fn default_subscriber_builder() -> fmt::Layer> { 19 | const WITH_FILE_PATH: bool = cfg!(debug_assertions); 20 | fmt::layer() 21 | .compact() 22 | .with_file(WITH_FILE_PATH) 23 | .with_line_number(WITH_FILE_PATH) 24 | .with_target(!WITH_FILE_PATH) 25 | .with_thread_ids(true) 26 | } 27 | 28 | type MyLayer = fmt::Layer>; 29 | 30 | fn init_tracing( 31 | customize: fn(MyLayer) -> MyLayer, 32 | ) { 33 | let layer = customize(default_subscriber_builder()); 34 | const DIRECTIVES: &str = if cfg!(debug_assertions) { 35 | concat!(env!("CARGO_CRATE_NAME"), "=trace") 36 | } else { 37 | concat!(env!("CARGO_CRATE_NAME"), "=info") 38 | }; 39 | let filter = EnvFilter::new(DIRECTIVES); 40 | let reg = tracing_subscriber::registry().with(layer.with_filter(filter)); 41 | tracing::subscriber::set_global_default(reg).unwrap(); 42 | panic::set_hook(Box::new(|panic| error!("{}", panic))); 43 | } 44 | 45 | pub fn init_local_tracing() { 46 | const MY_CONFIG: EncodedConfig = iso8601::Config::DEFAULT 47 | .set_time_precision(iso8601::TimePrecision::Second { 48 | decimal_digits: NonZeroU8::new(6), 49 | }) 50 | .encode(); 51 | init_tracing(|layer| layer.with_timer(LocalTime::new(Iso8601::))); 52 | } 53 | 54 | pub fn init_server_tracing() { 55 | init_tracing(|layer| layer.without_time().with_ansi(false)); 56 | } 57 | -------------------------------------------------------------------------------- /junowen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "junowen" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | name = "th19_junowen" 10 | crate-type = ['cdylib'] 11 | 12 | [features] 13 | simple-dll-injection = [] 14 | 15 | [build-dependencies] 16 | static_vcruntime.workspace = true 17 | 18 | [dependencies] 19 | anyhow.workspace = true 20 | async-trait.workspace = true 21 | bytes.workspace = true 22 | clipboard-win.workspace = true 23 | derive-new = "0.6.0" 24 | getset = "0.1.2" 25 | junowen-lib.workspace = true 26 | once_cell = "1.19.0" 27 | reqwest = { version = "0.12.7", features = ["json"] } 28 | rmp-serde.workspace = true 29 | serde.workspace = true 30 | serde_json.workspace = true 31 | sys-locale = "0.3.1" 32 | thiserror.workspace = true 33 | time = { version = "0.3.36", features = [] } 34 | tokio.workspace = true 35 | toml.workspace = true 36 | toml_edit = "0.22.20" 37 | tracing.workspace = true 38 | tracing-appender = "0.2.3" 39 | tracing-subscriber.workspace = true 40 | urlencoding = "2.1.3" 41 | windows.workspace = true 42 | -------------------------------------------------------------------------------- /junowen/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if cfg!(target_os = "windows") { 3 | static_vcruntime::metabuild(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /junowen/src/bin/junowen-standalone.rs: -------------------------------------------------------------------------------- 1 | mod lang; 2 | 3 | use std::{env::current_exe, process::ExitCode}; 4 | 5 | use junowen_lib::{hook_utils::do_dll_injection, lang::Lang}; 6 | use sys_locale::get_locales; 7 | 8 | use crate::lang::to_lang_source; 9 | 10 | fn create_lang() -> Lang { 11 | let lang = get_locales() 12 | .flat_map(|tag| { 13 | let primary_lang = tag.split('-').next().unwrap_or(&tag).to_owned(); 14 | [tag, primary_lang] 15 | }) 16 | .filter_map(|tag| to_lang_source(&tag)) 17 | .find_map(|file| toml::from_str(file).ok()) 18 | .unwrap_or_default(); 19 | Lang::new(lang) 20 | } 21 | 22 | fn main() -> ExitCode { 23 | let lang = create_lang(); 24 | 25 | let dll_path = current_exe() 26 | .unwrap() 27 | .as_path() 28 | .parent() 29 | .unwrap() 30 | .join(concat!(env!("CARGO_PKG_NAME"), ".dll")); 31 | if let Err(err) = do_dll_injection("th19.exe", &dll_path) { 32 | lang.print("failed injection into th19.exe"); 33 | println!(": {}", err); 34 | println!(); 35 | lang.println("you can close this window by pressing enter..."); 36 | let _ = std::io::stdin().read_line(&mut String::new()); 37 | return ExitCode::FAILURE; 38 | } 39 | 40 | lang.println("completed injection into th19.exe"); 41 | println!(); 42 | lang.println("you can close this window by pressing enter..."); 43 | let _ = std::io::stdin().read_line(&mut String::new()); 44 | ExitCode::SUCCESS 45 | } 46 | -------------------------------------------------------------------------------- /junowen/src/bin/lang/ja.toml: -------------------------------------------------------------------------------- 1 | "failed injection into th19.exe" = "th19.exe への注入に失敗しました" 2 | "completed injection into th19.exe" = "th19.exe への注入が完了しました" 3 | "you can close this window by pressing enter..." = "エンターキーを押してこのウィンドウを閉じることができます..." 4 | -------------------------------------------------------------------------------- /junowen/src/bin/lang/mod.rs: -------------------------------------------------------------------------------- 1 | pub fn to_lang_source(tag: &str) -> Option<&'static str> { 2 | match tag { 3 | "ja" => Some(include_str!("ja.toml")), 4 | _ => None, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /junowen/src/helper.rs: -------------------------------------------------------------------------------- 1 | use junowen_lib::structs::input_devices::InputDevices; 2 | 3 | pub fn inputed_number(input_devices: &InputDevices) -> Option { 4 | let raw_keys = input_devices.keyboard_input().raw_keys(); 5 | (0..=9).find(|i| raw_keys[(b'0' + i) as usize] & 0x80 != 0) 6 | } 7 | 8 | pub fn pushed_f1(input_devices: &InputDevices) -> bool { 9 | let raw_keys = input_devices.keyboard_input().raw_keys(); 10 | raw_keys[0x70] & 0x80 != 0 11 | } 12 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby.rs: -------------------------------------------------------------------------------- 1 | mod common_menu; 2 | mod helper; 3 | mod lobby; 4 | mod pure_p2p_guest; 5 | mod pure_p2p_offerer; 6 | mod room; 7 | mod title_menu_modifier; 8 | 9 | pub use {lobby::Lobby, title_menu_modifier::TitleMenuModifier}; 10 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/common_menu/menu_controller.rs: -------------------------------------------------------------------------------- 1 | use junowen_lib::structs::input_devices::{InputFlags, InputValue}; 2 | 3 | fn pulse(current: InputValue, prev: InputValue, flag: InputFlags) -> bool { 4 | current.0 & flag != None && prev.0 & flag == None 5 | } 6 | 7 | pub enum MenuControllerUpdateDecideResult { 8 | None, 9 | Wait, 10 | Decide, 11 | Cancel, 12 | } 13 | 14 | pub enum MenuControllerInputResult { 15 | None, 16 | Cancel, 17 | Decide, 18 | Up, 19 | Down, 20 | } 21 | 22 | #[derive(Default)] 23 | pub struct MenuController { 24 | repeat_up: u32, 25 | repeat_down: u32, 26 | decide_count: i32, 27 | } 28 | 29 | impl MenuController { 30 | pub fn update_decide(&mut self) -> MenuControllerUpdateDecideResult { 31 | if self.decide_count == 0 { 32 | return MenuControllerUpdateDecideResult::None; 33 | } 34 | if self.decide_count > 0 { 35 | self.decide_count += 1; 36 | if self.decide_count > 20 { 37 | self.decide_count = 0; 38 | return MenuControllerUpdateDecideResult::Decide; 39 | } 40 | } else { 41 | self.decide_count -= 1; 42 | if self.decide_count < -20 { 43 | self.decide_count = 0; 44 | return MenuControllerUpdateDecideResult::Cancel; 45 | } 46 | } 47 | MenuControllerUpdateDecideResult::Wait 48 | } 49 | 50 | fn cancel(&mut self, current_input: InputValue, prev_input: InputValue, instant: bool) -> bool { 51 | if !pulse(current_input, prev_input, InputFlags::CHARGE) 52 | && !pulse(current_input, prev_input, InputFlags::BOMB) 53 | && !pulse(current_input, prev_input, InputFlags::PAUSE) 54 | { 55 | return false; 56 | } 57 | if !instant { 58 | self.force_cancel(); 59 | } 60 | true 61 | } 62 | 63 | pub fn force_cancel(&mut self) { 64 | self.decide_count = -1; 65 | } 66 | 67 | fn decide(&mut self, current_input: InputValue, prev_input: InputValue, instant: bool) -> bool { 68 | if !pulse(current_input, prev_input, InputFlags::SHOT) 69 | && !pulse(current_input, prev_input, InputFlags::ENTER) 70 | { 71 | return false; 72 | } 73 | if !instant { 74 | self.decide_count = 1; 75 | } 76 | true 77 | } 78 | 79 | fn select(&mut self, current_input: InputValue, prev_input: InputValue) -> Option { 80 | let mut mv = None; 81 | if current_input.0 & InputFlags::UP != None 82 | && (prev_input.0 & InputFlags::UP == None || self.repeat_up > 0) 83 | { 84 | if [0, 25].contains(&self.repeat_up) { 85 | mv = Some(true); 86 | } 87 | self.repeat_up += 1; 88 | if self.repeat_up > 25 { 89 | self.repeat_up = 17; 90 | } 91 | } else { 92 | self.repeat_up = 0; 93 | } 94 | if current_input.0 & InputFlags::DOWN != None 95 | && (prev_input.0 & InputFlags::DOWN == None || self.repeat_down > 0) 96 | { 97 | if [0, 25].contains(&self.repeat_down) { 98 | mv = Some(false); 99 | } 100 | self.repeat_down += 1; 101 | if self.repeat_down > 25 { 102 | self.repeat_down = 17; 103 | } 104 | } else { 105 | self.repeat_down = 0; 106 | } 107 | mv 108 | } 109 | 110 | pub fn input( 111 | &mut self, 112 | current_input: InputValue, 113 | prev_input: InputValue, 114 | instant_decide: bool, 115 | instant_cancel: bool, 116 | ) -> MenuControllerInputResult { 117 | if self.cancel(current_input, prev_input, instant_cancel) { 118 | return MenuControllerInputResult::Cancel; 119 | } 120 | if self.decide(current_input, prev_input, instant_decide) { 121 | return MenuControllerInputResult::Decide; 122 | } 123 | if let Some(up) = self.select(current_input, prev_input) { 124 | if up { 125 | return MenuControllerInputResult::Up; 126 | } else { 127 | return MenuControllerInputResult::Down; 128 | } 129 | } 130 | MenuControllerInputResult::None 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/common_menu/menu_item.rs: -------------------------------------------------------------------------------- 1 | use getset::{CopyGetters, Getters, MutGetters, Setters}; 2 | 3 | use super::{menu::Menu, text_input::TextInput, Action, LobbyScene}; 4 | 5 | #[derive(Debug, Setters)] 6 | pub struct MenuPlainItem { 7 | #[set = "pub"] 8 | label: &'static str, 9 | enabled: bool, 10 | decided_action: u8, 11 | play_sound: bool, 12 | } 13 | 14 | #[derive(Debug, Getters, MutGetters, Setters)] 15 | pub struct MenuSubMenuItem { 16 | #[set = "pub"] 17 | label: &'static str, 18 | enabled: bool, 19 | decided_action: Option, 20 | #[getset(get = "pub", get_mut = "pub")] 21 | sub_menu: Menu, 22 | } 23 | 24 | #[derive(CopyGetters, Debug, Getters, MutGetters, Setters)] 25 | pub struct MenuTextInputItem { 26 | #[getset(get_copy = "pub", set = "pub")] 27 | label: &'static str, 28 | enabled: bool, 29 | decided_action: u8, 30 | #[getset(get = "pub", get_mut = "pub")] 31 | text_input: Box, 32 | } 33 | 34 | #[derive(CopyGetters, Debug, Setters)] 35 | pub struct MenuSubSceneItem { 36 | #[getset(get_copy = "pub", set = "pub")] 37 | label: &'static str, 38 | enabled: bool, 39 | #[get_copy = "pub"] 40 | sub_scene: LobbyScene, 41 | } 42 | 43 | #[derive(Debug)] 44 | pub enum MenuItem { 45 | Plain(MenuPlainItem), 46 | SubMenu(MenuSubMenuItem), 47 | TextInput(MenuTextInputItem), 48 | SubScene(MenuSubSceneItem), 49 | } 50 | 51 | impl MenuItem { 52 | pub fn plain(label: &'static str, decided_action: u8, play_sound: bool) -> Self { 53 | Self::Plain(MenuPlainItem { 54 | label, 55 | enabled: true, 56 | decided_action, 57 | play_sound, 58 | }) 59 | } 60 | 61 | pub fn sub_menu(label: &'static str, decided_action: Option, sub_menu: Menu) -> Self { 62 | Self::SubMenu(MenuSubMenuItem { 63 | label, 64 | enabled: true, 65 | decided_action, 66 | sub_menu, 67 | }) 68 | } 69 | 70 | pub fn sub_scene(label: &'static str, sub_scene: LobbyScene) -> Self { 71 | Self::SubScene(MenuSubSceneItem { 72 | label, 73 | enabled: true, 74 | sub_scene, 75 | }) 76 | } 77 | 78 | pub fn text_input( 79 | label: &'static str, 80 | decided_action: u8, 81 | changed_action: u8, 82 | name: &'static str, 83 | ) -> Self { 84 | Self::TextInput(MenuTextInputItem { 85 | label, 86 | enabled: true, 87 | decided_action, 88 | text_input: Box::new(TextInput::new(changed_action, name)), 89 | }) 90 | } 91 | 92 | // ---- 93 | 94 | pub fn label(&self) -> &str { 95 | match self { 96 | Self::Plain(item) => item.label, 97 | Self::SubMenu(item) => item.label, 98 | Self::TextInput(item) => item.label, 99 | Self::SubScene(scene) => scene.label, 100 | } 101 | } 102 | pub fn set_label(&mut self, label: &'static str) { 103 | match self { 104 | Self::Plain(item) => item.label = label, 105 | Self::SubMenu(item) => item.label = label, 106 | Self::TextInput(item) => item.label = label, 107 | Self::SubScene(scene) => scene.label = label, 108 | } 109 | } 110 | 111 | pub fn enabled(&self) -> bool { 112 | match self { 113 | Self::Plain(item) => item.enabled, 114 | Self::SubMenu(item) => item.enabled, 115 | Self::TextInput(item) => item.enabled, 116 | Self::SubScene(scene) => scene.enabled, 117 | } 118 | } 119 | pub fn set_enabled(&mut self, enabled: bool) { 120 | match self { 121 | Self::Plain(item) => item.enabled = enabled, 122 | Self::SubMenu(item) => item.enabled = enabled, 123 | Self::TextInput(item) => item.enabled = enabled, 124 | Self::SubScene(scene) => scene.enabled = enabled, 125 | } 126 | } 127 | 128 | pub fn decided_action(&self) -> Option { 129 | match self { 130 | Self::Plain(item) => Some(Action::new(item.decided_action, item.play_sound, None)), 131 | Self::SubMenu(item) => item 132 | .decided_action 133 | .map(|decided_action| Action::new(decided_action, true, None)), 134 | Self::TextInput(item) => Some(Action::new( 135 | item.decided_action, 136 | true, 137 | Some(item.text_input().value().to_owned()), 138 | )), 139 | Self::SubScene(_) => None, 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/common_menu/text_input.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, num::NonZeroU8}; 2 | 3 | use getset::Setters; 4 | use junowen_lib::Th19; 5 | use windows::Win32::UI::Input::KeyboardAndMouse::{MapVirtualKeyW, ToUnicode, MAPVK_VK_TO_VSC}; 6 | 7 | use crate::in_game_lobby::helper::render_label_value; 8 | 9 | fn to_ascii(vk: u32, current: &[u8; 256]) -> Option { 10 | let mut buf = [0u16; 2]; 11 | unsafe { 12 | let scan_code = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC); 13 | ToUnicode(vk, scan_code, Some(current), &mut buf, 0); 14 | } 15 | NonZeroU8::new(String::from_utf16_lossy(&buf).as_bytes()[0]) 16 | } 17 | 18 | #[derive(Debug)] 19 | struct TextInputState { 20 | prev: [u8; 256], 21 | current_vk: u8, 22 | current_vk_count: u32, 23 | } 24 | 25 | impl TextInputState { 26 | pub fn new(current: &[u8; 256]) -> Self { 27 | let mut zelf = Self { 28 | prev: [0; 256], 29 | current_vk: 0, 30 | current_vk_count: 0, 31 | }; 32 | zelf.tick(current); 33 | zelf 34 | } 35 | 36 | pub fn tick(&mut self, current: &[u8; 256]) -> Vec { 37 | let mut result = vec![]; 38 | for (vk, _) in current 39 | .iter() 40 | .enumerate() 41 | .filter(|&(vk, value)| value & 0x80 != 0 && self.prev[vk] & 0x80 == 0) 42 | { 43 | if let Some(ascii) = to_ascii(vk as u32, current) { 44 | result.push(ascii.get()); 45 | } 46 | let vk = vk as u8; 47 | if vk != self.current_vk { 48 | self.current_vk = vk; 49 | self.current_vk_count = 0; 50 | } 51 | } 52 | if current[self.current_vk as usize] & 0x80 != 0 { 53 | if self.current_vk_count > 30 { 54 | if let Some(ascii) = to_ascii(self.current_vk as u32, current) { 55 | result.push(ascii.get()); 56 | } 57 | } 58 | self.current_vk_count += 1; 59 | } else { 60 | self.current_vk_count = 0; 61 | } 62 | self.prev.copy_from_slice(current); 63 | result 64 | } 65 | } 66 | 67 | pub enum OnMenuInputResult { 68 | None, 69 | Cancel, 70 | Decide(u8, String), 71 | } 72 | 73 | #[derive(Debug, Setters)] 74 | pub struct TextInput { 75 | changed_action: u8, 76 | name: &'static str, 77 | #[getset(set = "pub")] 78 | value: String, 79 | state: Option, 80 | } 81 | 82 | impl TextInput { 83 | pub fn new(changed_action: u8, name: &'static str) -> Self { 84 | Self { 85 | changed_action, 86 | name, 87 | value: String::new(), 88 | state: None, 89 | } 90 | } 91 | 92 | pub fn value(&self) -> &str { 93 | &self.value 94 | } 95 | 96 | fn state_mut(&mut self) -> &mut TextInputState { 97 | self.state.as_mut().unwrap() 98 | } 99 | 100 | pub fn on_input_menu(&mut self, th19: &Th19) -> OnMenuInputResult { 101 | if self.state.is_none() { 102 | self.state = Some(TextInputState::new( 103 | th19.input_devices().keyboard_input().raw_keys(), 104 | )); 105 | return OnMenuInputResult::None; 106 | } 107 | for ascii in self 108 | .state_mut() 109 | .tick(th19.input_devices().keyboard_input().raw_keys()) 110 | { 111 | if (0x20..0x7f).contains(&ascii) { 112 | self.value.push(ascii as char); 113 | } else if ascii == 0x08 { 114 | self.value.pop(); 115 | } else if ascii == 0x0d { 116 | // CR 117 | return OnMenuInputResult::Decide(self.changed_action, self.value.clone()); 118 | } else if ascii == 0x1b { 119 | // ESC 120 | return OnMenuInputResult::Cancel; 121 | } 122 | } 123 | OnMenuInputResult::None 124 | } 125 | 126 | pub fn on_render_texts(&self, th19: &Th19, text_renderer: *const c_void) { 127 | render_label_value(th19, text_renderer, 480, 0, self.name, &self.value); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/helper.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::{structs::others::RenderingText, Th19}; 4 | 5 | pub fn render_title(th19: &Th19, text_renderer: *const c_void, text: &[u8]) { 6 | let mut rt = RenderingText::default(); 7 | rt.set_text(text); 8 | rt.set_x(640, th19.window_inner()); 9 | rt.set_y(64, th19.window_inner()); 10 | rt.color = 0xff000000; 11 | rt.font_type = 9; 12 | rt.drop_shadow = true; 13 | rt.horizontal_align = 0; 14 | th19.render_text(text_renderer, &rt); 15 | 16 | rt.color = 0xffffffff; 17 | rt.font_type = 7; 18 | th19.render_text(text_renderer, &rt); 19 | } 20 | 21 | pub fn render_menu_item( 22 | th19: &Th19, 23 | text_renderer: *const c_void, 24 | text: &[u8], 25 | y: u32, 26 | enabled: bool, 27 | selected: bool, 28 | ) { 29 | let mut rt = RenderingText::default(); 30 | rt.set_text(text); 31 | rt.set_x(640, th19.window_inner()); 32 | rt.set_y(y, th19.window_inner()); 33 | rt.color = menu_item_color(9, enabled, selected); 34 | rt.font_type = 9; 35 | rt.horizontal_align = 0; 36 | th19.render_text(text_renderer, &rt); 37 | 38 | rt.color = menu_item_color(7, enabled, selected); 39 | rt.font_type = 7; 40 | th19.render_text(text_renderer, &rt); 41 | } 42 | 43 | pub fn render_text_line(th19: &Th19, text_renderer: *const c_void, line: u32, text: &[u8]) { 44 | let mut rt = RenderingText::default(); 45 | rt.set_text(text); 46 | rt.set_x(32, th19.window_inner()); 47 | rt.set_y(160 + line * 32, th19.window_inner()); 48 | rt.color = 0xff000000; 49 | rt.font_type = 8; 50 | th19.render_text(text_renderer, &rt); 51 | 52 | rt.color = 0xffffffff; 53 | rt.font_type = 6; 54 | th19.render_text(text_renderer, &rt); 55 | } 56 | 57 | pub fn render_small_text_line(th19: &Th19, text_renderer: *const c_void, line: u32, text: &[u8]) { 58 | let mut rt = RenderingText::default(); 59 | rt.set_text(text); 60 | rt.set_x(32, th19.window_inner()); 61 | rt.set_y(160 + line * 16, th19.window_inner()); 62 | rt.font_type = 1; 63 | th19.render_text(text_renderer, &rt); 64 | } 65 | 66 | pub fn menu_item_color(font_type: u32, enabled: bool, selected: bool) -> u32 { 67 | if !enabled { 68 | match font_type { 69 | 9 => 0x40ffffff, 70 | 7 => 0xff808080, 71 | _ => unreachable!(), 72 | } 73 | } else if selected { 74 | match font_type { 75 | 9 => 0xff000000, 76 | 7 => 0xffffff80, 77 | _ => unreachable!(), 78 | } 79 | } else { 80 | match font_type { 81 | 9 => 0xff404040, 82 | 7 => 0xff808060, 83 | _ => unreachable!(), 84 | } 85 | } 86 | } 87 | 88 | pub fn render_label_value( 89 | th19: &Th19, 90 | text_renderer: *const c_void, 91 | height: u32, 92 | vertical_align: u32, 93 | label: &str, 94 | value: &str, 95 | ) { 96 | let mut rt = RenderingText::default(); 97 | rt.set_text(format!("{:<11}:", label).as_bytes()); 98 | rt.set_x(320, th19.window_inner()); 99 | rt.set_y(height, th19.window_inner()); 100 | rt.color = 0xffffffff; 101 | rt.font_type = 0; 102 | rt.horizontal_align = 1; 103 | rt.vertical_align = vertical_align; 104 | th19.render_text(text_renderer, &rt); 105 | 106 | rt.set_text(value.as_bytes()); 107 | rt.color = 0xffffffa0; 108 | rt.set_x(544, th19.window_inner()); 109 | th19.render_text(text_renderer, &rt); 110 | } 111 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/room.rs: -------------------------------------------------------------------------------- 1 | pub mod reserved; 2 | pub mod shared; 3 | 4 | use std::{f64::consts::PI, ffi::c_void}; 5 | 6 | use junowen_lib::{structs::others::RenderingText, Th19}; 7 | 8 | use crate::signaling::waiting_for_match::WaitingInRoom; 9 | 10 | use super::{ 11 | common_menu::CommonMenu, 12 | helper::{render_label_value, render_text_line}, 13 | }; 14 | 15 | fn progress_alphas(progress: f64) -> Vec { 16 | const LENGTH: f64 = 20.0; 17 | let progress = progress / 2.0 % 1.0; 18 | 19 | // 4PI ごとに波と凪が交互に来る関数 20 | let curve = |x: f64| ((x + PI).cos() + 1.0) / 2.0 * ((x + PI) / 2.0).cos().ceil(); 21 | 22 | (0..LENGTH as usize) 23 | .map(|i| { 24 | (curve((i as f64 / LENGTH / 2.0 - progress) * 4.0 * PI) * 0xff as f64).ceil() as u8 25 | }) 26 | .collect() 27 | } 28 | 29 | /// アルファと cos カーブを使った表現 30 | /// ボツ 31 | #[allow(unused)] 32 | fn render_progress_alpha(th19: &Th19, text_renderer: *const c_void, progress: f64) { 33 | let text = b"| |"; 34 | let x = 640; 35 | let y = 160 + 32 * 11; 36 | let mut rt = RenderingText::default(); 37 | rt.set_text(text); 38 | rt.set_x(x, th19.window_inner()); 39 | rt.set_y(y, th19.window_inner()); 40 | rt.color = 0xff000000; 41 | rt.font_type = 8; 42 | rt.horizontal_align = 0; 43 | th19.render_text(text_renderer, &rt); 44 | 45 | rt.color = 0xffffffff; 46 | rt.font_type = 6; 47 | th19.render_text(text_renderer, &rt); 48 | 49 | for (i, &alpha) in progress_alphas(progress).iter().enumerate() { 50 | let x = (650 - 200 + i * 20) as u32; 51 | 52 | rt.set_text(b"-"); 53 | rt.set_x(x, th19.window_inner()); 54 | rt.color = (0xff - alpha) as u32 * 0x1000000; 55 | rt.font_type = 8; 56 | th19.render_text(text_renderer, &rt); 57 | rt.color |= 0x00ffffff; 58 | rt.font_type = 6; 59 | th19.render_text(text_renderer, &rt); 60 | 61 | rt.set_text(b"#"); 62 | rt.color = alpha as u32 * 0x1000000; 63 | rt.font_type = 8; 64 | th19.render_text(text_renderer, &rt); 65 | rt.color |= 0x00ffffff; 66 | rt.font_type = 6; 67 | th19.render_text(text_renderer, &rt); 68 | } 69 | } 70 | 71 | fn progress_text(progress: f64) -> Vec { 72 | const BUFFER_TIME: f64 = 0.25; 73 | const LENGTH: f64 = 20.0 * (1.0 + BUFFER_TIME); 74 | let progress = ((progress / (1.0 + BUFFER_TIME) + 1.0) % 2.0 - 1.0) * LENGTH; 75 | let mut progress_text = vec![]; 76 | let (progress, left_char, right_char, left_len) = if progress >= 0.0 { 77 | (progress, b'#', b'-', progress as usize) 78 | } else { 79 | let progress = -progress; 80 | (progress, b'-', b'#', LENGTH as usize - progress as usize) 81 | }; 82 | let right_len = LENGTH as usize - left_len; 83 | progress_text.append(&mut vec![left_char; left_len]); 84 | if progress < LENGTH { 85 | progress_text.push(b'#'); 86 | } 87 | progress_text.append(&mut vec![right_char; right_len]); 88 | 89 | let mut text = vec![b'[']; 90 | progress_text[0..20].iter().for_each(|&x| text.push(x)); 91 | text.push(b']'); 92 | text 93 | } 94 | 95 | fn render_progress_item(th19: &Th19, text_renderer: *const c_void, alpha: u8, text: &[u8]) { 96 | let x = 640; 97 | let y = 160 + 32 * 11; 98 | let mut rt = RenderingText::default(); 99 | rt.set_text(text); 100 | rt.set_x(x, th19.window_inner()); 101 | rt.set_y(y, th19.window_inner()); 102 | rt.color = alpha as u32 * 0x1000000; 103 | rt.font_type = 8; 104 | rt.horizontal_align = 0; 105 | th19.render_text(text_renderer, &rt); 106 | 107 | rt.color = (alpha as u32 * 0x1000000) | 0x00ffffff; 108 | rt.font_type = 6; 109 | th19.render_text(text_renderer, &rt); 110 | } 111 | 112 | fn render_progress(th19: &Th19, text_renderer: *const c_void, progress: f64) { 113 | let base_text = progress_text(progress); 114 | render_progress_item(th19, text_renderer, 0xff, &base_text); 115 | } 116 | 117 | pub fn on_render_texts( 118 | menu: &CommonMenu, 119 | waiting: Option<&WaitingInRoom>, 120 | room_name: Option<&str>, 121 | th19: &Th19, 122 | text_renderer: *const c_void, 123 | ) { 124 | menu.on_render_texts(th19, text_renderer); 125 | 126 | if let Some(waiting) = waiting { 127 | let elapsed = waiting.elapsed(); 128 | render_progress(th19, text_renderer, elapsed.as_secs_f64() / 4.0); 129 | for (i, error) in waiting.errors().iter().rev().enumerate() { 130 | let error_msg = format!("Failed: {}", error); 131 | render_text_line(th19, text_renderer, 13 + i as u32, error_msg.as_bytes()); 132 | } 133 | } else if let Some(room_name) = room_name { 134 | render_label_value(th19, text_renderer, 240 - 56, 1, "Room name", room_name); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /junowen/src/in_game_lobby/room/shared.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::{structs::input_devices::InputValue, Th19}; 4 | 5 | use crate::{ 6 | file::SettingsRepo, signaling::waiting_for_match::WaitingForOpponentInSharedRoom, TOKIO_RUNTIME, 7 | }; 8 | 9 | use super::{ 10 | super::common_menu::{CommonMenu, LobbyScene, Menu, MenuItem, OnMenuInputResult}, 11 | on_render_texts, 12 | }; 13 | 14 | fn make_menu() -> CommonMenu { 15 | let items = vec![ 16 | MenuItem::plain("Enter the Room", 0, true), 17 | MenuItem::text_input("Change Room Name", 11, 12, "Room name"), 18 | ]; 19 | CommonMenu::new(false, 240 + 56, Menu::new("Shared Room", None, items, 0)) 20 | } 21 | 22 | pub struct SharedRoom { 23 | menu: CommonMenu, 24 | enter: bool, 25 | room_name: Option, 26 | } 27 | 28 | impl SharedRoom { 29 | pub fn new() -> Self { 30 | Self { 31 | menu: make_menu(), 32 | enter: false, 33 | room_name: None, 34 | } 35 | } 36 | 37 | fn room_name(&self) -> &str { 38 | self.room_name.as_ref().unwrap() 39 | } 40 | 41 | fn change_menu_to_enter(&mut self) { 42 | self.enter = false; 43 | let item = &mut self.menu.menu_mut().items_mut()[0]; 44 | item.set_label("Enter the Room"); 45 | let item = &mut self.menu.menu_mut().items_mut()[1]; 46 | item.set_enabled(true); 47 | } 48 | fn change_menu_to_leave(&mut self) { 49 | self.enter = true; 50 | let item = &mut self.menu.menu_mut().items_mut()[0]; 51 | item.set_label("Leave the Room"); 52 | let item = &mut self.menu.menu_mut().items_mut()[1]; 53 | item.set_enabled(false); 54 | } 55 | 56 | pub fn on_input_menu( 57 | &mut self, 58 | settings_repo: &SettingsRepo, 59 | current_input: InputValue, 60 | prev_input: InputValue, 61 | th19: &Th19, 62 | waiting: &mut Option, 63 | ) -> Option { 64 | if self.room_name.is_none() { 65 | self.room_name = Some(TOKIO_RUNTIME.block_on(settings_repo.shared_room_name(th19))); 66 | } 67 | if waiting.is_some() != self.enter { 68 | if waiting.is_some() { 69 | self.change_menu_to_leave(); 70 | } else { 71 | self.change_menu_to_enter(); 72 | } 73 | } 74 | 75 | if let Some(waiting) = waiting { 76 | waiting.recv(); 77 | } 78 | match self.menu.on_input_menu(current_input, prev_input, th19) { 79 | OnMenuInputResult::None => None, 80 | OnMenuInputResult::Cancel => Some(LobbyScene::Root), 81 | OnMenuInputResult::SubScene(_) => unreachable!(), 82 | OnMenuInputResult::Action(action) => match action.id() { 83 | 0 => { 84 | if waiting.is_none() { 85 | let room_name = self.room_name().to_owned(); 86 | *waiting = Some(WaitingForOpponentInSharedRoom::new(room_name)); 87 | self.change_menu_to_leave(); 88 | } else { 89 | *waiting = None; 90 | self.change_menu_to_enter(); 91 | } 92 | None 93 | } 94 | 11 => { 95 | let room_name = self.room_name().to_owned(); 96 | let MenuItem::TextInput(text_input_item) = 97 | self.menu.menu_mut().selected_item_mut() 98 | else { 99 | unreachable!() 100 | }; 101 | text_input_item.text_input_mut().set_value(room_name); 102 | None 103 | } 104 | 12 => { 105 | let new_room_name = action.value().unwrap().to_owned(); 106 | self.room_name = Some(new_room_name.clone()); 107 | TOKIO_RUNTIME.block_on(settings_repo.set_shared_room_name(new_room_name)); 108 | None 109 | } 110 | _ => unreachable!(), 111 | }, 112 | } 113 | } 114 | 115 | pub fn on_render_texts( 116 | &self, 117 | waiting: Option<&WaitingForOpponentInSharedRoom>, 118 | th19: &Th19, 119 | text_renderer: *const c_void, 120 | ) { 121 | on_render_texts( 122 | &self.menu, 123 | waiting, 124 | Some(self.room_name()), 125 | th19, 126 | text_renderer, 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /junowen/src/session.rs: -------------------------------------------------------------------------------- 1 | pub mod battle; 2 | mod delayed_inputs; 3 | mod session_message; 4 | pub mod spectator; 5 | pub mod spectator_host; 6 | 7 | use std::sync::mpsc; 8 | 9 | use anyhow::Result; 10 | use bytes::Bytes; 11 | use junowen_lib::connection::DataChannel; 12 | use rmp_serde::decode::Error; 13 | use serde::Serialize; 14 | use tokio::spawn; 15 | use tracing::debug; 16 | 17 | pub use session_message::{MatchInitial, RoundInitial}; 18 | 19 | fn to_channel( 20 | mut data_channel: DataChannel, 21 | decode: fn(input: &[u8]) -> Result, 22 | ) -> (mpsc::Sender, mpsc::Receiver) 23 | where 24 | T: Serialize + Send + 'static, 25 | { 26 | let (hook_outgoing_tx, hook_outgoing_rx) = std::sync::mpsc::channel(); 27 | let data_channel_message_sender = data_channel.message_sender.clone(); 28 | 29 | spawn(async move { 30 | let mut hook_outgoing_rx = hook_outgoing_rx; 31 | loop { 32 | let (msg, reusable) = 33 | tokio::task::spawn_blocking(move || (hook_outgoing_rx.recv(), hook_outgoing_rx)) 34 | .await 35 | .unwrap(); 36 | let msg = match msg { 37 | Ok(ok) => ok, 38 | Err(err) => { 39 | debug!("recv hook outgoing msg error: {}", err); 40 | return; 41 | } 42 | }; 43 | hook_outgoing_rx = reusable; 44 | let data = Bytes::from(rmp_serde::to_vec(&msg).unwrap()); 45 | if let Err(err) = data_channel_message_sender.send(data).await { 46 | debug!("send hook outgoing msg error: {}", err); 47 | return; 48 | } 49 | } 50 | }); 51 | 52 | let (hook_incoming_tx, hook_incoming_rx) = mpsc::channel(); 53 | spawn(async move { 54 | loop { 55 | let Some(data) = data_channel.recv().await else { 56 | return; 57 | }; 58 | let msg = decode(&data).unwrap(); 59 | if let Err(err) = hook_incoming_tx.send(msg) { 60 | debug!("send hook incoming msg error: {}", err); 61 | return; 62 | } 63 | } 64 | }); 65 | (hook_outgoing_tx, hook_incoming_rx) 66 | } 67 | -------------------------------------------------------------------------------- /junowen/src/session/battle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use getset::{CopyGetters, Getters, Setters}; 5 | use junowen_lib::connection::{DataChannel, PeerConnection}; 6 | use tracing::{info, trace}; 7 | 8 | use super::{ 9 | delayed_inputs::DelayedInputs, 10 | session_message::{MatchInitial, RoundInitial}, 11 | to_channel, 12 | }; 13 | 14 | #[derive(CopyGetters, Getters, Setters)] 15 | pub struct BattleSession { 16 | _conn: PeerConnection, 17 | #[getset(get = "pub", set = "pub")] 18 | remote_player_name: String, 19 | #[getset(get_copy = "pub")] 20 | host: bool, 21 | delayed_inputs: DelayedInputs, 22 | #[getset(set = "pub")] 23 | match_initial: Option, 24 | } 25 | 26 | impl Drop for BattleSession { 27 | fn drop(&mut self) { 28 | info!("session closed"); 29 | } 30 | } 31 | 32 | impl BattleSession { 33 | pub fn new(conn: PeerConnection, data_channel: DataChannel, host: bool) -> Self { 34 | let (hook_outgoing_tx, hook_incoming_rx) = 35 | to_channel(data_channel, |input| rmp_serde::from_slice(input)); 36 | Self { 37 | _conn: conn, 38 | remote_player_name: "".to_owned(), 39 | host, 40 | delayed_inputs: DelayedInputs::new(hook_outgoing_tx, hook_incoming_rx, host), 41 | match_initial: None, 42 | } 43 | } 44 | 45 | pub fn match_initial(&self) -> Option<&MatchInitial> { 46 | self.match_initial.as_ref() 47 | } 48 | 49 | pub fn delay(&self) -> u8 { 50 | self.delayed_inputs.delay() 51 | } 52 | 53 | pub fn init_match( 54 | &mut self, 55 | player_name: String, 56 | init: Option, 57 | ) -> Result<(String, Option), RecvError> { 58 | debug_assert!(self.host == init.is_some()); 59 | if let Some(init) = init { 60 | self.delayed_inputs 61 | .send_init_match((player_name, Some(init))); 62 | self.delayed_inputs.recv_init_match() 63 | } else { 64 | self.delayed_inputs.send_init_match((player_name, None)); 65 | self.delayed_inputs.recv_init_match() 66 | } 67 | } 68 | 69 | pub fn init_round( 70 | &mut self, 71 | init: Option, 72 | ) -> Result, RecvError> { 73 | debug_assert!(self.host == init.is_some()); 74 | trace!("init_round"); 75 | self.delayed_inputs.send_init_round(init); 76 | self.delayed_inputs.recv_init_round() 77 | } 78 | 79 | pub fn enqueue_input_and_dequeue( 80 | &mut self, 81 | input: u16, 82 | delay: Option, 83 | ) -> Result<(u16, u16), RecvError> { 84 | self.delayed_inputs.enqueue_input_and_dequeue(input, delay) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /junowen/src/session/session_message.rs: -------------------------------------------------------------------------------- 1 | use junowen_lib::structs::settings::GameSettings; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, Deserialize, Serialize)] 5 | pub struct MatchInitial { 6 | pub game_settings: GameSettings, 7 | } 8 | 9 | #[derive(Debug, Deserialize, Serialize)] 10 | pub struct RoundInitial { 11 | pub seed1: u32, 12 | pub seed2: u32, 13 | pub seed3: u32, 14 | pub seed4: u32, 15 | } 16 | 17 | /** input 以外はホストのみ発行できる */ 18 | #[derive(Debug, Deserialize, Serialize)] 19 | pub enum SessionMessage { 20 | InitMatch((String, Option)), 21 | InitRound(Option), 22 | Delay(u8), 23 | Input(u16), 24 | } 25 | -------------------------------------------------------------------------------- /junowen/src/session/spectator.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use derive_new::new; 5 | use getset::{CopyGetters, Getters, Setters}; 6 | use junowen_lib::{ 7 | connection::{DataChannel, PeerConnection}, 8 | structs::settings::GameSettings, 9 | }; 10 | use serde::{Deserialize, Serialize}; 11 | use tracing::{error, info}; 12 | 13 | use super::{session_message::RoundInitial, to_channel}; 14 | 15 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 16 | pub enum Screen { 17 | DifficultySelect, 18 | CharacterSelect, 19 | Game, 20 | } 21 | 22 | #[derive(new, Clone, Debug, Deserialize, CopyGetters, Serialize)] 23 | pub struct InitialState { 24 | #[get_copy = "pub"] 25 | screen: Screen, 26 | #[get_copy = "pub"] 27 | difficulty: u8, 28 | #[get_copy = "pub"] 29 | p1_character: u8, 30 | #[get_copy = "pub"] 31 | p1_card: u8, 32 | #[get_copy = "pub"] 33 | p2_character: u8, 34 | #[get_copy = "pub"] 35 | p2_card: u8, 36 | } 37 | 38 | #[derive(new, Clone, Debug, Deserialize, Getters, Serialize)] 39 | pub struct SpectatorInitial { 40 | #[get = "pub"] 41 | p1_name: String, 42 | #[get = "pub"] 43 | p2_name: String, 44 | #[get = "pub"] 45 | game_settings: GameSettings, 46 | #[get = "pub"] 47 | initial_state: InitialState, 48 | } 49 | 50 | #[derive(Debug, Deserialize, Serialize)] 51 | pub enum SpectatorSessionMessage { 52 | InitSpectator(SpectatorInitial), 53 | InitRound(RoundInitial), 54 | Inputs(u16, u16), 55 | } 56 | 57 | #[derive(CopyGetters, Getters, Setters)] 58 | pub struct SpectatorSession { 59 | _conn: PeerConnection, 60 | hook_incoming_rx: std::sync::mpsc::Receiver, 61 | spectator_initial: Option, 62 | round_initial: Option, 63 | } 64 | 65 | impl SpectatorSession { 66 | pub fn new(conn: PeerConnection, data_channel: DataChannel) -> Self { 67 | let (_hook_outgoing_tx, hook_incoming_rx) = 68 | to_channel(data_channel, |input| rmp_serde::from_slice(input)); 69 | Self { 70 | _conn: conn, 71 | hook_incoming_rx, 72 | spectator_initial: None, 73 | round_initial: None, 74 | } 75 | } 76 | 77 | pub fn spectator_initial(&self) -> Option<&SpectatorInitial> { 78 | self.spectator_initial.as_ref() 79 | } 80 | 81 | pub fn recv_init_spectator(&mut self) -> Result<(), RecvError> { 82 | let init = match self.hook_incoming_rx.recv()? { 83 | SpectatorSessionMessage::InitSpectator(init) => init, 84 | msg => { 85 | error!("unexpected message: {:?}", msg); 86 | return Err(RecvError); 87 | } 88 | }; 89 | self.spectator_initial = Some(init); 90 | Ok(()) 91 | } 92 | 93 | pub fn dequeue_init_round(&mut self) -> Result { 94 | if let Some(round_initial) = self.round_initial.take() { 95 | return Ok(round_initial); 96 | } 97 | loop { 98 | match self.hook_incoming_rx.recv()? { 99 | SpectatorSessionMessage::InitSpectator(init) => { 100 | error!("unexpected init spectator message: {:?}", init); 101 | return Err(RecvError); 102 | } 103 | SpectatorSessionMessage::InitRound(round_initial) => return Ok(round_initial), 104 | SpectatorSessionMessage::Inputs(..) => continue, 105 | } 106 | } 107 | } 108 | 109 | pub fn dequeue_inputs(&mut self) -> Result<(u16, u16), RecvError> { 110 | if self.round_initial.is_some() { 111 | return Ok((0, 0)); 112 | } 113 | match self.hook_incoming_rx.recv()? { 114 | SpectatorSessionMessage::InitSpectator(init) => { 115 | error!("unexpected init spectator message: {:?}", init); 116 | Err(RecvError) 117 | } 118 | SpectatorSessionMessage::InitRound(round_initial) => { 119 | self.round_initial = Some(round_initial); 120 | Ok((0, 0)) 121 | } 122 | SpectatorSessionMessage::Inputs(p1, p2) => Ok((p1, p2)), 123 | } 124 | } 125 | } 126 | 127 | impl Drop for SpectatorSession { 128 | fn drop(&mut self) { 129 | info!("spectator session guest closed"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /junowen/src/session/spectator_host.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use getset::{CopyGetters, Getters, Setters}; 3 | use junowen_lib::connection::{DataChannel, PeerConnection}; 4 | use tracing::info; 5 | 6 | use super::{ 7 | spectator::{SpectatorInitial, SpectatorSessionMessage}, 8 | to_channel, session_message::RoundInitial, 9 | }; 10 | 11 | #[derive(CopyGetters, Getters, Setters)] 12 | pub struct SpectatorHostSession { 13 | _conn: PeerConnection, 14 | hook_outgoing_tx: std::sync::mpsc::Sender, 15 | } 16 | 17 | impl SpectatorHostSession { 18 | pub fn new(conn: PeerConnection, data_channel: DataChannel) -> Self { 19 | let (hook_outgoing_tx, _hook_incoming_rx) = 20 | to_channel(data_channel, |input| rmp_serde::from_slice(input)); 21 | Self { 22 | _conn: conn, 23 | hook_outgoing_tx, 24 | } 25 | } 26 | 27 | pub fn send_init_spectator(&self, init: SpectatorInitial) -> Result<()> { 28 | Ok(self 29 | .hook_outgoing_tx 30 | .send(SpectatorSessionMessage::InitSpectator(init))?) 31 | } 32 | 33 | pub fn send_init_round(&self, init: RoundInitial) -> Result<()> { 34 | Ok(self 35 | .hook_outgoing_tx 36 | .send(SpectatorSessionMessage::InitRound(init))?) 37 | } 38 | 39 | pub fn send_inputs(&self, p1_input: u16, p2_input: u16) -> Result<()> { 40 | Ok(self 41 | .hook_outgoing_tx 42 | .send(SpectatorSessionMessage::Inputs(p1_input, p2_input))?) 43 | } 44 | } 45 | 46 | impl Drop for SpectatorHostSession { 47 | fn drop(&mut self) { 48 | info!("spectator session host closed"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /junowen/src/signaling.rs: -------------------------------------------------------------------------------- 1 | pub mod waiting_for_match; 2 | 3 | use anyhow::Error; 4 | use getset::{CopyGetters, Getters, MutGetters}; 5 | use junowen_lib::connection::{ 6 | signaling::{ 7 | socket::{ 8 | async_read_write_socket::SignalingServerMessage, channel_socket::ChannelSocket, 9 | SignalingSocket, 10 | }, 11 | CompressedSdp, 12 | }, 13 | DataChannel, PeerConnection, 14 | }; 15 | use tokio::sync::{mpsc, oneshot}; 16 | use tracing::info; 17 | 18 | use crate::TOKIO_RUNTIME; 19 | 20 | #[derive(CopyGetters, Getters, MutGetters)] 21 | pub struct Signaling { 22 | offer_rx: oneshot::Receiver, 23 | answer_rx: oneshot::Receiver, 24 | #[get_mut = "pub"] 25 | msg_tx: Option>, 26 | #[get = "pub"] 27 | offer: Option, 28 | #[get = "pub"] 29 | answer: Option, 30 | error_rx: oneshot::Receiver, 31 | #[get = "pub"] 32 | error: Option, 33 | connected_rx: oneshot::Receiver<()>, 34 | #[get_copy = "pub"] 35 | connected: bool, 36 | } 37 | 38 | impl Signaling { 39 | pub fn new( 40 | session_tx: mpsc::Sender, 41 | create_session: fn(PeerConnection, DataChannel) -> T, 42 | ) -> Self 43 | where 44 | T: Send + 'static, 45 | { 46 | let (offer_tx, offer_rx) = oneshot::channel(); 47 | let (answer_tx, answer_rx) = oneshot::channel(); 48 | let (msg_tx, msg_rx) = oneshot::channel(); 49 | let (error_tx, error_rx) = oneshot::channel(); 50 | let (connected_tx, connected_rx) = oneshot::channel(); 51 | TOKIO_RUNTIME.spawn(async move { 52 | let mut socket = ChannelSocket::new(offer_tx, answer_tx, msg_rx); 53 | let (conn, dc, _host) = match socket.receive_signaling().await { 54 | Ok(ok) => ok, 55 | Err(err) => { 56 | info!("Signaling failed: {}", err); 57 | let _ = error_tx.send(err); 58 | return; 59 | } 60 | }; 61 | tracing::trace!("signaling connected"); 62 | session_tx.send(create_session(conn, dc)).await.unwrap(); 63 | connected_tx.send(()).unwrap(); 64 | }); 65 | Self { 66 | offer_rx, 67 | answer_rx, 68 | msg_tx: Some(msg_tx), 69 | offer: None, 70 | answer: None, 71 | error_rx, 72 | error: None, 73 | connected_rx, 74 | connected: false, 75 | } 76 | } 77 | 78 | pub fn recv(&mut self) { 79 | if let Ok(offer) = self.offer_rx.try_recv() { 80 | self.offer = Some(offer); 81 | } 82 | if let Ok(answer) = self.answer_rx.try_recv() { 83 | self.answer = Some(answer); 84 | } 85 | if let Ok(error) = self.error_rx.try_recv() { 86 | self.error = Some(error); 87 | } 88 | if self.connected_rx.try_recv().is_ok() { 89 | self.connected = true; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /junowen/src/signaling/waiting_for_match.rs: -------------------------------------------------------------------------------- 1 | mod reserved_room_opponent_socket; 2 | mod reserved_room_spectator_host_socket; 3 | mod reserved_room_spectator_socket; 4 | mod shared_room_opponent_socket; 5 | mod socket; 6 | pub mod waiting_for_spectator; 7 | mod waiting_in_room; 8 | 9 | use derive_new::new; 10 | use tokio::sync::mpsc; 11 | 12 | use crate::session::{battle::BattleSession, spectator::SpectatorSession}; 13 | 14 | pub use waiting_for_spectator::{WaitingForPureP2pSpectator, WaitingForSpectator}; 15 | pub use waiting_in_room::{ 16 | WaitingForOpponentInReservedRoom, WaitingForOpponentInSharedRoom, 17 | WaitingForSpectatorHostInReservedRoom, WaitingInRoom, 18 | }; 19 | 20 | fn encode_room_name(room_name: &str) -> String { 21 | urlencoding::encode(room_name).replace("%20", "+") 22 | } 23 | 24 | #[derive(new)] 25 | pub struct WaitingForPureP2pOpponent { 26 | battle_session_rx: mpsc::Receiver, 27 | } 28 | 29 | pub enum WaitingForOpponent { 30 | SharedRoom(WaitingForOpponentInSharedRoom), 31 | ReservedRoom(WaitingForOpponentInReservedRoom), 32 | PureP2p(WaitingForPureP2pOpponent), 33 | } 34 | 35 | impl WaitingForOpponent { 36 | pub fn try_into_session_and_waiting_for_spectator( 37 | self, 38 | ) -> Result<(BattleSession, WaitingForSpectator), Self> { 39 | match self { 40 | Self::SharedRoom(waiting) => waiting 41 | .try_into_session() 42 | .map(|session| { 43 | ( 44 | session, 45 | WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()), 46 | ) 47 | }) 48 | .map_err(WaitingForOpponent::SharedRoom), 49 | Self::ReservedRoom(waiting) => waiting 50 | .try_into_session_and_waiting_for_spectator() 51 | .map_err(WaitingForOpponent::ReservedRoom), 52 | Self::PureP2p(mut waiting) => waiting 53 | .battle_session_rx 54 | .try_recv() 55 | .map(|session| { 56 | ( 57 | session, 58 | WaitingForSpectator::PureP2p(WaitingForPureP2pSpectator::standby()), 59 | ) 60 | }) 61 | .map_err(|_| Self::PureP2p(waiting)), 62 | } 63 | } 64 | } 65 | 66 | #[derive(new)] 67 | pub struct WaitingForPureP2pSpectatorHost { 68 | spectator_session_rx: mpsc::Receiver, 69 | } 70 | 71 | pub enum WaitingForSpectatorHost { 72 | PureP2p(WaitingForPureP2pSpectatorHost), 73 | ReservedRoom(WaitingForSpectatorHostInReservedRoom), 74 | } 75 | 76 | impl WaitingForSpectatorHost { 77 | pub fn try_into_session(self) -> Result { 78 | match self { 79 | Self::PureP2p(mut waiting) => waiting 80 | .spectator_session_rx 81 | .try_recv() 82 | .map_err(|_| WaitingForSpectatorHost::PureP2p(waiting)), 83 | Self::ReservedRoom(waiting) => waiting 84 | .try_into_session() 85 | .map_err(WaitingForSpectatorHost::ReservedRoom), 86 | } 87 | } 88 | } 89 | 90 | pub enum WaitingForMatch { 91 | Opponent(WaitingForOpponent), 92 | SpectatorHost(WaitingForSpectatorHost), 93 | } 94 | 95 | impl From for WaitingForMatch { 96 | fn from(value: WaitingForPureP2pOpponent) -> Self { 97 | WaitingForMatch::Opponent(WaitingForOpponent::PureP2p(value)) 98 | } 99 | } 100 | 101 | impl From for WaitingForMatch { 102 | fn from(value: WaitingForPureP2pSpectatorHost) -> Self { 103 | WaitingForMatch::SpectatorHost(WaitingForSpectatorHost::PureP2p(value)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /junowen/src/signaling/waiting_for_match/reserved_room_spectator_host_socket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Result}; 4 | use async_trait::async_trait; 5 | 6 | use junowen_lib::{ 7 | connection::signaling::{ 8 | socket::{OfferResponse, SignalingSocket}, 9 | CompressedSdp, 10 | }, 11 | signaling_server::reserved_room::{ 12 | PostReservedRoomKeepRequestBody, PostReservedRoomKeepResponse, 13 | PostReservedRoomKeepResponseOkBody, 14 | }, 15 | }; 16 | use tokio::sync::watch; 17 | use tracing::info; 18 | 19 | use crate::signaling::waiting_for_match::socket::retry_after; 20 | 21 | use super::{socket::sleep_or_abort_and_delete_room, encode_room_name}; 22 | 23 | pub struct SignalingServerReservedRoomSpectatorHostSocket { 24 | client: reqwest::Client, 25 | resource_url: String, 26 | key: String, 27 | abort_rx: watch::Receiver, 28 | } 29 | 30 | impl SignalingServerReservedRoomSpectatorHostSocket { 31 | pub fn new( 32 | origin: String, 33 | room_name: &str, 34 | key: String, 35 | abort_rx: watch::Receiver, 36 | ) -> Self { 37 | let encoded_room_name = encode_room_name(room_name); 38 | Self { 39 | client: reqwest::Client::new(), 40 | resource_url: format!("{}/reserved-room/{}", origin, encoded_room_name), 41 | key, 42 | abort_rx, 43 | } 44 | } 45 | 46 | pub fn into_key(self) -> String { 47 | self.key 48 | } 49 | 50 | async fn sleep_or_abort_and_delete_room(&mut self, retry_after: u32, key: &str) -> Result<()> { 51 | let url = &self.resource_url; 52 | sleep_or_abort_and_delete_room(retry_after, &mut self.abort_rx, &self.client, url, key) 53 | .await 54 | } 55 | } 56 | 57 | #[async_trait] 58 | impl SignalingSocket for SignalingServerReservedRoomSpectatorHostSocket { 59 | fn timeout() -> Duration { 60 | Duration::from_secs(10) 61 | } 62 | 63 | async fn offer(&mut self, desc: CompressedSdp) -> Result { 64 | let key = self.key.clone(); 65 | let url = format!("{}/keep", self.resource_url); 66 | let mut desc = Some(desc); 67 | loop { 68 | let body = PostReservedRoomKeepRequestBody::new(key.clone(), desc.take()); 69 | info!("POST {}", url); 70 | let res = self.client.post(&url).json(&body).send().await?; 71 | let status = res.status(); 72 | let retry_after = retry_after(&res); 73 | let body = res.text().await.ok(); 74 | let res = PostReservedRoomKeepResponse::parse(status, retry_after, body.as_deref())?; 75 | info!("{:?}", res); 76 | match res { 77 | PostReservedRoomKeepResponse::BadRequest => { 78 | bail!("bad request") 79 | } 80 | PostReservedRoomKeepResponse::Ok(body) => { 81 | let PostReservedRoomKeepResponseOkBody::SpectatorAnswer(body) = body else { 82 | bail!("invalid response"); 83 | }; 84 | return Ok(OfferResponse::Answer(body.into_spectator_answer())); 85 | } 86 | PostReservedRoomKeepResponse::NoContent { retry_after } => { 87 | self.sleep_or_abort_and_delete_room(retry_after, &key) 88 | .await?; 89 | } 90 | }; 91 | } 92 | } 93 | 94 | async fn answer(&mut self, _desc: CompressedSdp) -> Result<()> { 95 | unreachable!() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /junowen/src/signaling/waiting_for_match/reserved_room_spectator_socket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Error, Result}; 4 | use async_trait::async_trait; 5 | use junowen_lib::{ 6 | connection::signaling::{ 7 | socket::{OfferResponse, SignalingSocket}, 8 | CompressedSdp, 9 | }, 10 | signaling_server::reserved_room::{ 11 | GetReservedRoomResponse, PostReservedRoomSpectateRequestBody, 12 | PostReservedRoomSpectateResponse, 13 | }, 14 | }; 15 | use thiserror::Error; 16 | use tokio::sync::watch; 17 | use tracing::info; 18 | 19 | use super::{ 20 | encode_room_name, 21 | socket::{retry_after, sleep_or_abort}, 22 | }; 23 | 24 | #[derive(Error, Debug)] 25 | pub enum SignalingServerReservedRoomSpectatorSocketError { 26 | #[error("room not found")] 27 | RoomNotFound, 28 | #[error("match is not started")] 29 | MatchIsNotStarted, 30 | } 31 | 32 | pub struct SignalingServerReservedRoomSpectatorSocket { 33 | client: reqwest::Client, 34 | resource_url: String, 35 | abort_rx: watch::Receiver, 36 | } 37 | 38 | impl SignalingServerReservedRoomSpectatorSocket { 39 | pub fn new(origin: String, room_name: &str, abort_rx: watch::Receiver) -> Self { 40 | let encoded_room_name = encode_room_name(room_name); 41 | Self { 42 | client: reqwest::Client::new(), 43 | resource_url: format!("{}/reserved-room/{}", origin, encoded_room_name), 44 | abort_rx, 45 | } 46 | } 47 | } 48 | 49 | #[async_trait] 50 | impl SignalingSocket for SignalingServerReservedRoomSpectatorSocket { 51 | fn timeout() -> Duration { 52 | Duration::from_secs(10) 53 | } 54 | 55 | async fn offer(&mut self, _desc: CompressedSdp) -> Result { 56 | loop { 57 | info!("GET {}", self.resource_url); 58 | let res = self.client.get(&self.resource_url).send().await?; 59 | info!("{:?}", res); 60 | let retry_after = retry_after(&res) 61 | .ok_or_else(|| Error::msg("retry-after header not found in response"))?; 62 | let res = 63 | GetReservedRoomResponse::parse(res.status(), res.text().await.ok().as_deref())?; 64 | match res { 65 | GetReservedRoomResponse::NotFound => { 66 | bail!(SignalingServerReservedRoomSpectatorSocketError::RoomNotFound); 67 | } 68 | GetReservedRoomResponse::Ok(body) => { 69 | if body.opponent_offer().is_some() { 70 | bail!(SignalingServerReservedRoomSpectatorSocketError::MatchIsNotStarted); 71 | } 72 | if let Some(spectator_offer) = body.into_spectator_offer() { 73 | return Ok(OfferResponse::Offer(spectator_offer)); 74 | }; 75 | sleep_or_abort(retry_after, &mut self.abort_rx).await?; 76 | continue; 77 | } 78 | }; 79 | } 80 | } 81 | 82 | async fn answer(&mut self, desc: CompressedSdp) -> Result<()> { 83 | let url = format!("{}/spectate", self.resource_url); 84 | let json = PostReservedRoomSpectateRequestBody::new(desc); 85 | loop { 86 | let res = self.client.post(&url).json(&json).send().await?; 87 | let retry_after = retry_after(&res) 88 | .ok_or_else(|| Error::msg("retry-after header not found in response"))?; 89 | let res = PostReservedRoomSpectateResponse::parse(res.status())?; 90 | match res { 91 | PostReservedRoomSpectateResponse::Ok => return Ok(()), 92 | PostReservedRoomSpectateResponse::Conflict => { 93 | sleep_or_abort(retry_after, &mut self.abort_rx).await?; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /junowen/src/signaling/waiting_for_match/shared_room_opponent_socket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Result}; 4 | use async_trait::async_trait; 5 | use junowen_lib::{ 6 | connection::signaling::{ 7 | socket::{OfferResponse, SignalingSocket}, 8 | CompressedSdp, 9 | }, 10 | signaling_server::{ 11 | custom::{ 12 | PostSharedRoomKeepRequestBody, PostSharedRoomKeepResponse, PutSharedRoomResponse, 13 | }, 14 | room::{PostRoomJoinRequestBody, PostRoomJoinResponse, PutRoomRequestBody}, 15 | }, 16 | }; 17 | use tokio::sync::watch; 18 | use tracing::info; 19 | 20 | use super::{ 21 | encode_room_name, 22 | socket::{retry_after, sleep_or_abort_and_delete_room}, 23 | }; 24 | 25 | pub struct SignalingServerSharedRoomOpponentSocket { 26 | client: reqwest::Client, 27 | resource_url: String, 28 | abort_rx: watch::Receiver, 29 | } 30 | 31 | impl SignalingServerSharedRoomOpponentSocket { 32 | pub fn new(origin: String, room_name: &str, abort_rx: watch::Receiver) -> Self { 33 | let encoded_room_name = encode_room_name(room_name); 34 | Self { 35 | client: reqwest::Client::new(), 36 | resource_url: format!("{}/custom/{}", origin, encoded_room_name), 37 | abort_rx, 38 | } 39 | } 40 | 41 | async fn sleep_or_abort_and_delete_room(&mut self, retry_after: u32, key: &str) -> Result<()> { 42 | let url = &self.resource_url; 43 | sleep_or_abort_and_delete_room(retry_after, &mut self.abort_rx, &self.client, url, key) 44 | .await 45 | } 46 | } 47 | 48 | #[async_trait] 49 | impl SignalingSocket for SignalingServerSharedRoomOpponentSocket { 50 | fn timeout() -> Duration { 51 | Duration::from_secs(10) 52 | } 53 | 54 | async fn offer(&mut self, desc: CompressedSdp) -> Result { 55 | let url = &self.resource_url; 56 | let json = PutRoomRequestBody::new(desc); 57 | info!("PUT {}", url); 58 | let res = self.client.put(url).json(&json).send().await?; 59 | let res = 60 | PutSharedRoomResponse::parse(res.status(), retry_after(&res), &res.text().await?)?; 61 | info!("{:?}", res); 62 | let key = match res { 63 | PutSharedRoomResponse::Conflict { body, .. } => { 64 | return Ok(OfferResponse::Offer(body.into_offer())) 65 | } 66 | PutSharedRoomResponse::CreatedWithAnswer { body, .. } => { 67 | return Ok(OfferResponse::Answer(body.into_answer())); 68 | } 69 | PutSharedRoomResponse::CreatedWithKey { retry_after, body } => { 70 | let key = body.into_key(); 71 | self.sleep_or_abort_and_delete_room(retry_after, &key) 72 | .await?; 73 | key 74 | } 75 | }; 76 | 77 | let url = format!("{}/keep", self.resource_url); 78 | let body = PostSharedRoomKeepRequestBody::new(key.clone()); 79 | loop { 80 | info!("POST {}", url); 81 | let res = self.client.post(&url).json(&body).send().await?; 82 | let status = res.status(); 83 | let retry_after = retry_after(&res); 84 | let body = res.text().await.ok(); 85 | let res = PostSharedRoomKeepResponse::parse(status, retry_after, body.as_deref())?; 86 | info!("{:?}", res); 87 | match res { 88 | PostSharedRoomKeepResponse::BadRequest => { 89 | bail!("bad request") 90 | } 91 | PostSharedRoomKeepResponse::Ok(body) => { 92 | return Ok(OfferResponse::Answer(body.into_answer())); 93 | } 94 | PostSharedRoomKeepResponse::NoContent { retry_after } => { 95 | self.sleep_or_abort_and_delete_room(retry_after, &key) 96 | .await?; 97 | } 98 | } 99 | } 100 | } 101 | 102 | async fn answer(&mut self, desc: CompressedSdp) -> Result<()> { 103 | let url = format!("{}/join", self.resource_url); 104 | let json = PostRoomJoinRequestBody::new(desc); 105 | let res = self.client.post(url).json(&json).send().await?; 106 | let res = PostRoomJoinResponse::parse(res.status())?; 107 | match res { 108 | PostRoomJoinResponse::Ok => Ok(()), 109 | PostRoomJoinResponse::Conflict => bail!("room is full"), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /junowen/src/signaling/waiting_for_match/socket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Result}; 4 | 5 | use junowen_lib::signaling_server::room::DeleteRoomRequestBody; 6 | use reqwest::{header::RETRY_AFTER, Response}; 7 | use tokio::{sync::watch, time::sleep}; 8 | use tracing::info; 9 | 10 | pub fn retry_after(res: &Response) -> Option { 11 | res.headers() 12 | .get(RETRY_AFTER) 13 | .and_then(|x| x.to_str().ok()) 14 | .and_then(|x| x.parse::().ok()) 15 | } 16 | 17 | pub async fn sleep_or_abort(retry_after: u32, abort_rx: &mut watch::Receiver) -> Result<()> { 18 | let task1 = sleep(Duration::from_secs(retry_after as u64)); 19 | let task2 = abort_rx.wait_for(|&val| val); 20 | tokio::select! { 21 | _ = task1 => return Ok(()), 22 | _ = task2 => {}, 23 | }; 24 | bail!("abort"); 25 | } 26 | 27 | pub async fn sleep_or_abort_and_delete_room( 28 | retry_after: u32, 29 | abort_rx: &mut watch::Receiver, 30 | client: &reqwest::Client, 31 | url: &str, 32 | key: &str, 33 | ) -> Result<()> { 34 | let Err(err) = sleep_or_abort(retry_after, abort_rx).await else { 35 | return Ok(()); 36 | }; 37 | let body = DeleteRoomRequestBody::new(key.to_owned()); 38 | info!("DELETE {}", url); 39 | let res = client.delete(url).json(&body).send().await?; 40 | info!("{:?}", res.status()); 41 | Err(err) 42 | } 43 | -------------------------------------------------------------------------------- /junowen/src/state.rs: -------------------------------------------------------------------------------- 1 | mod battle_session_state; 2 | mod junowen_state; 3 | mod prepare; 4 | mod render_parts; 5 | mod spectator_session_state; 6 | 7 | use std::{ffi::c_void, fmt::Display}; 8 | 9 | use getset::{Getters, MutGetters}; 10 | use junowen_lib::{ 11 | structs::{others::RenderingText, selection::Selection}, 12 | Fn011560, Fn0b7d40, Fn0d5ae0, Fn10f720, Th19, 13 | }; 14 | use tracing::debug; 15 | 16 | use self::junowen_state::JunowenState; 17 | use crate::{ 18 | file::{Features, SettingsRepo}, 19 | in_game_lobby::{Lobby, TitleMenuModifier}, 20 | }; 21 | 22 | #[derive(Getters, MutGetters)] 23 | pub struct State { 24 | features: Vec, 25 | #[getset(get_mut = "pub")] 26 | th19: Th19, 27 | title_menu_modifier: TitleMenuModifier, 28 | lobby: Lobby, 29 | junowen_state: JunowenState, 30 | } 31 | 32 | impl State { 33 | pub async fn new(settings_repo: SettingsRepo, th19: Th19) -> Self { 34 | Self { 35 | features: settings_repo.features().await, 36 | th19, 37 | title_menu_modifier: TitleMenuModifier::new(), 38 | lobby: Lobby::new(settings_repo), 39 | junowen_state: JunowenState::Standby, 40 | } 41 | } 42 | 43 | fn abort_session(&mut self, err: impl Display) { 44 | debug!("session aborted: {}", err); 45 | self.junowen_state.abort_session(&mut self.th19); 46 | self.lobby.reset_depth(); 47 | } 48 | 49 | pub fn on_input_players(&mut self) { 50 | let has_session = self.junowen_state.has_session(); 51 | match self 52 | .junowen_state 53 | .on_input_players(&mut self.th19, self.lobby.waiting_for_match_mut()) 54 | { 55 | Ok(_) => { 56 | if has_session && self.junowen_state.has_session() { 57 | self.lobby.reset_depth(); 58 | } 59 | } 60 | Err(err) => { 61 | self.abort_session(err); 62 | } 63 | } 64 | } 65 | 66 | pub fn on_input_menu(&mut self) { 67 | if let Err(err) = self.junowen_state.on_input_menu( 68 | &mut self.th19, 69 | &mut self.title_menu_modifier, 70 | &mut self.lobby, 71 | ) { 72 | self.abort_session(err); 73 | } 74 | } 75 | 76 | pub fn render_object(&self, old: Fn0b7d40, obj_renderer: *const c_void, obj: *const c_void) { 77 | self.junowen_state 78 | .render_object(&self.title_menu_modifier, old, obj_renderer, obj); 79 | } 80 | 81 | pub fn render_text( 82 | &self, 83 | old: Fn0d5ae0, 84 | text_renderer: *const c_void, 85 | text: &mut RenderingText, 86 | ) -> u32 { 87 | self.junowen_state.render_text( 88 | &self.th19, 89 | &self.title_menu_modifier, 90 | old, 91 | text_renderer, 92 | text, 93 | ) 94 | } 95 | 96 | pub fn on_render_texts(&self, text_renderer: *const c_void) { 97 | self.junowen_state.on_render_texts( 98 | &self.features, 99 | &self.th19, 100 | &self.title_menu_modifier, 101 | &self.lobby, 102 | text_renderer, 103 | ); 104 | } 105 | 106 | pub fn on_round_over(&mut self) { 107 | if let Err(err) = self.junowen_state.on_round_over(&mut self.th19) { 108 | self.abort_session(err); 109 | } 110 | } 111 | 112 | pub fn is_online_vs(&self, this: *const Selection, old: Fn011560) -> u8 { 113 | self.junowen_state.is_online_vs(this, old) 114 | } 115 | 116 | pub fn on_rewrite_controller_assignments(&mut self, old_fn: fn(&mut Th19) -> Fn10f720) { 117 | self.junowen_state 118 | .on_rewrite_controller_assignments(&mut self.th19, old_fn); 119 | } 120 | 121 | pub fn on_loaded_game_settings(&mut self) { 122 | self.junowen_state.on_loaded_game_settings(&mut self.th19); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /junowen/src/state/battle_session_state/battle_game.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use derive_new::new; 5 | use getset::{Getters, MutGetters}; 6 | use junowen_lib::{structs::input_devices::InputValue, Th19}; 7 | 8 | use crate::{helper::inputed_number, session::battle::BattleSession}; 9 | 10 | use super::{spectator_host::SpectatorHostState, utils::init_round}; 11 | 12 | #[derive(new, Getters, MutGetters)] 13 | pub struct BattleGame { 14 | #[getset(get = "pub", get_mut = "pub")] 15 | session: BattleSession, 16 | #[getset(get = "pub")] 17 | spectator_host_state: SpectatorHostState, 18 | } 19 | 20 | impl BattleGame { 21 | pub fn inner_state(self) -> (BattleSession, SpectatorHostState) { 22 | (self.session, self.spectator_host_state) 23 | } 24 | 25 | pub fn update_th19(&mut self, th19: &mut Th19) -> Result<(), RecvError> { 26 | // -1フレーム目、0フレーム目は複数回呼ばれ、回数が不定なのでスキップする 27 | if th19.round_frame().unwrap().frame < 1 { 28 | let input_devices = th19.input_devices_mut(); 29 | input_devices 30 | .p1_input_mut() 31 | .set_current(InputValue::empty()); 32 | input_devices 33 | .p2_input_mut() 34 | .set_current(InputValue::empty()); 35 | return Ok(()); 36 | } 37 | let input_devices = th19.input_devices_mut(); 38 | let delay = if self.session.host() { 39 | inputed_number(input_devices) 40 | } else { 41 | None 42 | }; 43 | let (p1, p2) = self 44 | .session 45 | .enqueue_input_and_dequeue(input_devices.p1_input().current().bits() as u16, delay)?; 46 | input_devices 47 | .p1_input_mut() 48 | .set_current((p1 as u32).try_into().unwrap()); 49 | input_devices 50 | .p2_input_mut() 51 | .set_current((p2 as u32).try_into().unwrap()); 52 | 53 | self.spectator_host_state 54 | .update(false, None, th19, &self.session, p1, p2); 55 | 56 | Ok(()) 57 | } 58 | 59 | pub fn on_round_over(&mut self, th19: &mut Th19) -> Result<(), RecvError> { 60 | init_round(th19, &mut self.session, &mut self.spectator_host_state) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /junowen/src/state/battle_session_state/battle_select.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use derive_new::new; 5 | use getset::{Getters, MutGetters}; 6 | use junowen_lib::{ 7 | structs::app::{MainMenu, ScreenId}, 8 | th19_helpers::reset_cursors, 9 | Th19, 10 | }; 11 | use tracing::trace; 12 | 13 | use crate::{ 14 | helper::{inputed_number, pushed_f1}, 15 | session::{battle::BattleSession, MatchInitial}, 16 | }; 17 | 18 | use super::{spectator_host::SpectatorHostState, utils::init_round}; 19 | 20 | fn init_match(th19: &mut Th19, battle_session: &mut BattleSession) -> Result<(), RecvError> { 21 | trace!("init_match"); 22 | th19.set_no_wait(false); 23 | reset_cursors(th19); 24 | if battle_session.host() { 25 | let init = MatchInitial { 26 | game_settings: th19.game_settings_in_menu().unwrap(), 27 | }; 28 | let (remote_player_name, opt) = battle_session 29 | .init_match(th19.vs_mode().player_name().to_string(), Some(init.clone()))?; 30 | battle_session.set_remote_player_name(remote_player_name); 31 | debug_assert!(opt.is_none()); 32 | battle_session.set_match_initial(Some(init)); 33 | } else { 34 | let (remote_player_name, opt) = 35 | battle_session.init_match(th19.vs_mode().player_name().to_string(), None)?; 36 | battle_session.set_remote_player_name(remote_player_name); 37 | debug_assert!(opt.is_some()); 38 | battle_session.set_match_initial(opt); 39 | } 40 | Ok(()) 41 | } 42 | 43 | #[derive(new, Getters, MutGetters)] 44 | pub struct BattleSelect { 45 | #[getset(get = "pub", get_mut = "pub")] 46 | session: BattleSession, 47 | #[getset(get = "pub")] 48 | spectator_host_state: SpectatorHostState, 49 | #[new(value = "true")] 50 | first_time: bool, 51 | } 52 | 53 | impl BattleSelect { 54 | pub fn inner_state(self) -> (BattleSession, SpectatorHostState) { 55 | (self.session, self.spectator_host_state) 56 | } 57 | 58 | pub fn update_th19_on_input_players( 59 | &mut self, 60 | main_menu: &MainMenu, 61 | th19: &mut Th19, 62 | ) -> Result<(), RecvError> { 63 | if self.first_time { 64 | self.first_time = false; 65 | if self.session.match_initial().is_none() { 66 | init_match(th19, &mut self.session)?; 67 | } 68 | init_round(th19, &mut self.session, &mut self.spectator_host_state)?; 69 | } 70 | 71 | if main_menu.screen_id() == ScreenId::DifficultySelect { 72 | return Ok(()); 73 | } 74 | 75 | let input_devices = th19.input_devices_mut(); 76 | let delay = if self.session.host() { 77 | inputed_number(input_devices) 78 | } else { 79 | None 80 | }; 81 | let (p1, p2) = self 82 | .session 83 | .enqueue_input_and_dequeue(input_devices.p1_input().current().bits() as u16, delay)?; 84 | input_devices 85 | .p1_input_mut() 86 | .set_current((p1 as u32).try_into().unwrap()); 87 | input_devices 88 | .p2_input_mut() 89 | .set_current((p2 as u32).try_into().unwrap()); 90 | 91 | self.spectator_host_state 92 | .update(false, Some(main_menu), th19, &self.session, p1, p2); 93 | 94 | Ok(()) 95 | } 96 | 97 | pub fn update_th19_on_input_menu(&mut self, th19: &mut Th19) -> Result<(), RecvError> { 98 | let main_menu = th19.app().main_loop_tasks().find_main_menu().unwrap(); 99 | if main_menu.screen_id() != ScreenId::DifficultySelect { 100 | return Ok(()); 101 | } 102 | 103 | let input_devices = th19.input_devices(); 104 | let delay = if self.session.host() { 105 | inputed_number(input_devices) 106 | } else { 107 | None 108 | }; 109 | let menu_input = th19.menu_input_mut(); 110 | let (p1, p2) = self 111 | .session 112 | .enqueue_input_and_dequeue(menu_input.current().bits() as u16, delay)?; 113 | let input = if p1 != 0 { p1 } else { p2 }; 114 | menu_input.set_current((input as u32).try_into().unwrap()); 115 | 116 | let current_pushed = pushed_f1(input_devices); 117 | self.spectator_host_state.update( 118 | current_pushed, 119 | Some(main_menu), 120 | th19, 121 | &self.session, 122 | p1, 123 | p2, 124 | ); 125 | 126 | Ok(()) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /junowen/src/state/battle_session_state/in_session.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::c_void}; 2 | 3 | use junowen_lib::{structs::settings::GameSettings, Th19}; 4 | 5 | use crate::{ 6 | signaling::waiting_for_match::{WaitingForPureP2pSpectator, WaitingForSpectator}, 7 | state::render_parts::{render_footer, render_game_settings, render_names}, 8 | }; 9 | 10 | use super::spectator_host::SpectatorHostState; 11 | 12 | pub struct RenderingStatus<'a> { 13 | pub host: bool, 14 | pub delay: u8, 15 | pub p1_name: &'a str, 16 | pub p2_name: &'a str, 17 | pub game_settings: Option<&'a GameSettings>, 18 | pub spectator_host_state: Option<&'a SpectatorHostState>, 19 | } 20 | 21 | pub fn on_render_texts(th19: &Th19, text_renderer: *const c_void, status: RenderingStatus) { 22 | render_names(th19, text_renderer, status.p1_name, status.p2_name); 23 | if let Some(game_settings) = status.game_settings { 24 | render_game_settings(th19, text_renderer, game_settings); 25 | } 26 | 27 | let (msg2_rear, msg2_front) = if let Some(spectator_host_state) = status.spectator_host_state { 28 | if spectator_host_state.count_spectators() > 0 { 29 | ( 30 | " ", 31 | Cow::Owned(format!( 32 | "Spectator(s): {}", 33 | spectator_host_state.count_spectators() 34 | )), 35 | ) 36 | } else { 37 | match spectator_host_state.waiting() { 38 | WaitingForSpectator::PureP2p(waiting) => match waiting { 39 | WaitingForPureP2pSpectator::Standby { ready: false, .. } 40 | | WaitingForPureP2pSpectator::SignalingCodeRecved { ready: false, .. } 41 | | WaitingForPureP2pSpectator::SignalingCodeSent { ready: false, .. } => { 42 | ("", "".into()) 43 | } 44 | WaitingForPureP2pSpectator::Standby { .. } => ( 45 | " __ ", 46 | "(Press F1 to accept spectator from clipboard)".into(), 47 | ), 48 | WaitingForPureP2pSpectator::SignalingCodeRecved { .. } => ( 49 | " ", 50 | "(Generating signaling code...)".into(), 51 | ), 52 | WaitingForPureP2pSpectator::SignalingCodeSent { .. } => ( 53 | " ", 54 | "(Your signaling code has been copied to the clipboard)".into(), 55 | ), 56 | }, 57 | WaitingForSpectator::ReservedRoom(_) => ("", "".into()), 58 | } 59 | } 60 | } else { 61 | ("", "".into()) 62 | }; 63 | 64 | let delay_underline = if status.host { "_" } else { " " }; 65 | let msg_front/* _ */= format!("Delay: {} {}", status.delay, msg2_front); 66 | let msg_rear/* __ */= format!(" {} {}", delay_underline, msg2_rear); 67 | 68 | render_footer(th19, text_renderer, &msg_front, &msg_rear); 69 | } 70 | -------------------------------------------------------------------------------- /junowen/src/state/battle_session_state/utils.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use junowen_lib::Th19; 5 | 6 | use crate::session::{battle::BattleSession, RoundInitial}; 7 | 8 | use super::spectator_host::SpectatorHostState; 9 | 10 | pub fn init_round( 11 | th19: &mut Th19, 12 | battle_session: &mut BattleSession, 13 | spectator_host_state: &mut SpectatorHostState, 14 | ) -> Result<(), RecvError> { 15 | if battle_session.host() { 16 | let opt = battle_session.init_round(Some(RoundInitial { 17 | seed1: th19.rand_seed1().unwrap(), 18 | seed2: th19.rand_seed2().unwrap(), 19 | seed3: th19.rand_seed3().unwrap(), 20 | seed4: th19.rand_seed4().unwrap(), 21 | }))?; 22 | debug_assert!(opt.is_none()); 23 | } else { 24 | let init = battle_session.init_round(None)?.unwrap(); 25 | th19.set_rand_seed1(init.seed1).unwrap(); 26 | th19.set_rand_seed2(init.seed2).unwrap(); 27 | th19.set_rand_seed3(init.seed3).unwrap(); 28 | th19.set_rand_seed4(init.seed4).unwrap(); 29 | } 30 | spectator_host_state.send_init_round_if_connected(th19); 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /junowen/src/state/junowen_state/on_rewrite_controller_assignments.rs: -------------------------------------------------------------------------------- 1 | use junowen_lib::{Fn10f720, Th19}; 2 | use tracing::trace; 3 | 4 | pub fn on_rewrite_controller_assignments(th19: &mut Th19, old_fn: fn(&mut Th19) -> Fn10f720) { 5 | let input_devices = th19.input_devices_mut(); 6 | let old_p1_idx = input_devices.p1_idx(); 7 | trace!( 8 | "on_rewrite_controller_assignments: before old_p1_idx={}", 9 | old_p1_idx 10 | ); 11 | old_fn(th19)(); 12 | if old_p1_idx == 0 && input_devices.p1_idx() != 0 { 13 | trace!( 14 | "on_rewrite_controller_assignments: after input_devices.p1_idx()={}", 15 | input_devices.p1_idx() 16 | ); 17 | input_devices.set_p1_idx(0); 18 | trace!( 19 | "on_rewrite_controller_assignments: fixed input_devices.p1_idx()={}", 20 | input_devices.p1_idx() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /junowen/src/state/junowen_state/standby.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::{ 4 | structs::app::{MainMenu, ScreenId}, 5 | structs::others::RenderingText, 6 | Fn0b7d40, Fn0d5ae0, Th19, 7 | }; 8 | 9 | use crate::in_game_lobby::{Lobby, TitleMenuModifier}; 10 | use crate::signaling::waiting_for_match::{WaitingForMatch, WaitingForOpponent, WaitingInRoom}; 11 | 12 | fn is_title(main_menu: &MainMenu) -> bool { 13 | main_menu.screen_id() == ScreenId::Title 14 | } 15 | 16 | fn is_lobby(main_menu: &MainMenu, title_menu_modifier: &TitleMenuModifier) -> bool { 17 | main_menu.screen_id() == ScreenId::PlayerMatchupSelect && title_menu_modifier.selected_junowen() 18 | } 19 | 20 | pub fn update_th19_on_input_menu( 21 | th19: &mut Th19, 22 | title_menu_modifier: &mut TitleMenuModifier, 23 | lobby: &mut Lobby, 24 | ) { 25 | let Some(main_menu) = th19.app_mut().main_loop_tasks_mut().find_main_menu_mut() else { 26 | return; 27 | }; 28 | if is_title(main_menu) { 29 | title_menu_modifier.on_input_menu(main_menu, th19); 30 | } else if title_menu_modifier.start_lobby(main_menu) { 31 | lobby.on_input_menu(th19); 32 | } 33 | } 34 | 35 | pub fn render_text( 36 | th19: &Th19, 37 | title_menu_modifier: &TitleMenuModifier, 38 | old: Fn0d5ae0, 39 | text_renderer: *const c_void, 40 | text: &mut RenderingText, 41 | ) -> u32 { 42 | let Some(main_menu) = th19.app().main_loop_tasks().find_main_menu() else { 43 | return old(text_renderer, text); 44 | }; 45 | title_menu_modifier.render_text(main_menu, th19, old, text_renderer, text) 46 | } 47 | 48 | fn render_message(text_renderer: *const c_void, th19: &Th19, msg: &str, color: u32) { 49 | let mut text = RenderingText::default(); 50 | text.set_text(msg.as_bytes()); 51 | text.set_x(16, th19.window_inner()); 52 | text.set_y(4, th19.window_inner()); 53 | text.color = color; 54 | th19.render_text(text_renderer, &text); 55 | } 56 | 57 | fn render_waiting_message( 58 | room_type: &str, 59 | room: &WaitingInRoom, 60 | th19: &Th19, 61 | text_renderer: *const c_void, 62 | ) { 63 | let room_name = room.room_name(); 64 | let dot = ".".repeat((room.elapsed().as_secs() % 4) as usize); 65 | let msg = format!("Waiting in {} Room: {} {:<3}", room_type, room_name, dot); 66 | render_message(text_renderer, th19, &msg, 0xffc0c0c0); 67 | if !room.errors().is_empty() { 68 | let padding = " ".repeat(msg.chars().count()); 69 | let msg = format!("{} E({})", padding, room.errors().len()); 70 | render_message(text_renderer, th19, &msg, 0xffff2800); 71 | } 72 | } 73 | 74 | pub fn on_render_texts( 75 | th19: &Th19, 76 | title_menu_modifier: &TitleMenuModifier, 77 | lobby: &Lobby, 78 | text_renderer: *const c_void, 79 | ) { 80 | match lobby.waiting_for_match() { 81 | None 82 | | Some(WaitingForMatch::SpectatorHost(_)) 83 | | Some(WaitingForMatch::Opponent(WaitingForOpponent::PureP2p(_))) => {} 84 | Some(WaitingForMatch::Opponent(WaitingForOpponent::SharedRoom(waiting))) => { 85 | render_waiting_message("Shared", waiting, th19, text_renderer); 86 | } 87 | Some(WaitingForMatch::Opponent(WaitingForOpponent::ReservedRoom(waiting))) => { 88 | render_waiting_message("Reserved", waiting, th19, text_renderer); 89 | } 90 | } 91 | let Some(main_menu) = th19.app().main_loop_tasks().find_main_menu() else { 92 | return; 93 | }; 94 | if is_lobby(main_menu, title_menu_modifier) { 95 | lobby.on_render_texts(th19, text_renderer); 96 | } 97 | } 98 | 99 | pub fn render_object( 100 | title_menu_modifier: &TitleMenuModifier, 101 | old: Fn0b7d40, 102 | obj_renderer: *const c_void, 103 | obj: *const c_void, 104 | ) { 105 | if title_menu_modifier.selected_junowen() { 106 | let id = unsafe { *(obj.add(0x28) as *const u32) }; 107 | if (0xb4..=0xc0).contains(&id) { 108 | return; 109 | } 110 | } 111 | old(obj_renderer, obj); 112 | } 113 | -------------------------------------------------------------------------------- /junowen/src/state/prepare.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use getset::{CopyGetters, Getters, MutGetters, Setters}; 3 | use junowen_lib::{ 4 | structs::app::{MainMenu, ScreenId}, 5 | structs::selection::PlayerMatchup, 6 | th19_helpers::AutomaticInputs, 7 | Th19, 8 | }; 9 | 10 | fn to_automatic_inputs(prepare_state: u8) -> AutomaticInputs { 11 | match prepare_state { 12 | 0 => AutomaticInputs::TransitionToTitle, 13 | 1 => AutomaticInputs::ResolveKeyboardFullConflict, 14 | 2 => AutomaticInputs::TransitionToLocalVersusDifficultySelect(PlayerMatchup::HumanVsHuman), 15 | _ => unreachable!(), 16 | } 17 | } 18 | 19 | #[derive(new, CopyGetters, Getters, MutGetters, Setters)] 20 | pub struct Prepare { 21 | #[getset(get = "pub", get_mut = "pub")] 22 | session: T, 23 | /// 0: back to title, 1: resolve controller, 2: forward to difficulty select 24 | #[new(value = "0")] 25 | state: u8, 26 | } 27 | 28 | impl Prepare { 29 | pub fn inner_session(self) -> T { 30 | self.session 31 | } 32 | 33 | pub fn update_th19_on_input_players(&self, th19: &mut Th19) { 34 | th19.set_no_wait(true); 35 | to_automatic_inputs(self.state).on_input_players(th19); 36 | } 37 | 38 | pub fn update_th19_on_input_menu(&self, th19: &mut Th19) { 39 | let Some(main_menu) = th19.app_mut().main_loop_tasks_mut().find_main_menu_mut() else { 40 | return; 41 | }; 42 | let no_wait = to_automatic_inputs(self.state).on_input_menu(th19, main_menu); 43 | th19.set_no_wait(no_wait); 44 | } 45 | 46 | pub fn update_state(&mut self, main_menu: &MainMenu, th19: &Th19) -> bool { 47 | match self.state { 48 | 0 => { 49 | if main_menu.screen_id() != ScreenId::Title { 50 | return false; 51 | } 52 | let new_state = if th19.input_devices().is_conflict_input_device() { 53 | 1 54 | } else { 55 | 2 56 | }; 57 | self.state = new_state; 58 | false 59 | } 60 | 1 => { 61 | if th19.input_devices().is_conflict_input_device() { 62 | return false; 63 | } 64 | self.state = 0; 65 | false 66 | } 67 | 2 => { 68 | if main_menu.screen_id() != ScreenId::DifficultySelect { 69 | return false; 70 | } 71 | true 72 | } 73 | _ => unreachable!(), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /junowen/src/state/render_parts.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::{ 4 | structs::{others::RenderingText, settings::GameSettings}, 5 | Th19, 6 | }; 7 | 8 | pub fn render_names(th19: &Th19, text_renderer: *const c_void, p1_name: &str, p2_name: &str) { 9 | let mut text = RenderingText::default(); 10 | text.set_text(p1_name.as_bytes()); 11 | text.set_x(16, th19.window_inner()); 12 | text.set_y(4, th19.window_inner()); 13 | text.color = 0xffff8080; 14 | th19.render_text(text_renderer, &text); 15 | 16 | text.set_text(p2_name.as_bytes()); 17 | text.set_x(1264, th19.window_inner()); 18 | text.color = 0xff8080ff; 19 | text.horizontal_align = 2; 20 | th19.render_text(text_renderer, &text); 21 | } 22 | 23 | fn render_game_common_settings( 24 | th19: &Th19, 25 | text_renderer: *const c_void, 26 | game_settings: &GameSettings, 27 | ) { 28 | let mut text = RenderingText::default(); 29 | text.set_text(format!("Time Limit: {}", game_settings.time_limit()).as_bytes()); 30 | text.set_x(16, th19.window_inner()); 31 | text.set_y(4 + 32, th19.window_inner()); 32 | text.color = 0xffffffff; 33 | th19.render_text(text_renderer, &text); 34 | 35 | text.set_text(format!("Round: {}", game_settings.round()).as_bytes()); 36 | text.set_x(1280 - 16, th19.window_inner()); 37 | text.horizontal_align = 2; 38 | th19.render_text(text_renderer, &text); 39 | } 40 | 41 | fn render_game_players_settings( 42 | th19: &Th19, 43 | text_renderer: *const c_void, 44 | game_settings: &GameSettings, 45 | ) { 46 | let mut text = RenderingText::default(); 47 | 48 | let y = 870; 49 | let msg = format!( 50 | "Life: {}\nBarrier: {}", 51 | game_settings.p1_life() + 1, 52 | game_settings.p1_barrier() 53 | ); 54 | text.set_text(msg.as_bytes()); 55 | text.set_x(16, th19.window_inner()); 56 | text.set_y(y, th19.window_inner()); 57 | text.horizontal_align = 1; 58 | th19.render_text(text_renderer, &text); 59 | 60 | text.set_text(format!("Life: {}", game_settings.p2_life() + 1,).as_bytes()); 61 | text.set_x(1280 - 16, th19.window_inner()); 62 | text.horizontal_align = 2; 63 | th19.render_text(text_renderer, &text); 64 | 65 | text.set_text(format!("Barrier: {}", game_settings.p2_barrier()).as_bytes()); 66 | text.set_x(1280 - 16, th19.window_inner()); 67 | text.set_y(y + 28, th19.window_inner()); 68 | th19.render_text(text_renderer, &text); 69 | } 70 | 71 | pub fn render_game_settings( 72 | th19: &Th19, 73 | text_renderer: *const c_void, 74 | game_settings: &GameSettings, 75 | ) { 76 | render_game_common_settings(th19, text_renderer, game_settings); 77 | render_game_players_settings(th19, text_renderer, game_settings); 78 | } 79 | 80 | pub fn render_footer(th19: &Th19, text_renderer: *const c_void, msg_front: &str, msg_rear: &str) { 81 | let version = env!("CARGO_PKG_VERSION"); 82 | let version_blank = (0..version.len()).map(|_| " ").collect::(); 83 | 84 | let msg_front/* __ */= format!("Ju.N.Owen v{} {}", version, msg_front); 85 | let msg_rear/* ___ */= format!(" {} {}", version_blank, msg_rear); 86 | 87 | let mut text = RenderingText::default(); 88 | text.set_text(msg_rear.as_bytes()); 89 | text.set_x(16, th19.window_inner()); 90 | text.set_y(944, th19.window_inner()); 91 | text.color = 0xffffffff; 92 | text.font_type = 1; 93 | th19.render_text(text_renderer, &text); 94 | 95 | text.set_text(msg_front.as_bytes()); 96 | text.set_y(940, th19.window_inner()); 97 | th19.render_text(text_renderer, &text); 98 | } 99 | -------------------------------------------------------------------------------- /junowen/src/state/spectator_session_state/in_session.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use junowen_lib::Th19; 4 | 5 | use crate::state::render_parts::{render_footer, render_names}; 6 | 7 | pub fn on_render_texts_spectator( 8 | th19: &Th19, 9 | text_renderer: *const c_void, 10 | p1_name: &str, 11 | p2_name: &str, 12 | ) { 13 | render_names(th19, text_renderer, p1_name, p2_name); 14 | render_footer(th19, text_renderer, "(Spectating)", ""); 15 | } 16 | -------------------------------------------------------------------------------- /junowen/src/state/spectator_session_state/spectator_game.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use derive_new::new; 5 | use getset::{Getters, MutGetters}; 6 | use junowen_lib::{structs::input_devices::InputValue, Th19}; 7 | 8 | use crate::session::spectator::SpectatorSession; 9 | 10 | #[derive(new, Getters, MutGetters)] 11 | pub struct SpectatorGame { 12 | #[getset(get = "pub", get_mut = "pub")] 13 | session: SpectatorSession, 14 | } 15 | 16 | impl SpectatorGame { 17 | pub fn inner_session(self) -> SpectatorSession { 18 | self.session 19 | } 20 | 21 | pub fn update_th19(&mut self, th19: &mut Th19) -> Result<(), RecvError> { 22 | // -1フレーム目、0フレーム目は複数回呼ばれ、回数が不定なのでスキップする 23 | if th19.round_frame().unwrap().frame < 1 { 24 | let input_devices = th19.input_devices_mut(); 25 | input_devices 26 | .p1_input_mut() 27 | .set_current(InputValue::empty()); 28 | input_devices 29 | .p2_input_mut() 30 | .set_current(InputValue::empty()); 31 | return Ok(()); 32 | } 33 | let input_devices = th19.input_devices_mut(); 34 | let (p1, p2) = self.session.dequeue_inputs()?; 35 | input_devices 36 | .p1_input_mut() 37 | .set_current((p1 as u32).try_into().unwrap()); 38 | input_devices 39 | .p2_input_mut() 40 | .set_current((p2 as u32).try_into().unwrap()); 41 | Ok(()) 42 | } 43 | 44 | pub fn on_round_over(&mut self, th19: &mut Th19) -> Result<(), RecvError> { 45 | let init = self.session.dequeue_init_round()?; 46 | th19.set_rand_seed1(init.seed1).unwrap(); 47 | th19.set_rand_seed2(init.seed2).unwrap(); 48 | th19.set_rand_seed3(init.seed3).unwrap(); 49 | th19.set_rand_seed4(init.seed4).unwrap(); 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /junowen/src/state/spectator_session_state/spectator_select.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::RecvError; 2 | 3 | use anyhow::Result; 4 | use derive_new::new; 5 | use getset::{Getters, MutGetters}; 6 | use junowen_lib::{ 7 | structs::{ 8 | app::{MainMenu, ScreenId}, 9 | input_devices::InputValue, 10 | }, 11 | th19_helpers::reset_cursors, 12 | Th19, 13 | }; 14 | use tracing::trace; 15 | 16 | use crate::session::spectator::{self, SpectatorSession}; 17 | 18 | #[derive(new, Getters, MutGetters)] 19 | pub struct SpectatorSelect { 20 | #[getset(get = "pub", get_mut = "pub")] 21 | session: SpectatorSession, 22 | #[new(value = "0")] 23 | initializing_state: u8, 24 | } 25 | 26 | impl SpectatorSelect { 27 | pub fn inner_session(self) -> SpectatorSession { 28 | self.session 29 | } 30 | 31 | pub fn update_th19_on_input_players( 32 | &mut self, 33 | main_menu: &MainMenu, 34 | th19: &mut Th19, 35 | ) -> Result<(), RecvError> { 36 | if self.initializing_state == 0 { 37 | if self.session.spectator_initial().is_none() { 38 | self.initializing_state = 1; 39 | reset_cursors(th19); 40 | self.session.recv_init_spectator()?; 41 | } else { 42 | self.initializing_state = 2; 43 | } 44 | let round_initial = self.session.dequeue_init_round()?; 45 | th19.set_rand_seed1(round_initial.seed1).unwrap(); 46 | th19.set_rand_seed2(round_initial.seed2).unwrap(); 47 | th19.set_rand_seed3(round_initial.seed3).unwrap(); 48 | th19.set_rand_seed4(round_initial.seed4).unwrap(); 49 | } 50 | if main_menu.screen_id() == ScreenId::DifficultySelect { 51 | return Ok(()); 52 | } 53 | if self.initializing_state == 1 { 54 | let init = self.session.spectator_initial().unwrap(); 55 | match init.initial_state().screen() { 56 | spectator::Screen::DifficultySelect => { 57 | return Ok(()); 58 | } 59 | spectator::Screen::CharacterSelect => unimplemented!(), 60 | spectator::Screen::Game => unimplemented!(), 61 | } 62 | } 63 | if !th19.no_wait() { 64 | th19.set_no_wait(true); 65 | } 66 | 67 | let (p1, p2) = self.session.dequeue_inputs()?; 68 | let input_devices = th19.input_devices_mut(); 69 | input_devices 70 | .p1_input_mut() 71 | .set_current((p1 as u32).try_into().unwrap()); 72 | input_devices 73 | .p2_input_mut() 74 | .set_current((p2 as u32).try_into().unwrap()); 75 | 76 | Ok(()) 77 | } 78 | 79 | pub fn update_th19_on_input_menu( 80 | &mut self, 81 | main_menu: &mut MainMenu, 82 | th19: &mut Th19, 83 | ) -> Result<(), RecvError> { 84 | if main_menu.screen_id() != ScreenId::DifficultySelect { 85 | return Ok(()); 86 | } 87 | let menu = main_menu.menu_mut(); 88 | if self.initializing_state == 1 { 89 | let init = self.session.spectator_initial().unwrap(); 90 | trace!("spectator_initial: {:?}", init); 91 | let initial_state = init.initial_state(); 92 | match initial_state.screen() { 93 | spectator::Screen::DifficultySelect => { 94 | if menu.cursor() != initial_state.difficulty() as u32 { 95 | menu.set_cursor(initial_state.difficulty() as u32); 96 | th19.menu_input_mut().set_current(InputValue::empty()); 97 | return Ok(()); 98 | } 99 | let selection = th19.selection_mut(); 100 | selection.p1_mut().character = initial_state.p1_character() as u32; 101 | selection.p2_mut().character = initial_state.p2_character() as u32; 102 | th19.set_no_wait(false); 103 | self.initializing_state = 2; 104 | } 105 | spectator::Screen::CharacterSelect => unimplemented!(), 106 | spectator::Screen::Game => unimplemented!(), 107 | } 108 | } 109 | 110 | let (p1, p2) = self.session.dequeue_inputs()?; 111 | let input = if p1 != 0 { p1 } else { p2 }; 112 | th19.menu_input_mut() 113 | .set_current((input as u32).try_into().unwrap()); 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /junowen/src/tracing_helper.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroU8, panic}; 2 | 3 | use time::format_description::well_known::{iso8601, Iso8601}; 4 | use tracing::{error, Level}; 5 | use tracing_subscriber::{ 6 | fmt::{time::LocalTime, writer::MakeWriterExt}, 7 | prelude::__tracing_subscriber_SubscriberExt, 8 | EnvFilter, Layer, 9 | }; 10 | 11 | const MY_CONFIG: iso8601::EncodedConfig = iso8601::Config::DEFAULT 12 | .set_time_precision(iso8601::TimePrecision::Second { 13 | decimal_digits: NonZeroU8::new(3), 14 | }) 15 | .encode(); 16 | 17 | pub fn init_tracing(dir: &str, file_name: &str, ansi: bool) { 18 | let default_layer = || { 19 | const WITH_FILE_PATH: bool = cfg!(debug_assertions); 20 | tracing_subscriber::fmt::layer() 21 | .compact() 22 | .with_file(WITH_FILE_PATH) 23 | .with_line_number(WITH_FILE_PATH) 24 | .with_target(!WITH_FILE_PATH) 25 | .with_thread_ids(true) 26 | .with_timer(LocalTime::new(Iso8601::)) 27 | }; 28 | let writer = tracing_appender::rolling::never(dir, file_name); 29 | let writer = writer.with_max_level(Level::WARN); 30 | 31 | let layer = default_layer().with_ansi(false).with_writer(writer); 32 | 33 | if cfg!(debug_assertions) { 34 | const DIRECTIVES: &str = concat!(env!("CARGO_CRATE_NAME"), "=trace,junowen_lib=trace"); 35 | let make_filter = || EnvFilter::new(DIRECTIVES); 36 | tracing::subscriber::set_global_default( 37 | tracing_subscriber::registry().with( 38 | layer 39 | .with_filter(make_filter()) 40 | .and_then(default_layer().with_ansi(ansi).with_filter(make_filter())), 41 | ), 42 | ) 43 | .unwrap(); 44 | } else { 45 | const DIRECTIVES: &str = concat!(env!("CARGO_CRATE_NAME"), "=info,junowen_lib=info"); 46 | let make_filter = || EnvFilter::new(DIRECTIVES); 47 | tracing::subscriber::set_global_default( 48 | tracing_subscriber::registry().with(layer.with_filter(make_filter())), 49 | ) 50 | .unwrap(); 51 | } 52 | 53 | panic::set_hook(Box::new(|panic| error!("{}", panic))); 54 | } 55 | -------------------------------------------------------------------------------- /th19loader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "th19loader" 3 | edition = "2021" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | crate-type = ['cdylib'] 10 | name = "d3d9" 11 | 12 | [build-dependencies] 13 | static_vcruntime.workspace = true 14 | winres = "0.1" 15 | 16 | [dependencies] 17 | anyhow.workspace = true 18 | junowen-lib = { path = "../junowen-lib" } 19 | windows.workspace = true 20 | 21 | [package.metadata.winres] 22 | LegalCopyright = "© Progre" 23 | ProductName = "th19loader" 24 | FileDescription = "https://github.com/progre/junowen/" 25 | -------------------------------------------------------------------------------- /th19loader/build.rs: -------------------------------------------------------------------------------- 1 | use winres::WindowsResource; 2 | 3 | fn main() { 4 | if cfg!(target_os = "windows") { 5 | static_vcruntime::metabuild(); 6 | WindowsResource::new().compile().unwrap(); 7 | } 8 | } 9 | --------------------------------------------------------------------------------