diff --git a/crates/api/src/install/install.rs b/crates/api/src/install/install.rs index 079cd13..26621e8 100644 --- a/crates/api/src/install/install.rs +++ b/crates/api/src/install/install.rs @@ -1,3 +1,5 @@ +//! The module responsible for actually installing mods. + use anyhow::Result; use data::{conv::DbIntoArg, instance::Instance, Conn}; use install::{extract::extract_file, magic::detect_file_type}; @@ -7,6 +9,7 @@ use whcore::progress::ProgressCallback; use crate::plugin::Plugin; +/// Install a mod. pub async fn install_mod( db: &mut Conn, item: Mod, diff --git a/crates/api/src/install/mod.rs b/crates/api/src/install/mod.rs index 5440676..89c0d97 100644 --- a/crates/api/src/install/mod.rs +++ b/crates/api/src/install/mod.rs @@ -1,3 +1,5 @@ +//! The mod installer API. + pub mod install; pub mod progress; pub mod uninstall; diff --git a/crates/api/src/install/progress.rs b/crates/api/src/install/progress.rs index 4adb046..6e10023 100644 --- a/crates/api/src/install/progress.rs +++ b/crates/api/src/install/progress.rs @@ -1,3 +1,5 @@ +//! The progress API. + use std::{future::Future, pin::Pin}; use tauri::Manager; @@ -5,6 +7,7 @@ use tauri_specta::Event as TEvent; use crate::{EVENT_BUS, TAURI_HANDLE}; +/// A progress payload. #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, Type, )] @@ -27,6 +30,7 @@ impl TEvent for ProgressPayload { const NAME: &'static str = "progress_callback"; } +/// A progress callback for Tauri. pub fn tauri_progress( total: u64, current: u64, diff --git a/crates/api/src/install/uninstall.rs b/crates/api/src/install/uninstall.rs index 4a2b84b..6c3e531 100644 --- a/crates/api/src/install/uninstall.rs +++ b/crates/api/src/install/uninstall.rs @@ -1,3 +1,6 @@ +//! The uninstall API. +//! This module really hates the installer API, on an emotional level. + use std::{fs, path::PathBuf}; use anyhow::Result; @@ -9,6 +12,7 @@ use data::{ Conn, }; +/// Uninstall a mod. pub async fn uninstall_mod(db: &mut Conn, item: DbMod, instance: Instance) -> Result<()> { let paths = serde_json::from_str::>(&item.path)?; diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c35e419..0f93266 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,3 +1,12 @@ +#![warn(missing_docs, rustdoc::broken_intra_doc_links)] +//! # Wormhole's API. +//! +//! This implements all of the plugins, mod installers, +//! game supports, assets, and a lot more. +//! +//! In the future, this will hopefully be able to support +//! WASI-based plugins for extensibility. + use std::sync::Arc; use ::tauri::AppHandle; @@ -33,21 +42,27 @@ pub mod register; pub mod res; pub mod tauri; -#[cfg(test)] -pub mod test_util; - // This isn't just lazily initialized, it's also "lazy" because // I'm too lazy to actually pass around a struct. lazy_static! { + /// A handle to the Tauri app. + /// This will be [`None`] if the app hasn't been started yet, or + /// this is running in the web UI. pub static ref TAURI_HANDLE: Arc>> = Arc::new(Mutex::new(None)); + + /// The "event bus". This is really just an unbounded channel + /// of [`ProgressPayload`]s. This will eventually be expanded + /// to support more event types. pub static ref EVENT_BUS: Arc<(Sender, Receiver)> = Arc::new(unbounded()); } +/// Initializes the API, registering all of the default plugins. pub async fn init() { register_defaults().await; } +/// Gets the [`TypeMap`] for the API. pub fn type_map() -> TypeMap { let mut map = TypeMap::default(); diff --git a/crates/api/src/macros.rs b/crates/api/src/macros.rs index ae152e4..8a425ca 100644 --- a/crates/api/src/macros.rs +++ b/crates/api/src/macros.rs @@ -1,6 +1,11 @@ +//! Macros for the API. + +/// Create a function alias to a member function +/// in a local variable. #[macro_export] macro_rules! fn_alias { ($var: ident::$real: ident => $name: ident: ($($arg: ident: $ty: ident),*)$( -> $ret: ident)?) => { + #[allow(missing_docs)] #[tauri::command] #[specta::specta] fn $name(me: tauri::State<'_, $var>, $($arg: $ty),*) $(-> $ret)? { @@ -9,6 +14,7 @@ macro_rules! fn_alias { }; (dyn $var: ident::$real: ident => $name: ident: ($($arg: ident: $ty: ident),*)$( -> $ret: ident)?) => { + #[allow(missing_docs)] #[tauri::command] #[specta::specta] fn $name(me: tauri::State<'_, std::sync::Arc>>, $($arg: $ty),*) $(-> $ret)? { @@ -17,6 +23,7 @@ macro_rules! fn_alias { }; (dyn $var: ident::$real: ident => $name: ident: async ($($arg: ident: $ty: ident),*)$( -> $ret: ident)?) => { + #[allow(missing_docs)] #[tauri::command] #[specta::specta] async fn $name<'a>(me: tauri::State<'a, std::sync::Arc>>, $($arg: $ty),*) $(-> $ret)? { diff --git a/crates/api/src/plugin.rs b/crates/api/src/plugin.rs index 384d065..a60210c 100644 --- a/crates/api/src/plugin.rs +++ b/crates/api/src/plugin.rs @@ -1,3 +1,5 @@ +//! The plugin API. + use std::{collections::HashMap, path::PathBuf, sync::Arc}; use anyhow::Result; @@ -18,25 +20,49 @@ use whcore::{dirs::Dirs, manager::CoreManager}; use crate::install::{install::install_mod, progress::tauri_progress, uninstall::uninstall_mod}; lazy_static! { + /// A map of plugin identifiers to their resolvers. + /// This is a cache, as some resolvers are expensive to create + /// (e.g. CKAN, which refreshes two git repos every time it's created). pub static ref RESOLVERS: Arc>>>>> = Arc::new(Mutex::new(HashMap::new())); } +/// A plugin's metadata. This is useful for getting information +/// about the plugin on the frontend. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Type)] pub struct PluginInfo { + /// The plugin's identifier. pub id: &'static str, + + /// The plugin's game ID. pub game: i32, + + /// The plugin's display name. pub display_name: String, + + /// The plugin's icon URL. pub icon_url: String, + + /// The plugin's banner URL. pub banner_url: String, + + /// The plugin's fallback mod install directory. + /// If the installer can't automatically determine + /// where to install a mod, this will be used. pub fallback_dir: Option<&'static str>, + + /// The plugin's query resolvers (IDs). pub resolvers: Vec, } unsafe impl Send for PluginInfo {} unsafe impl Sync for PluginInfo {} -// TODO: Install hook (for Minecraft loader installation) +/// A plugin. +/// +/// This is the main interface for interacting with plugins. +/// This is essentially a support module for a game. Every operation +/// that Wormhole does goes through a plugin. #[async_trait] pub trait Plugin: Send + Sync { /// Create a new instance. @@ -131,6 +157,7 @@ pub trait Plugin: Send + Sync { None } + /// Get the plugin as a [`PluginInfo`]. async fn as_info(&self) -> Option { Some(PluginInfo { id: self.id(), @@ -149,8 +176,10 @@ pub trait Plugin: Send + Sync { }) } + /// Launch the game instance. async fn launch(&self, instance: Instance) -> Result; + /// Install a mod to the provided instance. async fn install_mod( &self, db: &mut Conn, @@ -174,6 +203,7 @@ pub trait Plugin: Send + Sync { Ok(()) } + /// Uninstall a mod from the provided instance. async fn uninstall_mod(&self, db: &mut Conn, item: DbMod, instance: Instance) -> Result<()> where Self: Sized, @@ -183,6 +213,7 @@ pub trait Plugin: Send + Sync { Ok(()) } + /// Install an instance after creation. async fn install_instance(&self, _inst: &Instance) -> Result<()> { Ok(()) } diff --git a/crates/api/src/plugins/common/mod.rs b/crates/api/src/plugins/common/mod.rs index 1816b7c..bfb2f21 100644 --- a/crates/api/src/plugins/common/mod.rs +++ b/crates/api/src/plugins/common/mod.rs @@ -1 +1,3 @@ +//! Common functions for plugins. + pub mod unity; diff --git a/crates/api/src/plugins/common/unity.rs b/crates/api/src/plugins/common/unity.rs index 414038b..95588ac 100644 --- a/crates/api/src/plugins/common/unity.rs +++ b/crates/api/src/plugins/common/unity.rs @@ -1,3 +1,5 @@ +//! The Unity game support module. + use std::path::PathBuf; use anyhow::Result; @@ -8,21 +10,45 @@ use tokio::process::{Child, Command}; use crate::plugin::Plugin; +/// A plugin implementation for Unity games. +/// This will automatically implement the [`Plugin`] trait. #[async_trait] pub trait UnityPlugin: Send + Sync { + /// Create a new instance of the plugin. fn new() -> Self where Self: Sized; + + /// The id of the plugin. fn id(&self) -> &'static str; + + /// The game of the plugin. fn game(&self) -> i32; + + /// The icon of the plugin. fn icon(&self) -> String; + + /// The banner of the plugin. fn banner(&self) -> String; + + /// The display name of the plugin. fn display(&self) -> String; + + /// The fallback install dir of the plugin. fn fallback(&self) -> Option<&'static str>; + + /// The executable for the plugin to run when + /// launching the game. fn executable(&self) -> &'static str; + + /// The install dir of the plugin. fn find(&self) -> Option; + + /// The name of the plugin. fn name(&self) -> &'static str; + + /// Create the resolvers for the plugin. async fn resolvers(&self) -> Vec>; } diff --git a/crates/api/src/plugins/ksp1.rs b/crates/api/src/plugins/ksp1.rs index 6b70b44..f2a79f5 100644 --- a/crates/api/src/plugins/ksp1.rs +++ b/crates/api/src/plugins/ksp1.rs @@ -1,3 +1,5 @@ +//! The KSP1 support module. + use std::path::PathBuf; use base64::{engine::general_purpose::STANDARD, Engine}; @@ -6,15 +8,18 @@ use whcore::finder::{finder::InstallFinder, pdlauncher::PrivateDivision, steam:: use super::common::unity::UnityPlugin; -pub const ICON_BYTES: &[u8] = include_bytes!("../assets/ksp1/icon.png"); -pub const BANNER_BYTES: &[u8] = include_bytes!("../assets/ksp1/banner.png"); +const ICON_BYTES: &[u8] = include_bytes!("../assets/ksp1/icon.png"); +const BANNER_BYTES: &[u8] = include_bytes!("../assets/ksp1/banner.png"); -// The expected size of KSP1's `steam_api64.dll` in bytes. -// This helps to make sure that the game is not pirated. -// File path: `[KSP1_ROOT]/KSP_x64_Data/Plugins/x86_64/steam_api64.dll` -// Information from: SteamDB, DepotDownloader, KSP1 Installed Files +/// The expected size of KSP1's `steam_api64.dll` in bytes. +/// This helps to make sure that the game is not pirated. +/// File path: `[KSP1_ROOT]/KSP_x64_Data/Plugins/x86_64/steam_api64.dll` +/// Information from: SteamDB, DepotDownloader, KSP1 Installed Files +/// +/// TODO: Actually use this information somewhere. pub const KSP1_STEAM_API_SIZE: u64 = 249120; +/// The plugin for KSP1. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Kerbal1Plugin; diff --git a/crates/api/src/plugins/ksp2.rs b/crates/api/src/plugins/ksp2.rs index 208da1b..cadfdc3 100644 --- a/crates/api/src/plugins/ksp2.rs +++ b/crates/api/src/plugins/ksp2.rs @@ -1,3 +1,5 @@ +//! The KSP2 support module. + use std::path::PathBuf; use base64::{engine::general_purpose::STANDARD, Engine}; @@ -6,15 +8,18 @@ use whcore::finder::{finder::InstallFinder, pdlauncher::PrivateDivision, steam:: use super::common::unity::UnityPlugin; -pub const ICON_BYTES: &[u8] = include_bytes!("../assets/ksp2/icon.png"); -pub const BANNER_BYTES: &[u8] = include_bytes!("../assets/ksp2/banner.png"); +const ICON_BYTES: &[u8] = include_bytes!("../assets/ksp2/icon.png"); +const BANNER_BYTES: &[u8] = include_bytes!("../assets/ksp2/banner.png"); -// The expected size of KSP2's `steam_api64.dll` in bytes. -// This helps to make sure that the game is not pirated. -// File path: `[KSP2_ROOT]/KSP2_x64_Data/Plugins/x86_64/steam_api64.dll` -// Information from: SteamDB, DepotDownloader, KSP2 Installed Files +/// The expected size of KSP2's `steam_api64.dll` in bytes. +/// This helps to make sure that the game is not pirated. +/// File path: `[KSP2_ROOT]/KSP2_x64_Data/Plugins/x86_64/steam_api64.dll` +/// Information from: SteamDB, DepotDownloader, KSP2 Installed Files +/// +/// TODO: Actually use this information somewhere. pub const KSP2_STEAM_API_SIZE: u64 = 295336; +/// The plugin for KSP2. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Kerbal2Plugin; diff --git a/crates/api/src/plugins/minecraft/mod.rs b/crates/api/src/plugins/minecraft.rs similarity index 93% rename from crates/api/src/plugins/minecraft/mod.rs rename to crates/api/src/plugins/minecraft.rs index 8ce989e..1dafed2 100644 --- a/crates/api/src/plugins/minecraft/mod.rs +++ b/crates/api/src/plugins/minecraft.rs @@ -1,3 +1,5 @@ +//! The Minecraft support module. + use std::path::PathBuf; use anyhow::Result; @@ -11,9 +13,10 @@ use whcore::manager::CoreManager; use crate::plugin::Plugin; -pub const ICON_BYTES: &[u8] = include_bytes!("../../assets/minecraft/icon.svg"); -pub const BANNER_BYTES: &[u8] = include_bytes!("../../assets/minecraft/banner.jpg"); +const ICON_BYTES: &[u8] = include_bytes!("../assets/minecraft/icon.svg"); +const BANNER_BYTES: &[u8] = include_bytes!("../assets/minecraft/banner.jpg"); +/// The plugin for Minecraft. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MinecraftPlugin; diff --git a/crates/api/src/plugins/mod.rs b/crates/api/src/plugins/mod.rs index 13128a8..367fec1 100644 --- a/crates/api/src/plugins/mod.rs +++ b/crates/api/src/plugins/mod.rs @@ -1,3 +1,5 @@ +//! Default plugins. + pub mod common; pub mod ksp1; pub mod ksp2; @@ -9,6 +11,7 @@ pub use minecraft::MinecraftPlugin; use crate::{plugin::Plugin, register::register_plugin, tauri::TauriPluginTrait}; +/// Default plugins. pub fn default_plugins() -> Vec> { vec![ Box::new(Kerbal1Plugin::new()), @@ -17,6 +20,7 @@ pub fn default_plugins() -> Vec> { ] } +/// Register default plugins. pub async fn register_defaults() { for plugin in default_plugins() { register_plugin(plugin).await; diff --git a/crates/api/src/register.rs b/crates/api/src/register.rs index 8785e9a..0283353 100644 --- a/crates/api/src/register.rs +++ b/crates/api/src/register.rs @@ -1,3 +1,5 @@ +//! Happily registering plugins since 2024. + use std::{collections::HashMap, sync::Arc}; use tokio::sync::Mutex; @@ -5,10 +7,13 @@ use tokio::sync::Mutex; use crate::tauri::TauriPluginTrait; lazy_static! { + /// A map of game IDs to their plugin. + /// This is where plugins are registered. pub static ref PLUGINS: Arc>>> = Arc::new(Mutex::new(HashMap::new())); } +/// Registers a plugin to the map. pub async fn register_plugin(plugin: Box) { PLUGINS.lock().await.insert(plugin.game(), plugin); } diff --git a/crates/api/src/res.rs b/crates/api/src/res.rs index 966bfe2..7d328f6 100644 --- a/crates/api/src/res.rs +++ b/crates/api/src/res.rs @@ -1,11 +1,21 @@ +//! The result of a plugin call. + use std::mem::transmute; +/// The result of a plugin call. +/// This was used back when I tried to implement WASI plugins +/// for the first time, but it's no longer used. I'm keeping it +/// so I have less work to do in the future. #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub enum PluginResult { + /// The plugin call was successful. Ok = 0, + + /// The plugin call failed. Error = 1, + /// The plugin call's result was unknown. #[default] Unknown = -1, } diff --git a/crates/api/src/tauri.rs b/crates/api/src/tauri.rs index e17da54..dc95579 100644 --- a/crates/api/src/tauri.rs +++ b/crates/api/src/tauri.rs @@ -1,3 +1,5 @@ +//! The Tauri plugin API. + use anyhow::Result; use data::{instance::Instance, mod_::DbMod, source::SourceMapping, Conn}; @@ -27,21 +29,30 @@ use crate::{ plugin::{Plugin as CPlugin, PluginInfo}, }; +/// A Tauri command invoker. pub type Invoker = Box) + Send + Sync + 'static>; +/// A Tauri plugin (struct form). pub struct TauriPlugin { plugin: Arc>, + /// The plugin's command invoker. pub handler: Arc>, + + /// The plugin's name. pub name: &'static str, + + /// The plugin's command handler and [`CollectFunctionsResult`]. pub cmds: (CollectFunctionsResult, Invoker), } impl TauriPlugin { + /// Create a new Tauri plugin based on its trait form. pub fn new(plugin: T) -> Result { Self::new_boxed(Box::new(plugin)) } + /// Create a new Tauri plugin based on the boxed version of its trait form. pub fn new_boxed(plugin: Box) -> Result { tauri_aliases!(); @@ -75,10 +86,17 @@ impl Plugin for TauriPlugin { } } +/// A Tauri plugin (trait form). +/// +/// This was originally used for my naive version of the web UI's +/// invoker system, but it's still used as the function proxy target +/// in the GUI. #[async_trait] pub trait TauriPluginTrait: CPlugin + Send + Sync { + /// Get the plugin's identifier. async fn info(&self) -> Option; + /// Get the plugin's search results. async fn search_mods( &self, resolver: SourceMapping, @@ -87,8 +105,10 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { opts: Option, ) -> Option>; + /// Get a mod's information. async fn get_mod(&self, resolver: SourceMapping, id: String) -> Option; + /// Get mod's available versions. async fn get_mod_versions( &self, resolver: SourceMapping, @@ -96,6 +116,7 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { id: String, ) -> Option>; + /// Get a mod's version. async fn get_mod_version( &self, resolver: SourceMapping, @@ -104,6 +125,7 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { version: String, ) -> Option; + /// Get the latest available version of a mod. async fn get_latest_version( &self, resolver: SourceMapping, @@ -111,6 +133,7 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { id: String, ) -> Option; + /// Get the download URL for a mod. async fn get_download_url( &self, resolver: SourceMapping, @@ -119,6 +142,7 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { version: Option, ) -> Option; + /// Install a mod. async fn install( &self, db: &mut Conn, @@ -127,10 +151,13 @@ pub trait TauriPluginTrait: CPlugin + Send + Sync { instance: Instance, ) -> Option<()>; + /// Uninstall a mod. async fn uninstall(&self, db: &mut Conn, item: DbMod, instance: Instance) -> Option<()>; + /// Launch the game with the given instance. async fn launch_game(&self, instance: Instance) -> Option<()>; + /// Get the plugin's sources (IDs). async fn sources(&self) -> Option>; } diff --git a/crates/api/src/test_util.rs b/crates/api/src/test_util.rs deleted file mode 100644 index a81bfe0..0000000 --- a/crates/api/src/test_util.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; - -use indicatif::{ProgressBar, ProgressStyle}; -use lazy_static::lazy_static; - -lazy_static! { - pub static ref LIBS_PBAR: Arc>> = Arc::new(Mutex::new(None)); - pub static ref ASSETS_PBAR: Arc>> = Arc::new(Mutex::new(None)); -} - -pub fn library_download_callback( - recv: u64, - total: u64, - _chunk: Vec, - path: &PathBuf, - start: bool, - end: bool, -) { - let path = path.clone(); - let file = path - .parent() - .unwrap() - .parent() - .unwrap() - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .to_string(); - - if start { - *LIBS_PBAR.lock().unwrap() = Some( - ProgressBar::new(total).with_style( - ProgressStyle::with_template( - "[{prefix}] {wide_bar:.cyan/blue} {bytes}/{total_bytes} {msg}", - ) - .unwrap(), - ), - ); - - LIBS_PBAR.lock().unwrap().as_mut().unwrap().set_prefix(file); - } else if end { - LIBS_PBAR.lock().unwrap().as_mut().unwrap().finish(); - *LIBS_PBAR.lock().unwrap() = None; - } else { - if let Some(bar) = LIBS_PBAR.lock().unwrap().as_mut() { - bar.set_position(recv); - } - } -} - -pub fn asset_download_callback( - recv: u64, - total: u64, - _chunk: Vec, - _path: &PathBuf, - start: bool, - end: bool, -) { - if start { - *ASSETS_PBAR.lock().unwrap() = Some( - ProgressBar::new(total).with_style( - ProgressStyle::with_template( - "[assets] {wide_bar:.cyan/blue} {pos:>7}/{len:>7} {msg}", - ) - .unwrap(), - ), - ); - } else if end { - ASSETS_PBAR.lock().unwrap().as_mut().unwrap().finish(); - *ASSETS_PBAR.lock().unwrap() = None; - } else { - ASSETS_PBAR - .lock() - .unwrap() - .as_mut() - .unwrap() - .set_position(recv); - } -}