diff --git a/mbf-agent/src/handlers/import.rs b/mbf-agent/src/handlers/import.rs index d8e74e3c..1d0ec6dc 100644 --- a/mbf-agent/src/handlers/import.rs +++ b/mbf-agent/src/handlers/import.rs @@ -3,8 +3,7 @@ use std::path::{Path, PathBuf}; use crate::{ downloads, mod_man::ModManager, - models::response::{self, ImportResultType, Response}, - paths, + models::response::{self, ImportResultType, Response}, parameters::PARAMETERS }; use anyhow::{anyhow, Context, Result}; use log::{debug, info, warn}; @@ -15,8 +14,8 @@ use mbf_zip::ZipFile; /// # Returns /// The [Response](requests::Response) to the request (variant `ImportResult`) pub(super) fn handle_import_mod_url(from_url: String) -> Result { - std::fs::create_dir_all(paths::MBF_DOWNLOADS)?; - let download_path = Path::new(paths::MBF_DOWNLOADS).join("import_from_url"); + std::fs::create_dir_all(&PARAMETERS.mbf_downloads)?; + let download_path = Path::new(&PARAMETERS.mbf_downloads).join("import_from_url"); info!("Downloading {}", from_url); let filename: Option = @@ -183,7 +182,7 @@ fn attempt_song_import(from_path: PathBuf) -> Result { let mut zip = ZipFile::open(song_handle).context("Song was invalid ZIP file")?; if zip.contains_file("info.dat") || zip.contains_file("Info.dat") { - let extract_path = Path::new(paths::CUSTOM_LEVELS) + let extract_path = Path::new(&PARAMETERS.custom_levels) .join(from_path.file_stem().expect("Must have file stem")); if extract_path.exists() { diff --git a/mbf-agent/src/handlers/mod.rs b/mbf-agent/src/handlers/mod.rs index a9fd81f9..58ee063e 100644 --- a/mbf-agent/src/handlers/mod.rs +++ b/mbf-agent/src/handlers/mod.rs @@ -7,8 +7,9 @@ use crate::{ mod_man::ModManager, models::{ request::Request, + request::RequestEnum, response::{self, Response}, - }, + }, parameters::PARAMETERS, }; use anyhow::{anyhow, Context, Result}; use log::info; @@ -28,11 +29,11 @@ mod utility; /// # Returns /// If successful, a [Response] to be sent back to the frontend. pub fn handle_request(request: Request) -> Result { - match request { - Request::GetModStatus { + match request.request { + RequestEnum::GetModStatus { override_core_mod_url, } => mod_status::handle_get_mod_status(override_core_mod_url), - Request::Patch { + RequestEnum::Patch { downgrade_to, remodding, manifest_mod, @@ -49,15 +50,15 @@ pub fn handle_request(request: Request) -> Result { override_core_mod_url, vr_splash_path, ), - Request::GetDowngradedManifest { version } => { + RequestEnum::GetDowngradedManifest { version } => { patching::handle_get_downgraded_manifest(version) } - Request::RemoveMod { id } => mod_management::handle_remove_mod(id), - Request::SetModsEnabled { statuses } => mod_management::handle_set_mods_enabled(statuses), - Request::Import { from_path } => import::handle_import(from_path, None), - Request::ImportUrl { from_url } => import::handle_import_mod_url(from_url), - Request::FixPlayerData => utility::handle_fix_player_data(), - Request::QuickFix { + RequestEnum::RemoveMod { id } => mod_management::handle_remove_mod(id), + RequestEnum::SetModsEnabled { statuses } => mod_management::handle_set_mods_enabled(statuses), + RequestEnum::Import { from_path } => import::handle_import(from_path, None), + RequestEnum::ImportUrl { from_url } => import::handle_import_mod_url(from_url), + RequestEnum::FixPlayerData => utility::handle_fix_player_data(), + RequestEnum::QuickFix { override_core_mod_url, wipe_existing_mods, } => utility::handle_quick_fix(override_core_mod_url, wipe_existing_mods), @@ -72,7 +73,7 @@ pub fn handle_request(request: Request) -> Result { /// An `Err` variant is returned on failure, for example if Beat Saber isn't installed or the result from `dumpsys` couldn't be parsed. fn get_app_version_only() -> Result { let dumpsys_output = Command::new("dumpsys") - .args(["package", crate::APK_ID]) + .args(["package", &PARAMETERS.apk_id]) .output() .context("Invoking dumpsys")?; let dumpsys_stdout = diff --git a/mbf-agent/src/handlers/patching.rs b/mbf-agent/src/handlers/patching.rs index bcb778fd..34f8b5df 100644 --- a/mbf-agent/src/handlers/patching.rs +++ b/mbf-agent/src/handlers/patching.rs @@ -4,7 +4,7 @@ use std::path::Path; use log::{info, warn}; -use crate::{mod_man::ModManager, models::response::Response, patching, paths}; +use crate::{mod_man::ModManager, models::response::Response, parameters::PARAMETERS, patching}; use anyhow::{anyhow, Context, Result}; /// Handles `GetDowngradedManifest` [Requests](requests::Request). @@ -41,7 +41,7 @@ pub(super) fn handle_patch( super::mod_status::get_app_info()?.ok_or(anyhow!("Cannot patch when app not installed"))?; let res_cache = crate::load_res_cache()?; - std::fs::create_dir_all(paths::TEMP)?; + std::fs::create_dir_all(&PARAMETERS.temp)?; // Either downgrade or just patch the current APK depending on the caller's choice. let patching_result = if let Some(to_version) = &downgrade_to { @@ -58,7 +58,7 @@ pub(super) fn handle_patch( ))?; patching::downgrade_and_mod_apk( - Path::new(paths::TEMP), + Path::new(&PARAMETERS.temp), &app_info, version_diffs, manifest_mod, @@ -69,7 +69,7 @@ pub(super) fn handle_patch( .context("Downgrading and patching APK") } else { patching::mod_current_apk( - Path::new(paths::TEMP), + Path::new(&PARAMETERS.temp), &app_info, manifest_mod, repatch, @@ -82,7 +82,7 @@ pub(super) fn handle_patch( }; // No matter what, make sure that all temporary files are gone. - std::fs::remove_dir_all(paths::TEMP)?; + std::fs::remove_dir_all(&PARAMETERS.temp)?; if let Some(splash_path) = vr_splash_path { std::fs::remove_file(splash_path)?; } diff --git a/mbf-agent/src/handlers/utility.rs b/mbf-agent/src/handlers/utility.rs index 43df4ebe..dd4b7033 100644 --- a/mbf-agent/src/handlers/utility.rs +++ b/mbf-agent/src/handlers/utility.rs @@ -2,7 +2,7 @@ use std::path::Path; -use crate::{data_fix, mod_man::ModManager, models::response::Response, patching, paths}; +use crate::{data_fix, mod_man::ModManager, models::response::Response, parameters::PARAMETERS, patching}; use anyhow::{anyhow, Context, Result}; use log::{debug, info, warn}; @@ -48,21 +48,21 @@ pub(super) fn handle_fix_player_data() -> Result { patching::kill_app()?; // Kill app, in case it's still stuck in a hanging state let mut did_work = false; - if Path::new(paths::DATAKEEPER_PLAYER_DATA).exists() { + if Path::new(&PARAMETERS.datakeeper_player_data).exists() { info!("Fixing color scheme issues"); - data_fix::fix_colour_schemes(paths::DATAKEEPER_PLAYER_DATA)?; + data_fix::fix_colour_schemes(&PARAMETERS.datakeeper_player_data)?; did_work = true; } - if Path::new(paths::PLAYER_DATA).exists() { + if Path::new(&PARAMETERS.player_data).exists() { info!("Backing up player data"); patching::backup_player_data()?; info!("Removing (potentially faulty) PlayerData.dat in game files"); - debug!("(removing {})", paths::PLAYER_DATA); - std::fs::remove_file(paths::PLAYER_DATA).context("Deleting faulty player data")?; - if Path::new(paths::PLAYER_DATA_BAK).exists() { - std::fs::remove_file(paths::PLAYER_DATA_BAK)?; + debug!("(removing {})", &PARAMETERS.player_data); + std::fs::remove_file(&PARAMETERS.player_data).context("Deleting faulty player data")?; + if Path::new(&PARAMETERS.player_data_bak).exists() { + std::fs::remove_file(&PARAMETERS.player_data_bak)?; } did_work = true; } else { diff --git a/mbf-agent/src/main.rs b/mbf-agent/src/main.rs index e3612d58..16102936 100644 --- a/mbf-agent/src/main.rs +++ b/mbf-agent/src/main.rs @@ -6,13 +6,14 @@ mod manifest; mod mod_man; mod models; mod patching; -mod paths; +mod parameters; use anyhow::{Context, Result}; use downloads::DownloadConfig; use log::{debug, error, warn, Level}; use mbf_res_man::res_cache::ResCache; use models::{request, response}; +use parameters::{init_parameters, PARAMETERS}; use serde::{Deserialize, Serialize}; use std::{ io::{BufRead, BufReader, Write}, @@ -22,9 +23,6 @@ use std::{ sync, }; -/// The ID of the APK file that MBF manages. -pub const APK_ID: &str = "com.beatgames.beatsaber"; - #[cfg(feature = "request_timing")] use log::info; #[cfg(feature = "request_timing")] @@ -33,7 +31,7 @@ use std::time::Instant; /// Attempts to delete legacy directories no longer used by MBF to free up space /// Logs on failure pub fn try_delete_legacy_dirs() { - for dir in paths::LEGACY_DIRS { + for dir in &PARAMETERS.legacy_dirs { if Path::new(dir).exists() { match std::fs::remove_dir_all(dir) { Ok(_) => debug!("Successfully removed legacy dir {dir}"), @@ -62,16 +60,16 @@ pub fn get_dl_cfg() -> &'static DownloadConfig<'static> { /// Creates a ResCache for downloading files using mbf_res_man /// This should be reused where possible. pub fn load_res_cache() -> Result> { - std::fs::create_dir_all(paths::RES_CACHE).expect("Failed to create resource cache folder"); + std::fs::create_dir_all(&PARAMETERS.res_cache).expect("Failed to create resource cache folder"); Ok(ResCache::new( - paths::RES_CACHE.into(), + (&PARAMETERS.res_cache).into(), mbf_res_man::default_agent::get_agent(), )) } pub fn get_apk_path() -> Result> { let pm_output = Command::new("pm") - .args(["path", APK_ID]) + .args(["path", &PARAMETERS.apk_id]) .output() .context("Working out APK path")?; if 8 > pm_output.stdout.len() { @@ -153,6 +151,9 @@ fn main() -> Result<()> { reader.read_line(&mut line)?; let req: request::Request = serde_json::from_str(&line)?; + // Set the parameters for this instance of the agent + init_parameters(&req.agent_parameters.game_id, req.agent_parameters.ignore_package_id); + // Set a panic hook that writes the panic as a JSON Log // (we don't do this in catch_unwind as we get an `Any` there, which doesn't implement Display) panic::set_hook(Box::new(|info| { diff --git a/mbf-agent/src/mod_man/loaded_mod.rs b/mbf-agent/src/mod_man/loaded_mod.rs index 234226d0..b5d53b9e 100644 --- a/mbf-agent/src/mod_man/loaded_mod.rs +++ b/mbf-agent/src/mod_man/loaded_mod.rs @@ -4,7 +4,7 @@ use std::{collections::HashSet, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; -use crate::paths; +use crate::parameters::PARAMETERS; use super::{util, ModInfo}; use anyhow::{Result, Context}; @@ -84,17 +84,17 @@ impl Mod { util::copy_files_from_mod_folder( &self.loaded_from, &self.manifest().mod_files, - paths::EARLY_MODS, + &PARAMETERS.early_mods, )?; util::copy_files_from_mod_folder( &self.loaded_from, &self.manifest().library_files, - paths::LIBS, + &PARAMETERS.libs, )?; util::copy_files_from_mod_folder( &self.loaded_from, &self.manifest().late_mod_files, - paths::LATE_MODS, + &PARAMETERS.late_mods, )?; self.copy_file_copies().context("Copying auxillary files")?; @@ -114,11 +114,11 @@ impl Mod { // Delete all mod binary files. util::remove_file_names_from_folder( self.manifest().mod_files.iter(), - paths::EARLY_MODS, + &PARAMETERS.early_mods, )?; util::remove_file_names_from_folder( self.manifest().late_mod_files.iter(), - paths::LATE_MODS, + &PARAMETERS.late_mods, )?; util::remove_file_names_from_folder( // Only delete libraries not in use (!) @@ -127,7 +127,7 @@ impl Mod { .library_files .iter() .filter(|lib_file| !retained_libs.contains(OsStr::new(lib_file))), - paths::LIBS, + &PARAMETERS.libs, )?; // Delete all file copies. @@ -198,9 +198,9 @@ impl Mod { /// destinations. fn check_if_files_copied(manifest: &ModInfo) -> Result { Ok( - util::files_exist_in_dir(paths::EARLY_MODS, manifest.mod_files.iter())? - && util::files_exist_in_dir(paths::LATE_MODS, manifest.late_mod_files.iter())? - && util::files_exist_in_dir(paths::LIBS, manifest.library_files.iter())? + util::files_exist_in_dir(&PARAMETERS.early_mods, manifest.mod_files.iter())? + && util::files_exist_in_dir(&PARAMETERS.late_mods, manifest.late_mod_files.iter())? + && util::files_exist_in_dir(&PARAMETERS.libs, manifest.library_files.iter())? && manifest .file_copies .iter() diff --git a/mbf-agent/src/mod_man/mod.rs b/mbf-agent/src/mod_man/mod.rs index 1264fa80..0e708c6a 100644 --- a/mbf-agent/src/mod_man/mod.rs +++ b/mbf-agent/src/mod_man/mod.rs @@ -28,7 +28,7 @@ use mbf_res_man::{ use mbf_zip::ZipFile; use semver::Version; -use crate::{downloads, paths}; +use crate::{downloads, parameters::PARAMETERS}; /// The JSON schema for the `mod.json` file within a qmod. /// This is the same schema used by QuestPatcher. @@ -76,7 +76,7 @@ impl<'cache> ModManager<'cache> { ) .expect("QMOD schema should be a valid JSON schema"), // Each game version stores its QMODs in a different directory. - qmods_dir: paths::QMODS.replace('$', &game_version), + qmods_dir: (&PARAMETERS.qmods).replace('$', &game_version), game_version, res_cache, mod_repo: None, @@ -89,10 +89,10 @@ impl<'cache> ModManager<'cache> { // Wipe all mod directories, if they exist. let to_remove = [ - paths::OLD_QMODS, - paths::LATE_MODS, - paths::EARLY_MODS, - paths::LIBS, + &PARAMETERS.old_qmods, + &PARAMETERS.late_mods, + &PARAMETERS.early_mods, + &PARAMETERS.libs, &self.qmods_dir, ]; for path in to_remove { @@ -450,14 +450,14 @@ impl<'cache> ModManager<'cache> { /// Will do nothing if the old mods directory does not exist. /// Returns true if any old QMODs were found fn load_old_qmods(&mut self) -> Result { - if !Path::new(paths::OLD_QMODS).exists() { + if !Path::new(&PARAMETERS.old_qmods).exists() { return Ok(false); } warn!("Migrating mods from legacy folder"); let mut found_qmod = false; for stat_result in - std::fs::read_dir(paths::OLD_QMODS).context("Reading old QMODs directory")? + std::fs::read_dir(&PARAMETERS.old_qmods).context("Reading old QMODs directory")? { let stat = stat_result?; @@ -474,7 +474,7 @@ impl<'cache> ModManager<'cache> { found_qmod = true; std::fs::remove_file(stat.path()).context("Deleting legacy mod")?; } - std::fs::remove_dir(paths::OLD_QMODS)?; + std::fs::remove_dir(&PARAMETERS.old_qmods)?; Ok(found_qmod) } @@ -741,13 +741,13 @@ impl<'cache> ModManager<'cache> { /// and the Packages directory that stores the extracted QMODs for the current game version. fn create_mods_dir(&self) -> Result<()> { std::fs::create_dir_all(&self.qmods_dir)?; - std::fs::create_dir_all(paths::LATE_MODS)?; - std::fs::create_dir_all(paths::EARLY_MODS)?; - std::fs::create_dir_all(paths::LIBS)?; + std::fs::create_dir_all(&PARAMETERS.late_mods)?; + std::fs::create_dir_all(&PARAMETERS.early_mods)?; + std::fs::create_dir_all(&PARAMETERS.libs)?; OpenOptions::new() .create(true) .write(true) - .open(paths::MODDATA_NOMEDIA) + .open(&PARAMETERS.moddata_nomedia) .context("Creating nomedia file")?; Ok(()) diff --git a/mbf-agent/src/models/request.rs b/mbf-agent/src/models/request.rs index ffe901bc..4218edac 100644 --- a/mbf-agent/src/models/request.rs +++ b/mbf-agent/src/models/request.rs @@ -1,12 +1,53 @@ //! Models used for communication *from the frontend to the backend*. use std::collections::HashMap; - use serde::Deserialize; +/// The default value for the game_id parameter. +fn default_game_id() -> String { + "com.beatgames.beatsaber".to_string() +} + +/// The default value for the ignore_package_id parameter. +fn default_ignore_package_id() -> bool { + false +} + +/// The default value for the agent_parameters parameter. +fn default_agent_parameters() -> AgentRequestParameters { + AgentRequestParameters { + game_id: default_game_id(), + ignore_package_id: default_ignore_package_id(), + } +} + +/// A struct that contains the game ID to be used for the agent and a flag indicating whether to skip the game ID check. +#[derive(Deserialize)] +pub struct AgentRequestParameters { + /// The ID of the Beat Saber APK on the device. + #[serde(default = "default_game_id")] + pub game_id: String, + + /// If true, the game ID check is skipped during qmod install. + #[serde(default = "default_ignore_package_id")] + pub ignore_package_id: bool, +} + +/// A struct that represents a request from the frontend to the agent. +#[derive(Deserialize)] +pub struct Request { + /// The parameters for the agent, including the game ID and whether to skip the game ID check. + #[serde(default = "default_agent_parameters")] + pub agent_parameters: AgentRequestParameters, + + #[serde(flatten)] + pub request: RequestEnum, +} + +/// An enum that represents the different types of requests that can be made to the agent. #[derive(Deserialize)] #[serde(tag = "type")] -pub enum Request { +pub enum RequestEnum { /// Gathers several pieces of data to check that the installation is modded appropriately, including: /// - Whether the APK is patched /// - The APK version diff --git a/mbf-agent/src/parameters.rs b/mbf-agent/src/parameters.rs new file mode 100644 index 00000000..1fcc11e6 --- /dev/null +++ b/mbf-agent/src/parameters.rs @@ -0,0 +1,161 @@ +//! Module containing all of the fixed file paths used by MBF, for easy changing throughout the project. + +use std::sync::{OnceLock, LazyLock}; + +/// A OnceLock that contains the default parameters for the agent. +static _PARAMETERS: OnceLock = OnceLock::new(); + +/// Initializes the parameters for the agent with the given game ID and ignore_package_id flag. +/// This function is to be called once at the start of the program to set up the parameters. +/// +/// # Arguments +/// +/// * `game_id` - The game ID to be used for the agent. +/// * `ignore_package_id` - A boolean flag indicating whether to skip the game ID check. +/// +/// # Returns +/// +/// Returns true if the parameters were successfully initialized, false if they were already initialized. +pub fn init_parameters(game_id: &str, ignore_package_id: bool) -> bool { + _PARAMETERS.set(AgentParameters::new(game_id, ignore_package_id)).is_ok() +} + +/// A static reference to the parameters for the agent. +pub static PARAMETERS: LazyLock = LazyLock::new(|| _PARAMETERS.get_or_init(|| AgentParameters::new("com.beatgames.beatsaber", false)).clone()); + +/// A struct that contains the parameters for the agent, including various file paths and settings. +#[derive(Clone)] +pub struct AgentParameters { + /// The APK ID of the app being modded. + pub apk_id: String, + + /// If true, the game ID check is skipped during qmod install. + pub _ignore_package_id: bool, + + /// Directory that QMOD files are stored in. + /// `$` is replaced with the game version + pub qmods: String, + + /// The legacy directory used to contain QMOD files in older builds of MBF. + pub old_qmods: String, + + /// The path of the `.nomedia` file added to ModData. + pub moddata_nomedia: String, + + /// Directory containing the modloader. + pub modloader_dir: String, + + /// Directory containing installed late mod files. + pub late_mods: String, + + /// Directory containing installed early mod files. + pub early_mods: String, + + /// Directory containing installed library files. + pub libs: String, + + /// The Android `files` directory for the app being modded. + pub _android_app_files: String, + + /// Path of the `PlayerData.dat` in the vanilla game. + pub player_data: String, + + /// Path of the backup `PlayerData.dat` in the vanilla game. + pub player_data_bak: String, + + /// Directory containing OBBs for the app. + pub obb_dir: String, + + /// Path to the `PlayerData.dat` of the `datakeeper` mod. + pub datakeeper_player_data: String, + + /// An auxillary path that `PlayerData.dat` is copied to when modding in case it is corrupted/lost for any other reason. + pub aux_data_backup: String, + + /// The folder that SongCore loads custom levels from. + pub custom_levels: String, + + /// A folder that MBF uses to download temporary files. + pub mbf_downloads: String, + + /// Temporary folder used by MBF during patching. + pub temp: String, + + /// Path to the MBF resource cache. + pub res_cache: String, + + /// Directories no longer used by MBF that should be deleted on startup if detected. + pub legacy_dirs: [String; 4], +} + +/// Implements the `AgentParameters` struct, which contains various paths and settings used by the agent. +impl AgentParameters { + /// Creates a new instance of `AgentParameters` with paths initialized based on the provided APK ID. + /// + /// # Arguments + /// + /// * `apk_id` - A string slice that holds the APK ID, which is used to construct various paths. + /// + /// # Returns + /// + /// Returns an instance of `AgentParameters` with all fields populated based on the provided APK ID. + /// + /// # Example + /// + /// ```rust + /// let apk_id = "com.example.app"; + /// let paths = AgentParameters::new(apk_id); + /// println!("{}", paths.qmods); // Outputs: /sdcard/ModData/com.example.app/Packages/$ + /// ``` + pub fn new<'a>(apk_id: &str, ignore_package_id: bool) -> AgentParameters { + let local_tmp = "/data/local/tmp"; + + let apk_id = format!("{apk_id}").to_string(); + let qmods = format!("/sdcard/ModData/{apk_id}/Packages/$").to_string(); + let old_qmods = "/sdcard/ModsBeforeFriday/Mods".to_string(); + let moddata_nomedia = format!("/sdcard/ModData/{apk_id}/.nomedia").to_string(); + let modloader_dir = format!("/sdcard/ModData/{apk_id}/Modloader").to_string(); + let late_mods = format!("{modloader_dir}/mods").to_string(); + let early_mods = format!("{modloader_dir}/early_mods").to_string(); + let libs = format!("{modloader_dir}/libs").to_string(); + let android_app_files = format!("/sdcard/Android/data/{apk_id}/files").to_string(); + let player_data = format!("{android_app_files}/PlayerData.dat").to_string(); + let player_data_bak = format!("{android_app_files}/PlayerData.dat.bak").to_string(); + let obb_dir = format!("/sdcard/Android/obb/{apk_id}/").to_string(); + let datakeeper_player_data = format!("/sdcard/ModData/{apk_id}/Mods/datakeeper/PlayerData.dat").to_string(); + let aux_data_backup = "/sdcard/ModsBeforeFriday/PlayerData.backup.dat".to_string(); + let custom_levels = format!("/sdcard/ModData/{apk_id}/Mods/SongCore/CustomLevels").to_string(); + let mbf_downloads = format!("{local_tmp}/mbf/downloads").to_string(); + let temp = format!("{local_tmp}/mbf/tmp").to_string(); + let res_cache = format!("{local_tmp}/mbf/res-cache").to_string(); + let legacy_dirs = [ + format!("{local_tmp}/mbf-downloads").to_string(), + format!("{local_tmp}/mbf-res-cache").to_string(), + format!("{local_tmp}/mbf-tmp").to_string(), + format!("{local_tmp}/mbf-uploads").to_string(), + ]; + + Self { + apk_id, + _ignore_package_id: ignore_package_id, + qmods, + old_qmods, + moddata_nomedia, + modloader_dir, + late_mods, + early_mods, + libs, + _android_app_files: android_app_files, + player_data, + player_data_bak, + obb_dir, + datakeeper_player_data, + aux_data_backup, + custom_levels, + mbf_downloads, + temp, + res_cache, + legacy_dirs + } + } +} diff --git a/mbf-agent/src/patching.rs b/mbf-agent/src/patching.rs index 35e7ac38..086abdf0 100644 --- a/mbf-agent/src/patching.rs +++ b/mbf-agent/src/patching.rs @@ -10,7 +10,7 @@ use crate::{ data_fix::fix_colour_schemes, downloads, models::response::{AppInfo, InstallStatus, ModLoader}, - paths, ModTag, APK_ID, + ModTag, parameters::PARAMETERS, }; use anyhow::{anyhow, Context, Result}; use log::{info, warn}; @@ -73,7 +73,7 @@ pub fn mod_current_apk( let obb_backup = temp_path.join("obbs"); std::fs::create_dir_all(&obb_backup)?; let obb_backups = - save_obbs(Path::new(paths::OBB_DIR), &obb_backup).context("Saving OBB files")?; + save_obbs(Path::new(&PARAMETERS.obb_dir), &obb_backup).context("Saving OBB files")?; patch_and_reinstall( libunity_path, @@ -128,7 +128,7 @@ pub fn downgrade_and_mod_apk( std::fs::create_dir_all(&obb_backup_dir).context("Creating OBB backup directory")?; let mut obb_backup_paths = Vec::new(); for obb_diff in &diffs.obb_diffs { - let obb_path = Path::new(paths::OBB_DIR).join(&obb_diff.file_name); + let obb_path = Path::new(&PARAMETERS.obb_dir).join(&obb_diff.file_name); if !obb_path.exists() { return Err(anyhow!( "Obb file {} did not exist, is the Beat Saber installation corrupt", @@ -146,7 +146,7 @@ pub fn downgrade_and_mod_apk( // Beat Saber DLC asset files do not have the .obb suffix. // If there are any DLC, then these have been deleted by the patching process so we return true so that the user can later be informed of this. - let contains_dlc = has_file_with_no_extension(paths::OBB_DIR).context("Checking for DLC")?; + let contains_dlc = has_file_with_no_extension(&PARAMETERS.obb_dir).context("Checking for DLC")?; patch_and_reinstall( libunity_path, @@ -178,7 +178,7 @@ fn has_file_with_no_extension(obb_dir: impl AsRef) -> Result { pub fn kill_app() -> Result<()> { info!("Killing Beat Saber"); - Command::new("am").args(&["force-stop", APK_ID]).output()?; + Command::new("am").args(&["force-stop", &PARAMETERS.apk_id]).output()?; Ok(()) } @@ -202,16 +202,16 @@ fn patch_and_reinstall( ) .context("Patching APK")?; - if Path::new(paths::PLAYER_DATA).exists() { + if Path::new(&PARAMETERS.player_data).exists() { info!("Backing up player data"); backup_player_data().context("Backing up player data")?; } else { info!("No player data to backup"); } - if Path::new(paths::DATAKEEPER_PLAYER_DATA).exists() { + if Path::new(&PARAMETERS.datakeeper_player_data).exists() { info!("Fixing colour schemes in backed up PlayerData.dat"); - match fix_colour_schemes(paths::DATAKEEPER_PLAYER_DATA) { + match fix_colour_schemes(&PARAMETERS.datakeeper_player_data) { Ok(_) => {} Err(err) => warn!("Failed to fix colour schemes: {err}"), } @@ -221,7 +221,7 @@ fn patch_and_reinstall( std::fs::remove_file(temp_apk_path)?; info!("Restoring OBB files"); - restore_obb_files(Path::new(paths::OBB_DIR), obb_paths).context("Restoring OBB files")?; + restore_obb_files(Path::new(&PARAMETERS.obb_dir), obb_paths).context("Restoring OBB files")?; // Player data is not restored back to the `files` directory as we cannot correctly set its permissions so that BS can access it. // (which causes a black screen that can only be fixed by manually deleting the file) @@ -230,18 +230,18 @@ fn patch_and_reinstall( } pub fn backup_player_data() -> Result<()> { - info!("Copying to {}", paths::AUX_DATA_BACKUP); + info!("Copying to {}", &PARAMETERS.aux_data_backup); - std::fs::create_dir_all(Path::new(paths::AUX_DATA_BACKUP).parent().unwrap())?; - std::fs::copy(paths::PLAYER_DATA, paths::AUX_DATA_BACKUP)?; + std::fs::create_dir_all(Path::new(&PARAMETERS.aux_data_backup).parent().unwrap())?; + std::fs::copy(&PARAMETERS.player_data, &PARAMETERS.aux_data_backup)?; - if Path::new(paths::DATAKEEPER_PLAYER_DATA).exists() { - warn!("Did not backup PlayerData.dat to datakeeper folder as there was already a PlayerData.dat there. - The player data is still safe in {}", paths::AUX_DATA_BACKUP); + if Path::new(&PARAMETERS.datakeeper_player_data).exists() { + warn!("Did not backup PlayerData.dat to datakeeper folder as there was already a PlayerData.dat there. + The player data is still safe in {}", &PARAMETERS.aux_data_backup); } else { - info!("Copying to {}", paths::DATAKEEPER_PLAYER_DATA); - std::fs::create_dir_all(Path::new(paths::DATAKEEPER_PLAYER_DATA).parent().unwrap())?; - std::fs::copy(paths::PLAYER_DATA, paths::DATAKEEPER_PLAYER_DATA)?; + info!("Copying to {}", &PARAMETERS.datakeeper_player_data); + std::fs::create_dir_all(Path::new(&PARAMETERS.datakeeper_player_data).parent().unwrap())?; + std::fs::copy(&PARAMETERS.player_data, &PARAMETERS.datakeeper_player_data)?; } Ok(()) @@ -253,7 +253,7 @@ fn reinstall_modded_app( ) -> Result<()> { info!("Reinstalling modded app"); Command::new("pm") - .args(["uninstall", APK_ID]) + .args(["uninstall", &PARAMETERS.apk_id]) .output() .context("Uninstalling vanilla APK")?; @@ -264,17 +264,17 @@ fn reinstall_modded_app( info!("Granting external storage permission"); Command::new("appops") - .args(["set", "--uid", APK_ID, "MANAGE_EXTERNAL_STORAGE", "allow"]) + .args(["set", "--uid", &PARAMETERS.apk_id, "MANAGE_EXTERNAL_STORAGE", "allow"]) .output()?; // Quest 1 specific permissions if device_pre_v51 { info!("Granting WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE (Quest 1)"); Command::new("pm") - .args(["grant", APK_ID, "android.permission.WRITE_EXTERNAL_STORAGE"]) + .args(["grant", &PARAMETERS.apk_id, "android.permission.WRITE_EXTERNAL_STORAGE"]) .output()?; Command::new("pm") - .args(["grant", APK_ID, "android.permission.READ_EXTERNAL_STORAGE"]) + .args(["grant", &PARAMETERS.apk_id, "android.permission.READ_EXTERNAL_STORAGE"]) .output()?; } @@ -357,7 +357,7 @@ fn save_libunity( temp_path: impl AsRef, version: &str, ) -> Result> { - let url = match external_res::get_libunity_url(res_cache, APK_ID, version)? { + let url = match external_res::get_libunity_url(res_cache, &PARAMETERS.apk_id, version)? { Some(url) => url, None => return Ok(None), // No libunity for this version }; @@ -404,7 +404,7 @@ fn restore_obb_files(restore_dir: &Path, obb_backups: Vec) -> Result<() } pub fn get_modloader_path() -> Result { - let modloaders_path = format!("/sdcard/ModData/{APK_ID}/Modloader/"); + let modloaders_path = format!("{}/", &PARAMETERS.modloader_dir); std::fs::create_dir_all(&modloaders_path)?; Ok(PathBuf::from(modloaders_path).join(MODLOADER_NAME)) @@ -572,12 +572,12 @@ pub fn get_modloader_installed(apk: &mut ZipFile) -> Result1.35.0, which all use OBBs so if the obb is not present /// the installation is invalid and we need to prompt the user to uninstall it. pub fn check_obb_present() -> Result { - if !Path::new(paths::OBB_DIR).exists() { + if !Path::new(&PARAMETERS.obb_dir).exists() { return Ok(false); } // Check if any of the files in the OBB directory have extension OBB - Ok(std::fs::read_dir(paths::OBB_DIR)?.any(|stat_res| { + Ok(std::fs::read_dir(&PARAMETERS.obb_dir)?.any(|stat_res| { stat_res.is_ok_and(|path| { path.path() .extension() diff --git a/mbf-agent/src/paths.rs b/mbf-agent/src/paths.rs deleted file mode 100644 index bcc8a77e..00000000 --- a/mbf-agent/src/paths.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Module containing all of the fixed file paths used by MBF, for easy changing throughout the project. - -use crate::APK_ID; -use const_format::formatcp; - -/// Directory that QMOD files are stored in. -/// `$` is replaced with the game version -pub const QMODS: &str = formatcp!("/sdcard/ModData/{APK_ID}/Packages/$"); -/// The legacy directory used to contain QMOD files in older builds of MBF. -pub const OLD_QMODS: &str = "/sdcard/ModsBeforeFriday/Mods"; -/// The path of the `.nomedia` file added to ModData. -pub const MODDATA_NOMEDIA: &str = formatcp!("/sdcard/ModData/{APK_ID}/.nomedia"); -/// Directory containing the modloader. -pub const MODLOADER_DIR: &str = formatcp!("/sdcard/ModData/{APK_ID}/Modloader"); -/// Directory containing installed late mod files. -pub const LATE_MODS: &str = formatcp!("{MODLOADER_DIR}/mods"); -/// Directory containing installed early mod files. -pub const EARLY_MODS: &str = formatcp!("{MODLOADER_DIR}/early_mods"); -/// Directory containing installed library files. -pub const LIBS: &str = formatcp!("{MODLOADER_DIR}/libs"); -/// The Android `files` directory for the app being modded. -pub const ANDROID_APP_FILES: &str = formatcp!("/sdcard/Android/data/{APK_ID}/files"); -/// Path of the `PlayerData.dat` in the vanilla game. -pub const PLAYER_DATA: &str = formatcp!("{ANDROID_APP_FILES}/PlayerData.dat"); -/// Path of the backup `PlayerData.dat` in the vanilla game. -pub const PLAYER_DATA_BAK: &str = formatcp!("{ANDROID_APP_FILES}/PlayerData.dat.bak"); -/// Directory containing OBBs for the app. -pub const OBB_DIR: &str = formatcp!("/sdcard/Android/obb/{APK_ID}/"); - -/// Path to the `PlayerData.dat` of the `datakeeper` mod. -pub const DATAKEEPER_PLAYER_DATA: &str = - "/sdcard/ModData/com.beatgames.beatsaber/Mods/datakeeper/PlayerData.dat"; -/// An auxillary path that `PlayerData.dat` is copied to when modding in case it is corrupted/lost for any other reason. -pub const AUX_DATA_BACKUP: &str = "/sdcard/ModsBeforeFriday/PlayerData.backup.dat"; - -/// The folder that SongCore loads custom levels from. -pub const CUSTOM_LEVELS: &str = formatcp!("/sdcard/ModData/{APK_ID}/Mods/SongCore/CustomLevels"); -/// A folder that MBF uses to download temporary files. -pub const MBF_DOWNLOADS: &str = "/data/local/tmp/mbf/downloads"; -/// Temporary folder used by MBF during patching. -pub const TEMP: &str = "/data/local/tmp/mbf/tmp"; -/// Path to the MBF resource cache. -pub const RES_CACHE: &str = "/data/local/tmp/mbf/res-cache"; -/// Directories no longer used by MBF that should be deleted on startup if detected. -pub const LEGACY_DIRS: &[&str] = &[ - "/data/local/tmp/mbf-downloads", - "/data/local/tmp/mbf-res-cache", - "/data/local/tmp/mbf-tmp", - "/data/local/tmp/mbf-uploads", -]; diff --git a/mbf-site/src/Agent.ts b/mbf-site/src/Agent.ts index e2ca1efa..5fb4541b 100644 --- a/mbf-site/src/Agent.ts +++ b/mbf-site/src/Agent.ts @@ -1,9 +1,10 @@ import { AdbSync, AdbSyncWriteOptions, Adb, encodeUtf8 } from '@yume-chan/adb'; import { Consumable, ConcatStringStream, TextDecoderStream, MaybeConsumable, ReadableStream } from '@yume-chan/stream-extra'; -import { Request, Response, LogMsg, ModStatus, Mods, FixedPlayerData, ImportResult, DowngradedManifest, Patched, ModSyncResult } from "./Messages"; +import { Request, Response, LogMsg, ModStatus, Mods, FixedPlayerData, ImportResult, DowngradedManifest, Patched, ModSyncResult, AgentParameters } from "./Messages"; import { AGENT_SHA1 } from './agent_manifest'; import { toast } from 'react-toastify'; import { Log } from './Logging'; +import { gameId, ignorePackageId } from './game_info'; const AgentPath: string = "/data/local/tmp/mbf-agent"; const UploadsPath: string = "/data/local/tmp/mbf/uploads/"; @@ -138,7 +139,14 @@ async function downloadAgent(): Promise { } async function sendRequest(adb: Adb, request: Request): Promise { - let command_buffer = encodeUtf8(JSON.stringify(request) + "\n"); + let wrappedRequest: AgentParameters & Request = { + agent_parameters: { + game_id: gameId, + ignore_package_id: ignorePackageId + }, + ...request + } + let command_buffer = encodeUtf8(JSON.stringify(wrappedRequest) + "\n"); let agentProcess = await adb.subprocess.spawn(AgentPath); diff --git a/mbf-site/src/DeviceModder.tsx b/mbf-site/src/DeviceModder.tsx index 8311cffc..e90ec990 100644 --- a/mbf-site/src/DeviceModder.tsx +++ b/mbf-site/src/DeviceModder.tsx @@ -15,6 +15,7 @@ import { wrapOperation } from './SyncStore'; import { OpenLogsButton } from './components/OpenLogsButton'; import { lte as semverLte } from 'semver'; import { useDeviceStore } from './DeviceStore'; +import { gameId } from './game_info'; interface DeviceModderProps { device: Adb, @@ -24,7 +25,7 @@ interface DeviceModderProps { } export async function uninstallBeatSaber(device: Adb) { - await device.subprocess.spawnAndWait("pm uninstall com.beatgames.beatsaber"); + await device.subprocess.spawnAndWait(`pm uninstall ${gameId}`); } const isDeveloperUrl: boolean = new URLSearchParams(window.location.search).get("dev") === "true"; diff --git a/mbf-site/src/Messages.ts b/mbf-site/src/Messages.ts index cf596339..74bf94cf 100644 --- a/mbf-site/src/Messages.ts +++ b/mbf-site/src/Messages.ts @@ -52,6 +52,13 @@ export interface GetDowngradedManifest { version: string } +export interface AgentParameters { + agent_parameters: { + game_id: String, + ignore_package_id: boolean + } +} + export type Request = GetModStatus | Patch | SetModsEnabled | diff --git a/mbf-site/src/components/OptionsMenu.tsx b/mbf-site/src/components/OptionsMenu.tsx index 43826b39..f2eef932 100644 --- a/mbf-site/src/components/OptionsMenu.tsx +++ b/mbf-site/src/components/OptionsMenu.tsx @@ -13,6 +13,7 @@ import { Log } from '../Logging'; import { Modal } from './Modal'; import { SplashScreenSelector } from './SplashScreenSelector'; import { useDeviceStore } from '../DeviceStore'; +import { gameId } from '../game_info'; interface OptionsMenuProps { setModStatus: (status: ModStatus) => void, @@ -48,7 +49,7 @@ function ModTools({ quit, modStatus, setModStatus }: { const setError = useSetError("Failed to kill Beat Saber process"); try { - await device.subprocess.spawnAndWait("am force-stop com.beatgames.beatsaber"); + await device.subprocess.spawnAndWait(`am force-stop ${gameId}`); toast.success("Successfully killed Beat Saber"); } catch(e) { setError(e); diff --git a/mbf-site/src/game_info.ts b/mbf-site/src/game_info.ts new file mode 100644 index 00000000..a31bf276 --- /dev/null +++ b/mbf-site/src/game_info.ts @@ -0,0 +1,2 @@ +export const gameId = new URLSearchParams(window.location.search).get("game_id") || "com.beatgames.beatsaber" +export const ignorePackageId = new URLSearchParams(window.location.search).get("ignore_package_id") == "true"