From 8d55e92fc99a63df0e3ec1a95dd93c4e322fe594 Mon Sep 17 00:00:00 2001 From: akanyan Date: Sun, 16 Mar 2025 17:55:38 +0000 Subject: [PATCH] feat: start checks --- TODO.md | 1 - rust/src/appdata.rs | 28 ++++---- rust/src/cmd.rs | 60 +++++++++++++++- rust/src/lib.rs | 11 +-- rust/src/model/config.rs | 104 ++-------------------------- rust/src/model/misc.rs | 9 +++ rust/src/model/mod.rs | 1 + rust/src/model/profile.rs | 102 +++++++++++++++++++++++++++ rust/src/modules/bepinex.rs | 2 +- rust/src/modules/display_windows.rs | 2 +- rust/src/modules/network.rs | 9 +-- rust/src/modules/segatools.rs | 2 +- rust/src/pkg_store.rs | 28 ++++---- rust/src/profiles/mod.rs | 29 ++++++-- rust/src/profiles/ongeki.rs | 8 +-- src/components/App.vue | 14 +++- src/components/LinkButton.vue | 23 ++++++ src/components/ModList.vue | 39 +++++++++-- src/components/ModListEntry.vue | 21 ++---- src/components/ModStore.vue | 12 ++-- src/components/ModStoreEntry.vue | 27 +++----- src/components/ModTitlecard.vue | 21 +++++- src/components/OptionList.vue | 4 +- src/components/StartButton.vue | 81 +++++++++++++++++++++- src/main.ts | 2 + src/stores.ts | 27 +++++--- 26 files changed, 456 insertions(+), 211 deletions(-) create mode 100644 rust/src/model/profile.rs create mode 100644 src/components/LinkButton.vue diff --git a/TODO.md b/TODO.md index bd33db9..813c713 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ ### Short-term - CHUNITHM support -- Start checks - https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63 ### Long-term diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index b4d3c66..611ea8f 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -1,19 +1,14 @@ use std::hash::{DefaultHasher, Hash, Hasher}; +use crate::model::config::GlobalConfig; use crate::profiles::AnyProfile; use crate::{model::misc::Game, pkg::PkgKey}; use crate::pkg_store::PackageStore; use crate::util; use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; use tauri::AppHandle; -#[derive(Serialize, Deserialize, Clone, Default)] -pub struct GlobalConfig { - pub recent_profile: Option<(Game, String)> -} - pub struct GlobalState { - pub remain_open: bool + pub remain_open: bool, } pub struct AppData { @@ -34,6 +29,8 @@ impl AppData { None => None }; + log::debug!("Recent profile: {:?}", profile); + AppData { profile: profile, pkgs: PackageStore::new(apph.clone()), @@ -43,7 +40,7 @@ impl AppData { } pub fn write(&self) -> Result<(), std::io::Error> { - std::fs::write(util::config_dir().join("config.json"), serde_json::to_string(&self.cfg)?) + std::fs::write(util::config_dir().join("config.json"), serde_json::to_string_pretty(&self.cfg)?) } pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> { @@ -72,12 +69,12 @@ impl AppData { let loc = pkg.loc .clone() .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; - profile.pkgs_mut().insert(key); + profile.mod_pkgs_mut().insert(key); for d in &loc.dependencies { _ = self.toggle_package(d.clone(), true); } } else { - profile.pkgs_mut().remove(&key); + profile.mod_pkgs_mut().remove(&key); for (ckey, pkg) in self.pkgs.get_all() { if let Some(loc) = pkg.loc { if loc.dependencies.contains(&key) { @@ -92,10 +89,13 @@ impl AppData { pub fn sum_packages(&self, p: &AnyProfile) -> String { let mut hasher = DefaultHasher::new(); - for pkg in p.pkgs().into_iter() { - let x = self.pkgs.get(&pkg).unwrap().loc.as_ref().unwrap(); - pkg.hash(&mut hasher); - x.version.hash(&mut hasher); + for key in p.mod_pkgs().into_iter() { + if let Ok(pkg) = self.pkgs.get(&key) { + if let Some(loc) = &pkg.loc { + key.hash(&mut hasher); + loc.version.hash(&mut hasher); + } + } } hasher.finish().to_string() } diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index cbc04ab..5c80627 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -5,12 +5,50 @@ use tokio::fs; use tauri::{AppHandle, Manager, State}; use crate::model::misc::Game; use crate::pkg::{Package, PkgKey}; -use crate::pkg_store::InstallResult; +use crate::pkg_store::{InstallResult, PackageStore}; use crate::profiles::ongeki::OngekiProfile; use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths}; use crate::appdata::AppData; +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) -> Result<(), String> { log::debug!("invoke: startline"); @@ -110,9 +148,25 @@ pub async fn get_all_packages(state: State<'_, Mutex>) -> Result>) -> Result<(), String> { log::debug!("invoke: fetch_listings"); + { + 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()); + } + } + + let listings = PackageStore::fetch_listings().await + .map_err(|e| e.to_string())?; + let mut appd = state.lock().await; - appd.pkgs.fetch_listings().await - .map_err(|e| e.to_string()) + appd.pkgs.process_fetched_listings(listings); + + Ok(()) } #[tauri::command] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index aa435d2..4ff29fa 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -144,6 +144,10 @@ pub async fn run(_args: Vec) { Ok(()) }) .invoke_handler(tauri::generate_handler![ + cmd::start_check, + cmd::startline, + cmd::kill, + cmd::get_package, cmd::get_all_packages, cmd::reload_all_packages, @@ -161,9 +165,6 @@ pub async fn run(_args: Vec) { cmd::get_current_profile, cmd::save_current_profile, - cmd::startline, - cmd::kill, - cmd::list_displays, cmd::list_platform_capabilities, cmd::list_directories, @@ -189,8 +190,8 @@ fn deep_link(app: AppHandle, args: Vec) { tauri::async_runtime::spawn(async move { let mutex = app.state::>(); let mut appd = mutex.lock().await; - if let Err(e) = appd.pkgs.fetch_listings().await { - log::warn!("Deep link fetch failed: {:?}", e); + if appd.pkgs.is_offline() { + log::warn!("Deep link installation failed: offline"); } else if let Err(e) = appd.pkgs.install_package(&key, true, true).await { log::warn!("Deep link installation failed: {}", e.to_string()); } diff --git a/rust/src/model/config.rs b/rust/src/model/config.rs index 66cb7e1..fee3d42 100644 --- a/rust/src/model/config.rs +++ b/rust/src/model/config.rs @@ -1,102 +1,10 @@ -use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::pkg::PkgKey; +use super::misc::Game; -#[derive(Deserialize, Serialize, Clone)] -pub struct Segatools { - pub target: PathBuf, - pub hook: Option, - pub io: Option, - pub amfs: PathBuf, - pub option: PathBuf, - pub appdata: PathBuf, - pub enable_aime: bool, - pub intel: bool, -} +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct GlobalConfig { + pub recent_profile: Option<(Game, String)>, -impl Default for Segatools { - fn default() -> Self { - Segatools { - target: PathBuf::default(), - hook: Some(PkgKey("segatools-mu3hook".to_owned())), - io: None, - amfs: PathBuf::default(), - option: PathBuf::default(), - appdata: PathBuf::from("appdata"), - enable_aime: false, - intel: false - } - } -} - -#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] -pub enum DisplayMode { - Window, - #[default] Borderless, - Fullscreen -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Display { - pub target: String, - pub rez: (i32, i32), - pub mode: DisplayMode, - pub rotation: i32, - pub frequency: i32, - pub borderless_fullscreen: bool, -} - -impl Default for Display { - fn default() -> Self { - Display { - target: "default".to_owned(), - rez: (1080, 1920), - mode: DisplayMode::Borderless, - rotation: 0, - frequency: 60, - borderless_fullscreen: true, - } - } -} - -#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] -pub enum NetworkType { - #[default] Remote, - Artemis, -} - -#[derive(Deserialize, Serialize, Clone, Default)] -pub struct Network { - pub network_type: NetworkType, - - pub local_path: PathBuf, - pub local_console: bool, - - pub remote_address: String, - pub keychip: String, - - pub subnet: String, - pub suffix: Option, -} - -#[derive(Deserialize, Serialize, Clone, Default)] -pub struct BepInEx { - pub console: bool, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Wine { - pub runtime: PathBuf, - pub prefix: PathBuf, -} - -impl Default for Wine { - fn default() -> Self { - Wine { - runtime: PathBuf::from("/usr/bin/wine"), - prefix: std::env::var("HOME") - .and_then(|home| Ok(PathBuf::from(home).join(".wine"))) - .unwrap_or_default() - } - } + #[serde(default)] + pub offline_mode: bool, } \ No newline at end of file diff --git a/rust/src/model/misc.rs b/rust/src/model/misc.rs index 144496c..2cfdfe4 100644 --- a/rust/src/model/misc.rs +++ b/rust/src/model/misc.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use crate::pkg::PkgKey; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub enum Game { @@ -26,3 +27,11 @@ impl std::fmt::Display for Game { } } } + +#[derive(Serialize, Deserialize)] +pub enum StartCheckError { + MissingRemotePackage(PkgKey), + MissingLocalPackage(PkgKey), + MissingDependency(PkgKey, PkgKey), + MissingTool(PkgKey), +} \ No newline at end of file diff --git a/rust/src/model/mod.rs b/rust/src/model/mod.rs index 1fbc3f8..98d9a10 100644 --- a/rust/src/model/mod.rs +++ b/rust/src/model/mod.rs @@ -1,5 +1,6 @@ pub mod local; pub mod misc; pub mod rainy; +pub mod profile; pub mod config; pub mod segatools_base; \ No newline at end of file diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs new file mode 100644 index 0000000..66cb7e1 --- /dev/null +++ b/rust/src/model/profile.rs @@ -0,0 +1,102 @@ +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use crate::pkg::PkgKey; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Segatools { + pub target: PathBuf, + pub hook: Option, + pub io: Option, + pub amfs: PathBuf, + pub option: PathBuf, + pub appdata: PathBuf, + pub enable_aime: bool, + pub intel: bool, +} + +impl Default for Segatools { + fn default() -> Self { + Segatools { + target: PathBuf::default(), + hook: Some(PkgKey("segatools-mu3hook".to_owned())), + io: None, + amfs: PathBuf::default(), + option: PathBuf::default(), + appdata: PathBuf::from("appdata"), + enable_aime: false, + intel: false + } + } +} + +#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +pub enum DisplayMode { + Window, + #[default] Borderless, + Fullscreen +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Display { + pub target: String, + pub rez: (i32, i32), + pub mode: DisplayMode, + pub rotation: i32, + pub frequency: i32, + pub borderless_fullscreen: bool, +} + +impl Default for Display { + fn default() -> Self { + Display { + target: "default".to_owned(), + rez: (1080, 1920), + mode: DisplayMode::Borderless, + rotation: 0, + frequency: 60, + borderless_fullscreen: true, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +pub enum NetworkType { + #[default] Remote, + Artemis, +} + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct Network { + pub network_type: NetworkType, + + pub local_path: PathBuf, + pub local_console: bool, + + pub remote_address: String, + pub keychip: String, + + pub subnet: String, + pub suffix: Option, +} + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct BepInEx { + pub console: bool, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Wine { + pub runtime: PathBuf, + pub prefix: PathBuf, +} + +impl Default for Wine { + fn default() -> Self { + Wine { + runtime: PathBuf::from("/usr/bin/wine"), + prefix: std::env::var("HOME") + .and_then(|home| Ok(PathBuf::from(home).join(".wine"))) + .unwrap_or_default() + } + } +} \ No newline at end of file diff --git a/rust/src/modules/bepinex.rs b/rust/src/modules/bepinex.rs index fd1790a..b3e0cc0 100644 --- a/rust/src/modules/bepinex.rs +++ b/rust/src/modules/bepinex.rs @@ -1,6 +1,6 @@ use anyhow::Result; use ini::Ini; -use crate::{model::config::BepInEx, profiles::ProfilePaths}; +use crate::{model::profile::BepInEx, profiles::ProfilePaths}; impl BepInEx { pub fn line_up(&self, p: &impl ProfilePaths) -> Result<()> { diff --git a/rust/src/modules/display_windows.rs b/rust/src/modules/display_windows.rs index ec0af80..3f01c6d 100644 --- a/rust/src/modules/display_windows.rs +++ b/rust/src/modules/display_windows.rs @@ -1,5 +1,5 @@ -use crate::model::config::{Display, DisplayMode}; +use crate::model::profile::{Display, DisplayMode}; use anyhow::Result; use displayz::{query_displays, DisplaySet}; use tauri::{AppHandle, Listener}; diff --git a/rust/src/modules/network.rs b/rust/src/modules/network.rs index 1b4ab9a..7540af8 100644 --- a/rust/src/modules/network.rs +++ b/rust/src/modules/network.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, process::Command}; use yaml_rust2::YamlLoader; use anyhow::{Result, anyhow}; use ini::Ini; -use crate::model::config::{Network, NetworkType}; +use crate::model::profile::{Network, NetworkType}; impl Network { pub fn line_up(&self, ini: &mut Ini) -> Result<()> { @@ -48,9 +48,10 @@ impl Network { cmd = Command::new("cmd.exe"); cmd.arg("/C"); - if self.local_console == true { - cmd.arg("start"); - } + // if self.local_console == true { + cmd.arg("start"); + cmd.arg("/min"); + // } } else { cmd = Command::new("sh"); } diff --git a/rust/src/modules/segatools.rs b/rust/src/modules/segatools.rs index 8811c8c..b939cbe 100644 --- a/rust/src/modules/segatools.rs +++ b/rust/src/modules/segatools.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; use ini::Ini; -use crate::{model::{config::Segatools, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; +use crate::{model::{profile::Segatools, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; impl Segatools { pub async fn line_up(&self, p: &impl ProfilePaths) -> Result { diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index 8b82251..56de818 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -12,9 +12,9 @@ use crate::download_handler::DownloadHandler; pub struct PackageStore { store: HashMap, - has_fetched: bool, app: AppHandle, - dlh: DownloadHandler + dlh: DownloadHandler, + offline: bool, } #[derive(Clone, Serialize, Deserialize)] @@ -31,9 +31,9 @@ impl PackageStore { pub fn new(app: AppHandle) -> PackageStore { PackageStore { store: HashMap::new(), - has_fetched: false, app: app.clone(), - dlh: DownloadHandler::new(app) + dlh: DownloadHandler::new(app), + offline: true } } @@ -75,11 +75,7 @@ impl PackageStore { Ok(()) } - pub async fn fetch_listings(&mut self) -> Result<()> { - if self.has_fetched { - return Ok(()); - } - + pub async fn fetch_listings() -> Result> { use async_compression::futures::bufread::GzipDecoder; use futures::{ io::{self, BufReader, ErrorKind}, @@ -87,6 +83,7 @@ impl PackageStore { }; let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/").await?; + let reader = response .bytes_stream() .map_err(|e| io::Error::new(ErrorKind::Other, e)) @@ -96,9 +93,14 @@ impl PackageStore { let mut data = String::new(); decoder.read_to_string(&mut data).await?; - let listings: Vec = serde_json::from_str(&data) - .expect("Invalid JSON"); + Ok(serde_json::from_str(&data)?) + } + pub fn is_offline(&self) -> bool { + self.offline + } + + pub fn process_fetched_listings(&mut self, listings: Vec) { for listing in listings { // This is None if the package has no versions for whatever reason if let Some(r) = Package::from_rainy(listing) { @@ -114,9 +116,7 @@ impl PackageStore { } } - self.has_fetched = true; - - Ok(()) + self.offline = false; } pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result { diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index 7a5a01b..aa39450 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -59,17 +59,30 @@ impl AnyProfile { } } } - - pub fn pkgs(&self) -> &BTreeSet { + pub fn mod_pkgs(&self) -> &BTreeSet { match self { Self::OngekiProfile(p) => &p.mods } } - pub fn pkgs_mut(&mut self) -> &mut BTreeSet { + pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet { match self { Self::OngekiProfile(p) => &mut p.mods } } + pub fn special_pkgs(&self) -> Vec { + let mut res = Vec::new(); + match self { + Self::OngekiProfile(p) => { + if let Some(hook) = &p.sgt.hook { + res.push(hook.clone()); + } + if let Some(io) = &p.sgt.io { + res.push(io.clone()); + } + } + } + res + } pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> { match self { Self::OngekiProfile(_p) => { @@ -80,7 +93,7 @@ impl AnyProfile { #[cfg(target_os = "windows")] if let Some(info) = info { - use crate::model::config::Display; + use crate::model::profile::Display; if res.is_ok() { Display::wait_for_exit(_app, info); } else { @@ -141,6 +154,14 @@ impl AnyProfile { } } +impl std::fmt::Debug for AnyProfile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::OngekiProfile(p) => f.debug_tuple("ongeki").field(&p.name).finish(), + } + } +} + pub async fn list_profiles() -> Result> { let path = std::fs::read_dir(util::config_dir())?; diff --git a/rust/src/profiles/ongeki.rs b/rust/src/profiles/ongeki.rs index 63e76f0..c993ddc 100644 --- a/rust/src/profiles/ongeki.rs +++ b/rust/src/profiles/ongeki.rs @@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri::Emitter; use std::{collections::BTreeSet, path::PathBuf, process::Stdio}; -use crate::model::config::BepInEx; +use crate::model::profile::BepInEx; use crate::profiles::fixed_name; -use crate::{model::{config::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util}; +use crate::{model::{profile::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util}; use super::{Profile, ProfileMeta, ProfilePaths}; use anyhow::{anyhow, Result}; use std::fs::File; @@ -25,7 +25,7 @@ pub struct OngekiProfile { pub bepinex: BepInEx, #[cfg(not(target_os = "windows"))] - pub wine: crate::model::config::Wine, + pub wine: crate::model::profile::Wine, } impl Profile for OngekiProfile { @@ -40,7 +40,7 @@ impl Profile for OngekiProfile { network: Network::default(), bepinex: BepInEx::default(), #[cfg(not(target_os = "windows"))] - wine: crate::model::config::Wine::default(), + wine: crate::model::profile::Wine::default(), }; p.save()?; std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base())?; diff --git a/src/components/App.vue b/src/components/App.vue index e59d2d5..b5203db 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -38,8 +38,8 @@ onMounted(async () => { await Promise.all([prf.reloadList(), prf.reload()]); if (prf.current !== null) { - await pkg.reloadAll(); currentTab.value = 0; + await pkg.reloadAll(); } fetch_promise.then(async () => { @@ -65,7 +65,7 @@ onMounted(async () => { >
{ />