feat(ui): initial xlua patch implementation

This commit is contained in:
Observer KRypt0n_ 2023-03-22 20:44:46 +02:00
parent ddc122d631
commit 907361df7b
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
19 changed files with 751 additions and 441 deletions

4
Cargo.lock generated
View file

@ -31,7 +31,7 @@ dependencies = [
[[package]]
name = "anime-game-core"
version = "1.4.0"
version = "1.4.2"
dependencies = [
"anyhow",
"bzip2",
@ -76,7 +76,7 @@ dependencies = [
[[package]]
name = "anime-launcher-sdk"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"anime-game-core",
"anyhow",

@ -1 +1 @@
Subproject commit 78bb7082472d882d68574dd05ce3c392ab7c387a
Subproject commit 26cc05815b405943faa258b59a3afd4c568b0afb

View file

@ -24,7 +24,11 @@ game-predownload-available = Vorab-Download von Spiel-Updates verfügbar: {$old}
game-update-available = Spiel-Update verfügbar: {$old} -> {$new}
game-outdated = Das Spiel ist zu veraltet und kann nicht mehr aktualisiert werden. Letzte Version: {$latest}
patch-version = Patch version
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = nicht verfügbar
patch-not-available-tooltip = Patch-Server sind unerreichbar
@ -36,6 +40,12 @@ patch-preparation = Vorbereitung
patch-preparation-tooltip = Patch ist in Entwicklung
patch-testing-tooltip = Test-Patch ist verfügbar
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = Ausgewählte version
recommended-only = Nur empfohlene

View file

@ -24,7 +24,11 @@ game-predownload-available = Game update pre-downloading available: {$old} -> {$
game-update-available = Game update available: {$old} -> {$new}
game-outdated = Game is too outdated and can't be updated. Latest version: {$latest}
patch-version = Patch version
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = not available
patch-not-available-tooltip = Patch servers are unreachable
@ -36,6 +40,12 @@ patch-preparation = preparation
patch-preparation-tooltip = Patch is in development
patch-testing-tooltip = Test patch is available
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = Selected version
recommended-only = Recommended only

View file

@ -24,7 +24,11 @@ game-predownload-available = Pre-descarga de actualización disponible: {$old} -
game-update-available = Actualización disponible: {$old} -> {$new}
game-outdated = El juego está demasiado desactualizado y no puede actualizarse. Última versión: {$latest}
patch-version = Versión del parche
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = No disponible
patch-not-available-tooltip = Los servidores del parche no pudieron contactarse
@ -36,6 +40,12 @@ patch-preparation = Preparación
patch-preparation-tooltip = El parche está en desarrollo
patch-testing-tooltip = Está disponible un parche de prueba
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = Versión seleccionada
recommended-only = Sólo recomendadas

View file

@ -24,7 +24,11 @@ game-predownload-available = Mise à jour du jeu disponible en pré-télécharge
game-update-available = Mise à jour du jeu disponible : {$old} -> {$new}
game-outdated = La version du jeu installée est trop ancienne et ne peut pas être mise à jour. Dernière version : {$latest}
patch-version = Version du patch
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = patch non disponible
patch-not-available-tooltip = Impossible d'accéder aux serveurs de patch
@ -36,6 +40,12 @@ patch-preparation = préparation
patch-preparation-tooltip = Le patch est en développement
patch-testing-tooltip = Patch de test disponible
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = Version sélectionnée
recommended-only = Versions recommandées uniquement

View file

@ -24,7 +24,11 @@ game-predownload-available = Доступна предзагрузка обно
game-update-available = Доступно обновление игры: {$old} -> {$new}
game-outdated = Версия игры слишком устаревшая и не может быть обновлена. Последняя версия: {$latest}
patch-version = Версия патча
player-patch-version = Версия основного патча
player-patch-version-description = Основной патч, позволяющий вам играть в игру на линуксе
xlua-patch-version = Версия патча xlua
xlua-patch-version-description = Дополнительный патч, устраняющий некоторые проблемы и улучшающий производительность на слабых ПК
patch-not-available = недоступен
patch-not-available-tooltip = Серверы патча недоступны
@ -36,6 +40,12 @@ patch-preparation = подготовка
patch-preparation-tooltip = Патч в разработке
patch-testing-tooltip = Доступна тестовая версия патча
patch-not-applied-tooltip = Патч не применен
apply-xlua-patch = Применить патч xlua
ask-superuser-permissions = Запрашивать права суперпользователя
ask-superuser-permissions-description = Лаунчер будет использовать их чтобы автоматически обновлять ваш hosts файл. Это не требуется при использовании Flatpak
selected-version = Выбранная версия
recommended-only = Только рекомендуемое

View file

@ -24,7 +24,11 @@ game-predownload-available = Güncelleme önceden indirilebilir: {$old} -> {$new
game-update-available = Güncelleme mevcut: {$old} -> {$new}
game-outdated = Oyun çok eski bu yüzden güncellenemez. En son sürüm: {$latest}
patch-version = Yama versiyonu
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = Mevcut değil
patch-not-available-tooltip = Yama sunucularına erişelemiyor
@ -36,6 +40,12 @@ patch-preparation = Hazırlık
patch-preparation-tooltip = Yama hala geliştiriliyor
patch-testing-tooltip = Test yaması mevcut
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = Seçilmiş versiyon
recommended-only = Sadece önerilenler

View file

@ -24,7 +24,11 @@ game-predownload-available = 可以预下载游戏更新: {$old} -> {$new}
game-update-available = 游戏版本更新: {$old} -> {$new}
game-outdated = 游戏版本过旧,无法更新。最新版本: {$latest}
patch-version = 补丁版本
player-patch-version = Player patch version
player-patch-version-description = Main patch that lets you play the game on Linux
xlua-patch-version = Xlua patch version
xlua-patch-version-description = Additional patch that fixes some issues and improves performance on low-end PCs
patch-not-available = 不可用
patch-not-available-tooltip = 无法连接补丁服务器
@ -36,6 +40,12 @@ patch-preparation = 开发中
patch-preparation-tooltip = 补丁还在开发中
patch-testing-tooltip = 有测试版补丁可用
patch-not-applied-tooltip = Patch is not applied
apply-xlua-patch = Apply xlua patch
ask-superuser-permissions = Ask superuser permissions
ask-superuser-permissions-description = Launcher will use them to automatically update your hosts file. This is not needed in flatpak edition
selected-version = 选择版本
recommended-only = 仅显示推荐版本

View file

@ -191,7 +191,8 @@ fn main() {
}
LauncherState::PredownloadAvailable { .. } |
LauncherState::MainPatchAvailable(UnityPlayerPatch { status: PatchStatus::NotAvailable, .. }) => {
LauncherState::UnityPlayerPatchAvailable(UnityPlayerPatch { status: PatchStatus::NotAvailable, .. }) |
LauncherState::XluaPatchAvailable(XluaPatch { status: PatchStatus::NotAvailable, .. }) => {
if just_run_game {
anime_launcher_sdk::game::run().expect("Failed to run the game");

View file

@ -0,0 +1,40 @@
use relm4::prelude::*;
use anime_launcher_sdk::config;
use crate::*;
use crate::i18n::*;
use super::{App, AppMsg};
pub fn apply_patch<T: PatchExt + Send + Sync + 'static>(sender: ComponentSender<App>, patch: T) {
match patch.status() {
PatchStatus::NotAvailable |
PatchStatus::Outdated { .. } |
PatchStatus::Preparation { .. } => unreachable!(),
PatchStatus::Testing { .. } |
PatchStatus::Available { .. } => {
sender.input(AppMsg::DisableButtons(true));
let config = config::get().unwrap();
std::thread::spawn(move || {
if let Err(err) = patch.apply(&config.game.path, config.patch.root) {
tracing::error!("Failed to patch the game");
sender.input(AppMsg::Toast {
title: tr("game-patching-error"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::DisableButtons(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: true,
show_status_page: true
});
});
}
}
}

View file

@ -0,0 +1,59 @@
use relm4::prelude::*;
use anime_launcher_sdk::config;
use anime_launcher_sdk::wincompatlib::prelude::*;
use crate::i18n::*;
use super::{App, AppMsg};
pub fn create_prefix(sender: ComponentSender<App>) {
let config = config::get().unwrap();
match config.get_selected_wine() {
Ok(Some(wine)) => {
sender.input(AppMsg::DisableButtons(true));
std::thread::spawn(move || {
let wine = wine
.to_wine(config.components.path, Some(config.game.wine.builds.join(&wine.name)))
.with_prefix(&config.game.wine.prefix)
.with_loader(WineLoader::Current)
.with_arch(WineArch::Win64);
if let Err(err) = wine.update_prefix::<&str>(None) {
tracing::error!("Failed to create wine prefix");
sender.input(AppMsg::Toast {
title: tr("wine-prefix-update-failed"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::DisableButtons(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: true
});
});
}
Ok(None) => {
tracing::error!("Failed to get selected wine executable");
sender.input(AppMsg::Toast {
title: tr("failed-get-selected-wine"),
description: None
});
}
Err(err) => {
tracing::error!("Failed to get selected wine executable: {err}");
sender.input(AppMsg::Toast {
title: tr("failed-get-selected-wine"),
description: Some(err.to_string())
});
}
}
}

View file

@ -0,0 +1,66 @@
use relm4::{
prelude::*,
Sender
};
use gtk::glib::clone;
use anime_launcher_sdk::config;
use anime_launcher_sdk::anime_game_core::installer::diff::VersionDiff;
use crate::*;
use crate::i18n::*;
use crate::ui::components::*;
use super::{App, AppMsg};
pub fn download_diff(sender: ComponentSender<App>, progress_bar_input: Sender<ProgressBarMsg>, diff: VersionDiff) {
sender.input(AppMsg::SetDownloading(true));
// TODO: add speed limit
std::thread::spawn(move || {
let config = config::get().unwrap();
#[allow(unused_must_use)]
let result = diff.install_to_by(config.game.path, config.launcher.temp, clone!(@strong sender => move |state| {
match &state {
InstallerUpdate::DownloadingError(err) => {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
InstallerUpdate::UnpackingError(err) => {
tracing::error!("Unpacking failed: {err}");
sender.input(AppMsg::Toast {
title: tr("unpacking-failed"),
description: Some(err.clone())
});
}
_ => ()
}
progress_bar_input.send(ProgressBarMsg::UpdateFromState(state));
}));
if let Err(err) = result {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::SetDownloading(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: true,
apply_patch_if_needed: false,
show_status_page: false
});
});
}

View file

@ -0,0 +1,115 @@
use relm4::{
prelude::*,
Sender
};
use gtk::glib::clone;
use anime_launcher_sdk::config;
use anime_launcher_sdk::components::wine;
use crate::*;
use crate::i18n::*;
use crate::ui::components::*;
use super::{App, AppMsg};
pub fn download_wine(sender: ComponentSender<App>, progress_bar_input: Sender<ProgressBarMsg>) {
let mut config = config::get().unwrap();
match wine::get_downloaded(&CONFIG.components.path, &config.game.wine.builds) {
Ok(downloaded) => {
// Select downloaded version
if !downloaded.is_empty() {
config.game.wine.selected = Some(downloaded[0].versions[0].name.clone());
config::update(config);
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: true
});
}
// Or download new one if none is available
else {
let latest = wine::Version::latest(&CONFIG.components.path).expect("Failed to get latest wine version");
// Choose selected wine version or use latest available one
let wine = match &config.game.wine.selected {
Some(version) => match wine::Version::find_in(&config.components.path, version) {
Ok(Some(version)) => version,
_ => latest
}
None => latest
};
// Download wine version
match Installer::new(wine.uri) {
Ok(mut installer) => {
if let Some(temp_folder) = &config.launcher.temp {
installer.temp_folder = temp_folder.to_path_buf();
}
installer.downloader
.set_downloading_speed(config.launcher.speed_limit)
.expect("Failed to set downloading speed limit");
sender.input(AppMsg::SetDownloading(true));
std::thread::spawn(clone!(@strong sender => move || {
#[allow(unused_must_use)]
installer.install(&config.game.wine.builds, clone!(@strong sender => move |state| {
match &state {
InstallerUpdate::DownloadingError(err) => {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
InstallerUpdate::UnpackingError(err) => {
tracing::error!("Unpacking failed: {err}");
sender.input(AppMsg::Toast {
title: tr("unpacking-failed"),
description: Some(err.clone())
});
}
_ => ()
}
progress_bar_input.send(ProgressBarMsg::UpdateFromState(state));
}));
config.game.wine.selected = Some(wine.name.clone());
config::update(config);
sender.input(AppMsg::SetDownloading(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: true
});
}));
}
Err(err) => sender.input(AppMsg::Toast {
title: tr("wine-install-failed"),
description: Some(err.to_string())
})
}
}
}
Err(err) => sender.input(AppMsg::Toast {
title: tr("downloaded-wine-list-failed"),
description: Some(err.to_string())
})
}
}

21
src/ui/main/launch.rs Normal file
View file

@ -0,0 +1,21 @@
use relm4::prelude::*;
use crate::i18n::*;
use super::{App, AppMsg};
pub fn launch(sender: ComponentSender<App>) {
sender.input(AppMsg::HideWindow);
std::thread::spawn(move || {
if let Err(err) = anime_launcher_sdk::game::run() {
tracing::error!("Failed to launch game: {err}");
sender.input(AppMsg::Toast {
title: tr("game-launching-failed"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::ShowWindow);
});
}

View file

@ -10,13 +10,16 @@ use adw::prelude::*;
use gtk::glib::clone;
mod repair_game;
mod apply_patch;
mod download_wine;
mod create_prefix;
mod download_diff;
mod launch;
use anime_launcher_sdk::config::launcher::LauncherStyle;
use anime_launcher_sdk::states::LauncherState;
use anime_launcher_sdk::wincompatlib::prelude::*;
use anime_launcher_sdk::components::loader::ComponentsLoader;
use anime_launcher_sdk::components::wine;
use std::path::Path;
use crate::*;
use crate::i18n::*;
@ -58,6 +61,9 @@ pub enum AppMsg {
/// Needed for chained executions (e.g. update one voice after another)
perform_on_download_needed: bool,
/// Automatically start patch applying if possible and needed
apply_patch_if_needed: bool,
/// Show status gathering progress page
show_status_page: bool
},
@ -66,11 +72,13 @@ pub enum AppMsg {
/// was retrieved from the API
SetGameDiff(Option<VersionDiff>),
/// Supposed to be called automatically on app's run when the latest patch version
/// Supposed to be called automatically on app's run when the latest UnityPlayer patch version
/// was retrieved from remote repos
SetUnityPlayerPatch(Option<UnityPlayerPatch>),
// TODO: xlua patch status
/// Supposed to be called automatically on app's run when the latest xlua patch version
/// was retrieved from remote repos
SetXluaPatch(Option<XluaPatch>),
/// Supposed to be called automatically on app's run when the launcher state was chosen
SetLauncherState(Option<LauncherState>),
@ -357,17 +365,18 @@ impl SimpleComponent for App {
gtk::Button {
#[watch]
set_label: &match model.state {
Some(LauncherState::Launch) => tr("launch"),
Some(LauncherState::PredownloadAvailable { .. }) => tr("launch"),
Some(LauncherState::MainPatchAvailable(_)) => tr("apply-patch"),
Some(LauncherState::WineNotInstalled) => tr("download-wine"),
Some(LauncherState::PrefixNotExists) => tr("create-prefix"),
Some(LauncherState::VoiceUpdateAvailable(_)) => tr("update"),
Some(LauncherState::VoiceOutdated(_)) => tr("update"),
Some(LauncherState::VoiceNotInstalled(_)) => tr("download"),
Some(LauncherState::GameUpdateAvailable(_)) => tr("update"),
Some(LauncherState::GameOutdated(_)) => tr("update"),
Some(LauncherState::GameNotInstalled(_)) => tr("download"),
Some(LauncherState::Launch) => tr("launch"),
Some(LauncherState::PredownloadAvailable { .. }) => tr("launch"),
Some(LauncherState::UnityPlayerPatchAvailable(_)) => tr("apply-patch"),
Some(LauncherState::XluaPatchAvailable(_)) => tr("apply-patch"),
Some(LauncherState::WineNotInstalled) => tr("download-wine"),
Some(LauncherState::PrefixNotExists) => tr("create-prefix"),
Some(LauncherState::VoiceUpdateAvailable(_)) => tr("update"),
Some(LauncherState::VoiceOutdated(_)) => tr("update"),
Some(LauncherState::VoiceNotInstalled(_)) => tr("download"),
Some(LauncherState::GameUpdateAvailable(_)) => tr("update"),
Some(LauncherState::GameOutdated(_)) => tr("update"),
Some(LauncherState::GameNotInstalled(_)) => tr("download"),
None => String::from("...")
},
@ -377,7 +386,8 @@ impl SimpleComponent for App {
Some(LauncherState::GameOutdated { .. }) |
Some(LauncherState::VoiceOutdated(_)) => false,
Some(LauncherState::MainPatchAvailable(UnityPlayerPatch { status, .. })) => match status {
Some(LauncherState::UnityPlayerPatchAvailable(UnityPlayerPatch { status, .. })) |
Some(LauncherState::XluaPatchAvailable(XluaPatch { status, .. })) => match status {
PatchStatus::NotAvailable |
PatchStatus::Outdated { .. } |
PatchStatus::Preparation { .. } => false,
@ -396,7 +406,8 @@ impl SimpleComponent for App {
Some(LauncherState::GameOutdated { .. }) |
Some(LauncherState::VoiceOutdated(_)) => &["warning"],
Some(LauncherState::MainPatchAvailable(UnityPlayerPatch { status, .. })) => match status {
Some(LauncherState::UnityPlayerPatchAvailable(UnityPlayerPatch { status, .. })) |
Some(LauncherState::XluaPatchAvailable(XluaPatch { status, .. })) => match status {
PatchStatus::NotAvailable |
PatchStatus::Outdated { .. } |
PatchStatus::Preparation { .. } => &["error"],
@ -415,7 +426,8 @@ impl SimpleComponent for App {
Some(LauncherState::GameOutdated { .. }) |
Some(LauncherState::VoiceOutdated(_)) => tr("main-window--version-outdated-tooltip"),
Some(LauncherState::MainPatchAvailable(UnityPlayerPatch { status, .. })) => match status {
Some(LauncherState::UnityPlayerPatchAvailable(UnityPlayerPatch { status, .. })) |
Some(LauncherState::XluaPatchAvailable(XluaPatch { status, .. })) => match status {
PatchStatus::NotAvailable => tr("main-window--patch-unavailable-tooltip"),
PatchStatus::Outdated { .. } |
@ -713,8 +725,8 @@ impl SimpleComponent for App {
}
}
// Get main patch status
let main_patch = match patch.unity_player_patch() {
// Get main UnityPlayer patch status
sender.input(AppMsg::SetUnityPlayerPatch(match patch.unity_player_patch() {
Ok(patch) => Some(patch),
Err(err) => {
@ -727,15 +739,30 @@ impl SimpleComponent for App {
None
}
};
}));
sender.input(AppMsg::SetUnityPlayerPatch(main_patch));
// Get additional xlua patch status
sender.input(AppMsg::SetXluaPatch(match patch.xlua_patch() {
Ok(patch) => Some(patch),
Err(err) => {
tracing::error!("Failed to fetch xlua patch info: {err}");
sender.input(AppMsg::Toast {
title: tr("patch-info-fetching-error"),
description: Some(err.to_string())
});
None
}
}));
tracing::info!("Updated patch status");
// Update launcher state
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: true
});
@ -755,7 +782,7 @@ impl SimpleComponent for App {
match msg {
// TODO: make function from this message like with toast
AppMsg::UpdateLauncherState { perform_on_download_needed, show_status_page } => {
AppMsg::UpdateLauncherState { perform_on_download_needed, apply_patch_if_needed, show_status_page } => {
if show_status_page {
sender.input(AppMsg::SetLoadingStatus(Some(Some(tr("loading-launcher-state")))));
} else {
@ -802,19 +829,22 @@ impl SimpleComponent for App {
} else {
self.disabled_buttons = false;
}
if perform_on_download_needed {
if let Some(state) = state {
match state {
LauncherState::VoiceUpdateAvailable(_) |
LauncherState::VoiceNotInstalled(_) |
LauncherState::GameUpdateAvailable(_) |
LauncherState::GameNotInstalled(_) => {
sender.input(AppMsg::PerformAction);
}
_ => ()
if let Some(state) = state {
match state {
LauncherState::VoiceUpdateAvailable(_) |
LauncherState::VoiceNotInstalled(_) |
LauncherState::GameUpdateAvailable(_) |
LauncherState::GameNotInstalled(_) if perform_on_download_needed => {
sender.input(AppMsg::PerformAction);
}
LauncherState::UnityPlayerPatchAvailable(_) |
LauncherState::XluaPatchAvailable(_) if apply_patch_if_needed => {
sender.input(AppMsg::PerformAction);
}
_ => ()
}
}
}
@ -829,6 +859,11 @@ impl SimpleComponent for App {
PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::SetUnityPlayerPatch(patch));
}
#[allow(unused_must_use)]
AppMsg::SetXluaPatch(patch) => unsafe {
PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::SetXluaPatch(patch));
}
AppMsg::SetLauncherState(state) => {
self.state = state;
}
@ -853,149 +888,7 @@ impl SimpleComponent for App {
PREFERENCES_WINDOW.as_ref().unwrap_unchecked().widget().present();
}
#[allow(unused_must_use)]
AppMsg::RepairGame => {
let config = config::get().unwrap();
let progress_bar_input = self.progress_bar.sender().clone();
progress_bar_input.send(ProgressBarMsg::UpdateCaption(Some(tr("verifying-files"))));
self.downloading = true;
std::thread::spawn(move || {
match repairer::try_get_integrity_files(None) {
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(), None) {
files.append(&mut voiceover_files);
}
}
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(0, 0));
let mut total = 0;
for file in &files {
total += file.size;
}
let median_size = total / config.launcher.repairer.threads;
let mut i = 0;
let (verify_sender, verify_receiver) = std::sync::mpsc::channel();
for _ in 0..config.launcher.repairer.threads {
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 = verify_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 [config.launcher.repairer.threads] copies of this sender + the original one
// receiver will return Err when all the senders will be dropped.
// [config.launcher.repairer.threads] 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(verify_sender);
let mut broken = Vec::new();
let mut processed = 0;
while let Ok((file, status)) = verify_receiver.recv() {
processed += file.size;
if !status {
broken.push(file);
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(processed, total));
}
if !broken.is_empty() {
progress_bar_input.send(ProgressBarMsg::UpdateCaption(Some(tr("repairing-files"))));
progress_bar_input.send(ProgressBarMsg::UpdateProgress(0, 0));
tracing::warn!("Found broken files:\n{}", broken.iter().fold(String::new(), |acc, file| acc + &format!("- {}\n", file.path.to_string_lossy())));
let total = broken.len() as f64;
// TODO: properly handle xlua patch
let is_patch_applied = UnityPlayerPatch::from_folder(&config.patch.path).unwrap()
.is_applied(&config.game.path).unwrap();
tracing::debug!("Patch status: {}", is_patch_applied);
fn should_ignore(path: &Path) -> bool {
for part in ["UnityPlayer.dll", "xlua.dll", "crashreport.exe", "upload_crash.exe", "vulkan-1.dll"] {
if path.ends_with(part) {
return true;
}
}
false
}
for (i, file) in broken.into_iter().enumerate() {
if !is_patch_applied || !should_ignore(&file.path) {
tracing::debug!("Repairing: {}", file.path.to_string_lossy());
if let Err(err) = file.repair(&config.game.path) {
sender.input(AppMsg::Toast {
title: tr("game-file-repairing-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to repair game file: {err}");
}
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(i as u64, total as u64));
}
}
}
Err(err) => {
tracing::error!("Failed to get inregrity failes: {err}");
sender.input(AppMsg::Toast {
title: tr("integrity-files-getting-error"),
description: Some(err.to_string())
});
}
}
sender.input(AppMsg::SetDownloading(false));
});
}
AppMsg::RepairGame => repair_game::repair_game(sender, self.progress_bar.sender().to_owned()),
#[allow(unused_must_use)]
AppMsg::PredownloadUpdate => {
@ -1033,6 +926,7 @@ impl SimpleComponent for App {
sender.input(AppMsg::SetDownloading(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: true
});
});
@ -1041,260 +935,22 @@ impl SimpleComponent for App {
AppMsg::PerformAction => unsafe {
match self.state.as_ref().unwrap_unchecked() {
LauncherState::MainPatchAvailable(UnityPlayerPatch { status: PatchStatus::NotAvailable, .. }) |
LauncherState::UnityPlayerPatchAvailable(UnityPlayerPatch { status: PatchStatus::NotAvailable, .. }) |
LauncherState::XluaPatchAvailable(XluaPatch { status: PatchStatus::NotAvailable, .. }) |
LauncherState::PredownloadAvailable { .. } |
LauncherState::Launch => {
sender.input(AppMsg::HideWindow);
LauncherState::Launch => launch::launch(sender),
std::thread::spawn(move || {
if let Err(err) = anime_launcher_sdk::game::run() {
tracing::error!("Failed to launch game: {err}");
LauncherState::UnityPlayerPatchAvailable(patch) => apply_patch::apply_patch(sender, patch.to_owned()),
LauncherState::XluaPatchAvailable(patch) => apply_patch::apply_patch(sender, patch.to_owned()),
sender.input(AppMsg::Toast {
title: tr("game-launching-failed"),
description: Some(err.to_string())
});
}
LauncherState::WineNotInstalled => download_wine::download_wine(sender, self.progress_bar.sender().to_owned()),
sender.input(AppMsg::ShowWindow);
});
}
LauncherState::MainPatchAvailable(patch) => {
let patch = patch.to_owned();
match patch.status() {
PatchStatus::NotAvailable |
PatchStatus::Outdated { .. } |
PatchStatus::Preparation { .. } => unreachable!(),
PatchStatus::Testing { .. } |
PatchStatus::Available { .. } => {
self.disabled_buttons = true;
let config = config::get().unwrap();
std::thread::spawn(move || {
if let Err(err) = patch.apply(&config.game.path, config.patch.root) {
tracing::error!("Failed to patch the game");
sender.input(AppMsg::Toast {
title: tr("game-patching-error"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::DisableButtons(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
show_status_page: true
});
});
}
}
}
LauncherState::WineNotInstalled => {
let mut config = config::get().unwrap();
match wine::get_downloaded(&CONFIG.components.path, &config.game.wine.builds) {
Ok(downloaded) => {
// Select downloaded version
if !downloaded.is_empty() {
config.game.wine.selected = Some(downloaded[0].versions[0].name.clone());
config::update(config);
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
show_status_page: true
});
}
// Or download new one if none is available
else {
let latest = wine::Version::latest(&CONFIG.components.path).expect("Failed to get latest wine version");
// Choose selected wine version or use latest available one
let wine = match &config.game.wine.selected {
Some(version) => match wine::Version::find_in(&config.components.path, version) {
Ok(Some(version)) => version,
_ => latest
}
None => latest
};
// Download wine version
match Installer::new(wine.uri) {
Ok(mut installer) => {
if let Some(temp_folder) = &config.launcher.temp {
installer.temp_folder = temp_folder.to_path_buf();
}
installer.downloader
.set_downloading_speed(config.launcher.speed_limit)
.expect("Failed to set downloading speed limit");
let progress_bar_input = self.progress_bar.sender().clone();
self.downloading = true;
std::thread::spawn(clone!(@strong sender => move || {
#[allow(unused_must_use)]
installer.install(&config.game.wine.builds, clone!(@strong sender => move |state| {
match &state {
InstallerUpdate::DownloadingError(err) => {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
InstallerUpdate::UnpackingError(err) => {
tracing::error!("Unpacking failed: {err}");
sender.input(AppMsg::Toast {
title: tr("unpacking-failed"),
description: Some(err.clone())
});
}
_ => ()
}
progress_bar_input.send(ProgressBarMsg::UpdateFromState(state));
}));
config.game.wine.selected = Some(wine.name.clone());
config::update(config);
sender.input(AppMsg::SetDownloading(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
show_status_page: true
});
}));
}
Err(err) => self.toast(tr("wine-install-failed"), Some(err.to_string()))
}
}
}
Err(err) => self.toast(tr("downloaded-wine-list-failed"), Some(err.to_string()))
}
}
LauncherState::PrefixNotExists => {
let config = config::get().unwrap();
match config.get_selected_wine() {
Ok(Some(wine)) => {
sender.input(AppMsg::DisableButtons(true));
std::thread::spawn(move || {
let wine = wine
.to_wine(config.components.path, Some(config.game.wine.builds.join(&wine.name)))
.with_prefix(&config.game.wine.prefix)
.with_loader(WineLoader::Current)
.with_arch(WineArch::Win64);
if let Err(err) = wine.update_prefix::<&str>(None) {
tracing::error!("Failed to create wine prefix");
sender.input(AppMsg::Toast {
title: tr("wine-prefix-update-failed"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::DisableButtons(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: false,
show_status_page: true
});
});
}
Ok(None) => {
tracing::error!("Failed to get selected wine executable");
sender.input(AppMsg::Toast {
title: tr("failed-get-selected-wine"),
description: None
});
}
Err(err) => {
tracing::error!("Failed to get selected wine executable: {err}");
sender.input(AppMsg::Toast {
title: tr("failed-get-selected-wine"),
description: Some(err.to_string())
});
}
}
}
LauncherState::PrefixNotExists => create_prefix::create_prefix(sender),
LauncherState::VoiceUpdateAvailable(diff) |
LauncherState::VoiceNotInstalled(diff) |
LauncherState::GameUpdateAvailable(diff) |
LauncherState::GameNotInstalled(diff) => {
self.downloading = true;
let progress_bar_input = self.progress_bar.sender().clone();
// TODO: add speed limit
std::thread::spawn(clone!(@strong diff => move || {
let config = config::get().unwrap();
#[allow(unused_must_use)]
let result = diff.install_to_by(config.game.path, config.launcher.temp, clone!(@strong sender => move |state| {
match &state {
InstallerUpdate::DownloadingError(err) => {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
InstallerUpdate::UnpackingError(err) => {
tracing::error!("Unpacking failed: {err}");
sender.input(AppMsg::Toast {
title: tr("unpacking-failed"),
description: Some(err.clone())
});
}
_ => ()
}
progress_bar_input.send(ProgressBarMsg::UpdateFromState(state));
}));
if let Err(err) = result {
tracing::error!("Downloading failed: {err}");
sender.input(AppMsg::Toast {
title: tr("downloading-failed"),
description: Some(err.to_string())
});
}
sender.input(AppMsg::SetDownloading(false));
sender.input(AppMsg::UpdateLauncherState {
perform_on_download_needed: true,
show_status_page: false
});
}));
}
LauncherState::GameNotInstalled(diff) => download_diff::download_diff(sender, self.progress_bar.sender().to_owned(), diff.to_owned()),
LauncherState::VoiceOutdated(_) |
LauncherState::GameOutdated(_) => ()

154
src/ui/main/repair_game.rs Normal file
View file

@ -0,0 +1,154 @@
use relm4::{
prelude::*,
Sender
};
use std::path::Path;
use anime_launcher_sdk::config;
use crate::*;
use crate::i18n::*;
use crate::ui::components::*;
use super::{App, AppMsg};
#[allow(unused_must_use)]
pub fn repair_game(sender: ComponentSender<App>, progress_bar_input: Sender<ProgressBarMsg>) {
let config = config::get().unwrap();
progress_bar_input.send(ProgressBarMsg::UpdateCaption(Some(tr("verifying-files"))));
sender.input(AppMsg::SetDownloading(true));
std::thread::spawn(move || {
match repairer::try_get_integrity_files(None) {
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(), None) {
files.append(&mut voiceover_files);
}
}
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(0, 0));
let mut total = 0;
for file in &files {
total += file.size;
}
let median_size = total / config.launcher.repairer.threads;
let mut i = 0;
let (verify_sender, verify_receiver) = std::sync::mpsc::channel();
for _ in 0..config.launcher.repairer.threads {
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 = verify_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 [config.launcher.repairer.threads] copies of this sender + the original one
// receiver will return Err when all the senders will be dropped.
// [config.launcher.repairer.threads] 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(verify_sender);
let mut broken = Vec::new();
let mut processed = 0;
while let Ok((file, status)) = verify_receiver.recv() {
processed += file.size;
if !status {
broken.push(file);
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(processed, total));
}
if !broken.is_empty() {
progress_bar_input.send(ProgressBarMsg::UpdateCaption(Some(tr("repairing-files"))));
progress_bar_input.send(ProgressBarMsg::UpdateProgress(0, 0));
tracing::warn!("Found broken files:\n{}", broken.iter().fold(String::new(), |acc, file| acc + &format!("- {}\n", file.path.to_string_lossy())));
let total = broken.len() as f64;
// TODO: properly handle xlua patch
let is_patch_applied = UnityPlayerPatch::from_folder(&config.patch.path).unwrap()
.is_applied(&config.game.path).unwrap();
tracing::debug!("Patch status: {}", is_patch_applied);
fn should_ignore(path: &Path) -> bool {
for part in ["UnityPlayer.dll", "xlua.dll", "crashreport.exe", "upload_crash.exe", "vulkan-1.dll"] {
if path.ends_with(part) {
return true;
}
}
false
}
for (i, file) in broken.into_iter().enumerate() {
if !is_patch_applied || !should_ignore(&file.path) {
tracing::debug!("Repairing: {}", file.path.to_string_lossy());
if let Err(err) = file.repair(&config.game.path) {
sender.input(AppMsg::Toast {
title: tr("game-file-repairing-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to repair game file: {err}");
}
}
progress_bar_input.send(ProgressBarMsg::UpdateProgress(i as u64, total as u64));
}
}
}
Err(err) => {
tracing::error!("Failed to get inregrity failes: {err}");
sender.input(AppMsg::Toast {
title: tr("integrity-files-getting-error"),
description: Some(err.to_string())
});
}
}
sender.input(AppMsg::SetDownloading(false));
});
}

View file

@ -107,6 +107,7 @@ pub struct GeneralApp {
game_diff: Option<VersionDiff>,
unity_player_patch: Option<UnityPlayerPatch>,
xlua_patch: Option<XluaPatch>,
style: LauncherStyle,
@ -129,10 +130,14 @@ pub enum GeneralAppMsg {
/// was retrieved from the API
SetGameDiff(Option<VersionDiff>),
/// Supposed to be called automatically on app's run when the latest patch version
/// Supposed to be called automatically on app's run when the latest UnityPlayer patch version
/// was retrieved from remote repos
SetUnityPlayerPatch(Option<UnityPlayerPatch>),
/// Supposed to be called automatically on app's run when the latest xlua patch version
/// was retrieved from remote repos
SetXluaPatch(Option<XluaPatch>),
// If one ever wich to change it to accept VoiceLocale
// I'd recommend to use clone!(@strong self.locale as locale => move |_| { .. })
// in the VoicePackage component
@ -368,7 +373,8 @@ impl SimpleAsyncComponent for GeneralApp {
},
adw::ActionRow {
set_title: &tr("patch-version"),
set_title: &tr("player-patch-version"),
set_subtitle: &tr("player-patch-version-description"),
add_suffix = &gtk::Label {
#[watch]
@ -417,7 +423,7 @@ impl SimpleAsyncComponent for GeneralApp {
if let Ok(true) = model.unity_player_patch.as_ref().unwrap_unchecked().is_applied(&CONFIG.game.path) {
String::new()
} else {
tr("patch-testing-tooltip")
tr("patch-not-applied-tooltip")
}
}
}
@ -425,6 +431,113 @@ impl SimpleAsyncComponent for GeneralApp {
None => String::new()
})
}
},
adw::ActionRow {
set_title: &tr("xlua-patch-version"),
set_subtitle: &tr("xlua-patch-version-description"),
add_suffix = &gtk::Label {
#[watch]
set_text: &match model.xlua_patch.as_ref() {
Some(patch) => match patch.status() {
PatchStatus::NotAvailable => tr("patch-not-available"),
PatchStatus::Outdated { current, .. } => tr_args("patch-outdated", [("current", current.to_string().into())]),
PatchStatus::Preparation { .. } => tr("patch-preparation"),
PatchStatus::Testing { version, .. } |
PatchStatus::Available { version, .. } => version.to_string()
}
None => String::from("?")
},
#[watch]
set_css_classes: match model.xlua_patch.as_ref() {
Some(patch) => match patch.status() {
PatchStatus::NotAvailable => &["error"],
PatchStatus::Outdated { .. } |
PatchStatus::Preparation { .. } |
PatchStatus::Testing { .. } => &["warning"],
PatchStatus::Available { .. } => unsafe {
if let Ok(true) = model.xlua_patch.as_ref().unwrap_unchecked().is_applied(&CONFIG.game.path) {
&["success"]
} else {
&["warning"]
}
}
}
None => &[]
},
#[watch]
set_tooltip_text: Some(&match model.xlua_patch.as_ref() {
Some(patch) => match patch.status() {
PatchStatus::NotAvailable => tr("patch-not-available-tooltip"),
PatchStatus::Outdated { current, latest, .. } => tr_args("patch-outdated-tooltip", [
("current", current.to_string().into()),
("latest", latest.to_string().into())
]),
PatchStatus::Preparation { .. } => tr("patch-preparation-tooltip"),
PatchStatus::Testing { .. } => tr("patch-testing-tooltip"),
PatchStatus::Available { .. } => unsafe {
if let Ok(true) = model.xlua_patch.as_ref().unwrap_unchecked().is_applied(&CONFIG.game.path) {
String::new()
} else {
tr("patch-not-applied-tooltip")
}
}
}
None => String::new()
})
}
}
},
add = &adw::PreferencesGroup {
adw::ActionRow {
set_title: &tr("apply-xlua-patch"),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center,
set_state: CONFIG.patch.apply_xlua,
connect_state_notify[sender] => move |switch| {
if is_ready() {
#[allow(unused_must_use)]
if let Ok(mut config) = config::get() {
config.patch.apply_xlua = switch.state();
config::update(config);
sender.output(PreferencesAppMsg::UpdateLauncherState);
}
}
}
}
},
adw::ActionRow {
set_title: &tr("ask-superuser-permissions"),
set_subtitle: &tr("ask-superuser-permissions-description"),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center,
set_state: CONFIG.patch.root,
connect_state_notify => |switch| {
if is_ready() {
if let Ok(mut config) = config::get() {
config.patch.root = switch.state();
config::update(config);
}
}
}
}
}
},
@ -615,6 +728,7 @@ impl SimpleAsyncComponent for GeneralApp {
game_diff: None,
unity_player_patch: None,
xlua_patch: None,
style: CONFIG.launcher.style,
@ -665,6 +779,10 @@ impl SimpleAsyncComponent for GeneralApp {
self.unity_player_patch = patch;
}
GeneralAppMsg::SetXluaPatch(patch) => {
self.xlua_patch = patch;
}
#[allow(unused_must_use)]
GeneralAppMsg::AddVoicePackage(index) => {
if let Some(package) = self.voice_packages.get(index.current_index()) {

View file

@ -28,10 +28,14 @@ pub enum PreferencesAppMsg {
/// was retrieved from the API
SetGameDiff(Option<VersionDiff>),
/// Supposed to be called automatically on app's run when the latest patch version
/// Supposed to be called automatically on app's run when the latest UnityPlayer patch version
/// was retrieved from remote repos
SetUnityPlayerPatch(Option<UnityPlayerPatch>),
/// Supposed to be called automatically on app's run when the latest xlua patch version
/// was retrieved from remote repos
SetXluaPatch(Option<XluaPatch>),
SetLauncherStyle(LauncherStyle),
UpdateLauncherState,
@ -129,6 +133,11 @@ impl SimpleAsyncComponent for PreferencesApp {
self.general.sender().send(GeneralAppMsg::SetUnityPlayerPatch(patch));
}
#[allow(unused_must_use)]
PreferencesAppMsg::SetXluaPatch(patch) => {
self.general.sender().send(GeneralAppMsg::SetXluaPatch(patch));
}
#[allow(unused_must_use)]
PreferencesAppMsg::SetLauncherStyle(style) => {
sender.output(Self::Output::SetLauncherStyle(style));
@ -138,6 +147,7 @@ impl SimpleAsyncComponent for PreferencesApp {
PreferencesAppMsg::UpdateLauncherState => {
sender.output(Self::Output::UpdateLauncherState {
perform_on_download_needed: false,
apply_patch_if_needed: false,
show_status_page: false
});
}