Files
STARTLINER/rust/src/cmd.rs
2025-04-21 22:05:37 +00:00

554 lines
16 KiB
Rust

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<AppData>>) -> Result<Vec<StartCheckError>, 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::<Mutex<AppData>>();
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.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<AppData>>,
key: PkgKey,
force: bool
) -> Result<InstallResult, String> {
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<AppData>>, 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<AppData>>, key: PkgKey) -> Result<Package, String> {
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<AppData>>, 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<AppData>>) -> 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<AppData>>) -> Result<HashMap<PkgKey, Package>, ()> {
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<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> {
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<AppData>>) -> 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<Vec<ProfileMeta>, 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<AppData>>,
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<AppData>>, 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<AppData>>,
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<AppData>>, 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<AppData>>) -> Result<Option<Profile>, ()> {
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<AppData>>, 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<AppData>>) -> 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<AppData>>, 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<AppData>>, export_keychip: bool, files: Vec<String>) -> 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<Vec<String>, ()> {
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<AppData>>, field: GlobalConfigField) -> Result<bool, ()> {
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<AppData>>, 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<Vec<(String, String)>, 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<Vec<String>, ()> {
log::debug!("invoke: list_displays");
Ok(Vec::new())
}
#[tauri::command]
pub async fn list_directories() -> Result<util::Dirs, ()> {
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<bool, ()> {
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<String, ()> {
Ok(include_str!("../../CHANGELOG.md").to_owned())
}
#[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, 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<AppData>>, target: String) -> Result<Vec<Patch>, 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)
}