use ini::Ini; use log; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; use tokio::sync::Mutex; use tokio::fs; use tauri::{AppHandle, Manager, State}; use crate::model::config::GlobalConfigField; use crate::model::misc::Game; use crate::model::patch::Patch; use crate::modules::package::prepare_dlls; use crate::pkg::{Package, PkgKey}; use crate::pkg_store::{InstallResult, PackageStore}; use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload}; use crate::appdata::{AppData, ToggleAction}; use crate::model::misc::StartCheckError; use crate::util; #[tauri::command] pub async fn start_check(state: State<'_, Mutex>) -> Result, String> { log::debug!("invoke: start_check"); let appd = state.lock().await; let prf = appd.profile.as_ref().ok_or_else(|| format!("No profile to check"))?; let pkgs = appd.pkgs.get_all(); let mut res = Vec::new(); for key in prf.mod_pkgs() { if let Some(pkg) = pkgs.get(key) { if let Some(loc) = &pkg.loc { for dep in &loc.dependencies { if !prf.mod_pkgs().contains(dep) { res.push(StartCheckError::MissingDependency(key.clone(), dep.clone())); } } } else { res.push(StartCheckError::MissingLocalPackage(key.clone())); } } else { res.push(StartCheckError::MissingRemotePackage(key.clone())); } } for key in prf.special_pkgs() { log::debug!("special: {}", key); if let Some(pkg) = pkgs.get(&key) { if pkg.loc.is_some() { continue; } } res.push(StartCheckError::MissingTool(key)); } Ok(res) } #[tauri::command] pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> { log::debug!("invoke: startline"); let state = app.state::>(); let mut hash = "".to_owned(); let appd = state.lock().await; let mut game_dlls = Vec::new(); let mut amd_dlls = Vec::new(); if let Some(p) = &appd.profile { hash = appd.sum_packages(p); (game_dlls, amd_dlls) = prepare_dlls(p.meta.game, p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())? } if let Some(p) = &appd.profile { log::debug!("{}", hash); let patches_enabled = appd.patches_enabled( &p.data.sgt.target, &p.data.sgt.target.parent().unwrap().join("amdaemon.exe") ).map_err(|e| e.to_string())?; let info = p.prepare_display() .map_err(|e| e.to_string())?; let lineup_res = p.line_up(hash, refresh, patches_enabled).await .map_err(|e| e.to_string()); #[cfg(target_os = "windows")] if let Some(info) = info { use crate::model::profile::Display; if lineup_res.is_ok() { Display::wait_for_exit(app.clone(), info); } else { Display::clean_up(&info).map_err(|e| e.to_string())?; } } lineup_res?; let app_clone = app.clone(); let p_clone = p.clone(); tauri::async_runtime::spawn(async move { if let Err(e) = p_clone.start(StartPayload { app: app_clone, game_dlls, amd_dlls }).await { log::error!("Startup failed:\n{}", e); } }); Ok(()) } else { Err("No profile".to_owned()) } } #[tauri::command] pub async fn kill() -> Result<(), String> { util::pkill("amdaemon.exe").await; // The start routine will kill the other process Ok(()) } #[tauri::command] pub async fn install_package( state: State<'_, tokio::sync::Mutex>, key: PkgKey, force: bool ) -> Result { log::debug!("invoke: install_package({})", key); let mut appd = state.lock().await; appd.pkgs.install_package(&key, force, true) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn delete_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result<(), String> { log::debug!("invoke: delete_package({})", key); let mut appd = state.lock().await; appd.pkgs.delete_package(&key, true) .await .map_err(|e| e.to_string())?; appd.toggle_package(key, ToggleAction::Disable) .map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result { log::debug!("invoke: get_package({})", key); let appd = state.lock().await; appd.pkgs.get(&key) .map_err(|e| e.to_string()) .cloned() } #[tauri::command] pub async fn toggle_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey, enable: bool) -> Result<(), String> { log::debug!("invoke: toggle_package({}, {})", key, enable); let mut appd = state.lock().await; appd.toggle_package(key, if enable { ToggleAction::EnableRecursive } else { ToggleAction::Disable }) .map_err(|e| e.to_string()) } #[tauri::command] pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex>) -> Result<(), String> { log::debug!("invoke: reload_all_packages"); let mut appd = state.lock().await; appd.pkgs.reload_all() .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_all_packages(state: State<'_, Mutex>) -> Result, ()> { log::debug!("invoke: get_all_packages"); let appd = state.lock().await; let pkgs_all = appd.pkgs.get_all(); log::debug!("pkgs_all: {:?}", pkgs_all); Ok(appd.pkgs.get_all()) } #[tauri::command] pub async fn get_game_packages(state: State<'_, Mutex>, game: Game) -> Result, ()> { log::debug!("invoke: get_game_packages {game}"); let appd = state.lock().await; Ok(appd.pkgs.get_game_list(game)) } #[tauri::command] pub async fn fetch_listings(state: State<'_, Mutex>) -> Result<(), String> { log::debug!("invoke: fetch_listings"); // let game; { let appd = state.lock().await; if !appd.pkgs.is_offline() { log::warn!("fetch_listings: already done"); return Ok(()); } if appd.cfg.offline_mode { log::info!("fetch_listings: skipped"); return Err("offline mode".to_owned()); } // if let Some(profile) = &appd.profile { // game = profile.meta.game; // } else { // return Err("No profile".to_owned()); // } } // Can be this lazy for now as there are only two short lists let listings1 = PackageStore::fetch_listings(Game::Ongeki).await .map_err(|e| e.to_string())?; let listings2 = PackageStore::fetch_listings(Game::Chunithm).await .map_err(|e| e.to_string())?; let mut appd = state.lock().await; appd.pkgs.process_fetched_listings(listings1, Game::Ongeki); appd.pkgs.process_fetched_listings(listings2, Game::Chunithm); appd.pkgs.save().await .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn list_profiles() -> Result, String> { log::debug!("invoke: list_profiles"); let list = crate::profiles::list_profiles().await.map_err(|e| e.to_string())?; Ok(list) } #[tauri::command] pub async fn init_profile( state: State<'_, Mutex>, game: Game, name: String ) -> Result<(), String> { log::debug!("invoke: init_profile({}, {})", game, name); let mut appd = state.lock().await; let new_profile = Profile::new(ProfileMeta { game, name }) .map_err(|e| format!("Unable to create profile: {}", e))?; appd.profile = Some(new_profile); Ok(()) } #[tauri::command] pub async fn load_profile(state: State<'_, Mutex>, game: Game, name: String) -> Result<(), String> { log::debug!("invoke: load_profile({} {:?})", game, name); let mut appd = state.lock().await; appd.switch_profile(game, name).map_err(|e| e.to_string())?; appd.fix(); Ok(()) } #[tauri::command] pub async fn rename_profile( state: State<'_, Mutex>, profile: ProfileMeta, name: String ) -> Result<(), String> { log::debug!("invoke: rename_profile({:?} {:?})", profile, name); let new_meta = ProfileMeta { game: profile.game.clone(), name: profiles::fixed_name(&ProfileMeta { game: profile.game.clone(), name }, false) }; if new_meta.name == profile.name { return Ok(()); } if new_meta.config_dir().exists() { return Err(format!("Profile {} already exists", &new_meta.name)); } fs::rename(profile.config_dir(), new_meta.config_dir()).await .map_err(|e| format!("Unable to rename: {}", e))?; if let Err(e) = fs::rename(profile.data_dir(), new_meta.data_dir()).await { log::warn!("Unable to move data dir {}->{}: {}", &profile.name, &new_meta.name, e); } let mut appd = state.lock().await; if let Some(current) = &mut appd.profile { if current.meta == profile { current.rename(new_meta.name); } } Ok(()) } #[tauri::command] pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> { log::debug!("invoke: duplicate_profile({:?})", profile); let new_meta = ProfileMeta { game: profile.game.clone(), name: profiles::fixed_name(&profile, true) }; util::copy_directory(profile.config_dir(), new_meta.config_dir(), false) .map_err(|e| format!("Unable to duplicate: {}", e))?; Ok(()) } #[tauri::command] pub async fn delete_profile(state: State<'_, Mutex>, profile: ProfileMeta) -> Result<(), String> { log::debug!("invoke: delete_profile({:?})", profile); util::remove_dir_all(profile.config_dir()) .await .map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?; if let Err(e) = util::remove_dir_all(profile.data_dir()).await { log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e); } let mut appd = state.lock().await; if let Some(current) = &mut appd.profile { if current.meta == profile { appd.profile = None; } } Ok(()) } #[tauri::command] pub async fn get_current_profile(state: State<'_, Mutex>) -> Result, ()> { log::debug!("invoke: get_current_profile"); let appd = state.lock().await; Ok(appd.profile.clone()) } #[tauri::command] pub async fn sync_current_profile(state: State<'_, Mutex>, data: ProfileData) -> Result<(), String> { log::debug!("invoke: sync_current_profile {:?}", data); let mut appd = state.lock().await; if let Some(p) = &mut appd.profile { p.sync(data); } Ok(()) } #[tauri::command] pub async fn save_current_profile(state: State<'_, Mutex>) -> Result<(), String> { log::debug!("invoke: save_current_profile"); let mut appd = state.lock().await; appd.fix(); match &mut appd.profile { Some(p) => { p.save().map_err(|e| e.to_string()) }, None => { Err("no profile to save".to_owned()) } } } #[tauri::command] pub async fn load_segatools_ini(state: State<'_, Mutex>, path: PathBuf) -> Result<(), String> { log::debug!("invoke: load_segatools_ini({:?})", path); let mut appd = state.lock().await; if let Some(p) = &mut appd.profile { let str = std::fs::read_to_string(path).map_err(|e| e.to_string())?; // Stupid path escape hack for the ini reader let str = str.replace("\\", "\\\\").replace("\\\\\\\\", "\\\\"); let ini = Ini::load_from_str(&str).map_err(|e| e.to_string())?; p.data.sgt.load_from_ini(&ini, p.config_dir()).map_err(|e| e.to_string())?; p.data.network.load_from_ini(&ini).map_err(|e| e.to_string())?; if let Some(kb) = &mut p.data.keyboard { kb.load_from_ini(&ini).map_err(|e| e.to_string())?; } p.save().map_err(|e| e.to_string())?; } Ok(()) } #[tauri::command] pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> { log::debug!("invoke: create_shortcut({:?})", profile_meta); util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string()) } #[tauri::command] pub async fn export_profile(state: State<'_, Mutex>, export_keychip: bool, files: Vec) -> Result<(), String> { log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len()); let appd = state.lock().await; match &appd.profile { Some(p) => { p.export(export_keychip, files) .map_err(|e| e.to_string())?; } None => { let err = "export_profile: no profile".to_owned(); log::error!("{}", err); return Err(err); } } Ok(()) } #[tauri::command] pub async fn import_profile(path: PathBuf) -> Result<(), String> { log::debug!("invoke: import_profile({:?})", path); Profile::import(path).map_err(|e| e.to_string()) } #[tauri::command] pub async fn list_platform_capabilities() -> Result, ()> { log::debug!("invoke: list_platform_capabilities"); #[cfg(target_os = "windows")] return Ok(vec!["display".to_owned()]); #[cfg(target_os = "linux")] return Ok(vec!["wine".to_owned()]); } #[tauri::command] pub async fn get_global_config(state: State<'_, Mutex>, field: GlobalConfigField) -> Result { log::debug!("invoke: get_global_config({field:?})"); let appd = state.lock().await; match field { GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode), GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates), GlobalConfigField::Verbose => Ok(appd.cfg.verbose) } } #[tauri::command] pub async fn set_global_config(state: State<'_, Mutex>, field: GlobalConfigField, value: bool) -> Result<(), String> { log::debug!("invoke: set_global_config({field:?}, {value})"); let mut appd = state.lock().await; match field { GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value, GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value, GlobalConfigField::Verbose => appd.cfg.verbose = value, }; appd.write().map_err(|e| e.to_string()) } #[tauri::command] #[cfg(target_os = "windows")] pub async fn list_displays() -> Result, String> { use winsafe::prelude::NativeBitflag; log::debug!("invoke: list_displays"); let mut res = Vec::new(); for displ_dev in winsafe::EnumDisplayDevices(None, None) { if let Ok(displ_dev) = displ_dev { if displ_dev.StateFlags.has(winsafe::co::DISPLAY_DEVICE::ATTACHED_TO_DESKTOP) { res.push((displ_dev.DeviceName(), displ_dev.DeviceString())); } } else { break; } } Ok(res) } #[tauri::command] #[cfg(not(target_os = "windows"))] pub async fn list_displays() -> Result, ()> { log::debug!("invoke: list_displays"); Ok(Vec::new()) } #[tauri::command] pub async fn list_directories() -> Result { log::debug!("invoke: list_directores"); Ok(util::all_dirs().clone()) } // Tauri fs api is useless #[tauri::command] pub async fn file_exists(path: String) -> Result { Ok(std::fs::exists(path).unwrap_or(false)) } // Easier than trying to get the barely-documented tauri permissions system to work #[tauri::command] pub async fn open_file(path: String) -> Result<(), String> { open::that(path).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn get_changelog() -> Result { Ok(include_str!("../../CHANGELOG.md").to_owned()) } #[tauri::command] pub async fn list_com_ports() -> Result, String> { let ports = serialport::available_ports().unwrap_or(Vec::new()); let mut res = BTreeMap::new(); for p in ports { log::debug!("port {}", p.port_name); if p.port_name.starts_with("COM") { if let Ok(parsed) = (p.port_name[3..]).parse() { res.insert(p.port_name, parsed); } } } Ok(res) } #[tauri::command] pub async fn list_patches(state: State<'_, Mutex>, target: String) -> Result, String> { log::debug!("invoke: list_patches({})", target); let mut appd = state.lock().await; appd.fix(); let list = appd.patch_vec.find_patches(target).map_err(|e| e.to_string())?; Ok(list) }