features: Add Texture Pack Management (#271)

This commit is contained in:
Tyler Wilding 2023-07-21 21:49:29 -06:00 committed by GitHub
parent bc25fc5714
commit ab4b38c792
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1378 additions and 426 deletions

1
src-tauri/Cargo.lock generated
View file

@ -2519,6 +2519,7 @@ dependencies = [
"flate2",
"fs_extra",
"futures-util",
"glob",
"log",
"reqwest",
"rev_buf_reader",

View file

@ -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"

View file

@ -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 {

View file

@ -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(())
}

View 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,
})
}

View file

@ -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(),
)

View file

@ -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(())
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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)
}

View file

@ -48,6 +48,10 @@
"$APP/**/*",
"$RESOURCE/**/*"
]
},
"protocol": {
"asset": true,
"assetScope": ["**"]
}
},
"windows": [
@ -65,9 +69,6 @@
"visible": true,
"focus": true
}
],
"security": {
"csp": null
}
]
}
}

View file

@ -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>

View file

@ -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!"
}

View file

@ -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&nbsp;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 () => {

View 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>

View file

@ -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"}
<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()}

View file

@ -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()}

View file

@ -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;

View file

@ -84,3 +84,7 @@ body {
.basis-9\/10 {
flex-basis: 90%;
}
.texture-pack-card img {
object-fit: contain;
}

View file

@ -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
View 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 };
}
);
}

View file

@ -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);
}
}

View 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>

View file

@ -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>

View file

@ -63,6 +63,7 @@
>
<Toggle
checked={showDependencyChanges}
color="orange"
on:change={(evt) => {
showDependencyChanges = evt.target.checked;
}}>{$_("update_button_hideDependencyChanges")}</Toggle

View file

@ -63,6 +63,7 @@
<div>
<Toggle
checked={currentBypassRequirementsVal}
color="orange"
on:change={async (evt) => {
if (evt.target.checked) {
const confirmed = await confirm(