├── .gitignore ├── config.toml ├── src ├── widgets │ ├── mod.rs │ ├── topbar.rs │ ├── detail_panel.rs │ └── list.rs ├── main.rs ├── encrypt.rs ├── localize.rs ├── patch.rs ├── api.rs └── gui.rs ├── resources └── icons │ └── icons.ttf ├── README.md ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── tea32.c └── .vscode └── launch.json /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [unstable] 2 | build-std = ["std"] -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod topbar; 2 | pub mod detail_panel; 3 | pub mod list; -------------------------------------------------------------------------------- /resources/icons/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clague/ds3os-loader-rs/HEAD/resources/icons/icons.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ds3os-loader-rs 2 | Another ds3os loader 3 | 4 | ## Usage 5 | 6 | This program only patch memory of the running process, so you need launch game from steam first. 7 | 8 | You may need some dev packages in debian-based distro or [you cannot launch](https://github.com/clague/ds3os-loader-rs/issues/1), for example: `libexpat-dev libfreetype-dev libssl-dev`. 9 | 10 | ## Build 11 | 12 | First you need to install rust compiler, you can set up compile environment by using the [rustup](https://rustup.rs/). 13 | 14 | Then clone this repo in your file system and run 15 | 16 | `cargo build --release` 17 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-18.04 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Check glibc version 20 | run: ldd --version 21 | - name: Build 22 | run: cargo build --release 23 | - name: Upload a Build Artifact 24 | uses: actions/upload-artifact@v3.1.0 25 | with: 26 | # Artifact name 27 | name: artifact 28 | # A file, directory or wildcard pattern that describes what to upload 29 | path: target/release/ds3os-loader 30 | # The desired behavior if no files are found using the provided path. 31 | ignore: Do not output any warnings or errors, the action does not fail 32 | if-no-files-found: warn 33 | 34 | retention-days: 90 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ds3os-loader" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [build-dependencies] 9 | cc = "1.0.73" 10 | 11 | [dependencies] 12 | reqwest = { version = "0.11.11", features = ["json"] } 13 | lazy_static = "1.4.0" 14 | serde_json = "1.0.82" 15 | serde = { version = "1.0.139", features = ["derive"] } 16 | anyhow = "1.0.58" 17 | iced = { version = "0.4.2", default-features = false, features = ["tokio", "glow", "glow_default_system_font"] } 18 | iced_aw = { version = "0.2", default-features = false, features = ["floating_button", "split"] } 19 | # iced = { path = "../iced/", default-features = false, features = ["tokio", "glow", "glow_default_system_font"] } 20 | # iced_aw = { path = "../iced_aw/", default-features = false, features = ["floating_button", "split"] } 21 | sysinfo = "0.24.7" 22 | bytes = "1.2.0" 23 | process-memory = "0.4.0" 24 | native-dialog = "0.6.3" 25 | sys-locale = "0.2.1" 26 | 27 | [profile.release] 28 | opt-level = 'z' 29 | lto = true 30 | codegen-units = 1 31 | panic = 'abort' 32 | -------------------------------------------------------------------------------- /tea32.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | const uint32_t delta=0x9e3779b9; // a key schedule constant 6 | uint32_t KEY[4]; // Key space for bit shifts 7 | 8 | void set_key(const uint32_t *k) { 9 | KEY[0] = k[0]; 10 | KEY[1] = k[1]; 11 | KEY[2] = k[2]; 12 | KEY[3] = k[3]; 13 | } 14 | 15 | void encrypt(uint32_t* v) { 16 | uint32_t v0=v[0], v1=v[1], sum=0, i; // set up 17 | for (i=0; i < 32; i++) { // basic cycle start 18 | sum += delta; 19 | v0 += ((v1<<4) + KEY[0]) ^ (v1 + sum) ^ ((v1>>5) + KEY[1]); 20 | v1 += ((v0<<4) + KEY[2]) ^ (v0 + sum) ^ ((v0>>5) + KEY[3]); 21 | } // end cycle 22 | v[0] = v0; 23 | v[1] = v1; 24 | } 25 | 26 | void decrypt (uint32_t* v) { 27 | uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; // set up 28 | for (i=0; i<32; i++) { // basic cycle start 29 | v1 -= ((v0<<4) + KEY[2]) ^ (v0 + sum) ^ ((v0>>5) + KEY[3]); 30 | v0 -= ((v1<<4) + KEY[0]) ^ (v1 + sum) ^ ((v1>>5) + KEY[1]); 31 | sum -= delta; 32 | } // end cycle 33 | v[0]=v0; v[1]=v1; 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'ds3os-loader'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=ds3os-loader", 15 | "--package=ds3os-loader" 16 | ], 17 | "filter": { 18 | "name": "ds3os-loader", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'ds3os-loader'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=ds3os-loader", 34 | "--package=ds3os-loader" 35 | ], 36 | "filter": { 37 | "name": "ds3os-loader", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | use std::fs::File; 3 | use std::io::Read; 4 | 5 | use iced::{Application, Settings, window}; 6 | use anyhow::Result; 7 | 8 | mod gui; 9 | mod api; 10 | mod patch; 11 | mod localize; 12 | mod encrypt; 13 | mod widgets; 14 | 15 | use crate::gui::LoaderMainInterface; 16 | use crate::localize::Language; 17 | 18 | fn main() -> Result<()> { 19 | localize::set_language(Language::Auto)?; 20 | 21 | let setting = Settings { 22 | id: None, 23 | window: window::Settings { 24 | size: (800, 600), 25 | position: window::Position::Default, 26 | min_size: None, 27 | max_size: None, 28 | resizable: true, 29 | decorations: true, 30 | transparent: false, 31 | always_on_top: false, 32 | icon: None, 33 | }, 34 | flags: (), 35 | default_font: { 36 | if cfg!(windows) { 37 | let mut buffer = Vec::new(); 38 | File::open(r#"C:\Windows\Fonts\simhei.ttf"#)?.read_to_end(&mut buffer)?; 39 | Some(Box::leak(buffer.into_boxed_slice())) 40 | } 41 | else { 42 | None 43 | } 44 | }, 45 | default_text_size: 16, 46 | text_multithreading: true, 47 | exit_on_close_request: true, 48 | antialiasing: false, 49 | try_opengles_first: false, 50 | }; 51 | 52 | LoaderMainInterface::run(setting)?; 53 | Ok(()) 54 | //crate::encrypt::test(); 55 | //Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /src/encrypt.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use bytes::{Bytes, BytesMut}; 3 | 4 | lazy_static! { 5 | pub static ref TEA_BLOCK_SIZE: usize = 8; 6 | } 7 | 8 | extern "C" { 9 | fn set_key(k: *const u32); 10 | fn encrypt(v: *mut u8); 11 | } 12 | 13 | pub fn tea32_encrypt(data: &BytesMut, key: &[u32;4]) -> Bytes { 14 | let length_rounded_to_block_size: usize = ((data.len() + (*TEA_BLOCK_SIZE - 1)) / *TEA_BLOCK_SIZE) * *TEA_BLOCK_SIZE; 15 | 16 | let mut output: BytesMut = BytesMut::with_capacity(length_rounded_to_block_size); 17 | output.extend_from_slice(&data[..]); 18 | 19 | unsafe { set_key(key.as_ptr()); } 20 | 21 | let mut block_offset: usize = 0; 22 | while block_offset < output.len() { 23 | tea32_encrypt_block(&mut output, block_offset); 24 | block_offset += *TEA_BLOCK_SIZE; 25 | } 26 | 27 | output.freeze() 28 | } 29 | 30 | pub fn tea32_encrypt_block(input: &mut BytesMut, input_offset: usize) 31 | { 32 | unsafe { 33 | encrypt(input.as_mut_ptr().offset(input_offset as isize)); 34 | } 35 | } 36 | 37 | // pub fn test() { 38 | // let mut output = BytesMut::from_iter(&[3u8, 2u8, 1u8, 0u8, 7u8, 6u8, 5u8, 4u8]); 39 | // unsafe { set_key([0u32;4].as_ptr()); } 40 | // tea32_encrypt_block(&mut output, 0); 41 | // println!("{:#?}", output.freeze().to_vec()); 42 | 43 | // use tea_soft::block_cipher::generic_array::GenericArray; 44 | // use tea_soft::block_cipher::{BlockCipher, NewBlockCipher}; 45 | 46 | // let key = GenericArray::from_slice(&[0u8;16]); 47 | // let mut block = GenericArray::clone_from_slice(&[0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]); 48 | // // Initialize cipher 49 | // let cipher = tea_soft::Tea32::new(&key); 50 | 51 | // // Encrypt block in-place 52 | // cipher.encrypt_block(&mut block); 53 | // println!("{:#?}", block.to_vec()); 54 | // } -------------------------------------------------------------------------------- /src/widgets/topbar.rs: -------------------------------------------------------------------------------- 1 | use iced::{button, Button, Command, Element, Length, Text, Row, Alignment}; 2 | 3 | pub struct TopBar { 4 | refresh_btn: button::State, 5 | import_btn: button::State, 6 | 7 | about_btn: button::State, 8 | } 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum TopBarMessage { 12 | RefreshServerList, 13 | ChooseConfigFile, 14 | ShowAbout, 15 | } 16 | 17 | impl TopBar { 18 | pub fn new() -> Self{ 19 | Self { 20 | refresh_btn: button::State::new(), 21 | import_btn: button::State::new(), 22 | about_btn: button::State::new(), 23 | } 24 | } 25 | 26 | pub fn update(&mut self, message: TopBarMessage) -> Command { 27 | match message { 28 | TopBarMessage::ChooseConfigFile => { 29 | 30 | }, 31 | TopBarMessage::ShowAbout => todo!(), 32 | _ => {}, 33 | } 34 | Command::none() 35 | } 36 | 37 | pub fn view(&mut self) -> Element { 38 | let refresh_btn = Button::new( 39 | &mut self.refresh_btn, 40 | Text::new("\u{E800}").font(crate::gui::ICON_FONT) 41 | ) 42 | .height(Length::Units(50)) 43 | .width(Length::Units(50)) 44 | .on_press(TopBarMessage::RefreshServerList); 45 | let import_btn = Button::new( 46 | &mut self.import_btn, 47 | Text::new("\u{E804}").font(crate::gui::ICON_FONT) 48 | ) 49 | .height(Length::Units(50)) 50 | .width(Length::Units(50)) 51 | .on_press(TopBarMessage::ChooseConfigFile); 52 | let about_btn = Button::new( 53 | &mut self.about_btn, 54 | Text::new("About") 55 | ) 56 | .height(Length::Units(50)) 57 | .width(Length::Units(50)) 58 | .on_press(TopBarMessage::ShowAbout); 59 | 60 | Row::new() 61 | .push(refresh_btn) 62 | .push(import_btn) 63 | .push(iced::Space::with_width(Length::Fill)) 64 | .push(about_btn) 65 | .spacing(10) 66 | .align_items(Alignment::Center) 67 | .height(Length::Units(100)) 68 | .into() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/widgets/detail_panel.rs: -------------------------------------------------------------------------------- 1 | use iced::{button, Button, Element, Length, Text, Alignment, Column, Scrollable, scrollable, TextInput, text_input}; 2 | use iced_aw::FloatingButton; 3 | 4 | use crate::api::Server; 5 | use crate::gui::{ICON_FONT, Icon}; 6 | use crate::localize::{TEXT_LOCALIZED_STRING, TextType::*}; 7 | pub struct DetailPanel { 8 | srcollable: scrollable::State, 9 | patch_btn: button::State, 10 | passwd_input: text_input::State, 11 | } 12 | 13 | impl DetailPanel { 14 | pub fn new() -> Self{ 15 | Self { 16 | srcollable: scrollable::State::new(), 17 | patch_btn: button::State::new(), 18 | passwd_input: text_input::State::new(), 19 | } 20 | } 21 | 22 | pub fn view(&mut self, server: Server, passwd: &str) -> Element { 23 | let name_text = Text::new(&format!("{}: {}", "Name", server.name)); 24 | 25 | let hostname_text = Text::new(&format!("{}: {}", "Hostname", server.hostname)); 26 | 27 | let private_hostname_text = Text::new(&format!("{}: {}", "Private Hostname", server.private_hostname)); 28 | 29 | let player_count_text = Text::new(&format!("{}: {}", "Player Count", server.player_count)); 30 | 31 | let password_required_text = Text::new(&format!("{}: {}", "Password: ", 32 | if server.password_required { 33 | TEXT_LOCALIZED_STRING[&PasswordRequired] 34 | } 35 | else { 36 | TEXT_LOCALIZED_STRING[&PasswordNotRequired] 37 | } 38 | )); 39 | 40 | let description_text = Text::new(&format!("{}: {}", "Description", server.description)); 41 | 42 | let col = Column::new() 43 | .push(name_text) 44 | .push(hostname_text) 45 | .push(private_hostname_text) 46 | .push(player_count_text) 47 | .push(password_required_text) 48 | .push(description_text) 49 | .spacing(10) 50 | .align_items(Alignment::Start); 51 | 52 | let scrollable = Scrollable::new(&mut self.srcollable) 53 | .push(col) 54 | .height(Length::Fill) 55 | .width(Length::Fill); 56 | 57 | let passwd_input = TextInput::new(&mut self.passwd_input, 58 | "Password", 59 | passwd, 60 | |s| crate::gui::Message::PasswordInput(s) 61 | ).size(32); 62 | 63 | let underlay = Column::new() 64 | .push(scrollable) 65 | .push(passwd_input); 66 | 67 | FloatingButton::new(&mut self.patch_btn, underlay, |state| { 68 | Button::new( 69 | state, 70 | Text::new(Icon::PlayLight) 71 | .width(Length::Shrink) 72 | .height(Length::Shrink) 73 | .font(ICON_FONT) 74 | .size(39), 75 | ) 76 | //.style(iced_aw::style::button::Primary), 77 | .on_press(crate::gui::Message::Patch) 78 | .padding(5) 79 | }).into() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/localize.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, hash::Hash}; 2 | 3 | use crate::gui::FailReason; 4 | 5 | use lazy_static::lazy_static; 6 | use anyhow::Result; 7 | use sys_locale::get_locale; 8 | 9 | #[derive(Eq, PartialEq, Hash)] 10 | pub enum Language { 11 | Auto, 12 | English, 13 | SChinese, 14 | } 15 | 16 | lazy_static! { 17 | static ref FAIL_REASON_LOCALIZED_STRING_: HashMap> = HashMap::from([ 18 | (Language::English, HashMap::from([ 19 | (FailReason::ChooseFileFail, "Invalid file choosen!"), 20 | (FailReason::RefreshListFail, "Can't refresh the server list!"), 21 | (FailReason::PatchFail, "Exception happened during the patch"), 22 | (FailReason::ListNoSelected, "Please select a server first!"), 23 | (FailReason::ProcessNotFound, "Game process not found, maybe you need open the game first."), 24 | (FailReason::FetchPublicKeyFail, "Can't fetch public key from the master server, most likely due to the incorrect password"), 25 | ])), 26 | (Language::SChinese, HashMap::from([ 27 | (FailReason::ChooseFileFail, "无效的配置文件!"), 28 | (FailReason::RefreshListFail, "无法刷新服务器列表!"), 29 | (FailReason::PatchFail, "修改内存过程中发生错误"), 30 | (FailReason::ListNoSelected, "请先选择一个服务器"), 31 | (FailReason::ProcessNotFound, "未找到游戏进程,也许你应该先打开游戏。"), 32 | (FailReason::FetchPublicKeyFail, "从主服务器获取公钥失败,一般是由于密码错误"), 33 | ])), 34 | ]); 35 | 36 | static ref TEXT_LOCALIZED_STRING_: HashMap> = HashMap::from([ 37 | (Language::English, HashMap::from([ 38 | (TextType::PasswordRequired, "Need password"), 39 | (TextType::PasswordNotRequired, "No password"), 40 | ])), 41 | (Language::SChinese, HashMap::from([ 42 | (TextType::PasswordRequired, "需要密码"), 43 | (TextType::PasswordNotRequired, "不需要密码"), 44 | ])), 45 | ]); 46 | 47 | pub static ref FAIL_REASON_LOCALIZED_STRING: &'static HashMap = &FAIL_REASON_LOCALIZED_STRING_[&Language::English]; 48 | 49 | pub static ref TEXT_LOCALIZED_STRING: &'static HashMap = &TEXT_LOCALIZED_STRING_[&Language::English]; 50 | } 51 | pub fn set_language(mut lang: Language) -> Result<()> { 52 | if lang == Language::Auto { 53 | let lang_str = get_locale().unwrap_or("en-US".into()); 54 | if lang_str.starts_with("zh") { 55 | lang = Language::SChinese; 56 | } 57 | else { 58 | lang = Language::English; 59 | } 60 | } 61 | unsafe { 62 | type F = &'static HashMap; 63 | type T = &'static HashMap; 64 | *(&(*FAIL_REASON_LOCALIZED_STRING) as *const F as *mut F) = &FAIL_REASON_LOCALIZED_STRING_[&lang]; 65 | *(&(*TEXT_LOCALIZED_STRING) as *const T as *mut T) = &TEXT_LOCALIZED_STRING_[&lang]; 66 | }; 67 | Ok(()) 68 | } 69 | 70 | #[derive(Eq, PartialEq, Hash)] 71 | pub enum TextType { 72 | PasswordRequired, 73 | PasswordNotRequired, 74 | } -------------------------------------------------------------------------------- /src/patch.rs: -------------------------------------------------------------------------------- 1 | //use std::process::Command; 2 | use anyhow::{Result, anyhow}; 3 | use sysinfo::{ProcessExt, System, SystemExt, ProcessRefreshKind, PidExt}; 4 | use lazy_static::lazy_static; 5 | use bytes::{Bytes, BytesMut, BufMut}; 6 | use process_memory::{Memory, Pid as PidHandle, TryIntoProcessHandle, DataMember}; 7 | 8 | use crate::encrypt::tea32_encrypt; 9 | 10 | 11 | lazy_static! { 12 | #[allow(non_upper_case_globals)] 13 | pub static ref SERVER_INFO_TEAENCRYPTION_KEY: [u32;4] = [ 14 | 0x4B694CD6, 15 | 0x96ADA235, 16 | 0xEC91D9D4, 17 | 0x23F562E5 18 | ]; 19 | pub static ref SERVER_INFO_PATCH_SIZE: usize = 520; 20 | 21 | // Maximum length of the UTF8 encoded public key. 22 | pub static ref SERVER_INFO_MAX_KEY_SIZE: usize= 430; 23 | 24 | // Maximum length of the UTF16 encoded hostname. 25 | pub static ref SERVER_INFO_MAX_HOST_SIZE: usize = 85; // Leave at least 2 bytes from the end of ServerInfoPatchSize for nullptr. 26 | 27 | // Offset into the data block that the hostname is placed. 28 | pub static ref SERVER_INFO_HOST_OFFSET: usize = 432; 29 | 30 | pub static ref SERVER_INFO_ADDRESS: usize = 0x144F4A5B1; 31 | } 32 | 33 | pub struct Patches { 34 | sys: System, 35 | } 36 | 37 | impl Patches { 38 | pub fn new() -> Self { 39 | Patches { 40 | sys: System::new(), 41 | } 42 | } 43 | // pub fn run_game() -> Result<()> { 44 | // Command::new("steam") 45 | // .arg("steam://run/374320") 46 | // .spawn()?; 47 | // Ok(()) 48 | // } 49 | 50 | pub fn find_process(&mut self) -> Result { 51 | self.sys.refresh_processes_specifics(ProcessRefreshKind::new()); 52 | let mut res: u32 = 0; 53 | 54 | // It seems that process name in linux is "DarkSoulsIII.ex", so keep the last "e" out 55 | for process in self.sys.processes_by_name("DarkSoulsIII.ex") { 56 | let pid: u32 = process.pid().as_u32(); 57 | if pid > res { 58 | res = pid; 59 | } 60 | } 61 | println!("Game's pid: {}", res); 62 | if res == 0 { Err(anyhow!("Can't find process")) } else { Ok(res) } 63 | } 64 | 65 | pub fn patch(pid: u32, hostname: &str, pubkey: &str) -> Result { 66 | let handle = (pid as i32 as PidHandle).try_into_process_handle()?; 67 | let mut member = DataMember::new_offset(handle, vec![*SERVER_INFO_ADDRESS]); 68 | 69 | let data_block = Self::encrypt(hostname, pubkey)?; 70 | let data_len = data_block.len(); 71 | let mut writed_len = 0; 72 | 73 | data_block.into_iter().for_each(|byte| { 74 | match member.write(&byte) { 75 | Ok(_) => { 76 | writed_len += 1; 77 | member.set_offset(vec![*SERVER_INFO_ADDRESS + writed_len]); 78 | }, 79 | Err(_) => () 80 | } 81 | }); 82 | if data_len != writed_len { 83 | Err(anyhow!("Exception happened during the patch!")) 84 | } 85 | else { 86 | Ok(writed_len) 87 | } 88 | } 89 | fn encrypt(hostname: &str, pubkey: &str) -> Result { 90 | let host_data: &[u8] = &hostname.encode_utf16().flat_map(|twin| {twin.to_le_bytes()} ).collect::>(); 91 | let key_data = pubkey.as_bytes(); 92 | 93 | if key_data.len() > *SERVER_INFO_MAX_KEY_SIZE { 94 | return Err(anyhow!("Key's size is too big!")) 95 | } 96 | 97 | if host_data.len() > *SERVER_INFO_MAX_HOST_SIZE { 98 | return Err(anyhow!("Host's size is too big!")) 99 | } 100 | 101 | let mut data_block: BytesMut = BytesMut::with_capacity(*SERVER_INFO_PATCH_SIZE); 102 | data_block.put_slice(key_data); 103 | data_block.put_bytes(0, *SERVER_INFO_HOST_OFFSET - key_data.len()); 104 | data_block.put_slice(host_data); 105 | 106 | data_block.resize(*SERVER_INFO_PATCH_SIZE, 0); 107 | 108 | Ok(tea32_encrypt(&data_block, &*SERVER_INFO_TEAENCRYPTION_KEY)) 109 | } 110 | } -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use anyhow::{Result, anyhow}; 3 | use serde::{Serialize, Deserialize}; 4 | use reqwest::{Client, header::*, Url, IntoUrl, Method}; 5 | use lazy_static::lazy_static; 6 | 7 | lazy_static! { 8 | pub static ref MASTER_SERVER_ADDR_DEF: &'static str = "http://ds3os-master.timleonard.uk:50020/api/v1/servers/"; 9 | } 10 | 11 | 12 | #[derive(Clone)] 13 | pub enum ApiVersion { 14 | V1, 15 | V2, 16 | Unknown 17 | } 18 | 19 | impl From for ApiVersion { 20 | fn from(n: u32) -> ApiVersion { 21 | match n { 22 | 1 => ApiVersion::V1, 23 | 2 => ApiVersion::V2, 24 | _ => ApiVersion::Unknown 25 | } 26 | } 27 | } 28 | 29 | impl From<&str> for ApiVersion { 30 | fn from(s: &str) -> ApiVersion { 31 | let ss = String::from(s); 32 | if ss.contains("1") { 33 | ApiVersion::V1 34 | } 35 | else if ss.contains("2") { 36 | ApiVersion::V2 37 | } 38 | else { 39 | ApiVersion::Unknown 40 | } 41 | } 42 | } 43 | 44 | impl Into for ApiVersion { 45 | fn into(self) -> String { 46 | match self { 47 | ApiVersion::V1 => "v1".to_string(), 48 | ApiVersion::V2 => "v2".to_string(), 49 | ApiVersion::Unknown => "".to_string() 50 | } 51 | } 52 | } 53 | 54 | #[derive(Serialize, Deserialize, Debug)] 55 | pub struct ApiResponse { 56 | status: String, 57 | #[serde(default)] 58 | message: String, 59 | #[serde(default)] 60 | servers: Vec, 61 | #[serde(rename = "PublicKey", default)] 62 | public_key: String, 63 | } 64 | 65 | 66 | #[derive(Serialize, Deserialize, Clone, Debug)] 67 | pub struct Server { 68 | #[serde(rename = "IpAddress", default)] 69 | pub ip_addr: String, 70 | #[serde(rename = "Hostname")] 71 | pub hostname: String, 72 | #[serde(rename = "PrivateHostname", default)] 73 | pub private_hostname: String, 74 | #[serde(rename = "Description", default)] 75 | pub description: String, 76 | #[serde(rename = "Name", default)] 77 | pub name: String, 78 | #[serde(rename = "PlayerCount", default)] 79 | pub player_count: u32, 80 | #[serde(rename = "PasswordRequired", default)] 81 | pub password_required: bool, 82 | #[serde(rename = "Password", default)] 83 | pub passwd: String, 84 | #[serde(rename = "ModsWhiteList", default)] 85 | pub mods_white_list: String, 86 | #[serde(rename = "ModsBlackList", default)] 87 | pub mods_black_list: String, 88 | #[serde(rename = "ModsRequiredList", default)] 89 | pub mods_required_list: String, 90 | #[serde(rename = "PublicKey", default)] 91 | pub pubkey: String, 92 | } 93 | 94 | #[allow(dead_code)] 95 | #[derive(Clone)] 96 | pub struct MasterServerApi { 97 | api_url: Url, 98 | http_client: Client, 99 | version: ApiVersion, 100 | } 101 | 102 | impl MasterServerApi { 103 | pub fn new(api_url: U, version: V) -> Result 104 | where 105 | U: IntoUrl, 106 | V: Into, 107 | { 108 | let mut headers = HeaderMap::new(); 109 | headers.insert(ACCEPT, HeaderValue::from_static("application/json")); 110 | 111 | let http_client = Client::builder() 112 | .default_headers(headers) 113 | .build() 114 | .unwrap_or_default(); 115 | 116 | Ok(MasterServerApi { 117 | api_url: api_url.into_url()?, 118 | http_client, 119 | version: version.into(), 120 | }) 121 | } 122 | 123 | pub async fn request(&self, method: Method, url: &Url, request_body: Option) -> Result 124 | where 125 | S: Serialize, 126 | { 127 | let builder = self.http_client.request(method, url.clone()); 128 | 129 | if let Some(r) = request_body { 130 | Ok(builder.json(&r) 131 | .send() 132 | .await? 133 | .json::() 134 | .await? 135 | ) 136 | } 137 | else { 138 | Ok(builder.send() 139 | .await? 140 | .json::() 141 | .await?) 142 | } 143 | } 144 | 145 | pub async fn list_servers(self) -> Result> { 146 | let res = self.request::(Method::GET, &self.api_url, None).await?; 147 | if res.status == "success" && res.servers.len() > 0{ 148 | Ok(res.servers) 149 | } 150 | else { 151 | Err(anyhow!("Master server return error!")) 152 | } 153 | } 154 | 155 | pub async fn get_pubkey(&self, ip_addr: &str, password: &str) -> Result { 156 | let req_body = json!({ 157 | "password": password, 158 | }); 159 | let res = self.request(Method::POST, &self.api_url.join(&format!("{}/public_key", ip_addr))?, Some(req_body)).await?; 160 | if res.status == "success" && !res.public_key.is_empty() { 161 | Ok(res.public_key) 162 | } 163 | else { 164 | //println!("{:#?}", res); 165 | Err(anyhow!(res.message)) 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /src/widgets/list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::MasterServerApi; 2 | 3 | use { 4 | crate::api::Server, 5 | crate::gui::FailReason, 6 | iced::{ 7 | Column, Length, Row, Space, Text, Scrollable, scrollable, 8 | Command, Element, button, Button, Alignment 9 | }, 10 | }; 11 | 12 | pub struct ServerRow { 13 | pub id: usize, 14 | pub server: Server, 15 | pub is_manual: bool, 16 | 17 | server_btn: button::State, 18 | } 19 | 20 | #[allow(dead_code)] 21 | #[derive(Clone, Debug)] 22 | pub enum RowMessage { 23 | Delete, 24 | ToggleSelection, 25 | } 26 | 27 | impl ServerRow { 28 | pub fn new(server: Server, id: usize, is_manual: bool) -> Self { 29 | Self { 30 | id, 31 | server, 32 | is_manual, 33 | server_btn: button::State::new(), 34 | } 35 | } 36 | 37 | pub fn view(&mut self, _selected: &usize) -> Element { 38 | Row::new() 39 | .push( 40 | Button::new( 41 | &mut self.server_btn, 42 | Row::new() 43 | .align_items(Alignment::Center) 44 | .push(Text::new(&self.server.name).width(Length::FillPortion(1))) 45 | .push(Text::new(&self.server.hostname).width(Length::FillPortion(1))) 46 | .push(Text::new(&self.server.player_count.to_string()).width(Length::FillPortion(1))) 47 | ) 48 | .padding(8) 49 | .width(Length::Fill) 50 | .on_press(RowMessage::ToggleSelection), 51 | ) 52 | .push(Space::with_width(Length::Units(15))) 53 | .align_items(Alignment::Center) 54 | .into() 55 | } 56 | } 57 | 58 | pub struct ServerList { 59 | pub rows: Vec, 60 | pub selected: usize, 61 | pub manual_server_offset: usize, 62 | 63 | scrollable: scrollable::State, 64 | } 65 | 66 | #[derive(Debug, Clone)] 67 | pub enum ListMessage { 68 | //SearchInputChanged(String), 69 | UpdateServerList, 70 | UpdateServerListComplete(Vec), 71 | ImportConfig(Vec), 72 | Fail(FailReason, String), 73 | RowMessage(usize, RowMessage), 74 | } 75 | 76 | impl ServerList { 77 | 78 | pub fn new() -> Self { 79 | Self::with_servers(Vec::new()) 80 | } 81 | pub fn with_servers(servers: Vec) -> Self { 82 | let mut id = 0; 83 | Self { 84 | rows: servers.into_iter().map(|server| { 85 | id += 1; 86 | ServerRow::new(server, id, false) 87 | }).collect(), 88 | selected: 0, 89 | manual_server_offset: 0, 90 | 91 | scrollable: scrollable::State::new(), 92 | } 93 | } 94 | 95 | pub fn update(&mut self, message: ListMessage, api: &MasterServerApi) -> Command { 96 | match message { 97 | ListMessage::UpdateServerList => { 98 | let api = api.clone(); 99 | return Command::perform( 100 | async move { 101 | api.list_servers().await 102 | }, 103 | move |res| { 104 | match res { 105 | Ok(servers) => { 106 | ListMessage::UpdateServerListComplete(servers) 107 | }, 108 | Err(e) => { 109 | ListMessage::Fail(FailReason::RefreshListFail, e.to_string()) 110 | } 111 | } 112 | } 113 | ); 114 | }, 115 | ListMessage::UpdateServerListComplete(servers) => { 116 | self.rebuild_list(servers); 117 | }, 118 | ListMessage::Fail(_, _) => {}, 119 | ListMessage::RowMessage(id, row_message) => { 120 | match row_message { 121 | RowMessage::Delete => { 122 | self.rows.retain(|row| row.id != id); 123 | } 124 | RowMessage::ToggleSelection => { 125 | match self.rows.iter_mut().filter(|row| row.id == id).next() { 126 | Some(_) => { 127 | self.selected = id 128 | }, 129 | None => {} 130 | } 131 | }, 132 | } 133 | } 134 | ListMessage::ImportConfig(servers) => { 135 | self.import(servers); 136 | } 137 | } 138 | Command::none() 139 | } 140 | 141 | pub fn view(&mut self, heads: [&str;3]) -> Element { 142 | let head = Row::with_children(heads.iter().map(|head| Text::new(*head).width(Length::FillPortion(1)).into()).collect()); 143 | let scrollable = Scrollable::new(&mut self.scrollable) 144 | .push( 145 | Column::with_children( 146 | self.rows.iter_mut().map( 147 | |row| { 148 | let id = row.id; 149 | row.view(&self.selected).map( 150 | move |row_message| ListMessage::RowMessage(id, row_message) 151 | ).into() 152 | } 153 | ).collect() 154 | ) 155 | ); 156 | 157 | Column::new() 158 | .push(head) 159 | .push(scrollable) 160 | .height(Length::Fill) 161 | .width(Length::Fill) 162 | .into() 163 | } 164 | 165 | pub fn find_by_id(&self, id: usize) -> Option<&ServerRow> { 166 | self.rows.iter().filter(|row| row.id == id).next() 167 | } 168 | 169 | pub fn find_by_id_mut(&mut self, id: usize) -> Option<&mut ServerRow> { 170 | self.rows.iter_mut().filter(|row| row.id == id).next() 171 | } 172 | 173 | pub fn find_selected_mut(&mut self) -> Option<&mut ServerRow> { 174 | self.find_by_id_mut(self.selected) 175 | } 176 | 177 | fn rebuild_list(&mut self, servers: Vec) { 178 | self.rows.retain(|row| row.is_manual); 179 | self.rows 180 | .append(&mut 181 | servers.into_iter() 182 | .enumerate() 183 | .map(|(id, server)| ServerRow::new(server, id + 2016, false)) 184 | .collect() 185 | ); 186 | } 187 | 188 | pub fn import(&mut self, mut servers: Vec) { 189 | servers.retain(|server| { 190 | self.rows 191 | .iter() 192 | .find(|row| row.server.hostname == server.hostname) 193 | .is_none() 194 | }); 195 | let offset = self.manual_server_offset; 196 | self.manual_server_offset += servers.len(); 197 | self.rows.splice(0..0, 198 | servers.into_iter() 199 | .enumerate() 200 | .map(|(id, server)| { 201 | ServerRow::new(server, id + offset, true) 202 | }) 203 | ); 204 | } 205 | } -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | use iced::{Application, executor, Command, Column, Font}; 2 | use iced_aw::{split, Split}; 3 | use native_dialog::{FileDialog, MessageDialog}; 4 | use std::io::BufReader; 5 | use anyhow::Result; 6 | use std::fs::File; 7 | 8 | use crate::api::{Server, MasterServerApi}; 9 | use crate::patch::Patches; 10 | use crate::widgets::list::{ServerList, ListMessage, RowMessage}; 11 | use crate::widgets::topbar::{TopBar, TopBarMessage}; 12 | use crate::widgets::detail_panel::DetailPanel; 13 | use crate::localize::FAIL_REASON_LOCALIZED_STRING; 14 | 15 | pub static ICON_FONT: Font = Font::External { 16 | name: "Icons", 17 | bytes: include_bytes!("../resources/icons/icons.ttf"), 18 | }; 19 | 20 | pub struct LoaderMainInterface { 21 | api: MasterServerApi, 22 | patch: Patches, 23 | cur_passwd: String, 24 | // The local state of the two buttons 25 | topbar: TopBar, 26 | server_list: ServerList, 27 | detail_panel: DetailPanel, 28 | split_pane: split::State, 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | pub enum Message { 33 | ListMessage(ListMessage), 34 | TopBarMessage(TopBarMessage), 35 | PasswordInput(String), 36 | Patch, 37 | Fail(FailReason, String), 38 | OnResize(u16), 39 | Nothing, 40 | } 41 | 42 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 43 | pub enum FailReason { 44 | ChooseFileFail, 45 | RefreshListFail, 46 | 47 | ListNoSelected, 48 | FetchPublicKeyFail, 49 | ProcessNotFound, 50 | PatchFail, 51 | } 52 | 53 | 54 | impl Application for LoaderMainInterface { 55 | type Executor = executor::Default; 56 | type Message = Message; 57 | type Flags = (); 58 | 59 | fn new(_flags: ()) -> (Self, Command) { 60 | ( 61 | LoaderMainInterface{ 62 | api: MasterServerApi::new(*crate::api::MASTER_SERVER_ADDR_DEF, 1).unwrap(), 63 | patch: Patches::new(), 64 | cur_passwd: String::new(), 65 | 66 | topbar: TopBar::new(), 67 | server_list: ServerList::new(), 68 | detail_panel: DetailPanel::new(), 69 | split_pane: split::State::new(None, split::Axis::Vertical), 70 | }, 71 | Command::perform(async {}, |_| Message::ListMessage(ListMessage::UpdateServerList)) // ugly 72 | ) 73 | } 74 | 75 | fn title(&self) -> String { 76 | "Dark Souls III - Another Open Server Loader".to_string() 77 | } 78 | 79 | fn update( 80 | &mut self, 81 | message: Self::Message 82 | ) -> Command { 83 | match message { 84 | Message::Patch => { 85 | if let Some(row) = self.server_list.find_selected_mut() { 86 | row.server.passwd = self.cur_passwd.clone(); 87 | 88 | match self.patch.find_process() { 89 | Ok(pid) => { 90 | let api = self.api.clone(); 91 | 92 | let ip_addr = row.server.ip_addr.clone(); 93 | let mut pubkey = row.server.pubkey.clone(); 94 | let hostname = row.server.hostname.clone(); 95 | let passwd = row.server.passwd.clone(); 96 | 97 | Command::perform(async move { 98 | if pubkey.is_empty() { 99 | pubkey = api 100 | .get_pubkey(&ip_addr, &passwd) 101 | .await 102 | .map_err(|e| (FailReason::FetchPublicKeyFail, e.to_string()))?; 103 | } 104 | Patches::patch(pid, &hostname, &pubkey).map_err(|e| (FailReason::PatchFail, e.to_string())).map(|_| ()) 105 | }, 106 | |r| { 107 | match r { 108 | Ok(_) => { 109 | Message::Nothing 110 | }, 111 | Err(e) => { 112 | Message::Fail(e.0, e.1) 113 | } 114 | } 115 | }) 116 | }, 117 | Err(e) => { 118 | self.update(Message::Fail(FailReason::ProcessNotFound, e.to_string())) 119 | } 120 | } 121 | } 122 | else { 123 | self.update(Message::Fail(FailReason::ListNoSelected, "No row is selected".into())) 124 | } 125 | }, 126 | 127 | Message::ListMessage(m) => { 128 | if let ListMessage::RowMessage(id, RowMessage::ToggleSelection) = m { 129 | if let Some(row) = self.server_list.find_by_id(id) { 130 | self.cur_passwd = row.server.passwd.clone(); 131 | } 132 | } 133 | self.server_list.update(m, &self.api).map(map_list_message) 134 | }, 135 | Message::TopBarMessage(m) => { 136 | match m { 137 | TopBarMessage::ChooseConfigFile => { 138 | let mes = match choose_config_file() { 139 | Ok(servers) => { 140 | Message::ListMessage(ListMessage::ImportConfig(servers)) 141 | }, 142 | Err(e) => { 143 | Message::Fail(FailReason::ChooseFileFail, e.to_string()) 144 | } 145 | }; 146 | self.update(mes) 147 | }, 148 | _ => { 149 | self.topbar.update(m).map(map_topbar_message) 150 | } 151 | } 152 | }, 153 | Message::Fail(reason, description) => { 154 | let text = format!("{}\nDetail: {}", FAIL_REASON_LOCALIZED_STRING[&reason], &description); 155 | if let Err(e) = MessageDialog::new() 156 | .set_title("Error") 157 | //.set_type(MessageType::Error) 158 | .set_text(&text) 159 | .show_alert() 160 | { 161 | println!("Error: {}", e); 162 | } 163 | Command::none() 164 | }, 165 | Message::OnResize(pos) => { 166 | self.split_pane.set_divider_position(pos); 167 | Command::none() 168 | }, 169 | Message::PasswordInput(s) => { 170 | self.cur_passwd = s; 171 | Command::none() 172 | } 173 | Message::Nothing => { Command::none() }, 174 | } 175 | } 176 | 177 | fn view(&mut self) -> iced::Element<'_, Self::Message> { 178 | let topbar = self.topbar 179 | .view() 180 | .map(map_topbar_message); 181 | let mut col = Column::new() 182 | .push(topbar); 183 | 184 | let heads = ["Name", "Address", "Player Count"]; 185 | 186 | if let Some(row) = self.server_list.rows.iter().filter(|row| row.id == self.server_list.selected).next() { 187 | let detail_panel = self.detail_panel.view(row.server.clone(), &self.cur_passwd); 188 | let split = Split::new( 189 | &mut self.split_pane, 190 | self.server_list.view(heads).map(map_list_message), 191 | detail_panel, 192 | Message::OnResize 193 | ); 194 | col = col.push(split); 195 | } 196 | else { 197 | col = col.push(self.server_list.view(heads).map(map_list_message)); 198 | } 199 | 200 | col.into() 201 | } 202 | } 203 | fn choose_config_file() -> Result> { 204 | Ok(FileDialog::new() 205 | .add_filter("Server Config File (*.ds3osconfig)", &["ds3osconfig"]) 206 | .add_filter("All files (*.*)", &["*"]) 207 | .set_location("~/") 208 | .show_open_multiple_file()? 209 | .into_iter() 210 | .filter_map(|path| { 211 | match File::open(path.clone()) { 212 | Ok(file) => { 213 | serde_json::from_reader::<_, Server>(BufReader::new(file)).ok() 214 | }, 215 | Err(e) => { 216 | println!("Import file '{}' failed! Reason: {}", path.to_string_lossy(), e.to_string()); 217 | None 218 | } 219 | } 220 | }).collect()) 221 | } 222 | 223 | fn map_list_message(m: ListMessage) -> Message { 224 | if let ListMessage::Fail(r, s) = m { 225 | Message::Fail(r, s) 226 | } 227 | else { 228 | Message::ListMessage(m) 229 | } 230 | } 231 | 232 | fn map_topbar_message(m: TopBarMessage) -> Message { 233 | if let TopBarMessage::RefreshServerList = m { 234 | Message::ListMessage(ListMessage::UpdateServerList) 235 | } 236 | else { 237 | Message::TopBarMessage(m) 238 | } 239 | } 240 | 241 | #[allow(dead_code)] 242 | pub enum Icon { 243 | Refresh, 244 | TrashBinLight, 245 | TrashBinDark, 246 | OpenFolder, 247 | PlayLight, 248 | PlayDark, 249 | } 250 | 251 | impl Into for Icon { 252 | fn into(self) -> String { 253 | match self { 254 | Icon::Refresh => "\u{E800}".into(), 255 | Icon::TrashBinLight => "\u{E801}".into(), 256 | Icon::TrashBinDark => "\u{E802}".into(), 257 | Icon::OpenFolder => "\u{E804}".into(), 258 | Icon::PlayLight => "\u{E805}".into(), 259 | Icon::PlayDark => "\u{E806}".into(), 260 | } 261 | } 262 | } --------------------------------------------------------------------------------