From 0102f8a4e54abd6d258f9338eadebc71780713a7 Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 30 Nov 2023 11:22:59 -0500 Subject: [PATCH] [ALL] Add run-game protocol verb --- ARCHITECTURE.md | 15 +++-- owmods_cli/src/main.rs | 34 +++++++---- owmods_core/src/protocol.rs | 58 ++++++++----------- owmods_gui/backend/src/commands.rs | 36 +++++++++--- owmods_gui/backend/src/main.rs | 11 ++-- owmods_gui/frontend/src/commands.ts | 2 +- .../main/top-bar/StartGameButton.tsx | 25 +++++++- .../main/top-bar/overflow/InstallFrom.tsx | 16 ++--- owmods_gui/frontend/src/types.d.ts | 8 ++- 9 files changed, 129 insertions(+), 76 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e9ab4f25..f6a2af4d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,7 +29,7 @@ - [Throttling](#throttling) - [Protocol Behavior](#protocol-behavior) - [General Structure](#general-structure) - - [Install Types](#install-types) + - [Verbs](#verbs) - [Examples](#examples) - [Notes](#notes) - [GUI Package](#gui-package) @@ -288,22 +288,24 @@ So if I had a progress bar that was a length of 90, and I increment by 1 every 1 ## Protocol Behavior - The mod manager can install mods from a URL or a URI. -- The manager uses the `owmods://` protocol to install mods. +- The mod manager can also run the game from a URI. +- The manager uses the `owmods://` protocol. ### General Structure -`owmods://install-type/payload` +`owmods://verb/payload` All URLs should start with owmods:// -Then they should follow with the install type they want like `install-mod` or `install-url` -Finally they should have the payload for the install +Then they should follow with the verb they want like `install-mod` or `install-url` +Finally they should have the payload for the action -### Install Types +### Verbs - `install-mod` - Installs a mod from the mods database, the payload should be the mod unique name - `install-url` - Installs a mod from a url, the payload should be the url to install from, **Not URI encoded** - `install-zip` - Installs a mod from a zip file, the payload should be the path to the zip file, note you shouldn't really need to use this because every user's computer is different, this is just used internally for drag and drop - `install-prerelease` - Installs a mod from a prerelease (in the mods database), the payload should be the mod unique name +- `run-game` - Runs the game, the payload should be a unique name for the mod to enable before running the game ### Examples @@ -311,6 +313,7 @@ Finally they should have the payload for the install - owmods://install-url/ - owmods://install-zip//home/user/Downloads/Mod.zip - owmods://install-prerelease/Raicuparta.NomaiVR +- owmods://run-game/Bwc9876.TimeSaver ### Notes diff --git a/owmods_cli/src/main.rs b/owmods_cli/src/main.rs index 0d182391..8819c04b 100644 --- a/owmods_cli/src/main.rs +++ b/owmods_cli/src/main.rs @@ -19,7 +19,7 @@ use owmods_core::{ remote::RemoteMod, }, open::{open_github, open_readme, open_shortcut}, - protocol::{ProtocolInstallType, ProtocolPayload}, + protocol::{ProtocolPayload, ProtocolVerb}, remove::{remove_failed_mod, remove_mod}, toggle::toggle_mod, updates::update_all, @@ -470,34 +470,35 @@ async fn run_from_cli(cli: BaseCli) -> Result<()> { clap_complete::generate(*shell, &mut cmd, name, &mut std::io::stdout()); } Commands::Protocol { uri } => { - info!("Installing from {}", uri); let remote_db = RemoteDatabase::fetch(&config.database_url).await?; let local_db = LocalDatabase::fetch(&config.owml_path)?; let payload = ProtocolPayload::parse(uri); - match payload.install_type { - ProtocolInstallType::InstallMod | ProtocolInstallType::InstallPreRelease => { + match payload.verb { + ProtocolVerb::InstallMod | ProtocolVerb::InstallPreRelease => { + info!("Installing from {}", payload.payload); install_mod_from_db( &payload.payload, &config, &remote_db, &local_db, r, - matches!(payload.install_type, ProtocolInstallType::InstallPreRelease), + matches!(payload.verb, ProtocolVerb::InstallPreRelease), ) .await?; } - ProtocolInstallType::InstallURL | ProtocolInstallType::InstallZip => { + ProtocolVerb::InstallURL | ProtocolVerb::InstallZip => { warn!("WARNING: This will install a mod from a potentially untrusted source, continue? (yes/no)"); let mut answer = String::new(); std::io::stdin().read_line(&mut answer)?; - if answer.trim() == "yes" { + answer = answer.trim().to_ascii_lowercase(); + if answer == "yes" || answer == "y" { info!("Installing from {}", payload.payload); - match payload.install_type { - ProtocolInstallType::InstallURL => { + match payload.verb { + ProtocolVerb::InstallURL => { install_mod_from_url(&payload.payload, None, &config, &local_db) .await?; } - ProtocolInstallType::InstallZip => { + ProtocolVerb::InstallZip => { install_mod_from_zip( &PathBuf::from(&payload.payload), &config, @@ -510,7 +511,18 @@ async fn run_from_cli(cli: BaseCli) -> Result<()> { warn!("Aborting"); } } - ProtocolInstallType::Unknown => { + ProtocolVerb::RunGame => { + let local_db = LocalDatabase::fetch(&config.owml_path)?; + let target_mod = local_db.get_mod(&payload.payload); + if let Some(target_mod) = target_mod { + info!("Launching game with {}", target_mod.manifest.name); + toggle_mod(&target_mod.manifest.unique_name, &local_db, true, true)?; + } else { + warn!("Mod {} not found, ignoring", payload.payload); + } + start_game(&local_db, &config, None, false).await?; + } + ProtocolVerb::Unknown => { error!("Unknown install type, ignoring"); } } diff --git a/owmods_core/src/protocol.rs b/owmods_core/src/protocol.rs index 8c92779a..0ddc2921 100644 --- a/owmods_core/src/protocol.rs +++ b/owmods_core/src/protocol.rs @@ -7,7 +7,7 @@ use typeshare::typeshare; #[typeshare] #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] -pub enum ProtocolInstallType { +pub enum ProtocolVerb { /// Install a mod from the mod database InstallMod, /// Install a mod from a URL @@ -16,18 +16,21 @@ pub enum ProtocolInstallType { InstallPreRelease, /// Install a mod from a zip file InstallZip, + /// Run the game while making sure the given mod is enabled + RunGame, /// Unknown install type, means the protocol link was invalid and therefore should be ignored Unknown, } -impl ProtocolInstallType { - /// Parse a string into a [ProtocolInstallType] +impl ProtocolVerb { + /// Parse a string into a [ProtocolVerb] pub fn parse(raw_str: &str) -> Self { match raw_str { "install-mod" => Self::InstallMod, "install-url" => Self::InstallURL, "install-prerelease" => Self::InstallPreRelease, "install-zip" => Self::InstallZip, + "run-game" => Self::RunGame, _ => Self::Unknown, } } @@ -36,22 +39,23 @@ impl ProtocolInstallType { #[allow(rustdoc::bare_urls)] /// Represents a payload receive by a protocol handler (link from the website) /// All URLs should start with owmods:// -/// Then they should follow with the install type they want like `install-mod` or `install-url` +/// Then they should follow with the verb they want like `install-mod` or `install-url` /// Finally they should have the payload for the install /// -/// If an invalid install type is given the [ProtocolInstallType] will be set to [ProtocolInstallType::Unknown] +/// If an invalid verb is given the [ProtocolVerb] will be set to [ProtocolVerb::Unknown] /// /// Some examples of valid URIs are: /// - owmods://install-mod/Bwc9876.TimeSaver /// - owmods://install-url/https://example.com/Mod.zip /// - owmods://install-zip//home/user/Downloads/Mod.zip /// - owmods://install-prerelease/Raicuparta.NomaiVR +/// - owmods://run-game/Bwc9876.TimeSaver #[typeshare] #[derive(Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ProtocolPayload { /// The type of install that should be done - pub install_type: ProtocolInstallType, + pub verb: ProtocolVerb, /// The payload for the install pub payload: String, } @@ -59,25 +63,25 @@ pub struct ProtocolPayload { impl ProtocolPayload { fn failed() -> Self { Self { - install_type: ProtocolInstallType::Unknown, + verb: ProtocolVerb::Unknown, payload: "".to_string(), } } /// Parse a string into a [ProtocolPayload] - /// If the string is invalid the [ProtocolInstallType] will be set to [ProtocolInstallType::Unknown] + /// If the string is invalid the [ProtocolVerb] will be set to [ProtocolVerb::Unknown] /// and the payload will be set to an empty string pub fn parse(raw_str: &str) -> Self { let re = Regex::new(r"^owmods://([^/]+)/(.+)$").unwrap(); if let Some(matches) = re.captures(raw_str) { - let install_type = matches + let verb = matches .get(1) - .map(|m| ProtocolInstallType::parse(m.as_str())) - .unwrap_or(ProtocolInstallType::Unknown); + .map(|m| ProtocolVerb::parse(m.as_str())) + .unwrap_or(ProtocolVerb::Unknown); let payload = matches.get(2).map(|m| m.as_str()); if let Some(payload) = payload { Self { - install_type, + verb, payload: payload.to_string(), } } else { @@ -96,54 +100,42 @@ mod tests { #[test] fn test_protocol_payload() { let payload = ProtocolPayload::parse("owmods://install-mod/Bwc9876.TimeSaver"); - assert!(matches!( - payload.install_type, - ProtocolInstallType::InstallMod - )); + assert!(matches!(payload.verb, ProtocolVerb::InstallMod)); assert_eq!(payload.payload, "Bwc9876.TimeSaver"); let payload = ProtocolPayload::parse("owmods://install-url/https://example.com/Mod.zip"); - assert!(matches!( - payload.install_type, - ProtocolInstallType::InstallURL - )); + assert!(matches!(payload.verb, ProtocolVerb::InstallURL)); assert_eq!(payload.payload, "https://example.com/Mod.zip"); let payload = ProtocolPayload::parse("owmods://install-zip//home/user/Downloads/Mod.zip"); - assert!(matches!( - payload.install_type, - ProtocolInstallType::InstallZip - )); + assert!(matches!(payload.verb, ProtocolVerb::InstallZip)); assert_eq!(payload.payload, "/home/user/Downloads/Mod.zip"); let payload = ProtocolPayload::parse("owmods://install-prerelease/Raicuparta.NomaiVR"); - assert!(matches!( - payload.install_type, - ProtocolInstallType::InstallPreRelease - )); + assert!(matches!(payload.verb, ProtocolVerb::InstallPreRelease)); assert_eq!(payload.payload, "Raicuparta.NomaiVR"); } #[test] fn test_protocol_payload_invalid() { let payload = ProtocolPayload::parse("ow://asdf"); - assert!(matches!(payload.install_type, ProtocolInstallType::Unknown)); + assert!(matches!(payload.verb, ProtocolVerb::Unknown)); assert_eq!(payload.payload, ""); let payload = ProtocolPayload::parse("owmods://install-mod"); - assert!(matches!(payload.install_type, ProtocolInstallType::Unknown)); + assert!(matches!(payload.verb, ProtocolVerb::Unknown)); assert_eq!(payload.payload, ""); let payload = ProtocolPayload::parse("owmods://install-url"); - assert!(matches!(payload.install_type, ProtocolInstallType::Unknown)); + assert!(matches!(payload.verb, ProtocolVerb::Unknown)); assert_eq!(payload.payload, ""); let payload = ProtocolPayload::parse("owmods://install-zip"); - assert!(matches!(payload.install_type, ProtocolInstallType::Unknown)); + assert!(matches!(payload.verb, ProtocolVerb::Unknown)); assert_eq!(payload.payload, ""); let payload = ProtocolPayload::parse("owmods://install-prerelease"); - assert!(matches!(payload.install_type, ProtocolInstallType::Unknown)); + assert!(matches!(payload.verb, ProtocolVerb::Unknown)); assert_eq!(payload.payload, ""); } } diff --git a/owmods_gui/backend/src/commands.rs b/owmods_gui/backend/src/commands.rs index ad76aab0..69fea8a1 100644 --- a/owmods_gui/backend/src/commands.rs +++ b/owmods_gui/backend/src/commands.rs @@ -25,7 +25,7 @@ use owmods_core::{ open::{open_github, open_readme, open_shortcut}, owml::OWMLConfig, progress::bars::{ProgressBar, ProgressBars}, - protocol::{ProtocolInstallType, ProtocolPayload}, + protocol::{ProtocolPayload, ProtocolVerb}, remove::{remove_failed_mod, remove_mod}, socket::{LogServer, SocketMessageType}, updates::check_mod_needs_update, @@ -868,14 +868,32 @@ pub async fn get_alert(state: tauri::State<'_, State>) -> Result { } #[tauri::command] -pub async fn pop_protocol_url(state: tauri::State<'_, State>, handle: tauri::AppHandle) -> Result { - let mut protocol_url = state.protocol_url.write().await; - if let Some(url) = protocol_url.as_ref() { - handle - .typed_emit_all(&Event::ProtocolInvoke(url.clone())) - .ok(); +pub async fn pop_protocol_url( + state: tauri::State<'_, State>, + handle: tauri::AppHandle, + id: &str, +) -> Result { + /// Amount of listeners that need to be active before we can emit the event + const PROTOCOL_LISTENER_AMOUNT: usize = 2; + + let id = id.to_string(); + + let mut protocol_listeners = state.protocol_listeners.write().await; + if protocol_listeners.contains(&id) { + return Ok(()); + } + protocol_listeners.push(id.clone()); + + if protocol_listeners.len() >= PROTOCOL_LISTENER_AMOUNT { + let mut protocol_url = state.protocol_url.write().await; + if let Some(url) = protocol_url.as_ref() { + handle + .typed_emit_all(&Event::ProtocolInvoke(url.clone())) + .ok(); + } + *protocol_url = None; } - *protocol_url = None; + Ok(()) } @@ -963,7 +981,7 @@ pub async fn register_drop_handler(window: tauri::Window) -> Result { handle.typed_emit_all(&Event::DragLeave(())).ok(); handle .typed_emit_all(&Event::ProtocolInvoke(ProtocolPayload { - install_type: ProtocolInstallType::InstallZip, + verb: ProtocolVerb::InstallZip, payload: f.to_str().unwrap().to_string(), })) .ok(); diff --git a/owmods_gui/backend/src/main.rs b/owmods_gui/backend/src/main.rs index 466511b4..861eaf9d 100644 --- a/owmods_gui/backend/src/main.rs +++ b/owmods_gui/backend/src/main.rs @@ -16,7 +16,7 @@ use owmods_core::{ db::{LocalDatabase, RemoteDatabase}, file::get_app_path, progress::bars::ProgressBars, - protocol::{ProtocolInstallType, ProtocolPayload}, + protocol::{ProtocolPayload, ProtocolVerb}, }; use time::macros::format_description; @@ -52,6 +52,8 @@ pub struct State { game_log: StatePart, /// The protocol url used to invoke the program, if any. This is should only be gotten once and removed after protocol_url: StatePart>, + /// How many protocol listeners are currently active + protocol_listeners: StatePart>, /// The progress bars of installs/updates/downloads/etc. progress_bars: StatePart, /// A list of unique names of mods that currently have an operation being performed on them @@ -76,6 +78,7 @@ fn main() -> Result<(), Box> { gui_config: manage(gui_config), game_log: manage(HashMap::new()), protocol_url: manage(url), + protocol_listeners: manage(vec![]), progress_bars: manage(ProgressBars::new()), mods_in_progress: manage(vec![]), }) @@ -100,12 +103,12 @@ fn main() -> Result<(), Box> { let res = tauri_plugin_deep_link::register("owmods", move |request| { let protocol_payload = ProtocolPayload::parse(&request); - match protocol_payload.install_type { - ProtocolInstallType::Unknown => {} + match protocol_payload.verb { + ProtocolVerb::Unknown => {} _ => { debug!( "Invoking {:?} with {} from protocol", - protocol_payload.install_type, protocol_payload.payload + protocol_payload.verb, protocol_payload.payload ); handle .typed_emit_all(&Event::ProtocolInvoke(protocol_payload)) diff --git a/owmods_gui/frontend/src/commands.ts b/owmods_gui/frontend/src/commands.ts index b9e3b7e0..ff9bb228 100644 --- a/owmods_gui/frontend/src/commands.ts +++ b/owmods_gui/frontend/src/commands.ts @@ -79,7 +79,7 @@ const commandInfo = { fixDeps: $>("fix_mod_deps"), checkDBForIssues: $>("db_has_issues"), getAlert: $>("get_alert"), - popProtocolURL: $("pop_protocol_url"), + popProtocolURL: $>("pop_protocol_url"), checkOWML: $>("check_owml"), getDownloads: $>("get_downloads"), clearDownloads: $("clear_downloads"), diff --git a/owmods_gui/frontend/src/components/main/top-bar/StartGameButton.tsx b/owmods_gui/frontend/src/components/main/top-bar/StartGameButton.tsx index a11bc121..a881023b 100644 --- a/owmods_gui/frontend/src/components/main/top-bar/StartGameButton.tsx +++ b/owmods_gui/frontend/src/components/main/top-bar/StartGameButton.tsx @@ -1,10 +1,11 @@ import { Button } from "@mui/material"; import { PlayArrow as PlayIcon } from "@mui/icons-material"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { commands } from "@commands"; import { useGetTranslation } from "@hooks"; import { dialog } from "@tauri-apps/api"; import { simpleOnError } from "../../../errorHandling"; +import { listen } from "@events"; const StartGameButton = () => { const getTranslation = useGetTranslation(); @@ -37,6 +38,28 @@ const StartGameButton = () => { task(); }, [getTranslation]); + useEffect(() => { + const unsubscribe = listen("protocolInvoke", (protocolPayload) => { + commands.checkOWML().then((valid) => { + if (valid && protocolPayload.verb === "runGame") { + commands + .toggleMod( + { uniqueName: protocolPayload.payload, enabled: true, recursive: true }, + false + ) + .catch(() => + console.warn(`Mod ${protocolPayload.payload} Not Found, Ignoring...`) + ) + .finally(() => { + onPlay(); + }); + } + }); + }); + commands.popProtocolURL({ id: "run" }); + return unsubscribe; + }, [onPlay]); + return (