diff --git a/assets/wine.json b/assets/wine.json index c1db475..9ae5d73 100644 --- a/assets/wine.json +++ b/assets/wine.json @@ -3,6 +3,45 @@ "title": "Wine-GE-Proton", "subtitle": null, "versions": [ + { + "family": "Wine-GE-Proton", + "name": "lutris-GE-Proton7-22-x86_64", + "title": "Wine-GE-Proton 7-22", + "uri": "https://github.com/GloriousEggroll/wine-ge-custom/releases/download/GE-Proton7-22/wine-lutris-GE-Proton7-22-x86_64.tar.xz", + "files": { + "wine": "bin/wine64", + "wineserver": "bin/wineserver", + "wineboot": "bin/wineboot", + "winecfg": "lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, + { + "family": "Wine-GE-Proton", + "name": "lutris-GE-Proton7-20-x86_64", + "title": "Wine-GE-Proton 7-20", + "uri": "https://github.com/GloriousEggroll/wine-ge-custom/releases/download/GE-Proton7-20/wine-lutris-GE-Proton7-20-x86_64.tar.xz", + "files": { + "wine": "bin/wine64", + "wineserver": "bin/wineserver", + "wineboot": "bin/wineboot", + "winecfg": "lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, + { + "family": "Wine-GE-Proton", + "name": "lutris-GE-Proton7-18-x86_64", + "title": "Wine-GE-Proton 7-18", + "uri": "https://github.com/GloriousEggroll/wine-ge-custom/releases/download/GE-Proton7-18/wine-lutris-GE-Proton7-18-x86_64.tar.xz", + "files": { + "wine": "bin/wine64", + "wineserver": "bin/wineserver", + "wineboot": "bin/wineboot", + "winecfg": "lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, { "family": "Wine-GE-Proton", "name": "lutris-GE-Proton7-16-x86_64", @@ -204,6 +243,32 @@ "title": "GE-Proton", "subtitle": "This version includes its own DXVK builds and you can use DXVK_ASYNC variable", "versions": [ + { + "family": "GE-Proton", + "name": "GE-Proton7-26", + "title": "GE-Proton 7-26", + "uri": "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton7-26/GE-Proton7-26.tar.gz", + "files": { + "wine": "files/bin/wine64", + "wineserver": "files/bin/wineserver", + "wineboot": "files/bin/wineboot", + "winecfg": "files/lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, + { + "family": "GE-Proton", + "name": "GE-Proton7-24", + "title": "GE-Proton 7-24", + "uri": "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton7-24/GE-Proton7-24.tar.gz", + "files": { + "wine": "files/bin/wine64", + "wineserver": "files/bin/wineserver", + "wineboot": "files/bin/wineboot", + "winecfg": "files/lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, { "family": "GE-Proton", "name": "GE-Proton7-20", @@ -431,6 +496,19 @@ "title": "Lutris", "subtitle": null, "versions": [ + { + "family": "Lutris", + "name": "lutris-7.2-2-x86_64", + "title": "Lutris 7.2-2", + "uri": "https://github.com/lutris/wine/releases/download/lutris-wine-7.2-2/wine-lutris-7.2-2-x86_64.tar.xz", + "files": { + "wine": "bin/wine64", + "wineserver": "bin/wineserver", + "wineboot": "bin/wineboot", + "winecfg": "lib64/wine/x86_64-windows/winecfg.exe" + }, + "recommended": true + }, { "family": "Lutris", "name": "lutris-7.2-x86_64", diff --git a/src/lib/dxvk.rs b/src/lib/dxvk.rs index 77384ec..62c801f 100644 --- a/src/lib/dxvk.rs +++ b/src/lib/dxvk.rs @@ -61,6 +61,10 @@ pub struct Version { } impl Version { + pub fn latest() -> Result { + Ok(List::get()?.vanilla[0].clone()) + } + pub fn is_downloaded_in(&self, folder: T) -> bool { std::path::Path::new(&format!("{}/{}", folder.to_string(), self.name)).exists() } diff --git a/src/lib/mod.rs b/src/lib/mod.rs index c7a6b00..4cd0a39 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -4,4 +4,5 @@ pub mod tasks; pub mod game; pub mod dxvk; pub mod wine; +pub mod wine_prefix; pub mod launcher; diff --git a/src/lib/wine.rs b/src/lib/wine.rs index d51c498..bd1e2dd 100644 --- a/src/lib/wine.rs +++ b/src/lib/wine.rs @@ -53,6 +53,10 @@ pub struct Version { } impl Version { + pub fn latest() -> Result { + Ok(List::get()?[0].versions[0].clone()) + } + pub fn is_downloaded_in(&self, folder: T) -> bool { std::path::Path::new(&format!("{}/{}", folder.to_string(), self.name)).exists() } diff --git a/src/lib/wine_prefix.rs b/src/lib/wine_prefix.rs new file mode 100644 index 0000000..500e116 --- /dev/null +++ b/src/lib/wine_prefix.rs @@ -0,0 +1,51 @@ +use std::path::Path; +use std::process::Command; + +pub struct WinePrefix { + pub path: String +} + +impl WinePrefix { + pub fn new(path: T) -> Self { + Self { path: path.to_string() } + } + + pub fn exists(&self) -> bool { + Path::new(&format!("{}/drive_c", self.path)).exists() + } + + fn wineboot(&self, runners_folder: T, runner: super::wine::Version, command: &str) -> std::io::Result<()> { + let runners_folder = runners_folder.to_string(); + + let wineboot = format!("{}/{}", &runners_folder, runner.files.wineboot); + let wineserver = format!("{}/{}", &runners_folder, runner.files.wineserver); + + Command::new(wineboot) + .env("WINESERVER", wineserver) + .env("WINEPREFIX", &self.path) + .arg(command) + .output()?; + + Ok(()) + } + + pub fn update(&self, runners_folder: T, runner: super::wine::Version) -> std::io::Result<()> { + self.wineboot(runners_folder, runner, "-u") + } + + pub fn end(&self, runners_folder: T, runner: super::wine::Version) -> std::io::Result<()> { + self.wineboot(runners_folder, runner, "-e") + } + + pub fn kill(&self, runners_folder: T, runner: super::wine::Version) -> std::io::Result<()> { + self.wineboot(runners_folder, runner, "-k") + } + + pub fn restart(&self, runners_folder: T, runner: super::wine::Version) -> std::io::Result<()> { + self.wineboot(runners_folder, runner, "-r") + } + + pub fn shutdown(&self, runners_folder: T, runner: super::wine::Version) -> std::io::Result<()> { + self.wineboot(runners_folder, runner, "-s") + } +} diff --git a/src/main.rs b/src/main.rs index 440e3ed..cc06dcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,13 @@ async fn main() { // FIXME: doesn't work? set_application_name("An Anime Game Launcher"); + // Create default launcher folder if needed + let launcher_dir = lib::consts::launcher_dir().unwrap(); + + if !std::path::Path::new(&launcher_dir).exists() { + std::fs::create_dir_all(launcher_dir).expect("Failed to create default launcher dir"); + } + // Create app let application = gtk::Application::new( Some(APP_ID), diff --git a/src/ui/main.rs b/src/ui/main.rs index fbcbff3..b1e09e7 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -6,6 +6,7 @@ use gtk4::glib::clone; use std::rc::Rc; use std::cell::Cell; +use std::io::Error; use anime_game_core::prelude::*; @@ -18,6 +19,36 @@ use crate::lib::config; use crate::lib::game; use crate::lib::tasks; use crate::lib::launcher::states::LauncherState; +use crate::lib::wine_prefix::WinePrefix; +use crate::lib::wine::Version as WineVersion; +use crate::lib::dxvk::Version as DxvkVersion; + +fn prettify_bytes(bytes: u64) -> String { + if bytes > 1024 * 1024 * 1024 { + format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0) + } + + else if bytes > 1024 * 1024 { + format!("{:.2} MB", bytes as f64 / 1024.0 / 1024.0) + } + + else if bytes > 1024 { + format!("{:.2} KB", bytes as f64 / 1024.0) + } + + else { + format!("{:.2} B", bytes) + } +} + +fn toast_error(app: &App, msg: &str, err: Error) { + app.update(Actions::ToastError(Rc::new(( + String::from(msg), err + )))).unwrap(); + + app.update(Actions::HideProgressBar).unwrap(); + app.update_state(); +} /// This structure is used to describe widgets used in application /// @@ -104,6 +135,7 @@ impl AppWidgets { format!("GTK version: {}.{}.{}", gtk::major_version(), gtk::minor_version(), gtk::micro_version()), format!("Libadwaita version: {}.{}.{}", adw::major_version(), adw::minor_version(), adw::micro_version()), format!("Pango version: {}", gtk::pango::version_string().unwrap_or("?".into())), + format!("Cairo version: {}", gtk::cairo::version_string()), ].join("\n"))); // Add preferences page to the leaflet @@ -122,9 +154,11 @@ pub enum Actions { PreferencesGoBack, PerformButtonEvent, DownloadDiff(Rc), + UpdateProgressByState(Rc<(InstallerUpdate, Option)>), ShowProgressBar, UpdateProgress { fraction: Rc, title: Rc }, - HideProgressBar + HideProgressBar, + ToastError(Rc<(String, Error)>) } impl Actions { @@ -211,7 +245,9 @@ impl App { receiver.attach(None, move |action| { // Some debug output match &action { - Actions::UpdateProgress { .. } => (), + // Actions::UpdateProgress { .. } | + // Actions::UpdateProgressByState(_) => (), + action => println!("[main] [update] action: {:?}", action) } @@ -266,76 +302,156 @@ impl App { Actions::DownloadDiff(diff) => { match config::get() { - Ok(config) => { - fn to_gb(bytes: u64) -> f64 { - (bytes as f64 / 1024.0 / 1024.0 / 1024.0 * 100.0).ceil() / 100.0 - } - + Ok(mut config) => { let diff = (*diff).clone(); let this = this.clone(); std::thread::spawn(move || { let this = this.clone(); - diff.install_to_by(config.game.path, config.launcher.temp, move |state| { - match state { - InstallerUpdate::DownloadingStarted(_) => { - this.update(Actions::ShowProgressBar).unwrap(); + this.update(Actions::ShowProgressBar).unwrap(); - this.update(Actions::UpdateProgress { - fraction: Rc::new(0.0), - title: Rc::new(String::from("Downloading...")) - }).unwrap(); - } + // Download wine version if not installed + match WineVersion::latest() { + Ok(wine) => match Installer::new(wine.uri) { + Ok(mut installer) => { + let (send, recv) = std::sync::mpsc::channel(); + let wine_title = wine.title.clone(); - InstallerUpdate::DownloadingProgress(curr, total) => { - // To reduce amount of action requests - if curr % 10000 < 200 { - let progress = curr as f64 / total as f64; + installer.install(&config.game.wine.builds, clone!(@strong this => move |state| { + match state { + InstallerUpdate::UnpackingFinished => { + send.send(true).unwrap(); + } - this.update(Actions::UpdateProgress { - fraction: Rc::new(progress), - title: Rc::new(format!( - "Downloading: {:.2}% ({} of {} GB)", - progress * 100.0, - to_gb(curr), - to_gb(total) - )) - }).unwrap(); + InstallerUpdate::DownloadingError(_) | + InstallerUpdate::UnpackingError => { + send.send(false).unwrap(); + } + + _ => () + } + + this.update(Actions::UpdateProgressByState(Rc::new((state, Some(wine_title.clone()))))).unwrap(); + })); + + // Block thread until downloading finished + if recv.recv().unwrap() { + config.game.wine.selected = Some(wine.name); + + config::update(config.clone()); } + + else { + println!("I'm tired, Boss!"); + + return; + } + }, + Err(err) => { + toast_error(&this, "Failed to init wine version installer", err.into()); + + return; } + }, + Err(err) => { + toast_error(&this, "Failed to load wine versions list", err.into()); - InstallerUpdate::UnpackingStarted(_) => { - this.update(Actions::UpdateProgress { - fraction: Rc::new(0.0), - title: Rc::new(String::from("Unpacking...")) - }).unwrap(); - } - - InstallerUpdate::UnpackingProgress(curr, total) => { - let progress = curr as f64 / total as f64; - - this.update(Actions::UpdateProgress { - fraction: Rc::new(progress), - title: Rc::new(format!( - "Unpacking: {:.2}% ({} of {} GB)", - progress * 100.0, - to_gb(curr), - to_gb(total) - )) - }).unwrap(); - } - - InstallerUpdate::DownloadingFinished => (), - - InstallerUpdate::UnpackingFinished => { - this.update(Actions::HideProgressBar).unwrap(); - this.update_state(); - } - - InstallerUpdate::DownloadingError(err) => this.toast_error("Failed to download game", err), - InstallerUpdate::UnpackingError => this.toast_error("Failed to unpack game", "?") + return; } + } + + // Create prefix if needed + let prefix = WinePrefix::new(&config.game.wine.prefix); + + if !prefix.exists() { + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Creating prefix...")) + }).unwrap(); + + match config.try_get_selected_wine_info() { + Some(wine_version) => { + if let Err(err) = prefix.update(&config.game.wine.builds, wine_version) { + toast_error(&this, "Failed to create wineprefix", err); + + return; + } + }, + None => return + } + } + + // Download and apply DXVK if not installed + match DxvkVersion::latest() { + Ok(dxvk) => match Installer::new(&dxvk.uri) { + Ok(mut installer) => { + let (send, recv) = std::sync::mpsc::channel(); + let dxvk_title = dxvk.name.clone(); + + installer.install(&config.game.dxvk.builds, clone!(@strong this => move |state| { + match state { + InstallerUpdate::UnpackingFinished => { + send.send(true).unwrap(); + } + + InstallerUpdate::DownloadingError(_) | + InstallerUpdate::UnpackingError => { + send.send(false).unwrap(); + } + + _ => () + } + + this.update(Actions::UpdateProgressByState(Rc::new((state, Some(dxvk_title.clone()))))).unwrap(); + })); + + // Block thread until downloading finished + if recv.recv().unwrap() { + config.game.dxvk.selected = Some(dxvk.name.clone()); + + config::update(config.clone()); + } + + else { + return; + } + + // Apply DXVK + this.update(Actions::UpdateProgress { + fraction: Rc::new(100.0), + title: Rc::new(String::from("Applying DXVK...")) + }).unwrap(); + + match dxvk.apply(&config.game.dxvk.builds, &config.game.wine.prefix) { + Ok(_) => { + config.game.dxvk.selected = Some(dxvk.name); + + config::update(config.clone()); + }, + Err(err) => { + toast_error(&this, "Failed to apply DXVK", err); + + return; + } + } + }, + Err(err) => { + toast_error(&this, "Failed to init wine version installer", err.into()); + + return; + } + }, + Err(err) => { + toast_error(&this, "Failed to load wine versions list", err.into()); + + return; + } + } + + // Download diff + diff.install_to_by(config.game.path, config.launcher.temp, move |state| { + this.update(Actions::UpdateProgressByState(Rc::new((state, None)))).unwrap(); }).unwrap(); }); }, @@ -347,6 +463,74 @@ impl App { } } + Actions::UpdateProgressByState(state) => { + // let (state, suffix) = (&*state).clone(); + + match &state.0 { + InstallerUpdate::DownloadingStarted(_) => { + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Downloading...")) + }).unwrap(); + } + + InstallerUpdate::DownloadingProgress(curr, total) => { + // To reduce amount of action requests + // if curr % 10000 < 200 { + let progress = *curr as f64 / *total as f64; + + this.update(Actions::UpdateProgress { + fraction: Rc::new(progress), + title: Rc::new(format!( + "Downloading{}: {:.2}% ({} of {})", + if let Some(suffix) = &state.1 { format!(" {}", suffix) } else { String::new() }, + progress * 100.0, + prettify_bytes(*curr), + prettify_bytes(*total) + )) + }).unwrap(); + // } + } + + InstallerUpdate::UnpackingStarted(_) => { + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Unpacking...")) + }).unwrap(); + } + + InstallerUpdate::UnpackingProgress(curr, total) => { + let progress = *curr as f64 / *total as f64; + + this.update(Actions::UpdateProgress { + fraction: Rc::new(progress), + title: Rc::new(format!( + "Unpacking{}: {:.2}% ({} of {})", + if let Some(suffix) = &state.1 { format!(" {}", suffix) } else { String::new() }, + progress * 100.0, + prettify_bytes(*curr), + prettify_bytes(*total) + )) + }).unwrap(); + } + + InstallerUpdate::DownloadingFinished => (), + + InstallerUpdate::UnpackingFinished => { + this.update(Actions::HideProgressBar).unwrap(); + this.update_state(); + } + + InstallerUpdate::DownloadingError(err) => { + toast_error(&this, "Failed to download", err.clone().into()); + } + + InstallerUpdate::UnpackingError => { + toast_error(&this, "Failed to unpack", Error::last_os_error()); + } + } + } + Actions::ShowProgressBar => { this.widgets.progress_bar.set_text(None); this.widgets.progress_bar.set_fraction(0.0); @@ -364,6 +548,12 @@ impl App { this.widgets.launch_game_group.show(); this.widgets.progress_bar_group.hide(); } + + Actions::ToastError(toast) => { + let (msg, err) = (toast.0.clone(), toast.1.to_string()); + + this.toast_error(msg, err); + } } glib::Continue(true)