mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-19 14:47:36 -04:00
features: Add Texture Pack Management (#271)
This commit is contained in:
parent
bc25fc5714
commit
ab4b38c792
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -2519,6 +2519,7 @@ dependencies = [
|
|||
"flate2",
|
||||
"fs_extra",
|
||||
"futures-util",
|
||||
"glob",
|
||||
"log",
|
||||
"reqwest",
|
||||
"rev_buf_reader",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<Vec<String>, 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<LauncherConfig>>,
|
||||
game_name: String,
|
||||
cleanup_list: Vec<String>,
|
||||
) -> 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<LauncherConfig>>,
|
||||
game_name: String,
|
||||
packs: Vec<String>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
|
316
src-tauri/src/commands/features.rs
Normal file
316
src-tauri/src/commands/features.rs
Normal file
|
@ -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<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
has_metadata: bool,
|
||||
#[serde(skip_deserializing)]
|
||||
cover_image_path: Option<String>,
|
||||
name: String,
|
||||
version: String,
|
||||
author: String,
|
||||
release_date: String,
|
||||
supported_games: Vec<String>,
|
||||
description: String,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_extracted_texture_pack_info(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<HashMap<String, TexturePackInfo>, 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::<TexturePackInfo>(&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<LauncherConfig>>,
|
||||
game_name: String,
|
||||
zip_path: String,
|
||||
) -> Result<bool, CommandError> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_texture_pack_data(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<GameJobStepOutput, CommandError> {
|
||||
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<LauncherConfig>>,
|
||||
game_name: String,
|
||||
packs: Vec<String>,
|
||||
) -> Result<GameJobStepOutput, CommandError> {
|
||||
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,
|
||||
})
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -94,12 +94,27 @@ impl Serialize for SupportedGame {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameFeatureConfig {
|
||||
pub texture_packs: Vec<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub version_folder: Option<String>,
|
||||
pub features: Option<GameFeatureConfig>,
|
||||
}
|
||||
|
||||
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<std::path::PathBuf>) -> LauncherConfig {
|
||||
match config_dir {
|
||||
Some(config_dir) => {
|
||||
|
@ -439,4 +500,71 @@ impl LauncherConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_enabled_textured_packs(&self, game_name: &String) -> Vec<String> {
|
||||
// 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<String>,
|
||||
) -> 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<String>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<tauri::Error>) {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extract_textures(app_handle: tauri::AppHandle, textures_array: Vec<String>) {
|
||||
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<u8> = 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<TexturePack, io::Error> {
|
||||
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<TexturePack> {
|
||||
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<TexturePack> = 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
|
||||
}
|
|
@ -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<u8> = 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<u8> = 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<bool, Box<dyn std::error::Error>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -48,6 +48,10 @@
|
|||
"$APP/**/*",
|
||||
"$RESOURCE/**/*"
|
||||
]
|
||||
},
|
||||
"protocol": {
|
||||
"asset": true,
|
||||
"assetScope": ["**"]
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
|
@ -65,9 +69,6 @@
|
|||
"visible": true,
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
<Route
|
||||
path="/:game_name/features/:feature"
|
||||
component={GameFeature}
|
||||
primary={false}
|
||||
let:params
|
||||
/>
|
||||
<Route
|
||||
path="/jak2"
|
||||
component={GameInProgress}
|
||||
|
@ -89,7 +95,6 @@
|
|||
let:params
|
||||
/>
|
||||
<Route path="/faq" component={Help} primary={false} />
|
||||
<Route path="/textures" component={Textures} primary={false} />
|
||||
<Route path="/update" component={Update} primary={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
|
|
|
@ -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")}</Button
|
||||
>
|
||||
<!-- TODO - texture replacements left out for now, get everything else working end-to-end first -->
|
||||
<!-- <Button
|
||||
class="text-center font-semibold focus:ring-0 focus:outline-none inline-flex items-center justify-center px-5 py-2 text-sm text-white border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800"
|
||||
><Chevron placement="top">Features</Chevron></Button
|
||||
<Button
|
||||
class="text-center font-semibold focus:ring-0 focus:outline-none inline-flex items-center justify-center px-2 py-2 text-sm text-white border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800"
|
||||
>{$_("gameControls_button_features")}</Button
|
||||
>
|
||||
<Dropdown placement="top-end">
|
||||
<DropdownItem>Texture Replacements</DropdownItem>
|
||||
</Dropdown> -->
|
||||
<Dropdown placement="top-end" class="!bg-slate-900">
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
navigate(`/${getInternalName(activeGame)}/features/texture_packs`);
|
||||
}}
|
||||
>
|
||||
{$_("gameControls_button_features_textures")}
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<Button
|
||||
class="text-center font-semibold focus:ring-0 focus:outline-none inline-flex items-center justify-center px-2 py-2 text-sm text-white border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800"
|
||||
>
|
||||
{$_("gameControls_button_advanced")}
|
||||
</Button>
|
||||
<Dropdown placement="top-end" frameClass="!bg-slate-900">
|
||||
<Dropdown placement="top-end" class="!bg-slate-900">
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
launchGame(getInternalName(activeGame), true);
|
||||
|
@ -112,7 +118,7 @@
|
|||
>
|
||||
<Icon icon="material-symbols:settings" width={24} height={24} />
|
||||
</Button>
|
||||
<Dropdown placement="top-end" frameClass="!bg-slate-900">
|
||||
<Dropdown placement="top-end" class="!bg-slate-900">
|
||||
<!-- TODO - screenshot folder? how do we even configure where those go? -->
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
|
|
413
src/components/games/features/texture-packs/TexturePacks.svelte
Normal file
413
src/components/games/features/texture-packs/TexturePacks.svelte
Normal file
|
@ -0,0 +1,413 @@
|
|||
<!--
|
||||
- verify mod JSON file with a json schema https://docs.rs/jsonschema/latest/jsonschema/
|
||||
-->
|
||||
<!-- NOTE - this does not attempt to verify that the user has not manually messed with the texture_replacements folder.
|
||||
This is no different than how we don't verify the user hasn't messed with goal_src -->
|
||||
|
||||
<!-- TODO - collecting rating metrics / number of users might be cool (same for mods) -->
|
||||
<!-- TODO - instead of currently allowing full access - explicitly allow the install folder in the Rust layer https://docs.rs/tauri/1.4.1/tauri/scope/struct.FsScope.html -->
|
||||
<!-- TODO - check supported games, not bothering right now cause there's only 1! -->
|
||||
|
||||
<script lang="ts">
|
||||
import { getInternalName, SupportedGame } from "$lib/constants";
|
||||
import {
|
||||
cleanupEnabledTexturePacks,
|
||||
getEnabledTexturePacks,
|
||||
} from "$lib/rpc/config";
|
||||
import {
|
||||
extractNewTexturePack,
|
||||
listExtractedTexturePackInfo,
|
||||
} from "$lib/rpc/features";
|
||||
import { filePrompt } from "$lib/utils/file";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { convertFileSrc } from "@tauri-apps/api/tauri";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Spinner,
|
||||
} from "flowbite-svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { navigate } from "svelte-navigator";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let activeGame: SupportedGame;
|
||||
|
||||
let loaded = false;
|
||||
let extractedPackInfo: any = undefined;
|
||||
let availablePacks = [];
|
||||
let availablePacksOriginal = [];
|
||||
|
||||
let addingPack = false;
|
||||
let packAddingError = "";
|
||||
|
||||
let enabledPacks = [];
|
||||
let packsToDelete = [];
|
||||
|
||||
onMount(async () => {
|
||||
await update_pack_list();
|
||||
loaded = true;
|
||||
});
|
||||
|
||||
async function update_pack_list() {
|
||||
availablePacks = [];
|
||||
availablePacksOriginal = [];
|
||||
let currentlyEnabledPacks = await getEnabledTexturePacks(
|
||||
getInternalName(activeGame)
|
||||
);
|
||||
extractedPackInfo = await listExtractedTexturePackInfo(
|
||||
getInternalName(activeGame)
|
||||
);
|
||||
// Finalize `availablePacks` list
|
||||
// - First, cleanup any packs that were enabled but can no longer be found
|
||||
let cleanupPackList = [];
|
||||
let filteredCurrentlyEnabledPacks = [];
|
||||
for (const [packName, packInfo] of Object.entries(extractedPackInfo)) {
|
||||
if (!currentlyEnabledPacks.includes(packName)) {
|
||||
cleanupPackList.push(packName);
|
||||
} else {
|
||||
filteredCurrentlyEnabledPacks.push(packName);
|
||||
}
|
||||
}
|
||||
await cleanupEnabledTexturePacks(
|
||||
getInternalName(activeGame),
|
||||
cleanupPackList
|
||||
);
|
||||
// - secondly, add the ones that are enabled so they are at the top of the list
|
||||
for (const pack of currentlyEnabledPacks) {
|
||||
availablePacks.push({
|
||||
name: pack,
|
||||
enabled: true,
|
||||
toBeDeleted: false,
|
||||
});
|
||||
}
|
||||
// - lastly, add the rest that are available but not enabled
|
||||
for (const [packName, packInfo] of Object.entries(extractedPackInfo)) {
|
||||
if (!filteredCurrentlyEnabledPacks.includes(packName)) {
|
||||
availablePacks.push({
|
||||
name: packName,
|
||||
enabled: false,
|
||||
toBeDeleted: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
availablePacks = availablePacks; // assignment for reactivity
|
||||
availablePacksOriginal = JSON.parse(JSON.stringify(availablePacks));
|
||||
}
|
||||
|
||||
function pending_changes(current, original): boolean {
|
||||
return JSON.stringify(current) !== JSON.stringify(original);
|
||||
}
|
||||
|
||||
function num_textures_in_pack(packName: string): number {
|
||||
return extractedPackInfo[packName]["fileList"].length;
|
||||
}
|
||||
|
||||
function tag_name_to_color(
|
||||
tagName: string
|
||||
):
|
||||
| "none"
|
||||
| "red"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "pink"
|
||||
| "blue"
|
||||
| "dark"
|
||||
| "primary" {
|
||||
if (
|
||||
tagName === "enhancement" ||
|
||||
tagName === "overhaul" ||
|
||||
tagName === "highres"
|
||||
) {
|
||||
return "indigo";
|
||||
} else if (tagName == "parody" || tagName === "themed") {
|
||||
return "pink";
|
||||
} else if (tagName === "mods") {
|
||||
return "purple";
|
||||
} else {
|
||||
return "dark";
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all enabled packs, flag and files that are in the relevant pack
|
||||
function find_pack_conflicts(relevantPackName: string): Set<String> {
|
||||
let conflicts: Set<String> = new Set();
|
||||
for (const filePath of extractedPackInfo[relevantPackName]["fileList"]) {
|
||||
for (const [packName, packInfo] of Object.entries(extractedPackInfo)) {
|
||||
if (packName === relevantPackName) {
|
||||
continue;
|
||||
}
|
||||
if (packInfo["fileList"].includes(filePath)) {
|
||||
conflicts.add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
async function addNewTexturePack() {
|
||||
addingPack = true;
|
||||
packAddingError = "";
|
||||
const texturePackPath = await filePrompt(
|
||||
["zip"],
|
||||
"ZIP",
|
||||
"Select a texture pack"
|
||||
);
|
||||
if (texturePackPath !== null) {
|
||||
const success = await extractNewTexturePack(
|
||||
getInternalName(activeGame),
|
||||
texturePackPath
|
||||
);
|
||||
if (success) {
|
||||
// if the user made any changes, attempt to restore them after
|
||||
let preexistingChanges = undefined;
|
||||
if (pending_changes(availablePacks, availablePacksOriginal)) {
|
||||
preexistingChanges = JSON.parse(JSON.stringify(availablePacks));
|
||||
}
|
||||
await update_pack_list();
|
||||
if (preexistingChanges !== undefined) {
|
||||
for (const preexisingPack of preexistingChanges) {
|
||||
for (const pack of availablePacks) {
|
||||
if (pack.name === preexisingPack.name) {
|
||||
pack.enabled = preexisingPack.enabled;
|
||||
pack.toBeDeleted = preexisingPack.toBeDeleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
availablePacks = availablePacks;
|
||||
}
|
||||
} else {
|
||||
packAddingError = $_("features_textures_invalidPack");
|
||||
}
|
||||
}
|
||||
addingPack = false;
|
||||
}
|
||||
|
||||
async function applyTexturePacks() {
|
||||
enabledPacks = [];
|
||||
packsToDelete = [];
|
||||
for (const pack of availablePacks) {
|
||||
if (pack.enabled) {
|
||||
enabledPacks.push(pack.name);
|
||||
} else if (pack.toBeDeleted) {
|
||||
packsToDelete.push(pack.name);
|
||||
}
|
||||
}
|
||||
dispatch("job", {
|
||||
type: "updateTexturePacks",
|
||||
enabledPacks,
|
||||
packsToDelete,
|
||||
});
|
||||
}
|
||||
|
||||
function moveTexturePack(dst: number, src: number) {
|
||||
const temp = availablePacks[dst];
|
||||
availablePacks[dst] = availablePacks[src];
|
||||
availablePacks[src] = temp;
|
||||
availablePacks = availablePacks;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full bg-slate-900">
|
||||
{#if !loaded}
|
||||
<div class="flex flex-col h-full justify-center items-center">
|
||||
<Spinner color="yellow" size={"12"} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pb-20 overflow-y-auto p-4">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
outline
|
||||
class="flex-shrink border-solid rounded text-white hover:dark:text-slate-900 hover:bg-white font-semibold px-2 py-2"
|
||||
on:click={async () =>
|
||||
navigate(`/${getInternalName(activeGame)}`, { replace: true })}
|
||||
aria-label="back to game page"
|
||||
><Icon
|
||||
icon="material-symbols:arrow-left-alt"
|
||||
width="20"
|
||||
height="20"
|
||||
/></Button
|
||||
>
|
||||
<Button
|
||||
class="flex-shrink border-solid rounded bg-orange-400 hover:bg-orange-600 text-sm text-slate-900 font-semibold px-5 py-2"
|
||||
on:click={addNewTexturePack}
|
||||
aria-label="add a new texture pack"
|
||||
disabled={addingPack}
|
||||
>
|
||||
{#if addingPack}
|
||||
<Spinner class="mr-3" size="4" color="white" />
|
||||
{/if}
|
||||
{$_("features_textures_addNewPack")}</Button
|
||||
>
|
||||
{#if pending_changes(availablePacks, availablePacksOriginal)}
|
||||
<Button
|
||||
class="flex-shrink border-solid rounded bg-green-400 hover:bg-green-500 text-sm text-slate-900 font-semibold px-5 py-2"
|
||||
on:click={applyTexturePacks}
|
||||
aria-label="apply texture changes"
|
||||
>{$_("features_textures_applyChanges")}</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if packAddingError !== ""}
|
||||
<div class="flex flex-row font-bold mt-3">
|
||||
<Alert color="red" class="flex-grow">
|
||||
{packAddingError}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row font-bold mt-3">
|
||||
<h2>{$_("features_textures_listHeading")}</h2>
|
||||
</div>
|
||||
<div class="flex flex-row text-sm">
|
||||
<p>
|
||||
{$_("features_textures_description")}
|
||||
</p>
|
||||
</div>
|
||||
{#each availablePacks as pack, packIndex}
|
||||
{#if !pack.toBeDeleted}
|
||||
<div class="flex flex-row gap-2 mt-3">
|
||||
<!-- Placeholder image -->
|
||||
<Card
|
||||
img={convertFileSrc(
|
||||
extractedPackInfo[pack.name]["coverImagePath"]
|
||||
)}
|
||||
horizontal
|
||||
class="texture-pack-card max-w-none md:max-w-none basis-full"
|
||||
padding="md"
|
||||
>
|
||||
<div class="flex flex-row mt-auto">
|
||||
<h2 class="text-xl font-bold tracking-tight text-white">
|
||||
{extractedPackInfo[pack.name]["name"]}
|
||||
<span class="text-xs text-gray-500" />
|
||||
</h2>
|
||||
</div>
|
||||
<p class="font-bold text-xs text-gray-500">
|
||||
{extractedPackInfo[pack.name]["version"]} by {extractedPackInfo[
|
||||
pack.name
|
||||
]["author"]}
|
||||
</p>
|
||||
<p class="font-bold text-gray-500 text-xs">
|
||||
{extractedPackInfo[pack.name]["releaseDate"]}
|
||||
</p>
|
||||
<p class="font-bold text-gray-500 text-xs">
|
||||
{$_("features_textures_replacedCount")} - {num_textures_in_pack(
|
||||
pack.name
|
||||
)}
|
||||
</p>
|
||||
<p class="mt-2 mb-4 font-normal text-gray-400 leading-tight">
|
||||
{extractedPackInfo[pack.name]["description"]}
|
||||
</p>
|
||||
{#if extractedPackInfo[pack.name]["tags"].length > 0}
|
||||
<div class="flex flex-row gap-2">
|
||||
{#each extractedPackInfo[pack.name]["tags"] as tag}
|
||||
<Badge border color={tag_name_to_color(tag)}>{tag}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Buttons -->
|
||||
<div class="mt-2 flex flex-row gap-2">
|
||||
<Button
|
||||
size={"xs"}
|
||||
color={pack.enabled ? "green" : "red"}
|
||||
on:click={() => {
|
||||
pack.enabled = !pack.enabled;
|
||||
}}
|
||||
>
|
||||
{pack.enabled
|
||||
? $_("features_textures_enabled")
|
||||
: $_("features_textures_disabled")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row gap-2">
|
||||
{#if pack.enabled}
|
||||
{#if packIndex !== 0}
|
||||
<Button
|
||||
outline
|
||||
class="!p-1.5 rounded-md border-blue-500 text-blue-500 hover:bg-blue-600"
|
||||
aria-label="move texture pack up in order"
|
||||
on:click={() => {
|
||||
moveTexturePack(packIndex - 1, packIndex);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="material-symbols:arrow-upward"
|
||||
width="15"
|
||||
height="15"
|
||||
/>
|
||||
</Button>
|
||||
{/if}
|
||||
{#if packIndex !== availablePacks.length - 1}
|
||||
<Button
|
||||
outline
|
||||
class="!p-1.5 rounded-md border-blue-500 text-blue-500 hover:bg-blue-600"
|
||||
aria-label="move texture pack down in order"
|
||||
on:click={() => {
|
||||
moveTexturePack(packIndex + 1, packIndex);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="material-symbols:arrow-downward"
|
||||
width="15"
|
||||
height="15"
|
||||
/>
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
outline
|
||||
class="!p-1.5 rounded-md border-red-500 text-red-500 hover:bg-red-600"
|
||||
aria-label="delete texture pack"
|
||||
on:click={() => {
|
||||
pack.toBeDeleted = true;
|
||||
pack.enabled = false;
|
||||
}}
|
||||
>
|
||||
<Icon icon="material-symbols:delete" width="15" height="15" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- double computation, TODO - separate component -->
|
||||
{#if find_pack_conflicts(pack.name).size > 0}
|
||||
<Accordion flush class="mt-2">
|
||||
<AccordionItem paddingFlush="p-2">
|
||||
<span
|
||||
slot="header"
|
||||
class="flex gap-2 text-yellow-300 text-sm"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clip-rule="evenodd"
|
||||
/></svg
|
||||
>
|
||||
<span> {$_("features_textures_conflictsDetected")}</span>
|
||||
</span>
|
||||
<div slot="arrowup" />
|
||||
<div slot="arrowdown" />
|
||||
<pre
|
||||
class="mb-2 text-gray-500 dark:text-gray-400 text-xs">{[
|
||||
...find_pack_conflicts(pack.name),
|
||||
]
|
||||
.join("\n")
|
||||
.trim()}</pre>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -12,23 +12,24 @@
|
|||
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;
|
||||
|
||||
// This is basically a stripped down `GameSetup` component that doesn't care about user initiation,
|
||||
// requirement checking, etc
|
||||
//
|
||||
// It's used to provide almost the same interface as the normal installation, with logs, etc
|
||||
// but for arbitrary jobs. Such as updating versions, decompiling, or compiling.
|
||||
onMount(async () => {
|
||||
if (jobType === "decompile") {
|
||||
async function setupDecompileJob() {
|
||||
installationError = undefined;
|
||||
progressTracker.init([
|
||||
{
|
||||
|
@ -50,7 +51,9 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
progressTracker.proceed();
|
||||
} else if (jobType === "compile") {
|
||||
}
|
||||
|
||||
async function setupCompileJob() {
|
||||
installationError = undefined;
|
||||
progressTracker.init([
|
||||
{
|
||||
|
@ -72,7 +75,9 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
progressTracker.proceed();
|
||||
} else if (jobType === "updateGame") {
|
||||
}
|
||||
|
||||
async function setupUpdateGameJob() {
|
||||
installationError = undefined;
|
||||
progressTracker.init([
|
||||
{
|
||||
|
@ -120,6 +125,86 @@
|
|||
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
|
||||
//
|
||||
// It's used to provide almost the same interface as the normal installation, with logs, etc
|
||||
// but for arbitrary jobs. Such as updating versions, decompiling, or compiling.
|
||||
onMount(async () => {
|
||||
if (jobType === "decompile") {
|
||||
await setupDecompileJob();
|
||||
} else if (jobType === "compile") {
|
||||
await setupCompileJob();
|
||||
} else if (jobType === "updateGame") {
|
||||
await setupUpdateGameJob();
|
||||
} else if (jobType === "updateTexturePacks") {
|
||||
await setupTexturePacks();
|
||||
}
|
||||
});
|
||||
|
||||
function dispatchCompleteJob() {
|
||||
|
@ -146,11 +231,12 @@
|
|||
{:else if $progressTracker.overallStatus === "failed"}
|
||||
<div class="flex flex-col mt-auto">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Alert color="red" class="dark:bg-slate-900 flex-grow" accent={true}>
|
||||
<Alert color="red" class="dark:bg-slate-900 flex-grow">
|
||||
<span class="font-medium text-red-500"
|
||||
>{$_("setup_installationFailed")}
|
||||
</span><span class="text-white"> {installationError}</span>
|
||||
</Alert>
|
||||
<!-- TODO - no button to go back! -->
|
||||
<Button
|
||||
class="border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800 text-sm text-white font-semibold px-5 py-2"
|
||||
on:click={async () => await generateSupportPackage()}
|
||||
|
|
|
@ -134,11 +134,16 @@
|
|||
{:else if $progressTracker.overallStatus === "failed"}
|
||||
<div class="flex flex-col mt-auto">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Alert color="red" class="dark:bg-slate-900 flex-grow" accent={true}>
|
||||
<Alert
|
||||
color="red"
|
||||
class="dark:bg-slate-900 flex-grow border-t-4"
|
||||
rounded={false}
|
||||
>
|
||||
<span class="font-medium text-red-500"
|
||||
>{$_("setup_installationFailed")}
|
||||
</span><span class="text-white"> {installationError}</span>
|
||||
</Alert>
|
||||
<!-- TODO - no button to go back -->
|
||||
<Button
|
||||
class="border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800 text-sm text-white font-semibold px-5 py-2"
|
||||
on:click={async () => await generateSupportPackage()}
|
||||
|
|
|
@ -25,13 +25,16 @@
|
|||
function getNavItemStyle(itemName: string, pathName: string): string {
|
||||
let style =
|
||||
"flex items-center hover:grayscale-0 hover:opacity-100 duration-500 text-orange-400 duration-500";
|
||||
if (itemName === "jak1" && (pathName === "/jak1" || pathName === "/")) {
|
||||
if (
|
||||
itemName === "jak1" &&
|
||||
(pathName.startsWith("/jak1") || pathName === "/")
|
||||
) {
|
||||
return style;
|
||||
} else if (itemName === "jak2" && pathName === "/jak2") {
|
||||
} else if (itemName === "jak2" && pathName.startsWith("/jak2")) {
|
||||
return style;
|
||||
} else if (itemName === "jak3" && pathName === "/jak3") {
|
||||
} else if (itemName === "jak3" && pathName.startsWith("/jak3")) {
|
||||
return style;
|
||||
} else if (itemName === "jakx" && pathName === "/jakx") {
|
||||
} else if (itemName === "jakx" && pathName.startsWith("/jakx")) {
|
||||
return style;
|
||||
} else if (itemName === "settings" && pathName.startsWith("/settings")) {
|
||||
return style;
|
||||
|
|
|
@ -84,3 +84,7 @@ body {
|
|||
.basis-9\/10 {
|
||||
flex-basis: 90%;
|
||||
}
|
||||
|
||||
.texture-pack-card img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
|
@ -123,3 +123,55 @@ export async function setBypassRequirements(bypass: boolean): Promise<void> {
|
|||
export async function getBypassRequirements(): Promise<boolean> {
|
||||
return await invoke_rpc("get_bypass_requirements", {}, () => false);
|
||||
}
|
||||
|
||||
export async function getEnabledTexturePacks(
|
||||
gameName: string
|
||||
): Promise<string[]> {
|
||||
return await invoke_rpc(
|
||||
"get_enabled_texture_packs",
|
||||
{ gameName: gameName },
|
||||
() => []
|
||||
);
|
||||
}
|
||||
|
||||
export async function cleanupEnabledTexturePacks(
|
||||
gameName: string,
|
||||
cleanupList: string[]
|
||||
): Promise<void> {
|
||||
return await invoke_rpc(
|
||||
"cleanup_enabled_texture_packs",
|
||||
{
|
||||
gameName: gameName,
|
||||
cleanupList: cleanupList,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
// TODO - just make this a generic interface for both binaries/feature jobs
|
||||
interface FeatureJobOutput {
|
||||
msg: string | null;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
function failed(msg: string): FeatureJobOutput {
|
||||
return { success: false, msg };
|
||||
}
|
||||
|
||||
export async function setEnabledTexturePacks(
|
||||
gameName: string,
|
||||
packs: string[]
|
||||
): Promise<FeatureJobOutput> {
|
||||
return await invoke_rpc(
|
||||
"set_enabled_texture_packs",
|
||||
{
|
||||
gameName: gameName,
|
||||
packs: packs,
|
||||
},
|
||||
() => failed("Failed to update texture pack list"),
|
||||
undefined,
|
||||
() => {
|
||||
return { success: true, msg: null };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
72
src/lib/rpc/features.ts
Normal file
72
src/lib/rpc/features.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { invoke_rpc } from "./rpc";
|
||||
|
||||
// TODO - toasts
|
||||
// TODO - just make this a generic interface for both binaries/feature jobs
|
||||
interface FeatureJobOutput {
|
||||
msg: string | null;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
function failed(msg: string): FeatureJobOutput {
|
||||
return { success: false, msg };
|
||||
}
|
||||
|
||||
export async function listExtractedTexturePackInfo(
|
||||
gameName: string
|
||||
): Promise<any> {
|
||||
return await invoke_rpc(
|
||||
"list_extracted_texture_pack_info",
|
||||
{
|
||||
gameName: gameName,
|
||||
},
|
||||
() => []
|
||||
);
|
||||
}
|
||||
|
||||
export async function extractNewTexturePack(
|
||||
gameName: string,
|
||||
pathToZip: string
|
||||
): Promise<boolean | undefined> {
|
||||
return await invoke_rpc(
|
||||
"extract_new_texture_pack",
|
||||
{
|
||||
gameName: gameName,
|
||||
zipPath: pathToZip,
|
||||
},
|
||||
() => undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateTexturePackData(
|
||||
gameName: string
|
||||
): Promise<FeatureJobOutput> {
|
||||
return await invoke_rpc(
|
||||
"update_texture_pack_data",
|
||||
{
|
||||
gameName: gameName,
|
||||
},
|
||||
() => failed("Failed to delete texture packs"),
|
||||
undefined,
|
||||
() => {
|
||||
return { success: true, msg: null };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTexturePacks(
|
||||
gameName: string,
|
||||
packs: string[]
|
||||
): Promise<FeatureJobOutput> {
|
||||
return await invoke_rpc(
|
||||
"delete_texture_packs",
|
||||
{
|
||||
gameName: gameName,
|
||||
packs: packs,
|
||||
},
|
||||
() => failed("Failed to delete texture packs"),
|
||||
undefined,
|
||||
() => {
|
||||
return { success: true, msg: null };
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { filePrompt } from "$lib/utils/file";
|
||||
import { path } from "@tauri-apps/api";
|
||||
import { copyFile } from "@tauri-apps/api/fs";
|
||||
import { appDir, join } from "@tauri-apps/api/path";
|
||||
|
||||
export async function texturePackPrompt(): Promise<string> {
|
||||
try {
|
||||
const path = await filePrompt(
|
||||
["ZIP", "zip"],
|
||||
"Texture Pack Zip File",
|
||||
"Select a Texture Pack"
|
||||
);
|
||||
await copyTexturePackToZipFolder(path);
|
||||
return path;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTexturePackToZipFolder(pathToPack) {
|
||||
// split the pack name from the pathToPack, append it the dest path
|
||||
const packName = await path.basename(pathToPack);
|
||||
const textureZipDir = await join(
|
||||
await appDir(),
|
||||
"data/texture_zips",
|
||||
`${packName}`
|
||||
);
|
||||
try {
|
||||
await copyFile(pathToPack, textureZipDir, {});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
72
src/routes/GameFeature.svelte
Normal file
72
src/routes/GameFeature.svelte
Normal file
|
@ -0,0 +1,72 @@
|
|||
<script>
|
||||
import { fromRoute, SupportedGame } from "$lib/constants";
|
||||
import { useParams } from "svelte-navigator";
|
||||
import { onMount } from "svelte";
|
||||
import { Spinner } from "flowbite-svelte";
|
||||
import GameJob from "../components/games/job/GameJob.svelte";
|
||||
import TexturePacks from "../components/games/features/texture-packs/TexturePacks.svelte";
|
||||
|
||||
const params = useParams();
|
||||
let activeGame = SupportedGame.Jak1;
|
||||
let selectedFeature = "texture_packs";
|
||||
let componentLoaded = false;
|
||||
|
||||
let gameJobToRun = undefined;
|
||||
|
||||
let texturePacksToEnable = [];
|
||||
let texturePacksToDelete = [];
|
||||
|
||||
onMount(async () => {
|
||||
// Figure out what game we are displaying
|
||||
if (
|
||||
$params["game_name"] !== undefined &&
|
||||
$params["game_name"] !== null &&
|
||||
$params["game_name"] !== ""
|
||||
) {
|
||||
activeGame = fromRoute($params["game_name"]);
|
||||
} else {
|
||||
activeGame = SupportedGame.Jak1;
|
||||
}
|
||||
if (
|
||||
$params["feature"] !== undefined &&
|
||||
$params["feature"] !== null &&
|
||||
$params["feature"] !== ""
|
||||
) {
|
||||
selectedFeature = $params["feature"];
|
||||
} else {
|
||||
selectedFeature = "texture_packs";
|
||||
}
|
||||
|
||||
componentLoaded = true;
|
||||
});
|
||||
|
||||
async function runGameJob(event) {
|
||||
gameJobToRun = event.detail.type;
|
||||
texturePacksToEnable = event.detail.enabledPacks;
|
||||
texturePacksToDelete = event.detail.packsToDelete;
|
||||
}
|
||||
|
||||
async function gameJobFinished() {
|
||||
gameJobToRun = undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full bg-slate-900">
|
||||
{#if !componentLoaded}
|
||||
<div class="flex flex-col h-full justify-center items-center">
|
||||
<Spinner color="yellow" size={"12"} />
|
||||
</div>
|
||||
{:else if gameJobToRun !== undefined}
|
||||
<div class="flex flex-col p-5 h-full">
|
||||
<GameJob
|
||||
{activeGame}
|
||||
jobType={gameJobToRun}
|
||||
{texturePacksToEnable}
|
||||
{texturePacksToDelete}
|
||||
on:jobFinished={gameJobFinished}
|
||||
/>
|
||||
</div>
|
||||
{:else if selectedFeature === "texture_packs"}
|
||||
<TexturePacks {activeGame} on:job={runGameJob} />
|
||||
{/if}
|
||||
</div>
|
|
@ -1,184 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
// import { extractTextures, getAllTexturePacks } from "$lib/rpc/commands";
|
||||
import { texturePackPrompt } from "$lib/textures/textures";
|
||||
import { appDir, join } from "@tauri-apps/api/path";
|
||||
import { removeDir, removeFile } from "@tauri-apps/api/fs";
|
||||
import { SupportedGame } from "$lib/constants";
|
||||
import { confirm } from "@tauri-apps/api/dialog";
|
||||
import {
|
||||
Alert,
|
||||
Table,
|
||||
TableBody,
|
||||
TableBodyCell,
|
||||
TableBodyRow,
|
||||
TableHead,
|
||||
TableHeadCell,
|
||||
Checkbox,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Tooltip,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
interface TexturePack {
|
||||
author: String;
|
||||
description: String;
|
||||
version: String;
|
||||
path: String;
|
||||
}
|
||||
|
||||
let packs: Array<TexturePack> = [];
|
||||
let selectedTexturePacks: string[] = [];
|
||||
$: disabled = false;
|
||||
|
||||
// TODO - deferring this work
|
||||
|
||||
onMount(async () => {
|
||||
// packs = await getAllTexturePacks();
|
||||
});
|
||||
|
||||
async function handleSelectedPacks(pack) {
|
||||
if (!selectedTexturePacks.find((packs) => packs === pack)) {
|
||||
// add pack to be compiled
|
||||
selectedTexturePacks.push(pack);
|
||||
selectedTexturePacks = selectedTexturePacks;
|
||||
} else {
|
||||
// remove pack from to be compiled
|
||||
selectedTexturePacks = selectedTexturePacks.filter(
|
||||
(packs) => packs !== pack
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddTexturePack() {
|
||||
try {
|
||||
await texturePackPrompt();
|
||||
// packs = await getAllTexturePacks();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteTexturePack() {
|
||||
disabled = true;
|
||||
// TODO: Update this confirmation to an in-app modal
|
||||
const confirmed = await confirm(
|
||||
"Are you sure you would like to delete this pack?",
|
||||
{
|
||||
title: "Texture Packs",
|
||||
type: "warning",
|
||||
}
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
for (let pack of selectedTexturePacks) {
|
||||
console.log("Deleting texture pack: ", pack);
|
||||
try {
|
||||
// delete the file from the texture_zips directory
|
||||
await removeFile(pack);
|
||||
// delete the relative object from the packs array to update the table
|
||||
packs = packs.filter((obj) => {
|
||||
return obj.path !== pack;
|
||||
});
|
||||
// empty the selectedTexturePacks array
|
||||
selectedTexturePacks = [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
async function handleCompileTextures() {
|
||||
disabled = true;
|
||||
// persist the installed texture packs in local storage
|
||||
|
||||
// reset the texture_replacements folder to default (deleting the dir should work for this)
|
||||
try {
|
||||
const textureReplacementDir = await join(
|
||||
await appDir(),
|
||||
"data/texture_replacements"
|
||||
);
|
||||
await removeDir(textureReplacementDir, { recursive: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
// extract texture packs in (proper) order to texture_replacements (proper order: for overridding purposes)
|
||||
// await extractTextures(selectedTexturePacks);
|
||||
// await decompile game (similar to GameControls function, maybe that function should be moved into a seperate file)
|
||||
// await decompileFromFile(SupportedGame.Jak1);
|
||||
// should be ready to play (fingers crossed)
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
disabled = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ml-20">
|
||||
<div class="flex flex-col h-[560px] max-h-[560px] p-8 gap-2">
|
||||
TODO
|
||||
<!-- {#if packs && packs.length > 0}
|
||||
<Table hoverable={true}>
|
||||
<TableHead>
|
||||
<TableHeadCell class="!p-4" />
|
||||
<TableHeadCell>Author</TableHeadCell>
|
||||
<TableHeadCell>Description</TableHeadCell>
|
||||
<TableHeadCell>Version</TableHeadCell>
|
||||
</TableHead>
|
||||
<TableBody class="divide-y">
|
||||
{#each packs as pack}
|
||||
<TableBodyRow id={pack.path}>
|
||||
<TableBodyCell class="!p-4">
|
||||
<Checkbox
|
||||
on:click={() => handleSelectedPacks(pack.path)}
|
||||
{disabled}
|
||||
/>
|
||||
</TableBodyCell>
|
||||
<TableBodyCell>{pack.author}</TableBodyCell>
|
||||
<TableBodyCell tdClass="overflow-clip"
|
||||
>{pack.description}</TableBodyCell
|
||||
>
|
||||
<TableBodyCell>{pack.version}</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{:else}
|
||||
<Alert color="yellow" accent rounded={false}
|
||||
>No Texture Packs Installed, get started by adding a pack below.</Alert
|
||||
>
|
||||
{/if}
|
||||
<ButtonGroup class="ml-auto mt-auto">
|
||||
<Button
|
||||
size="md"
|
||||
class="!rounded-none"
|
||||
color="green"
|
||||
{disabled}
|
||||
on:click={handleAddTexturePack}>Add Pack</Button
|
||||
>
|
||||
<Tooltip rounded={false}>Add a new pack to the table</Tooltip>
|
||||
<Button
|
||||
class="!rounded-none"
|
||||
color="red"
|
||||
disabled={disabled || selectedTexturePacks.length === 0}
|
||||
on:click={handleDeleteTexturePack}>Delete Pack</Button
|
||||
>
|
||||
<Tooltip rounded={false}>Delete selected pack(s) from the table</Tooltip>
|
||||
<Button
|
||||
class="!rounded-none"
|
||||
color="dark"
|
||||
{disabled}
|
||||
on:click={async () => await handleCompileTextures()}
|
||||
>Compile Changes</Button
|
||||
>
|
||||
<Tooltip rounded={false}
|
||||
>Compile the game with the selected packs in the order they were
|
||||
selected</Tooltip
|
||||
>
|
||||
</ButtonGroup> -->
|
||||
</div>
|
||||
</div>
|
|
@ -63,6 +63,7 @@
|
|||
>
|
||||
<Toggle
|
||||
checked={showDependencyChanges}
|
||||
color="orange"
|
||||
on:change={(evt) => {
|
||||
showDependencyChanges = evt.target.checked;
|
||||
}}>{$_("update_button_hideDependencyChanges")}</Toggle
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
<div>
|
||||
<Toggle
|
||||
checked={currentBypassRequirementsVal}
|
||||
color="orange"
|
||||
on:change={async (evt) => {
|
||||
if (evt.target.checked) {
|
||||
const confirmed = await confirm(
|
||||
|
|
Loading…
Reference in a new issue