diff --git a/Cargo.toml b/Cargo.toml index b55e58b..49f0b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "anime-game-launcher" -version = "0.5.2" +version = "0.5.3" description = "Anime Game launcher" authors = ["Nikita Podvirnyy "] license = "GPL-3.0" diff --git a/anime-game-core b/anime-game-core index e040b40..5abbea2 160000 --- a/anime-game-core +++ b/anime-game-core @@ -1 +1 @@ -Subproject commit e040b40c0598c48630d7a06110a4aaccbcea6c53 +Subproject commit 5abbea2f1152d25c2855c43d55133a3edc414bc0 diff --git a/assets/ui/preferences/general.blp b/assets/ui/preferences/general.blp index 3a14052..8735ef9 100644 --- a/assets/ui/preferences/general.blp +++ b/assets/ui/preferences/general.blp @@ -26,6 +26,16 @@ Adw.PreferencesPage page { title: "Game voiceovers"; subtitle: "Select voice packages used in game"; } + + Gtk.Box { + orientation: horizontal; + spacing: 8; + margin-top: 16; + + Gtk.Button repair_game { + label: "Repair game"; + } + } } Adw.PreferencesGroup { diff --git a/src/lib/config/mod.rs b/src/lib/config/mod.rs index f8a4184..578215f 100644 --- a/src/lib/config/mod.rs +++ b/src/lib/config/mod.rs @@ -181,14 +181,31 @@ impl Config { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Launcher { pub language: String, - pub temp: Option + pub temp: Option, + pub repairer: Repairer } impl Default for Launcher { fn default() -> Self { Self { language: String::from("en-us"), - temp: launcher_dir() + temp: launcher_dir(), + repairer: Repairer::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repairer { + pub threads: u8, + pub fast: bool +} + +impl Default for Repairer { + fn default() -> Self { + Self { + threads: 4, + fast: false } } } diff --git a/src/ui/main.rs b/src/ui/main.rs index a1d5486..3b0d503 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -131,6 +131,7 @@ pub enum Actions { OpenPreferencesPage, PreferencesGoBack, PerformButtonEvent, + RepairGame, ShowProgressBar, UpdateProgress { fraction: Rc, title: Rc }, HideProgressBar, @@ -224,7 +225,10 @@ impl App { receiver.attach(None, move |action| { // Some debug output - println!("[main] [update] action: {:?}", action); + match &action { + Actions::UpdateProgress { .. } => (), + action => println!("[main] [update] action: {:?}", action) + } match action { Actions::OpenPreferencesPage => { @@ -401,8 +405,6 @@ impl App { config::update(config); - this.widgets.progress_bar.hide(); - this.update_state(); } } @@ -518,6 +520,165 @@ impl App { } } + Actions::RepairGame => { + match config::get() { + Ok(config) => { + let this = this.clone(); + + std::thread::spawn(move || { + match repairer::try_get_integrity_files() { + Ok(mut files) => { + // Add voiceovers files + let game = Game::new(&config.game.path); + + if let Ok(voiceovers) = game.get_voice_packages() { + for package in voiceovers { + if let Ok(mut voiceover_files) = repairer::try_get_voice_integrity_files(package.locale()) { + files.append(&mut voiceover_files); + } + } + } + + this.update(Actions::ShowProgressBar).unwrap(); + + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Verifying files: 0%")) + }).unwrap(); + + const VERIFIER_THREADS_NUM: u64 = 4; + + let mut total = 0; + + for file in &files { + total += file.size; + } + + let median_size = total / VERIFIER_THREADS_NUM; + let mut i = 0; + + let (sender, receiver) = std::sync::mpsc::channel(); + + for _ in 0..VERIFIER_THREADS_NUM { + let mut thread_files = Vec::new(); + let mut thread_files_size = 0; + + while i < files.len() { + thread_files.push(files[i].clone()); + + thread_files_size += files[i].size; + i += 1; + + if thread_files_size >= median_size { + break; + } + } + + let game_path = config.game.path.clone(); + let thread_sender = sender.clone(); + + std::thread::spawn(move || { + for file in thread_files { + let status = if config.launcher.repairer.fast { + file.fast_verify(&game_path) + } else { + file.verify(&game_path) + }; + + thread_sender.send((file, status)).unwrap(); + } + }); + } + + // We have VERIFIER_THREADS_NUM copies of this sender + the original one + // receiver will return Err when all the senders will be dropped. + // VERIFIER_THREADS_NUM senders will be dropped when threads will finish verifying files + // but this one will live as long as current thread exists so we should drop it manually + drop(sender); + + let mut broken = Vec::new(); + let mut processed = 0; + + while let Ok((file, status)) = receiver.recv() { + processed += file.size; + + if !status { + broken.push(file); + } + + let progress = processed as f64 / total as f64; + + this.update(Actions::UpdateProgress { + fraction: Rc::new(progress), + title: Rc::new(format!("Verifying files: {:.2}%", progress * 100.0)) + }).unwrap(); + } + + if broken.len() > 0 { + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Repairing files: 0%")) + }).unwrap(); + + println!("Found broken files:"); + + for file in &broken { + println!(" - {}", file.path); + } + + let total = broken.len() as f64; + + let is_patch_applied = match Patch::try_fetch(config.patch.servers) { + Ok(patch) => patch.is_applied(&config.game.path).unwrap_or(true), + Err(_) => true + }; + + println!("Patch status: {}", is_patch_applied); + + fn should_ignore(path: &str) -> bool { + for part in ["UnityPlayer.dll", "xlua.dll", "crashreport.exe", "upload_crash.exe", "vulkan-1.dll"] { + if path.contains(part) { + return true; + } + } + + false + } + + for (i, file) in broken.into_iter().enumerate() { + if !is_patch_applied || !should_ignore(&file.path) { + if let Err(err) = file.repair(&config.game.path) { + this.update(Actions::ToastError(Rc::new(( + String::from("Failed to repair game file"), err + )))).unwrap(); + } + } + + let progress = i as f64 / total; + + this.update(Actions::UpdateProgress { + fraction: Rc::new(progress), + title: Rc::new(format!("Repairing files: {:.2}%", progress * 100.0)) + }).unwrap(); + } + } + + this.update(Actions::HideProgressBar).unwrap(); + }, + Err(err) => { + this.update(Actions::ToastError(Rc::new(( + String::from("Failed to get integrity files"), err + )))).unwrap(); + + this.update(Actions::HideProgressBar).unwrap(); + } + } + }); + }, + Err(err) => this.toast_error("Failed to load config", err) + } + } + Actions::ShowProgressBar => { this.widgets.progress_bar.show(); } @@ -567,6 +728,8 @@ impl App { pub fn set_state(&self, state: LauncherState) { println!("[main] [set_state] state: {:?}", &state); + self.widgets.progress_bar.hide(); + self.widgets.launch_game.set_tooltip_text(None); self.widgets.launch_game.set_sensitive(true); diff --git a/src/ui/preferences/general.rs b/src/ui/preferences/general.rs index d036205..d1fc41e 100644 --- a/src/ui/preferences/general.rs +++ b/src/ui/preferences/general.rs @@ -33,6 +33,8 @@ pub struct AppWidgets { pub voiceovers_row: adw::ExpanderRow, pub voieover_components: Rc>, + pub repair_game: gtk::Button, + pub game_version: gtk::Label, pub patch_version: gtk::Label, @@ -62,6 +64,8 @@ impl AppWidgets { voiceovers_row: get_object(&builder, "voiceovers_row")?, voieover_components: Default::default(), + repair_game: get_object(&builder, "repair_game")?, + game_version: get_object(&builder, "game_version")?, patch_version: get_object(&builder, "patch_version")?, @@ -160,6 +164,7 @@ impl AppWidgets { #[derive(Debug, Clone, glib::Downgrade)] pub enum Actions { VoiceoverPerformAction(Rc), + RepairGame, DxvkPerformAction(Rc), WinePerformAction(Rc<(usize, usize)>), UpdateDxvkComboRow, @@ -226,6 +231,8 @@ impl App { /// Add default events and values to the widgets fn init_events(self) -> Self { + self.widgets.repair_game.connect_clicked(Actions::RepairGame.into_fn(&self)); + // Voiceover download/delete button event for (i, row) in (&*self.widgets.voieover_components).into_iter().enumerate() { row.button.connect_clicked(clone!(@weak self as this => move |_| { @@ -325,6 +332,16 @@ impl App { println!("[general page] [update] action: {:?}", &action); match action { + Actions::RepairGame => { + let option = (&*this.app).take(); + this.app.set(option.clone()); + + let app = option.unwrap(); + + app.update(super::main::Actions::PreferencesGoBack).unwrap(); + app.update(super::main::Actions::RepairGame).unwrap(); + } + Actions::VoiceoverPerformAction(i) => { let component = this.widgets.voieover_components[*i].clone();