diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2c52bb1..8d59f4a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2519,6 +2519,7 @@ dependencies = [ "flate2", "fs_extra", "futures-util", + "glob", "log", "reqwest", "rev_buf_reader", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3645b63..dac52e4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ fern = { version = "0.6.1", features = ["date-based", "colored"] } flate2 = "1.0.26" fs_extra = "1.3.0" futures-util = "0.3.26" +glob = "0.3.1" log = "0.4.19" reqwest = { version = "0.11", features = ["json"] } rev_buf_reader = "0.3.0" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 8e117ac..d105e77 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2,6 +2,7 @@ use serde::{Serialize, Serializer}; pub mod binaries; pub mod config; +pub mod features; pub mod game; pub mod logging; pub mod support; @@ -32,6 +33,8 @@ pub enum CommandError { BinaryExecution(String), #[error("{0}")] Support(String), + #[error("{0}")] + GameFeatures(String), } impl Serialize for CommandError { diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 496b3f5..7aa9a2a 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -352,3 +352,42 @@ pub async fn set_bypass_requirements( })?; Ok(()) } + +#[tauri::command] +pub async fn get_enabled_texture_packs( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, +) -> Result, CommandError> { + let config_lock = config.lock().await; + Ok(config_lock.game_enabled_textured_packs(&game_name)) +} + +#[tauri::command] +pub async fn cleanup_enabled_texture_packs( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, + cleanup_list: Vec, +) -> Result<(), CommandError> { + let mut config_lock = config.lock().await; + config_lock + .cleanup_game_enabled_texture_packs(&game_name, cleanup_list) + .map_err(|_| { + CommandError::Configuration("Unable to cleanup enabled texture packs".to_owned()) + })?; + Ok(()) +} + +#[tauri::command] +pub async fn set_enabled_texture_packs( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, + packs: Vec, +) -> Result<(), CommandError> { + let mut config_lock = config.lock().await; + config_lock + .set_game_enabled_texture_packs(&game_name, packs) + .map_err(|_| { + CommandError::Configuration("Unable to persist change to enabled texture packs".to_owned()) + })?; + Ok(()) +} diff --git a/src-tauri/src/commands/features.rs b/src-tauri/src/commands/features.rs new file mode 100644 index 0000000..bc5b90f --- /dev/null +++ b/src-tauri/src/commands/features.rs @@ -0,0 +1,316 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + config::LauncherConfig, + util::{ + file::{create_dir, delete_dir, overwrite_dir}, + zip::{check_if_zip_contains_top_level_dir, extract_zip_file}, + }, +}; + +use super::CommandError; + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TexturePackInfo { + #[serde(skip_deserializing)] + file_list: Vec, + #[serde(skip_deserializing)] + has_metadata: bool, + #[serde(skip_deserializing)] + cover_image_path: Option, + name: String, + version: String, + author: String, + release_date: String, + supported_games: Vec, + description: String, + tags: Vec, +} + +#[tauri::command] +pub async fn list_extracted_texture_pack_info( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, +) -> Result, CommandError> { + let config_lock = config.lock().await; + let install_path = match &config_lock.installation_dir { + None => return Ok(HashMap::new()), + Some(path) => Path::new(path), + }; + + let expected_path = Path::new(install_path) + .join("features") + .join(&game_name) + .join("texture-packs"); + if !expected_path.exists() || !expected_path.is_dir() { + log::info!( + "No {} folder found, returning no texture packs", + expected_path.display() + ); + return Ok(HashMap::new()); + } + + let entries = std::fs::read_dir(&expected_path).map_err(|_| { + CommandError::GameFeatures(format!( + "Unable to read texture packs from {}", + expected_path.display() + )) + })?; + + let mut package_map = HashMap::new(); + + for entry in entries { + let entry = entry?; + let entry_path = entry.path(); + if entry_path.is_dir() { + let directory_name = entry_path + .file_name() + .and_then(|os_str| os_str.to_str()) + .map(String::from) + .ok_or_else(|| { + CommandError::GameFeatures(format!("Unable to get directory name for {:?}", entry_path)) + })?; + // Get a list of all texture files for this pack + log::info!("Texture pack dir name: {}", directory_name); + let mut file_list = Vec::new(); + for entry in glob::glob( + &entry_path + .join("texture_replacements/**/*.png") + .to_string_lossy(), + ) + .expect("Failed to read glob pattern") + { + match entry { + Ok(path) => { + let relative_path = path + .strip_prefix(&entry_path.join("texture_replacements")) + .map_err(|_| { + CommandError::GameFeatures(format!( + "Unable to read texture packs from {}", + expected_path.display() + )) + })?; + file_list.push(relative_path.display().to_string().replace("\\", "/")); + } + Err(e) => println!("{:?}", e), + } + } + let cover_image_path = match entry_path.join("cover.png").exists() { + true => Some(entry_path.join("cover.png").to_string_lossy().to_string()), + false => None, + }; + let mut pack_info = TexturePackInfo { + file_list, + has_metadata: false, + cover_image_path, + name: directory_name.to_owned(), + version: "Unknown Version".to_string(), + author: "Unknown Author".to_string(), + release_date: "Unknown Release Date".to_string(), + supported_games: vec![game_name.clone()], // if no info, assume it's supported + description: "Unknown Description".to_string(), + tags: vec![], + }; + // Read metadata if it's available + match entry_path.join("metadata.json").exists() { + true => { + match std::fs::read_to_string(entry_path.join("metadata.json")) { + Ok(content) => { + // Serialize from json + match serde_json::from_str::(&content) { + Ok(pack_metadata) => { + pack_info.name = pack_metadata.name; + pack_info.version = pack_metadata.version; + pack_info.author = pack_metadata.author; + pack_info.release_date = pack_metadata.release_date; + pack_info.description = pack_metadata.description; + pack_info.tags = pack_metadata.tags; + } + Err(err) => { + log::error!("Unable to parse {}: {}", &content, err); + } + } + } + Err(err) => { + log::error!( + "Unable to read {}: {}", + entry_path.join("metadata.json").display(), + err + ); + } + }; + } + false => {} + } + package_map.insert(directory_name, pack_info); + } + } + + Ok(package_map) +} + +#[tauri::command] +pub async fn extract_new_texture_pack( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, + zip_path: String, +) -> Result { + let config_lock = config.lock().await; + let install_path = match &config_lock.installation_dir { + None => { + return Err(CommandError::GameFeatures( + "No installation directory set, can't extract texture pack".to_string(), + )) + } + Some(path) => Path::new(path), + }; + + // First, we'll check the zip file to make sure it has a `texture_replacements` folder before extracting + let zip_path_buf = PathBuf::from(zip_path); + let texture_pack_name = match zip_path_buf.file_stem() { + Some(name) => name.to_string_lossy().to_string(), + None => { + return Err(CommandError::GameFeatures( + "Unable to get texture pack name from zip file path".to_string(), + )); + } + }; + let valid_zip = + check_if_zip_contains_top_level_dir(&zip_path_buf, "texture_replacements".to_string()) + .map_err(|err| { + log::error!("Unable to read texture replacement zip file: {}", err); + CommandError::GameFeatures(format!("Unable to read texture replacement pack: {}", err)) + })?; + if !valid_zip { + log::error!( + "Invalid texture pack, no top-level `texture_replacements` folder: {}", + zip_path_buf.display() + ); + return Ok(false); + } + // It's valid, let's extract it. The name of the zip becomes the folder, if one already exists it will be deleted! + let destination_dir = &install_path + .join("features") + .join(game_name) + .join("texture-packs") + .join(&texture_pack_name); + // TODO - delete it + create_dir(destination_dir).map_err(|err| { + log::error!("Unable to create directory for texture pack: {}", err); + CommandError::GameFeatures(format!( + "Unable to create directory for texture pack: {}", + err + )) + })?; + extract_zip_file(&zip_path_buf, &destination_dir, false).map_err(|err| { + log::error!("Unable to read extract replacement pack: {}", err); + CommandError::GameFeatures(format!("Unable to extract texture pack: {}", err)) + })?; + Ok(true) +} + +// TODO - remove duplication +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GameJobStepOutput { + pub success: bool, + pub msg: Option, +} + +#[tauri::command] +pub async fn update_texture_pack_data( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, +) -> Result { + let config_lock = config.lock().await; + let install_path = match &config_lock.installation_dir { + None => { + return Ok(GameJobStepOutput { + success: false, + msg: Some("No installation directory set, can't extract texture pack".to_string()), + }); + } + Some(path) => Path::new(path), + }; + + let game_texture_pack_dir = install_path + .join("active") + .join(&game_name) + .join("data") + .join("texture_replacements"); + // Reset texture replacement directory + delete_dir(&game_texture_pack_dir)?; + create_dir(&game_texture_pack_dir)?; + for pack in config_lock.game_enabled_textured_packs(&game_name) { + let texture_pack_dir = install_path + .join("features") + .join(&game_name) + .join("texture-packs") + .join(&pack) + .join("texture_replacements"); + log::info!("Appending textures from: {}", texture_pack_dir.display()); + match overwrite_dir(&texture_pack_dir, &game_texture_pack_dir) { + Ok(_) => (), + Err(err) => { + log::error!("Unable to update texture replacements: {}", err); + return Ok(GameJobStepOutput { + success: false, + msg: Some(format!("Unable to update texture replacements: {}", err)), + }); + } + } + } + + Ok(GameJobStepOutput { + success: true, + msg: None, + }) +} + +#[tauri::command] +pub async fn delete_texture_packs( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, + packs: Vec, +) -> Result { + let config_lock = config.lock().await; + let install_path = match &config_lock.installation_dir { + None => { + return Ok(GameJobStepOutput { + success: false, + msg: Some("No installation directory set, can't extract texture pack".to_string()), + }); + } + Some(path) => Path::new(path), + }; + + let texture_pack_dir = install_path + .join("features") + .join(&game_name) + .join("texture-packs"); + + for pack in packs { + log::info!("Deleting texture pack: {}", pack); + match delete_dir(&texture_pack_dir.join(&pack)) { + Ok(_) => (), + Err(err) => { + log::error!("Unable to delete texture pack: {}", err); + return Ok(GameJobStepOutput { + success: false, + msg: Some(format!("Unable to delete texture pack: {}", err)), + }); + } + } + } + + Ok(GameJobStepOutput { + success: true, + msg: None, + }) +} diff --git a/src-tauri/src/commands/versions.rs b/src-tauri/src/commands/versions.rs index 9863714..cea747e 100644 --- a/src-tauri/src/commands/versions.rs +++ b/src-tauri/src/commands/versions.rs @@ -111,7 +111,7 @@ pub async fn download_version( })?; // Extract the zip file - extract_and_delete_zip_file(&download_path, &dest_dir).map_err(|_| { + extract_and_delete_zip_file(&download_path, &dest_dir, true).map_err(|_| { CommandError::VersionManagement( "Unable to successfully extract downloaded version".to_owned(), ) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index f529abf..7d80b28 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -94,12 +94,27 @@ impl Serialize for SupportedGame { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GameFeatureConfig { + pub texture_packs: Vec, +} + +impl GameFeatureConfig { + fn default() -> Self { + Self { + texture_packs: vec![], + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GameConfig { pub is_installed: bool, pub version: Option, pub version_folder: Option, + pub features: Option, } impl GameConfig { @@ -108,6 +123,7 @@ impl GameConfig { is_installed: false, version: None, version_folder: None, + features: Some(GameFeatureConfig::default()), } } } @@ -173,6 +189,51 @@ impl LauncherConfig { } } + fn get_supported_game_config_mut( + &mut self, + game_name: &String, + ) -> Result<&mut GameConfig, ConfigError> { + let game = match SupportedGame::from_str(game_name) { + Err(_) => { + log::warn!("Game is not supported: {}", game_name); + return Err(ConfigError::Configuration( + "Game is not supported".to_owned(), + )); + } + Ok(game) => game, + }; + match self.games.get_mut(&game) { + None => { + log::error!("Supported game missing from games map: {}", game_name); + return Err(ConfigError::Configuration(format!( + "Supported game missing from games map: {game_name}" + ))); + } + Some(cfg) => Ok(cfg), + } + } + + fn get_supported_game_config(&mut self, game_name: &String) -> Result<&GameConfig, ConfigError> { + let game = match SupportedGame::from_str(game_name) { + Err(_) => { + log::warn!("Game is not supported: {}", game_name); + return Err(ConfigError::Configuration( + "Game is not supported".to_owned(), + )); + } + Ok(game) => game, + }; + match self.games.get(&game) { + None => { + log::error!("Supported game missing from games map: {}", game_name); + return Err(ConfigError::Configuration(format!( + "Supported game missing from games map: {game_name}" + ))); + } + Some(cfg) => Ok(cfg), + } + } + pub fn load_config(config_dir: Option) -> LauncherConfig { match config_dir { Some(config_dir) => { @@ -439,4 +500,71 @@ impl LauncherConfig { } } } + + pub fn game_enabled_textured_packs(&self, game_name: &String) -> Vec { + // TODO - refactor out duplication + match SupportedGame::from_str(game_name) { + Ok(game) => { + // Retrieve relevant game from config + match self.games.get(&game) { + Some(game) => match &game.features { + Some(features) => features.texture_packs.to_owned(), + None => Vec::new(), + }, + None => { + log::warn!( + "Could not find game to check which texture packs are enabled: {}", + game_name + ); + Vec::new() + } + } + } + Err(_) => { + log::warn!( + "Could not find game to check which texture packs are enabled: {}", + game_name + ); + Vec::new() + } + } + } + + pub fn cleanup_game_enabled_texture_packs( + &mut self, + game_name: &String, + cleanup_list: Vec, + ) -> Result<(), ConfigError> { + if !cleanup_list.is_empty() { + return Ok(()); + } + let game_config = self.get_supported_game_config_mut(game_name)?; + if let Some(features) = &mut game_config.features { + features + .texture_packs + .retain(|pack| !cleanup_list.contains(pack)); + self.save_config()?; + } + Ok(()) + } + + pub fn set_game_enabled_texture_packs( + &mut self, + game_name: &String, + packs: Vec, + ) -> Result<(), ConfigError> { + let game_config = self.get_supported_game_config_mut(game_name)?; + match &mut game_config.features { + Some(features) => { + features.texture_packs = packs; + } + None => { + game_config.features = Some(GameFeatureConfig { + texture_packs: packs, + }); + } + } + self.save_config()?; + Ok(()) + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9c33830..6c28471 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,7 +13,6 @@ use std::io::Write; mod commands; mod config; -mod textures; mod util; fn log_crash(panic_info: Option<&std::panic::PanicInfo>, error: Option) { @@ -124,9 +123,10 @@ fn main() { // // This allows us to avoid hacky globals, and pass around information (in this case, the config) // to the relevant places - app.manage(tokio::sync::Mutex::new( - config::LauncherConfig::load_config(app.path_resolver().app_config_dir()), + let config = tokio::sync::Mutex::new(config::LauncherConfig::load_config( + app.path_resolver().app_config_dir(), )); + app.manage(config); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -137,6 +137,7 @@ fn main() { commands::binaries::run_compiler, commands::binaries::run_decompiler, commands::binaries::update_data_directory, + commands::config::set_enabled_texture_packs, commands::config::delete_old_data_directory, commands::config::finalize_installation, commands::config::get_active_tooling_version_folder, @@ -155,8 +156,14 @@ fn main() { commands::config::set_bypass_requirements, commands::config::set_install_directory, commands::config::set_locale, + commands::config::get_enabled_texture_packs, + commands::config::cleanup_enabled_texture_packs, commands::game::reset_game_settings, commands::game::uninstall_game, + commands::features::update_texture_pack_data, + commands::features::extract_new_texture_pack, + commands::features::list_extracted_texture_pack_info, + commands::features::delete_texture_packs, commands::logging::frontend_log, commands::support::generate_support_package, commands::versions::download_version, diff --git a/src-tauri/src/textures.rs b/src-tauri/src/textures.rs deleted file mode 100644 index 20ef0b3..0000000 --- a/src-tauri/src/textures.rs +++ /dev/null @@ -1,91 +0,0 @@ -// these commands are not yet used, allow dead code in this module -#![allow(dead_code)] - -use serde::{Deserialize, Serialize}; -use std::{ - fs, - io::{self, Read}, - path::{Path, PathBuf}, -}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct TexturePack { - author: String, - description: String, - version: String, - path: Option, -} - -#[tauri::command] -pub async fn extract_textures(app_handle: tauri::AppHandle, textures_array: Vec) { - let text_dir = app_handle - .path_resolver() - .app_data_dir() - .unwrap() - .join("data/texture_replacements"); - - for path in textures_array { - println!( - "Not extracting (not yet implemented) texture pack to {}: {path:?}", - text_dir.display(), - ); - // let archive: Vec = fs::read(&path.clone()).unwrap(); - // // The third parameter allows you to strip away toplevel directories. - // // If `archive` contained a single directory, its contents would be extracted instead. - // match zip_extract::extract(Cursor::new(archive), &target_dir, true) { - // Ok(_) => continue, - // Err(err) => println!("{:?}", err), - // } - } -} - -fn read_texture_json_file(file_path: PathBuf) -> Result { - let zipfile = std::fs::File::open(&file_path)?; - let mut zip = zip::ZipArchive::new(zipfile).unwrap(); - - // TODO: Figure out some top level schenanigans here similar to the zip extract ignoring toplevel - let mut contents = String::new(); - zip - .by_name("texture_replacements/about.json")? - .read_to_string(&mut contents)?; - - let pack: TexturePack = TexturePack { - path: Some(file_path), - ..serde_json::from_str(&contents).unwrap() - }; - Ok(pack) -} - -#[tauri::command] -pub fn get_all_texture_packs(dir: String) -> Vec { - let dir_path = Path::new(&dir).exists(); - if !dir_path { - println!("Textures directory doesn't exist, creating it now."); - fs::create_dir(dir).unwrap(); - return Vec::new(); - } - - let entries = fs::read_dir(dir).unwrap(); - - let mut texture_pack_data: Vec = Vec::new(); - for entry in entries { - let path = entry.unwrap().path(); - match path.extension() { - Some(ext) if ext == "zip" => { - let files = match read_texture_json_file(path.clone()) { - Ok(pack) => pack, - Err(_e) => { - // if the about.json file isn't inside of the expected directory this error happens - // TODO: add this error to a logs file so players know when they install a bad texture pack - println!("File doesn't have proper about.json: {path:?}"); - continue; - } - }; - texture_pack_data.push(files); - } - _ => continue, - } - } - - texture_pack_data -} diff --git a/src-tauri/src/util/zip.rs b/src-tauri/src/util/zip.rs index 25f5913..12e051f 100644 --- a/src-tauri/src/util/zip.rs +++ b/src-tauri/src/util/zip.rs @@ -1,4 +1,4 @@ -use std::io::Cursor; +use std::io::{BufReader, Cursor}; use std::path::PathBuf; use std::{ fs::File, @@ -90,12 +90,42 @@ pub fn append_file_to_zip( Ok(()) } +pub fn extract_zip_file( + zip_path: &PathBuf, + extract_dir: &Path, + strip_top_dir: bool, +) -> Result<(), zip_extract::ZipExtractError> { + let archive: Vec = std::fs::read(zip_path)?; + zip_extract::extract(Cursor::new(archive), extract_dir, strip_top_dir)?; + Ok(()) +} + pub fn extract_and_delete_zip_file( zip_path: &PathBuf, extract_dir: &Path, + strip_top_dir: bool, ) -> Result<(), zip_extract::ZipExtractError> { - let archive: Vec = std::fs::read(zip_path)?; - zip_extract::extract(Cursor::new(archive), extract_dir, true)?; + extract_zip_file(zip_path, extract_dir, strip_top_dir)?; std::fs::remove_file(zip_path)?; Ok(()) } + +pub fn check_if_zip_contains_top_level_dir( + zip_path: &PathBuf, + expected_dir: String, +) -> Result> { + let file = File::open(zip_path)?; + let reader = BufReader::new(file); + let mut zip = zip::ZipArchive::new(reader)?; + for i in 0..zip.len() { + let file = zip.by_index(i)?; + if !file.is_dir() { + continue; + } + // Check if the entry is a directory and has the desired folder name + if file.name().starts_with(&expected_dir) { + return Ok(true); + } + } + Ok(false) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ed46a7b..bda55f9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -48,6 +48,10 @@ "$APP/**/*", "$RESOURCE/**/*" ] + }, + "protocol": { + "asset": true, + "assetScope": ["**"] } }, "windows": [ @@ -65,9 +69,6 @@ "visible": true, "focus": true } - ], - "security": { - "csp": null - } + ] } } diff --git a/src/App.svelte b/src/App.svelte index e9b65f2..d0297a5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -7,7 +7,6 @@ import Sidebar from "./components/sidebar/Sidebar.svelte"; import Background from "./components/background/Background.svelte"; import Header from "./components/header/Header.svelte"; - import Textures from "./routes/Textures.svelte"; import Update from "./routes/Update.svelte"; import GameInProgress from "./components/games/GameInProgress.svelte"; import { isInDebugMode } from "$lib/utils/common"; @@ -16,6 +15,7 @@ import { toastStore } from "$lib/stores/ToastStore"; import { isLoading } from "svelte-i18n"; import { getLocale, setLocale } from "$lib/rpc/config"; + import GameFeature from "./routes/GameFeature.svelte"; let revokeSpecificActions = false; @@ -76,6 +76,12 @@ primary={false} let:params /> + - diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 2381a77..af1035e 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -39,6 +39,8 @@ "requirements_button_bypass_warning_1": "If you believe the requirement checks are false, you can bypass them.", "requirements_button_bypass_warning_2": "However, if you are wrong you should expect issues installing or running the game!", "gameControls_button_play": "Play", + "gameControls_button_features": "Features", + "gameControls_button_features_textures": "Texture Packs", "gameControls_button_advanced": "Advanced", "gameControls_button_playInDebug": "Play in Debug Mode", "gameControls_button_openREPL": "Open REPL", @@ -119,5 +121,17 @@ "splash_step_checkingDirectories": "Checking Directories", "splash_step_pickInstallFolder": "Pick an Installation Folder", "splash_step_finishingUp": "Finishing Up", - "splash_step_errorOpening": "Problem opening Launcher" + "splash_step_errorOpening": "Problem opening Launcher", + "gameJob_deleteTexturePacks": "Deleting Packs", + "gameJob_enablingTexturePacks": "Enabling Packs", + "gameJob_applyTexturePacks": "Applying Packs", + "features_textures_invalidPack": "Invalid texture pack format, ensure it contains a top-level `texture_replacements` folder.", + "features_textures_addNewPack": "Add New Pack", + "features_textures_applyChanges": "Apply Texture Changes", + "features_textures_listHeading": "Currently Added Packs", + "features_textures_description": "You can enable as many packs as you want, but if multiple packs replace the same file the order matters. For example if two packs replace the grass, the first pack in the list will take precedence.", + "features_textures_replacedCount": "Textures replaced", + "features_textures_enabled": "Enabled", + "features_textures_disabled": "Disabled", + "features_textures_conflictsDetected": "Conflicts Detected!" } diff --git a/src/components/games/GameControls.svelte b/src/components/games/GameControls.svelte index f9db498..ea62c80 100644 --- a/src/components/games/GameControls.svelte +++ b/src/components/games/GameControls.svelte @@ -16,6 +16,7 @@ import { platform } from "@tauri-apps/api/os"; import { launchGame, openREPL } from "$lib/rpc/binaries"; import { _ } from "svelte-i18n"; + import { navigate } from "svelte-navigator"; export let activeGame: SupportedGame; @@ -55,20 +56,25 @@ launchGame(getInternalName(activeGame), false); }}>{$_("gameControls_button_play")} - - + + { + navigate(`/${getInternalName(activeGame)}/features/texture_packs`); + }} + > + {$_("gameControls_button_features_textures")} + + - + { launchGame(getInternalName(activeGame), true); @@ -112,7 +118,7 @@ > - + { diff --git a/src/components/games/features/texture-packs/TexturePacks.svelte b/src/components/games/features/texture-packs/TexturePacks.svelte new file mode 100644 index 0000000..2d8b501 --- /dev/null +++ b/src/components/games/features/texture-packs/TexturePacks.svelte @@ -0,0 +1,413 @@ + + + + + + + + + +
+ {#if !loaded} +
+ +
+ {:else} +
+
+ + + {#if pending_changes(availablePacks, availablePacksOriginal)} + + {/if} +
+ {#if packAddingError !== ""} +
+ + {packAddingError} + +
+ {/if} +
+

{$_("features_textures_listHeading")}

+
+
+

+ {$_("features_textures_description")} +

+
+ {#each availablePacks as pack, packIndex} + {#if !pack.toBeDeleted} +
+ + +
+

+ {extractedPackInfo[pack.name]["name"]} + +

+
+

+ {extractedPackInfo[pack.name]["version"]} by {extractedPackInfo[ + pack.name + ]["author"]} +

+

+ {extractedPackInfo[pack.name]["releaseDate"]} +

+

+ {$_("features_textures_replacedCount")} - {num_textures_in_pack( + pack.name + )} +

+

+ {extractedPackInfo[pack.name]["description"]} +

+ {#if extractedPackInfo[pack.name]["tags"].length > 0} +
+ {#each extractedPackInfo[pack.name]["tags"] as tag} + {tag} + {/each} +
+ {/if} + +
+ +
+
+ {#if pack.enabled} + {#if packIndex !== 0} + + {/if} + {#if packIndex !== availablePacks.length - 1} + + {/if} + {/if} + +
+ + {#if find_pack_conflicts(pack.name).size > 0} + + + + + {$_("features_textures_conflictsDetected")} + +
+
+
{[
+                        ...find_pack_conflicts(pack.name),
+                      ]
+                        .join("\n")
+                        .trim()}
+ + + {/if} + +
+ {/if} + {/each} +
+ {/if} +
diff --git a/src/components/games/job/GameJob.svelte b/src/components/games/job/GameJob.svelte index 0863809..3fe7e2a 100644 --- a/src/components/games/job/GameJob.svelte +++ b/src/components/games/job/GameJob.svelte @@ -12,16 +12,184 @@ runDecompiler, updateDataDirectory, } from "$lib/rpc/binaries"; - import { finalizeInstallation } from "$lib/rpc/config"; + import { + finalizeInstallation, + setEnabledTexturePacks, + } from "$lib/rpc/config"; import { generateSupportPackage } from "$lib/rpc/support"; import { _ } from "svelte-i18n"; + import { deleteTexturePacks, updateTexturePackData } from "$lib/rpc/features"; export let activeGame: SupportedGame; export let jobType: Job; + export let texturePacksToDelete: string[] = []; + export let texturePacksToEnable: string[] = []; + const dispatch = createEventDispatcher(); let installationError = undefined; + async function setupDecompileJob() { + installationError = undefined; + progressTracker.init([ + { + status: "queued", + label: $_("setup_decompile"), + }, + { + status: "queued", + label: $_("setup_done"), + }, + ]); + progressTracker.start(); + let resp = await runDecompiler("", getInternalName(activeGame), true); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + progressTracker.proceed(); + } + + async function setupCompileJob() { + installationError = undefined; + progressTracker.init([ + { + status: "queued", + label: $_("setup_compile"), + }, + { + status: "queued", + label: $_("setup_done"), + }, + ]); + progressTracker.start(); + let resp = await runCompiler("", getInternalName(activeGame), true); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + progressTracker.proceed(); + } + + async function setupUpdateGameJob() { + installationError = undefined; + progressTracker.init([ + { + status: "queued", + label: $_("setup_copyFiles"), + }, + { + status: "queued", + label: $_("setup_decompile"), + }, + { + status: "queued", + label: $_("setup_compile"), + }, + { + status: "queued", + label: $_("setup_done"), + }, + ]); + progressTracker.start(); + let resp = await updateDataDirectory(getInternalName(activeGame)); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + resp = await runDecompiler("", getInternalName(activeGame), true); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + resp = await runCompiler("", getInternalName(activeGame)); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + await finalizeInstallation("jak1"); + progressTracker.proceed(); + } + + async function setupTexturePacks() { + installationError = undefined; + let jobs = []; + if (texturePacksToDelete.length > 0) { + jobs.push({ + status: "queued", + label: $_("gameJob_deleteTexturePacks"), + }); + } + jobs.push( + { + status: "queued", + label: $_("gameJob_enablingTexturePacks"), + }, + { + status: "queued", + label: $_("gameJob_applyTexturePacks"), + }, + { + status: "queued", + label: $_("setup_decompile"), + } + ); + progressTracker.init(jobs); + progressTracker.start(); + if (texturePacksToDelete.length > 0) { + let resp = await deleteTexturePacks( + getInternalName(activeGame), + texturePacksToDelete + ); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + } + let resp = await setEnabledTexturePacks( + getInternalName(activeGame), + texturePacksToEnable + ); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + resp = await updateTexturePackData(getInternalName(activeGame)); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + resp = await runDecompiler("", getInternalName(activeGame), true); + progressTracker.updateLogs(await getEndOfLogs()); + if (!resp.success) { + progressTracker.halt(); + installationError = resp.msg; + return; + } + progressTracker.proceed(); + } + // This is basically a stripped down `GameSetup` component that doesn't care about user initiation, // requirement checking, etc // @@ -29,96 +197,13 @@ // but for arbitrary jobs. Such as updating versions, decompiling, or compiling. onMount(async () => { if (jobType === "decompile") { - installationError = undefined; - progressTracker.init([ - { - status: "queued", - label: $_("setup_decompile"), - }, - { - status: "queued", - label: $_("setup_done"), - }, - ]); - progressTracker.start(); - let resp = await runDecompiler("", getInternalName(activeGame), true); - progressTracker.updateLogs(await getEndOfLogs()); - if (!resp.success) { - progressTracker.halt(); - installationError = resp.msg; - return; - } - progressTracker.proceed(); - progressTracker.proceed(); + await setupDecompileJob(); } else if (jobType === "compile") { - installationError = undefined; - progressTracker.init([ - { - status: "queued", - label: $_("setup_compile"), - }, - { - status: "queued", - label: $_("setup_done"), - }, - ]); - progressTracker.start(); - let resp = await runCompiler("", getInternalName(activeGame), true); - progressTracker.updateLogs(await getEndOfLogs()); - if (!resp.success) { - progressTracker.halt(); - installationError = resp.msg; - return; - } - progressTracker.proceed(); - progressTracker.proceed(); + await setupCompileJob(); } else if (jobType === "updateGame") { - installationError = undefined; - progressTracker.init([ - { - status: "queued", - label: $_("setup_copyFiles"), - }, - { - status: "queued", - label: $_("setup_decompile"), - }, - { - status: "queued", - label: $_("setup_compile"), - }, - { - status: "queued", - label: $_("setup_done"), - }, - ]); - progressTracker.start(); - let resp = await updateDataDirectory(getInternalName(activeGame)); - progressTracker.updateLogs(await getEndOfLogs()); - if (!resp.success) { - progressTracker.halt(); - installationError = resp.msg; - return; - } - progressTracker.proceed(); - resp = await runDecompiler("", getInternalName(activeGame), true); - progressTracker.updateLogs(await getEndOfLogs()); - if (!resp.success) { - progressTracker.halt(); - installationError = resp.msg; - return; - } - progressTracker.proceed(); - resp = await runCompiler("", getInternalName(activeGame)); - progressTracker.updateLogs(await getEndOfLogs()); - if (!resp.success) { - progressTracker.halt(); - installationError = resp.msg; - return; - } - progressTracker.proceed(); - await finalizeInstallation("jak1"); - progressTracker.proceed(); + await setupUpdateGameJob(); + } else if (jobType === "updateTexturePacks") { + await setupTexturePacks(); } }); @@ -146,11 +231,12 @@ {:else if $progressTracker.overallStatus === "failed"}
- + {$_("setup_installationFailed")} {installationError} +