├── .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