From 80836f32ec449194f55766587ad4b6227424d1fb Mon Sep 17 00:00:00 2001 From: ALEZ-DEV Date: Fri, 14 Mar 2025 22:43:31 +0100 Subject: [PATCH] can now download the game --- babylonia-terminal-gui/src/manager.rs | 116 ++++++- babylonia-terminal-gui/src/ui/mod.rs | 4 +- babylonia-terminal-gui/src/ui/pages/game.rs | 305 ++++++++++++++++-- .../src/ui/pages/steps/download_components.rs | 3 +- 4 files changed, 391 insertions(+), 37 deletions(-) diff --git a/babylonia-terminal-gui/src/manager.rs b/babylonia-terminal-gui/src/manager.rs index bf9d919..78493d2 100644 --- a/babylonia-terminal-gui/src/manager.rs +++ b/babylonia-terminal-gui/src/manager.rs @@ -14,9 +14,12 @@ use wincompatlib::prelude::Proton; use crate::ui::{ self, - pages::steps::{ + pages::{ self, - download_components::{self, DownloadComponentsPageWidgets}, + steps::{ + self, + download_components::{self, DownloadComponentsPageWidgets}, + }, }, }; @@ -51,6 +54,7 @@ pub enum HandleGameProcessMsg { RunGame, } +#[derive(Debug)] pub struct HandleGameProcess; impl Worker for HandleGameProcess { @@ -81,6 +85,114 @@ impl Worker for HandleGameProcess { } } +#[derive(Debug)] +pub enum HandleGameInstallationMsg { + StartInstallation(Arc), + StartPatch, + StartUpdate(Arc), +} + +#[derive(Debug)] +pub struct HandleGameInstallation; + +impl Worker for HandleGameInstallation { + type Init = (); + + type Input = HandleGameInstallationMsg; + + type Output = pages::game::GamePageMsg; + + fn init(init: Self::Init, sender: relm4::ComponentSender) -> Self { + Self + } + + fn update(&mut self, message: Self::Input, sender: relm4::ComponentSender) { + match message { + HandleGameInstallationMsg::StartInstallation(progress_bar) => { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + sender.output(pages::game::GamePageMsg::SetIsDownloading(true)); + + let game_dir = if let Some(dir) = GameConfig::get_config().await.game_dir { + dir + } else { + GameConfig::get_config_directory().await + }; + + if let Err(error) = GameManager::install_game(game_dir, progress_bar).await + { + sender.output(pages::game::GamePageMsg::ShowError(format!( + "Error while downloading the game : {}", + error + ))); + }; + + sender.output(pages::game::GamePageMsg::SetIsDownloading(false)); + }); + } + HandleGameInstallationMsg::StartPatch => { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + sender.output(pages::game::GamePageMsg::SetIsPatching(true)); + + let game_dir = if let Some(dir) = GameConfig::get_config().await.game_dir { + dir + } else { + GameConfig::get_config_directory().await + }; + + if let Err(error) = GameManager::patch_game(game_dir).await { + sender.output(pages::game::GamePageMsg::ShowError(format!( + "Error while patching the game : {}", + error + ))); + }; + + sender.output(pages::game::GamePageMsg::SetIsPatching(false)); + }); + } + HandleGameInstallationMsg::StartUpdate(progress_bar) => { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + sender.output(pages::game::GamePageMsg::SetIsDownloading(true)); + + if let Err(error) = GameManager::update_game().await { + sender.output(pages::game::GamePageMsg::ShowError(format!( + "Error while updating the game : {}", + error + ))); + }; + + let game_dir = if let Some(dir) = GameConfig::get_config().await.game_dir { + dir + } else { + GameConfig::get_config_directory().await + }; + + if let Err(error) = GameManager::install_game(game_dir, progress_bar).await + { + sender.output(pages::game::GamePageMsg::ShowError(format!( + "Error while updating the game : {}", + error + ))); + }; + + sender.output(pages::game::GamePageMsg::SetIsDownloading(true)); + }); + } + } + } +} + #[derive(Debug)] pub enum HandleComponentInstallationMsg { StartInstallation( diff --git a/babylonia-terminal-gui/src/ui/mod.rs b/babylonia-terminal-gui/src/ui/mod.rs index bd31372..041612c 100644 --- a/babylonia-terminal-gui/src/ui/mod.rs +++ b/babylonia-terminal-gui/src/ui/mod.rs @@ -4,6 +4,7 @@ use crate::{manager, IS_DEVEL}; use babylonia_terminal_sdk::game_state::GameState; use log::debug; +use pages::game::GamePageMsg; use relm4::{ self, component::AsyncConnector, @@ -51,7 +52,7 @@ impl MainWindow { .forward(sender.input_sender(), identity); let game_page = pages::game::GamePage::builder() - .launch(()) + .launch(game_state.clone()) .forward(sender.input_sender(), identity); let about_page = pages::about::AboutPage::builder().launch(()); @@ -269,6 +270,7 @@ impl SimpleAsyncComponent for MainWindow { } MainWindowMsg::UpdateGameState => { self.game_state = GameState::get_current_state().await.unwrap(); + self.game_page.sender().send(GamePageMsg::UpdateGameState); debug!( "is_environment_ready : {}", self.game_state.is_environment_ready() diff --git a/babylonia-terminal-gui/src/ui/pages/game.rs b/babylonia-terminal-gui/src/ui/pages/game.rs index 4de1f25..7fffdcc 100644 --- a/babylonia-terminal-gui/src/ui/pages/game.rs +++ b/babylonia-terminal-gui/src/ui/pages/game.rs @@ -1,29 +1,46 @@ -use std::convert::identity; +use std::{convert::identity, fmt::format}; -use libadwaita::prelude::PreferencesPageExt; +use arboard::Clipboard; +use babylonia_terminal_sdk::game_state::GameState; +use libadwaita::prelude::{MessageDialogExt, PreferencesPageExt}; +use log::error; use relm4::{ adw, gtk::{ self, - prelude::{ButtonExt, WidgetExt}, + prelude::{ButtonExt, GtkWindowExt, OrientableExt, WidgetExt}, }, prelude::{AsyncComponentParts, SimpleAsyncComponent}, Component, RelmWidgetExt, WorkerController, }; -use crate::{manager, ui::MainWindowMsg, APP_RESOURCE_PATH}; - -//use adw::prelude::*; -//use libadwaita as adw; +use crate::{ + manager, + ui::{MainWindowMsg, MAIN_WINDOW}, + APP_RESOURCE_PATH, +}; +#[derive(Debug)] pub struct GamePage { + game_state: GameState, game_handler: WorkerController, + installation_handler: WorkerController, is_game_running: bool, + is_downloading: bool, + is_patching: bool, + progress_bar_reporter: std::sync::Arc, + progress_bar_message: String, + fraction: f64, } #[derive(Debug)] pub enum GamePageMsg { SetIsGameRunning(bool), + SetIsDownloading(bool), + SetIsPatching(bool), + UpdateGameState, + UpdateProgressBar(u64, u64), + ShowError(String), } #[relm4::component(pub, async)] @@ -32,54 +49,186 @@ impl SimpleAsyncComponent for GamePage { type Output = MainWindowMsg; - type Init = (); + type Init = GameState; view! { - adw::PreferencesPage { - add = &adw::PreferencesGroup { - gtk::Picture { - set_resource: Some(&format!("{APP_RESOURCE_PATH}/icons/hicolor/scalable/apps/icon.png")), - set_vexpand: true, - set_width_request: 350, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + adw::PreferencesPage { + add = &adw::PreferencesGroup { + gtk::Picture { + set_resource: Some(&format!("{APP_RESOURCE_PATH}/icons/hicolor/scalable/apps/icon.png")), + set_vexpand: true, + set_width_request: 350, + }, + + gtk::Label { + set_label: "Babylonia Terminal", + set_margin_top: 24, + add_css_class: "title-1", + }, }, - gtk::Label { - set_label: "Babylonia Terminal", - set_margin_top: 24, - add_css_class: "title-1", - }, - }, + add = &adw::PreferencesGroup { + set_margin_vertical: 48, - add = &adw::PreferencesGroup { - set_margin_vertical: 48, + gtk::Button { + set_css_classes: &["suggested-action", "pill"], - gtk::Button { - set_css_classes: &["suggested-action", "pill"], + set_label: "Install", + set_hexpand: false, + set_width_request: 200, - set_label: "Start game", - set_hexpand: false, - set_width_request: 200, + set_visible: model.game_state == GameState::GameNotInstalled, - #[watch] - set_sensitive: !model.is_game_running, - connect_clicked[sender = model.game_handler.sender().clone()] => move |_| { - sender.send(manager::HandleGameProcessMsg::RunGame).unwrap(); + #[watch] + set_sensitive: !model.is_downloading, + connect_clicked[sender = model.installation_handler.sender().clone(), progress_bar = model.progress_bar_reporter.clone()] => move |_| { + sender.send(manager::HandleGameInstallationMsg::StartInstallation(progress_bar.clone())).unwrap(); + }, + }, + + gtk::Button { + set_css_classes: &["suggested-action", "pill"], + + set_label: "Apply patch", + set_hexpand: false, + set_width_request: 200, + + set_visible: model.game_state == GameState::GameNotPatched, + + #[watch] + set_sensitive: !model.is_patching, + connect_clicked[sender = model.installation_handler.sender().clone()] => move |_| { + sender.send(manager::HandleGameInstallationMsg::StartPatch).unwrap(); + }, + }, + + gtk::Button { + set_css_classes: &["suggested-action", "pill"], + + set_label: "Start game", + set_hexpand: false, + set_width_request: 200, + + set_visible: model.game_state == GameState::GameInstalled, + + #[watch] + set_sensitive: !model.is_game_running, + connect_clicked[sender = model.game_handler.sender().clone()] => move |_| { + sender.send(manager::HandleGameProcessMsg::RunGame).unwrap(); + }, + }, + + gtk::Button { + set_css_classes: &["suggested-action", "pill"], + + set_label: "Update", + set_hexpand: false, + set_width_request: 200, + + set_visible: model.game_state == GameState::GameNeedUpdate, + + #[watch] + set_sensitive: !model.is_downloading, + connect_clicked[sender = model.installation_handler.sender().clone(), progress_bar = model.progress_bar_reporter.clone()] => move |_| { + sender.send(manager::HandleGameInstallationMsg::StartUpdate(progress_bar.clone())).unwrap(); + }, }, }, }, - }, + + gtk::ProgressBar { + #[watch] + set_fraction: model.fraction, + + #[watch] + set_visible: (model.game_state == GameState::GameNotInstalled || model.game_state == GameState::GameNeedUpdate) && model.is_downloading && model.fraction != 0.0, + + #[watch] + set_text: Some(&model.progress_bar_message), + set_show_text: true, + }, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + #[watch] + set_visible: (model.game_state == GameState::GameNotInstalled || model.game_state == GameState::GameNeedUpdate) && model.is_downloading && model.fraction == 0.0, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_halign: gtk::Align::Center, + set_margin_bottom: 24, + + gtk::Label { + set_label: "Checking game files", + + add_css_class: "title-2", + }, + + gtk::Spinner { + set_spinning: true, + set_margin_start: 24, + } + }, + + gtk::Label { + set_label: "This can take some time, please wait...", + + add_css_class: "title-4", + }, + }, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + #[watch] + set_visible: model.game_state == GameState::GameNotPatched && model.is_patching, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + + gtk::Label { + set_label: "Patching game", + + add_css_class: "title-2", + }, + + gtk::Spinner { + set_spinning: true, + } + }, + + gtk::Label { + set_label: "This can take some time, please wait...", + + add_css_class: "title-4", + }, + } + } } async fn init( - _: Self::Init, + game_state: Self::Init, root: Self::Root, sender: relm4::AsyncComponentSender, ) -> relm4::prelude::AsyncComponentParts { let model = GamePage { + progress_bar_reporter: ProgressBarGameInstallationReporter::create(sender.clone()), + game_state, game_handler: manager::HandleGameProcess::builder() .detach_worker(()) .forward(sender.input_sender(), identity), + installation_handler: manager::HandleGameInstallation::builder() + .detach_worker(()) + .forward(sender.input_sender(), identity), is_game_running: false, + is_downloading: false, + is_patching: false, + fraction: 0.0, + progress_bar_message: String::new(), }; let widgets = view_output!(); @@ -90,6 +239,96 @@ impl SimpleAsyncComponent for GamePage { async fn update(&mut self, message: Self::Input, _: relm4::AsyncComponentSender) -> () { match message { GamePageMsg::SetIsGameRunning(value) => self.is_game_running = value, + GamePageMsg::SetIsDownloading(value) => self.is_downloading = value, + GamePageMsg::SetIsPatching(value) => self.is_patching = value, + GamePageMsg::UpdateGameState => { + self.game_state = GameState::get_current_state().await.unwrap() + } //TODO: remove unwrap() + GamePageMsg::UpdateProgressBar(current, max_progress) => { + self.fraction = if current == 0 { + 0.0 + } else { + current as f64 / max_progress as f64 + }; + + //1024^3 = 1073741824 + let current_gb = current as f64 / 1073741824 as f64; + let max_gb = max_progress as f64 / 1073741824 as f64; + + self.progress_bar_message = format!( + "Downloading : {:.2}% ({:.2} / {:.2}GiB)", + self.fraction * 100 as f64, + current_gb, + max_gb, + ); + } + GamePageMsg::ShowError(message) => { + let dialog = unsafe { + adw::MessageDialog::new( + MAIN_WINDOW.as_ref(), + Some("Something went wrong"), + Some(&message), + ) + }; + + dialog.add_response("close", "Close"); + dialog.add_response("copy", "Copy"); + + dialog.set_response_appearance("copy", adw::ResponseAppearance::Suggested); + + dialog.connect_response(Some("copy"), move |_, _| { + if let Err(err) = Clipboard::new().unwrap().set_text(&message.clone()) { + error!("Failed to copy the error to the clipboard : {}", err); + } + }); + + dialog.present(); + } } } } + +#[derive(Debug)] +struct ProgressBarGameInstallationReporterPrivate { + max_progress: Option, +} + +#[derive(Debug)] +pub struct ProgressBarGameInstallationReporter { + private: std::sync::Mutex>, + sender: relm4::AsyncComponentSender, +} + +impl ProgressBarGameInstallationReporter { + fn create(page: relm4::AsyncComponentSender) -> std::sync::Arc { + std::sync::Arc::new(Self { + private: std::sync::Mutex::new(None), + sender: page, + }) + } +} + +impl downloader::progress::Reporter for ProgressBarGameInstallationReporter { + fn setup(&self, max_progress: Option, message: &str) { + let private = ProgressBarGameInstallationReporterPrivate { max_progress }; + + let mut guard = self.private.lock().unwrap(); + *guard = Some(private); + } + + fn progress(&self, current: u64) { + if let Some(p) = self.private.lock().unwrap().as_mut() { + self.sender.input(GamePageMsg::UpdateProgressBar( + current, + p.max_progress.unwrap(), + )); + } + } + + fn set_message(&self, _: &str) {} + + fn done(&self) { + let mut guard = self.private.lock().unwrap(); + *guard = None; + } +} diff --git a/babylonia-terminal-gui/src/ui/pages/steps/download_components.rs b/babylonia-terminal-gui/src/ui/pages/steps/download_components.rs index 450eb24..860d621 100644 --- a/babylonia-terminal-gui/src/ui/pages/steps/download_components.rs +++ b/babylonia-terminal-gui/src/ui/pages/steps/download_components.rs @@ -251,7 +251,7 @@ impl SimpleAsyncComponent for DownloadComponentsPage { }, gtk::Box { - set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, set_orientation: gtk::Orientation::Horizontal, gtk::Label { @@ -265,6 +265,7 @@ impl SimpleAsyncComponent for DownloadComponentsPage { gtk::Spinner { set_spinning: true, + set_margin_start: 24, #[watch] set_visible: model.currently_installing == CurrentlyInstalling::Fonts || model.currently_installing == CurrentlyInstalling::Denpendecies,