mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-19 14:47:36 -04:00
backend: cleanup error handling hacks on the rust layer
This commit is contained in:
parent
920b45bab0
commit
e13c12b895
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -2156,6 +2156,7 @@ dependencies = [
|
|||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
"wgpu",
|
||||
|
|
|
@ -30,6 +30,7 @@ sysinfo = "0.28.0"
|
|||
wgpu = "0.15.1"
|
||||
walkdir = "2.3.2"
|
||||
dir-diff = "0.3.2"
|
||||
thiserror = "1.0.38"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
|
|
@ -1,67 +1,41 @@
|
|||
use fs_extra::dir::copy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::command;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::util::open_dir_in_os;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
pub mod binaries;
|
||||
pub mod config;
|
||||
pub mod extractor;
|
||||
pub mod game;
|
||||
pub mod support;
|
||||
pub mod versions;
|
||||
pub mod window;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CommandError {
|
||||
ArchitectureNotx86,
|
||||
AVXNotSupported,
|
||||
Unknown,
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
NetworkRequest(#[from] reqwest::Error),
|
||||
#[error("{0}")]
|
||||
Configuration(String),
|
||||
#[error(transparent)]
|
||||
TauriEvent(#[from] tauri::Error),
|
||||
#[error("{0}")]
|
||||
Installation(String),
|
||||
#[error("{0}")]
|
||||
VersionManagement(String),
|
||||
#[error("{0}")]
|
||||
InvalidPath(String),
|
||||
#[error("{0}")]
|
||||
BinaryExecution(String),
|
||||
#[error("{0}")]
|
||||
Support(String),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_highest_simd() -> Result<String, CommandError> {
|
||||
return highest_simd().await;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
async fn highest_simd() -> Result<String, CommandError> {
|
||||
if is_x86_feature_detected!("avx2") {
|
||||
return Ok("AVX2".to_string());
|
||||
} else if is_x86_feature_detected!("avx") {
|
||||
return Ok("AVX".to_string());
|
||||
} else {
|
||||
return Err(CommandError::AVXNotSupported);
|
||||
impl Serialize for CommandError {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
fn highest_simd() -> Result<String, CommandError> {
|
||||
return Err(CommandError::ArchitectureNotx86);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn open_dir(dir: String) {
|
||||
return open_dir_in_os(dir);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn copy_dir(dir_src: String, dir_dest: String) -> bool {
|
||||
let mut options = fs_extra::dir::CopyOptions::new();
|
||||
options.copy_inside = true;
|
||||
options.overwrite = true;
|
||||
options.content_only = true;
|
||||
if let Err(_e) = copy(dir_src, dir_dest, &options) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn close_splashscreen(window: tauri::Window) {
|
||||
// Close splashscreen
|
||||
if let Some(splashscreen) = window.get_window("splashscreen") {
|
||||
splashscreen.close().unwrap();
|
||||
}
|
||||
// Show main window
|
||||
window.get_window("main").unwrap().show().unwrap();
|
||||
}
|
||||
|
|
260
src-tauri/src/commands/binaries.rs
Normal file
260
src-tauri/src/commands/binaries.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::config::LauncherConfig;
|
||||
|
||||
use super::CommandError;
|
||||
|
||||
// TODO - update data dir command
|
||||
|
||||
fn bin_ext(filename: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
return format!("{}.exe", filename);
|
||||
}
|
||||
return filename.to_string();
|
||||
}
|
||||
|
||||
struct CommonConfigData {
|
||||
install_path: std::path::PathBuf,
|
||||
active_version: String,
|
||||
active_version_folder: String,
|
||||
}
|
||||
|
||||
fn common_prelude(
|
||||
config: &tokio::sync::MutexGuard<LauncherConfig>,
|
||||
) -> Result<CommonConfigData, CommandError> {
|
||||
let install_path = match &config.installation_dir {
|
||||
None => {
|
||||
return Err(CommandError::BinaryExecution(format!(
|
||||
"No installation directory set, can't perform operation"
|
||||
)))
|
||||
}
|
||||
Some(path) => Path::new(path),
|
||||
};
|
||||
|
||||
let active_version = config
|
||||
.active_version
|
||||
.as_ref()
|
||||
.ok_or(CommandError::BinaryExecution(format!(
|
||||
"No active version set, can't perform operation"
|
||||
)))?;
|
||||
|
||||
let active_version_folder =
|
||||
config
|
||||
.active_version_folder
|
||||
.as_ref()
|
||||
.ok_or(CommandError::BinaryExecution(format!(
|
||||
"No active version folder set, can't perform operation"
|
||||
)))?;
|
||||
|
||||
Ok(CommonConfigData {
|
||||
install_path: install_path.to_path_buf(),
|
||||
active_version: active_version.clone(),
|
||||
active_version_folder: active_version_folder.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_data_dir(
|
||||
config_info: &CommonConfigData,
|
||||
game_name: &String,
|
||||
) -> Result<PathBuf, CommandError> {
|
||||
let data_folder = config_info
|
||||
.install_path
|
||||
.join("active")
|
||||
.join(game_name)
|
||||
.join("data");
|
||||
if !data_folder.exists() {
|
||||
return Err(CommandError::BinaryExecution(format!(
|
||||
"Could not locate relevant data directory '{}', can't perform operation",
|
||||
data_folder.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
Ok(data_folder)
|
||||
}
|
||||
|
||||
struct ExecutableLocation {
|
||||
executable_dir: PathBuf,
|
||||
executable_path: PathBuf,
|
||||
}
|
||||
|
||||
fn get_exec_location(
|
||||
config_info: &CommonConfigData,
|
||||
executable_name: &str,
|
||||
) -> Result<ExecutableLocation, CommandError> {
|
||||
let exec_dir = config_info
|
||||
.install_path
|
||||
.join("versions")
|
||||
.join(&config_info.active_version_folder)
|
||||
.join(&config_info.active_version);
|
||||
let exec_path = exec_dir.join(bin_ext(executable_name));
|
||||
if !exec_path.exists() {
|
||||
return Err(CommandError::BinaryExecution(format!(
|
||||
"Could not find the required binary '{}', can't perform operation",
|
||||
exec_path.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
Ok(ExecutableLocation {
|
||||
executable_dir: exec_dir,
|
||||
executable_path: exec_path,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extract_and_validate_iso(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name)?;
|
||||
let exec_info = get_exec_location(&config_info, "extractor")?;
|
||||
|
||||
let mut args = vec![
|
||||
path_to_iso.clone(),
|
||||
"--extract".to_string(),
|
||||
"--validate".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
];
|
||||
if Path::new(&path_to_iso.clone()).is_dir() {
|
||||
args.push("--folder".to_string());
|
||||
}
|
||||
// TODO - tee logs and handle error codes
|
||||
let output = Command::new(exec_info.executable_path)
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_decompiler(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name)?;
|
||||
let exec_info = get_exec_location(&config_info, "extractor")?;
|
||||
|
||||
let mut source_path = path_to_iso;
|
||||
if source_path.is_empty() {
|
||||
source_path = data_folder
|
||||
.join("iso_data")
|
||||
.join(game_name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// TODO - tee logs and handle error codes
|
||||
let output = Command::new(&exec_info.executable_path)
|
||||
.args([
|
||||
source_path,
|
||||
"--decompile".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_compiler(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name)?;
|
||||
let exec_info = get_exec_location(&config_info, "extractor")?;
|
||||
|
||||
let mut source_path = path_to_iso;
|
||||
if source_path.is_empty() {
|
||||
source_path = data_folder
|
||||
.join("iso_data")
|
||||
.join(game_name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// TODO - tee logs and handle error codes
|
||||
let output = Command::new(&exec_info.executable_path)
|
||||
.args([
|
||||
source_path,
|
||||
"--compile".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_repl(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<(), CommandError> {
|
||||
// TODO - explore a linux option though this is very annoying because without doing a ton of research
|
||||
// we seem to have to handle various terminals. Which honestly we should probably do on windows too
|
||||
//
|
||||
// So maybe we can make a menu where the user will specify what terminal to use / what launch-options to use
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name)?;
|
||||
let exec_info = get_exec_location(&config_info, "goalc")?;
|
||||
// TODO - handle error
|
||||
let output = Command::new("cmd")
|
||||
.args([
|
||||
"/K",
|
||||
"start",
|
||||
&bin_ext("goalc"),
|
||||
"--proj-path",
|
||||
&data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn launch_game(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
in_debug: bool,
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name)?;
|
||||
let exec_info = get_exec_location(&config_info, "gk")?;
|
||||
|
||||
let mut args = vec!["-boot".to_string(), "-fakeiso".to_string()];
|
||||
// TODO - order unfortunately matters for gk args, this will be fixed eventually...
|
||||
if in_debug {
|
||||
args.push("-debug".to_string());
|
||||
}
|
||||
args.push("-proj-path".to_string());
|
||||
args.push(data_folder.to_string_lossy().into_owned());
|
||||
// TODO - tee logs for SURE
|
||||
let output = Command::new(exec_info.executable_path)
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
use crate::config::LauncherConfig;
|
||||
use tauri::Manager;
|
||||
|
||||
use super::CommandError;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_install_directory(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<String>, ()> {
|
||||
) -> Result<Option<String>, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
match config_lock.installation_dir {
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(None),
|
||||
Some(_) => Ok(Some(
|
||||
config_lock.installation_dir.as_ref().unwrap().to_string(),
|
||||
)),
|
||||
Some(dir) => Ok(Some(dir.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,27 +18,50 @@ pub async fn get_install_directory(
|
|||
pub async fn set_install_directory(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
new_dir: String,
|
||||
) -> Result<(), ()> {
|
||||
) -> Result<(), CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
config_lock.set_install_directory(new_dir);
|
||||
config_lock.set_install_directory(new_dir).map_err(|_| {
|
||||
CommandError::Configuration(format!("Unable to persist installation directory"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_avx_supported() -> Result<bool, ()> {
|
||||
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
|
||||
return Ok(true);
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_avx_requirement_met(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
let config_lock = config.lock().await;
|
||||
) -> Result<bool, CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
match config_lock.requirements.avx {
|
||||
None => Ok(None),
|
||||
Some(_) => Ok(config_lock.requirements.avx),
|
||||
None => {
|
||||
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
|
||||
config_lock.requirements.avx = Some(false);
|
||||
} else {
|
||||
config_lock.requirements.avx = Some(false);
|
||||
}
|
||||
config_lock.save_config().map_err(|_| {
|
||||
CommandError::Configuration(format!("Unable to persist avx requirement change"))
|
||||
})?;
|
||||
Ok(config_lock.requirements.avx.unwrap_or(false))
|
||||
}
|
||||
Some(val) => Ok(val),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - investigate moving the OpenGL check into the rust layer via `wgpu`
|
||||
// for now, we return potentially undefined so the frontend can update the value via sidecar
|
||||
#[tauri::command]
|
||||
pub async fn is_opengl_requirement_met(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
) -> Result<Option<bool>, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
match config_lock.requirements.opengl {
|
||||
None => Ok(None),
|
||||
|
@ -46,15 +69,33 @@ pub async fn is_opengl_requirement_met(
|
|||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_opengl_requirement_met(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
requirement_met: bool,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
config_lock
|
||||
.set_opengl_requirement_met(requirement_met)
|
||||
.map_err(|_| {
|
||||
CommandError::Configuration(format!("Unable to persist opengl requirement change"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn finalize_installation(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
) -> Result<(), CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
config_lock.update_installed_game_version(game_name, true);
|
||||
app_handle.emit_all("gameInstalled", {}).unwrap();
|
||||
config_lock
|
||||
.update_installed_game_version(game_name, true)
|
||||
.map_err(|_| {
|
||||
CommandError::Configuration(format!("Unable to persist game installation status"))
|
||||
})?;
|
||||
app_handle.emit_all("gameInstalled", {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -62,7 +103,7 @@ pub async fn finalize_installation(
|
|||
pub async fn is_game_installed(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<bool, ()> {
|
||||
) -> Result<bool, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
Ok(config_lock.is_game_installed(game_name))
|
||||
}
|
||||
|
@ -71,7 +112,7 @@ pub async fn is_game_installed(
|
|||
pub async fn get_installed_version(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<String, ()> {
|
||||
) -> Result<String, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
// TODO - seriously, convert the config into a damn map
|
||||
match game_name.as_str() {
|
||||
|
@ -87,8 +128,9 @@ pub async fn get_installed_version(
|
|||
pub async fn get_installed_version_folder(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<String, ()> {
|
||||
) -> Result<String, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
// TODO - seriously, convert the config into a damn map
|
||||
match game_name.as_str() {
|
||||
"jak1" => Ok(config_lock.games.jak1.version_folder.clone().unwrap()),
|
||||
"jak2" => Ok(config_lock.games.jak2.version_folder.clone().unwrap()),
|
||||
|
@ -97,3 +139,39 @@ pub async fn get_installed_version_folder(
|
|||
_ => Ok("".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_active_version_change(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
version_folder: String,
|
||||
new_active_version: String,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
config_lock
|
||||
.set_active_version_folder(version_folder)
|
||||
.map_err(|_| {
|
||||
CommandError::Configuration(format!("Unable to persist active version folder change"))
|
||||
})?;
|
||||
config_lock
|
||||
.set_active_version(new_active_version)
|
||||
.map_err(|_| CommandError::Configuration(format!("Unable to persist active version change")))?;
|
||||
app_handle.emit_all("toolingVersionChanged", {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_active_version(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<String>, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
Ok(config_lock.active_version.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_active_version_folder(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<String>, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
Ok(config_lock.active_version_folder.clone())
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
use std::{path::Path, process::Command};
|
||||
|
||||
use crate::config::LauncherConfig;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extract_and_validate_iso(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
// TODO - be smarter
|
||||
// TODO - make folder if it doesnt exist
|
||||
// TODO - copy over the data folder
|
||||
// TODO - log it to a file
|
||||
// TODO - check error code
|
||||
let install_path = Path::new(path);
|
||||
let binary_dir = install_path.join("versions/official/v0.1.32/");
|
||||
let data_folder = install_path.join("active/jak1/data");
|
||||
let executable_location = binary_dir.join("extractor.exe");
|
||||
let mut args = vec![
|
||||
path_to_iso.clone(),
|
||||
"--extract".to_string(),
|
||||
"--validate".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
];
|
||||
if Path::new(&path_to_iso.clone()).is_dir() {
|
||||
args.push("--folder".to_string());
|
||||
}
|
||||
let output = Command::new(&executable_location)
|
||||
.args(args)
|
||||
.current_dir(binary_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_decompiler(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
let install_path = Path::new(path);
|
||||
let data_folder = install_path.join("active/jak1/data");
|
||||
|
||||
let mut source_path = path_to_iso;
|
||||
if source_path.is_empty() {
|
||||
// TODO - we could probably be more explicit here using a param
|
||||
source_path = data_folder
|
||||
.join("iso_data/jak1")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// TODO - be smarter
|
||||
// TODO - make folder if it doesnt exist
|
||||
// TODO - copy over the data folder
|
||||
// TODO - log it to a file
|
||||
let binary_dir = install_path.join("versions/official/v0.1.32/");
|
||||
let executable_location = binary_dir.join("extractor.exe");
|
||||
let output = Command::new(&executable_location)
|
||||
.args([
|
||||
source_path,
|
||||
"--decompile".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(binary_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_compiler(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
path_to_iso: String,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
let install_path = Path::new(path);
|
||||
let data_folder = install_path.join("active/jak1/data");
|
||||
|
||||
let mut source_path = path_to_iso;
|
||||
if source_path.is_empty() {
|
||||
// TODO - we could probably be more explicit here using a param
|
||||
source_path = data_folder
|
||||
.join("iso_data/jak1")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
// TODO - be smarter
|
||||
// TODO - make folder if it doesnt exist
|
||||
// TODO - copy over the data folder
|
||||
// TODO - log it to a file
|
||||
let binary_dir = install_path.join("versions/official/v0.1.32/");
|
||||
let executable_location = binary_dir.join("extractor.exe");
|
||||
let output = Command::new(&executable_location)
|
||||
.args([
|
||||
source_path,
|
||||
"--compile".to_string(),
|
||||
"--proj-path".to_string(),
|
||||
data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(binary_dir)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +1,25 @@
|
|||
use std::{path::Path, process::Command};
|
||||
use std::path::Path;
|
||||
|
||||
use tauri::{api::path::config_dir, Manager};
|
||||
|
||||
use crate::config::LauncherConfig;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn launch_game(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
in_debug: bool,
|
||||
) -> Result<(), ()> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
// TODO - be smarter
|
||||
// TODO - make folder if it doesnt exist
|
||||
// TODO - copy over the data folder
|
||||
// TODO - log it to a file
|
||||
// TODO - check error code
|
||||
let install_path = Path::new(path);
|
||||
let binary_dir = install_path.join("versions/official/v0.1.32/");
|
||||
let data_folder = install_path.join("active/jak1/data");
|
||||
let executable_location = binary_dir.join("gk.exe");
|
||||
let mut args = vec!["-boot".to_string(), "-fakeiso".to_string()];
|
||||
// TODO - order unfortunately matters for gk args, this will be fixed eventually...
|
||||
if in_debug {
|
||||
args.push("-debug".to_string());
|
||||
}
|
||||
args.push("-proj-path".to_string());
|
||||
args.push(data_folder.to_string_lossy().into_owned());
|
||||
let output = Command::new(&executable_location)
|
||||
.args(args)
|
||||
.current_dir(binary_dir)
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::CommandError;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_game(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
) -> Result<(), CommandError> {
|
||||
let mut config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
None => Err(CommandError::InvalidPath(format!(
|
||||
"Can't uninstalled the game, no installation directory found"
|
||||
))),
|
||||
Some(path) => {
|
||||
// TODO - cleanup
|
||||
let data_folder = Path::new(path).join("active/jak1/data");
|
||||
let data_folder = Path::new(path).join("active").join(&game_name).join("data");
|
||||
std::fs::remove_dir_all(data_folder.join("decompiler_out"));
|
||||
std::fs::remove_dir_all(data_folder.join("iso_data"));
|
||||
std::fs::remove_dir_all(data_folder.join("out"));
|
||||
|
@ -84,73 +52,3 @@ pub async fn reset_game_settings(game_name: String) -> Result<(), ()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(target_os = "windows")]
|
||||
// #[tauri::command]
|
||||
// pub async fn open_repl(proj_path: PathBuf, curr_dir: PathBuf) {
|
||||
// tauri::async_runtime::spawn(async move {
|
||||
// use std::process::Command as StdCommand;
|
||||
// let repl = StdCommand::new("cmd.exe")
|
||||
// .args([
|
||||
// "/K",
|
||||
// "start",
|
||||
// "goalc",
|
||||
// "--proj-path",
|
||||
// proj_path.to_str().as_ref().unwrap(),
|
||||
// ])
|
||||
// .current_dir(curr_dir)
|
||||
// .spawn()
|
||||
// .unwrap();
|
||||
// });
|
||||
// }
|
||||
|
||||
// #[cfg(target_os = "linux")]
|
||||
// #[tauri::command]
|
||||
// pub async fn open_repl(proj_path: PathBuf, curr_dir: PathBuf) {
|
||||
// tauri::async_runtime::spawn(async move {
|
||||
// use tauri::api::process::Command;
|
||||
// let tauri_cmd = Command::new_sidecar("goalc")
|
||||
// .unwrap()
|
||||
// .current_dir(curr_dir)
|
||||
// .args(["--proj-path", proj_path.to_str().as_ref().unwrap()])
|
||||
// .spawn();
|
||||
// });
|
||||
// }
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_repl(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
game_name: String,
|
||||
) -> Result<(), ()> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
// TODO - be smarter
|
||||
// TODO - make folder if it doesnt exist
|
||||
// TODO - copy over the data folder
|
||||
// TODO - log it to a file
|
||||
// TODO - check error code
|
||||
// TODO - explore a linux option though this is very annoying because without doing a ton of research
|
||||
// we seem to have to handle various terminals. Which honestly we should probably do on windows too
|
||||
//
|
||||
// So maybe we can make a menu where the user will specify what terminal to use / what launch-options to use
|
||||
let install_path = Path::new(path);
|
||||
let binary_dir = install_path.join("versions/official/v0.1.32/");
|
||||
let data_folder = install_path.join("active/jak1/data");
|
||||
let executable_location = binary_dir.join("goalc.exe");
|
||||
let output = Command::new("cmd")
|
||||
.args([
|
||||
"/K",
|
||||
"start",
|
||||
"goalc.exe",
|
||||
"--proj-path",
|
||||
&data_folder.to_string_lossy().into_owned(),
|
||||
])
|
||||
.current_dir(binary_dir)
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,15 @@ use std::{
|
|||
use sysinfo::{CpuExt, DiskExt, System, SystemExt};
|
||||
use zip::write::FileOptions;
|
||||
|
||||
use tauri::Manager;
|
||||
use tauri::api::path::config_dir;
|
||||
|
||||
use crate::{
|
||||
config::LauncherConfig,
|
||||
util::zip::{append_dir_contents_to_zip, append_file_to_zip},
|
||||
};
|
||||
|
||||
use super::CommandError;
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GPUInfo {
|
||||
|
@ -58,138 +60,201 @@ pub struct SupportPackage {
|
|||
pub disk_info: Vec<String>,
|
||||
pub gpu_info: Vec<GPUInfo>,
|
||||
pub game_info: PerGameInfo,
|
||||
pub launcher_version: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn generate_support_package(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
save_path: String,
|
||||
) -> Result<(), ()> {
|
||||
app_handle: tauri::AppHandle,
|
||||
user_path: String,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut package = SupportPackage::default();
|
||||
let config_lock = config.lock().await;
|
||||
// TODO - ask the user for the directory
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
// System Information
|
||||
let mut system_info = System::new_all();
|
||||
system_info.refresh_all();
|
||||
package.total_memory_megabytes = system_info.total_memory() / 1024 / 1024;
|
||||
package.cpu_name = system_info.cpus()[0].name().to_string();
|
||||
package.cpu_vendor = system_info.cpus()[0].vendor_id().to_string();
|
||||
package.cpu_brand = system_info.cpus()[0].brand().to_string();
|
||||
package.os_name = system_info.os_version().unwrap_or("unknown".to_string());
|
||||
package.os_name_long = system_info
|
||||
.long_os_version()
|
||||
.unwrap_or("unknown".to_string());
|
||||
package.os_kernel_ver = system_info
|
||||
.kernel_version()
|
||||
.unwrap_or("unknown".to_string());
|
||||
|
||||
for disk in system_info.disks() {
|
||||
package.disk_info.push(format!(
|
||||
"{}:{}-{}GB/{}GB",
|
||||
disk.mount_point().to_string_lossy(),
|
||||
disk.name().to_string_lossy(),
|
||||
disk.available_space() / 1024 / 1024 / 1024,
|
||||
disk.total_space() / 1024 / 1024 / 1024
|
||||
))
|
||||
}
|
||||
|
||||
// TODO - consider adding a regex for all file appending so we skip weird files that weren't expected
|
||||
|
||||
// TODO - maybe long-term this can replace glewinfo / support vulkan?
|
||||
let gpu_info_instance = wgpu::Instance::default();
|
||||
for a in gpu_info_instance.enumerate_adapters(wgpu::Backends::all()) {
|
||||
let info = a.get_info();
|
||||
let mut gpu_info = GPUInfo::default();
|
||||
gpu_info.name = info.name;
|
||||
gpu_info.driver_name = info.driver;
|
||||
gpu_info.driver_info = info.driver_info;
|
||||
package.gpu_info.push(gpu_info);
|
||||
}
|
||||
|
||||
// Create zip file
|
||||
let save_path = Path::new(&path.clone()).join("support-package.zip");
|
||||
let save_file = std::fs::File::create(save_path).expect("TODO");
|
||||
let mut zip_file = zip::ZipWriter::new(save_file);
|
||||
|
||||
// Save OpenGOAL config folder (this includes saves and settings)
|
||||
let game_config_dir = Path::new("C:\\Users\\xtvas\\AppData\\Roaming\\OpenGOAL");
|
||||
append_dir_contents_to_zip(&mut zip_file, &game_config_dir, "Game Settings and Saves")
|
||||
.expect("good");
|
||||
|
||||
// Save Launcher config folder
|
||||
// TODO - prompt on first startup to delete data folder
|
||||
let launcher_logs = Path::new("C:\\Users\\xtvas\\AppData\\Roaming\\OpenGOAL-Launcher");
|
||||
append_dir_contents_to_zip(
|
||||
&mut zip_file,
|
||||
&launcher_logs.join("logs"),
|
||||
"Launcher Settings and Logs/logs",
|
||||
)
|
||||
.expect("good");
|
||||
append_file_to_zip(
|
||||
&mut zip_file,
|
||||
&launcher_logs.join("settings.json"),
|
||||
"Launcher Settings and Logs/settings.json",
|
||||
)
|
||||
.expect("TODO");
|
||||
|
||||
// Save Logs
|
||||
// TODO - for all games
|
||||
let jak1_log_dir = Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\log");
|
||||
append_dir_contents_to_zip(&mut zip_file, &jak1_log_dir, "Game Logs and ISO Info/Jak 1")
|
||||
.expect("TODO");
|
||||
|
||||
// Per Game Info
|
||||
let texture_repl_dir =
|
||||
Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\texture_replacements");
|
||||
package.game_info.jak1.has_texture_packs =
|
||||
texture_repl_dir.exists() && !texture_repl_dir.read_dir().unwrap().next().is_none();
|
||||
let build_info_path = Path::new(
|
||||
"C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data\\iso_data\\jak1\\buildinfo.json",
|
||||
);
|
||||
append_file_to_zip(
|
||||
&mut zip_file,
|
||||
&build_info_path,
|
||||
"Game Logs and ISO Info/Jak 1/buildinfo.json",
|
||||
)
|
||||
.expect("TODO");
|
||||
let data_dir = Path::new("C:\\Users\\xtvas\\Downloads\\yee\\active\\jak1\\data");
|
||||
let version_data_dir =
|
||||
Path::new("C:\\Users\\xtvas\\Downloads\\yee\\versions\\official\\v0.1.32\\data");
|
||||
package
|
||||
.game_info
|
||||
.jak1
|
||||
.release_integrity
|
||||
.decompiler_folder_modified = dir_diff::is_different(
|
||||
data_dir.join("decompiler"),
|
||||
version_data_dir.join("decompiler"),
|
||||
)
|
||||
.unwrap();
|
||||
package
|
||||
.game_info
|
||||
.jak1
|
||||
.release_integrity
|
||||
.game_folder_modified =
|
||||
dir_diff::is_different(data_dir.join("game"), version_data_dir.join("game")).unwrap();
|
||||
package.game_info.jak1.release_integrity.goal_src_modified =
|
||||
dir_diff::is_different(data_dir.join("goal_src"), version_data_dir.join("goal_src"))
|
||||
.unwrap();
|
||||
|
||||
// Dump High Level Info
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::DEFLATE)
|
||||
.unix_permissions(0o755);
|
||||
zip_file.start_file("support-info.json", options).unwrap();
|
||||
let mut json_buffer = Vec::new();
|
||||
let json_writer = BufWriter::new(&mut json_buffer);
|
||||
serde_json::to_writer_pretty(json_writer, &package).expect("TODO");
|
||||
zip_file.write_all(&json_buffer).expect("TODO");
|
||||
|
||||
zip_file.finish().expect("TODO");
|
||||
|
||||
Ok(())
|
||||
let install_path = match &config_lock.installation_dir {
|
||||
None => {
|
||||
return Err(CommandError::Support(format!(
|
||||
"No installation directory set, can't generate the support package"
|
||||
)))
|
||||
}
|
||||
Some(path) => Path::new(path),
|
||||
};
|
||||
|
||||
// System Information
|
||||
let mut system_info = System::new_all();
|
||||
system_info.refresh_all();
|
||||
package.total_memory_megabytes = system_info.total_memory() / 1024 / 1024;
|
||||
package.cpu_name = system_info.cpus()[0].name().to_string();
|
||||
package.cpu_vendor = system_info.cpus()[0].vendor_id().to_string();
|
||||
package.cpu_brand = system_info.cpus()[0].brand().to_string();
|
||||
package.os_name = system_info.os_version().unwrap_or("unknown".to_string());
|
||||
package.os_name_long = system_info
|
||||
.long_os_version()
|
||||
.unwrap_or("unknown".to_string());
|
||||
package.os_kernel_ver = system_info
|
||||
.kernel_version()
|
||||
.unwrap_or("unknown".to_string());
|
||||
package.launcher_version = app_handle.package_info().version.to_string();
|
||||
|
||||
for disk in system_info.disks() {
|
||||
package.disk_info.push(format!(
|
||||
"{}:{}-{}GB/{}GB",
|
||||
disk.mount_point().to_string_lossy(),
|
||||
disk.name().to_string_lossy(),
|
||||
disk.available_space() / 1024 / 1024 / 1024,
|
||||
disk.total_space() / 1024 / 1024 / 1024
|
||||
))
|
||||
}
|
||||
|
||||
// TODO - maybe long-term this can replace glewinfo / support vulkan?
|
||||
let gpu_info_instance = wgpu::Instance::default();
|
||||
for a in gpu_info_instance.enumerate_adapters(wgpu::Backends::all()) {
|
||||
let info = a.get_info();
|
||||
let mut gpu_info = GPUInfo::default();
|
||||
gpu_info.name = info.name;
|
||||
gpu_info.driver_name = info.driver;
|
||||
gpu_info.driver_info = info.driver_info;
|
||||
package.gpu_info.push(gpu_info);
|
||||
}
|
||||
|
||||
// Create zip file
|
||||
let save_path = Path::new(&user_path).join("support-package.zip");
|
||||
let save_file = std::fs::File::create(save_path)
|
||||
.map_err(|_| CommandError::Support(format!("Unable to create support file")))?;
|
||||
let mut zip_file = zip::ZipWriter::new(save_file);
|
||||
|
||||
// Save OpenGOAL config folder (this includes saves and settings)
|
||||
let game_config_dir = match config_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Support(format!(
|
||||
"Couldn't determine application config directory"
|
||||
)))
|
||||
}
|
||||
Some(path) => path.join("OpenGOAL"),
|
||||
};
|
||||
append_dir_contents_to_zip(&mut zip_file, &game_config_dir, "Game Settings and Saves").map_err(
|
||||
|_| {
|
||||
CommandError::Support(format!(
|
||||
"Unable to append game settings and saves to the support package"
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
// TODO - don't fail fast so eagerly (when a path isn't found just continue on)
|
||||
|
||||
// Save Launcher config folder
|
||||
// TODO - prompt on first startup to delete data folder
|
||||
let launcher_config_dir = match app_handle.path_resolver().app_config_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Support(format!(
|
||||
"Couldn't determine launcher config directory"
|
||||
)))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
append_dir_contents_to_zip(
|
||||
&mut zip_file,
|
||||
&launcher_config_dir.join("logs"),
|
||||
"Launcher Settings and Logs/logs",
|
||||
)
|
||||
.map_err(|_| {
|
||||
CommandError::Support(format!(
|
||||
"Unable to append launcher logs to the support package"
|
||||
))
|
||||
})?;
|
||||
append_file_to_zip(
|
||||
&mut zip_file,
|
||||
&launcher_config_dir.join("settings.json"),
|
||||
"Launcher Settings and Logs/settings.json",
|
||||
)
|
||||
.map_err(|_| {
|
||||
CommandError::Support(format!(
|
||||
"Unable to append launcher settings to the support package"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Save Logs
|
||||
let active_version_dir = install_path.join("active");
|
||||
// TODO - for all games
|
||||
let jak1_log_dir = active_version_dir.join("jak1").join("data").join("log");
|
||||
append_dir_contents_to_zip(&mut zip_file, &jak1_log_dir, "Game Logs and ISO Info/Jak 1")
|
||||
.map_err(|_| CommandError::Support(format!("Unable to append game logs to support package")))?;
|
||||
|
||||
// Per Game Info
|
||||
let texture_repl_dir = active_version_dir
|
||||
.join("jak1")
|
||||
.join("data")
|
||||
.join("texture_replacements");
|
||||
package.game_info.jak1.has_texture_packs =
|
||||
texture_repl_dir.exists() && !texture_repl_dir.read_dir().unwrap().next().is_none();
|
||||
let build_info_path = active_version_dir
|
||||
.join("jak1")
|
||||
.join("data")
|
||||
.join("iso_data")
|
||||
.join("jak1")
|
||||
.join("buildinfo.json");
|
||||
append_file_to_zip(
|
||||
&mut zip_file,
|
||||
&build_info_path,
|
||||
"Game Logs and ISO Info/Jak 1/buildinfo.json",
|
||||
)
|
||||
.map_err(|_| {
|
||||
CommandError::Support(format!("Unable to append iso metadata to support package"))
|
||||
})?;
|
||||
|
||||
if config_lock.active_version_folder.is_some() && config_lock.active_version_folder.is_some() {
|
||||
let data_dir = active_version_dir.join("jak1").join("data");
|
||||
let version_data_dir = install_path
|
||||
.join("versions")
|
||||
.join(config_lock.active_version_folder.as_ref().unwrap())
|
||||
.join(config_lock.active_version.as_ref().unwrap())
|
||||
.join("data");
|
||||
package
|
||||
.game_info
|
||||
.jak1
|
||||
.release_integrity
|
||||
.decompiler_folder_modified = dir_diff::is_different(
|
||||
data_dir.join("decompiler"),
|
||||
version_data_dir.join("decompiler"),
|
||||
)
|
||||
.unwrap_or(true);
|
||||
package
|
||||
.game_info
|
||||
.jak1
|
||||
.release_integrity
|
||||
.game_folder_modified =
|
||||
dir_diff::is_different(data_dir.join("game"), version_data_dir.join("game")).unwrap_or(true);
|
||||
package.game_info.jak1.release_integrity.goal_src_modified =
|
||||
dir_diff::is_different(data_dir.join("goal_src"), version_data_dir.join("goal_src"))
|
||||
.unwrap_or(true);
|
||||
}
|
||||
|
||||
// Dump High Level Info
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::DEFLATE)
|
||||
.unix_permissions(0o755);
|
||||
zip_file
|
||||
.start_file("support-info.json", options)
|
||||
.map_err(|_| {
|
||||
CommandError::Support(format!(
|
||||
"Create high level support info entry in support package"
|
||||
))
|
||||
})?;
|
||||
let mut json_buffer = Vec::new();
|
||||
let json_writer = BufWriter::new(&mut json_buffer);
|
||||
serde_json::to_writer_pretty(json_writer, &package).map_err(|_| {
|
||||
CommandError::Support(format!(
|
||||
"Unable to write high-level support info to the support package"
|
||||
))
|
||||
})?;
|
||||
zip_file.write_all(&json_buffer).map_err(|_| {
|
||||
CommandError::Support(format!(
|
||||
"Unable to write high-level support info to the support package"
|
||||
))
|
||||
})?;
|
||||
zip_file
|
||||
.finish()
|
||||
.map_err(|_| CommandError::Support(format!("Unable to finalize zip file")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,132 +1,144 @@
|
|||
use futures_util::StreamExt;
|
||||
use std::{io::Cursor, path::Path};
|
||||
use tauri::Manager;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{config::LauncherConfig, util};
|
||||
use crate::{
|
||||
config::LauncherConfig,
|
||||
util::{
|
||||
file::{create_dir, delete_dir_or_folder},
|
||||
network::download_file,
|
||||
os::open_dir_in_os,
|
||||
zip::extract_and_delete_zip_file,
|
||||
},
|
||||
};
|
||||
|
||||
use super::CommandError;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_downloaded_versions(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
version_folder: String,
|
||||
) -> Result<Vec<String>, ()> {
|
||||
) -> Result<Vec<String>, CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(Vec::new()),
|
||||
Some(path) => {
|
||||
let expected_path = Path::new(path).join("versions").join(version_folder);
|
||||
if !expected_path.is_dir() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
match std::fs::read_dir(expected_path) {
|
||||
Err(_) => Ok(Vec::new()),
|
||||
Ok(entries) => Ok(
|
||||
entries
|
||||
.filter_map(|e| {
|
||||
e.ok().and_then(|d| {
|
||||
let p = d.path();
|
||||
if p.is_dir() {
|
||||
Some(
|
||||
p.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or("".into()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
let install_path = match &config_lock.installation_dir {
|
||||
None => return Ok(Vec::new()),
|
||||
Some(path) => Path::new(path),
|
||||
};
|
||||
|
||||
let expected_path = Path::new(install_path)
|
||||
.join("versions")
|
||||
.join(version_folder);
|
||||
if !expected_path.exists() || !expected_path.is_dir() {
|
||||
log::info!(
|
||||
"No {} folder found, returning no releases",
|
||||
expected_path.display()
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let entries = std::fs::read_dir(&expected_path).map_err(|_| {
|
||||
CommandError::VersionManagement(format!(
|
||||
"Unable to read versions from {}",
|
||||
expected_path.display()
|
||||
))
|
||||
})?;
|
||||
Ok(
|
||||
entries
|
||||
.filter_map(|e| {
|
||||
e.ok().and_then(|d| {
|
||||
let p = d.path();
|
||||
if p.is_dir() {
|
||||
Some(
|
||||
p.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or("".into()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_official_version(
|
||||
pub async fn download_version(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
version: String,
|
||||
version_folder: String,
|
||||
url: String,
|
||||
) -> Result<(), bool> {
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Ok(()),
|
||||
Some(path) => {
|
||||
// TODO - severe lack of safety here!
|
||||
// TODO - make the dir and the file name
|
||||
let expected_path = Path::new(&path).join("versions/official/test.zip");
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client.get(url);
|
||||
let res = req.send().await.expect("");
|
||||
let total = res.content_length().expect("");
|
||||
|
||||
let mut file = File::create(expected_path).await.expect("");
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.expect("");
|
||||
file.write_all(&chunk).await.expect("");
|
||||
}
|
||||
|
||||
let target_dir = Path::new(&path).join("versions/official/").join(version);
|
||||
|
||||
let zip_path = Path::new(&path).join("versions/official/test.zip");
|
||||
|
||||
let archive: Vec<u8> = std::fs::read(&zip_path.clone()).unwrap();
|
||||
zip_extract::extract(Cursor::new(archive), &target_dir, true).expect("");
|
||||
|
||||
std::fs::remove_file(zip_path).expect("TODO");
|
||||
|
||||
Ok(())
|
||||
let install_path = match &config_lock.installation_dir {
|
||||
None => {
|
||||
return Err(CommandError::VersionManagement(format!(
|
||||
"Cannot install version, no installation directory set"
|
||||
)))
|
||||
}
|
||||
}
|
||||
Some(path) => Path::new(path),
|
||||
};
|
||||
|
||||
let dest_dir = install_path
|
||||
.join("versions")
|
||||
.join(&version_folder)
|
||||
.join(&version);
|
||||
|
||||
// Delete the directory if it exists, and create it from scratch
|
||||
delete_dir_or_folder(&dest_dir).map_err(|_| {
|
||||
CommandError::VersionManagement(format!(
|
||||
"Unable to prepare destination folder '{}' for download",
|
||||
dest_dir.display()
|
||||
))
|
||||
})?;
|
||||
create_dir(&dest_dir).map_err(|_| {
|
||||
CommandError::VersionManagement(format!(
|
||||
"Unable to prepare destination folder '{}' for download",
|
||||
dest_dir.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
let download_path = install_path
|
||||
.join("versions")
|
||||
.join(version_folder)
|
||||
.join(format!("{}.zip", version));
|
||||
|
||||
// Download the file
|
||||
download_file(&url, &download_path).await.map_err(|_| {
|
||||
CommandError::VersionManagement(format!("Unable to successfully download version"))
|
||||
})?;
|
||||
|
||||
// Extract the zip file
|
||||
extract_and_delete_zip_file(&download_path, &dest_dir).map_err(|_| {
|
||||
CommandError::VersionManagement(format!("Unable to successfully extract downloaded version"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn go_to_version_folder(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
version_folder: String,
|
||||
) -> Result<(), ()> {
|
||||
) -> Result<(), CommandError> {
|
||||
let config_lock = config.lock().await;
|
||||
match &config_lock.installation_dir {
|
||||
None => Err(()),
|
||||
Some(path) => {
|
||||
let expected_path = Path::new(path).join("versions").join(version_folder);
|
||||
util::open_dir_in_os(expected_path.to_string_lossy().into_owned());
|
||||
Ok(())
|
||||
let install_path = match &config_lock.installation_dir {
|
||||
None => {
|
||||
return Err(CommandError::VersionManagement(format!(
|
||||
"Cannot go to version folder, no installation directory set"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(path) => Path::new(path),
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_active_version_change(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
version_folder: String,
|
||||
new_active_version: String,
|
||||
) -> Result<(), ()> {
|
||||
let mut config_lock = config.lock().await;
|
||||
// TODO - error checking
|
||||
config_lock.set_active_version_folder(version_folder);
|
||||
config_lock.set_active_version(new_active_version);
|
||||
app_handle.emit_all("toolingVersionChanged", {}).unwrap();
|
||||
let folder_path = Path::new(install_path)
|
||||
.join("versions")
|
||||
.join(version_folder);
|
||||
create_dir(&folder_path).map_err(|_| {
|
||||
CommandError::VersionManagement(format!(
|
||||
"Unable to go to create version folder '{}' in order to open it",
|
||||
folder_path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
open_dir_in_os(folder_path.to_string_lossy().into_owned())
|
||||
.map_err(|_| CommandError::VersionManagement(format!("Unable to go to open folder in OS")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_active_version(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<String>, ()> {
|
||||
let config_lock = config.lock().await;
|
||||
Ok(config_lock.active_version.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_active_version_folder(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
) -> Result<Option<String>, ()> {
|
||||
let config_lock = config.lock().await;
|
||||
Ok(config_lock.active_version_folder.clone())
|
||||
}
|
||||
|
|
11
src-tauri/src/commands/window.rs
Normal file
11
src-tauri/src/commands/window.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn close_splashscreen(window: tauri::Window) {
|
||||
// Close splashscreen
|
||||
if let Some(splashscreen) = window.get_window("splashscreen") {
|
||||
splashscreen.close().unwrap();
|
||||
}
|
||||
// Show main window
|
||||
window.get_window("main").unwrap().show().unwrap();
|
||||
}
|
|
@ -11,9 +11,18 @@
|
|||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
#[error("{0}")]
|
||||
Configuration(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SupportedGame {
|
||||
JAK1,
|
||||
|
@ -171,38 +180,57 @@ impl LauncherConfig {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn save_config(&self) {
|
||||
match &self.settings_path {
|
||||
pub fn save_config(&self) -> Result<(), ConfigError> {
|
||||
let settings_path = match &self.settings_path {
|
||||
None => {
|
||||
log::warn!("Can't save the settings file, as no path was initialized!");
|
||||
return Err(ConfigError::Configuration(format!(
|
||||
"No settings path defined, unable to save settings!"
|
||||
)));
|
||||
}
|
||||
Some(path) => {
|
||||
let file = fs::File::create(path).expect("TODO");
|
||||
serde_json::to_writer_pretty(file, &self);
|
||||
}
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
let file = fs::File::create(settings_path)?;
|
||||
serde_json::to_writer_pretty(file, &self)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_install_directory(&mut self, new_dir: String) {
|
||||
pub fn set_install_directory(&mut self, new_dir: String) -> Result<(), ConfigError> {
|
||||
self.installation_dir = Some(new_dir);
|
||||
self.save_config();
|
||||
self.save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_version(&mut self, new_version: String) {
|
||||
pub fn set_opengl_requirement_met(&mut self, new_val: bool) -> Result<(), ConfigError> {
|
||||
self.requirements.opengl = Some(new_val);
|
||||
self.save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_version(&mut self, new_version: String) -> Result<(), ConfigError> {
|
||||
self.active_version = Some(new_version);
|
||||
self.save_config();
|
||||
self.save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_version_folder(&mut self, new_version_folder: String) {
|
||||
pub fn set_active_version_folder(
|
||||
&mut self,
|
||||
new_version_folder: String,
|
||||
) -> Result<(), ConfigError> {
|
||||
self.active_version_folder = Some(new_version_folder);
|
||||
self.save_config();
|
||||
self.save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO - this pattern isn't great. It's made worse by trying to be backwards compatible though
|
||||
// with the old format
|
||||
// TODO - this pattern isn't great. It's made awkward by trying to be backwards compatible
|
||||
// with the old format though
|
||||
//
|
||||
// I think there should be an enum involved here somewhere/somehow
|
||||
pub fn update_installed_game_version(&mut self, game_name: String, installed: bool) {
|
||||
pub fn update_installed_game_version(
|
||||
&mut self,
|
||||
game_name: String,
|
||||
installed: bool,
|
||||
) -> Result<(), ConfigError> {
|
||||
match game_name.as_str() {
|
||||
"jak1" => {
|
||||
self.games.jak1.is_installed = installed;
|
||||
|
@ -246,7 +274,8 @@ impl LauncherConfig {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
self.save_config();
|
||||
self.save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_game_installed(&self, game_name: String) -> bool {
|
||||
|
|
|
@ -11,10 +11,6 @@ mod commands;
|
|||
mod config;
|
||||
mod textures;
|
||||
mod util;
|
||||
use commands::{close_splashscreen, copy_dir, get_highest_simd, open_dir};
|
||||
use textures::{extract_textures, get_all_texture_packs};
|
||||
|
||||
pub type FFIResult<T> = Result<T, String>;
|
||||
|
||||
fn main() {
|
||||
// TODO - switch to https://github.com/daboross/fern so we can setup easy logging
|
||||
|
@ -40,35 +36,30 @@ fn main() {
|
|||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::config::get_install_directory,
|
||||
commands::config::set_install_directory,
|
||||
commands::config::is_avx_requirement_met,
|
||||
commands::config::is_opengl_requirement_met,
|
||||
commands::binaries::extract_and_validate_iso,
|
||||
commands::binaries::launch_game,
|
||||
commands::binaries::open_repl,
|
||||
commands::binaries::run_compiler,
|
||||
commands::binaries::run_decompiler,
|
||||
commands::config::finalize_installation,
|
||||
commands::config::is_game_installed,
|
||||
commands::config::get_installed_version,
|
||||
commands::config::get_active_version_folder,
|
||||
commands::config::get_active_version,
|
||||
commands::config::get_install_directory,
|
||||
commands::config::get_installed_version_folder,
|
||||
commands::versions::list_downloaded_versions,
|
||||
commands::versions::download_official_version,
|
||||
commands::versions::go_to_version_folder,
|
||||
commands::versions::save_active_version_change,
|
||||
commands::versions::get_active_version,
|
||||
commands::versions::get_active_version_folder,
|
||||
commands::extractor::extract_and_validate_iso,
|
||||
commands::extractor::run_decompiler,
|
||||
commands::extractor::run_compiler,
|
||||
commands::game::launch_game,
|
||||
commands::game::uninstall_game,
|
||||
commands::config::get_installed_version,
|
||||
commands::config::is_avx_requirement_met,
|
||||
commands::config::is_avx_supported,
|
||||
commands::config::is_game_installed,
|
||||
commands::config::is_opengl_requirement_met,
|
||||
commands::config::save_active_version_change,
|
||||
commands::config::set_install_directory,
|
||||
commands::game::reset_game_settings,
|
||||
commands::game::open_repl,
|
||||
commands::game::uninstall_game,
|
||||
commands::support::generate_support_package,
|
||||
// Requirements Checking
|
||||
get_highest_simd,
|
||||
open_dir,
|
||||
copy_dir,
|
||||
close_splashscreen,
|
||||
extract_textures,
|
||||
get_all_texture_packs
|
||||
commands::versions::download_version,
|
||||
commands::versions::go_to_version_folder,
|
||||
commands::versions::list_downloaded_versions,
|
||||
commands::window::close_splashscreen
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error building tauri app")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Cursor, Read},
|
||||
io::{self, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +1,4 @@
|
|||
use std::process::Command;
|
||||
|
||||
pub mod file;
|
||||
pub mod network;
|
||||
pub mod os;
|
||||
pub mod zip;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const FILE_OPENING_PROGRAM: &str = "explorer";
|
||||
#[cfg(target_os = "linux")]
|
||||
const FILE_OPENING_PROGRAM: &str = "explorer";
|
||||
#[cfg(target_os = "macos")]
|
||||
const FILE_OPENING_PROGRAM: &str = "explorer";
|
||||
|
||||
pub fn open_dir_in_os(dir: String) {
|
||||
Command::new(FILE_OPENING_PROGRAM)
|
||||
.arg(dir) // <- Specify the directory you'd like to open.
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
|
|
20
src-tauri/src/util/file.rs
Normal file
20
src-tauri/src/util/file.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub fn delete_dir_or_folder(path: &PathBuf) -> Result<(), std::io::Error> {
|
||||
if path.exists() {
|
||||
if path.is_dir() {
|
||||
std::fs::remove_dir_all(path)?;
|
||||
} else {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_dir(path: &PathBuf) -> Result<(), std::io::Error> {
|
||||
if path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(path)?;
|
||||
Ok(())
|
||||
}
|
28
src-tauri/src/util/network.rs
Normal file
28
src-tauri/src/util/network.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use futures_util::StreamExt;
|
||||
use std::path::PathBuf;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NetworkError {
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
NetworkRequest(#[from] reqwest::Error),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub async fn download_file(url: &String, destination: &PathBuf) -> Result<(), NetworkError> {
|
||||
let client = reqwest::Client::new();
|
||||
let req = client.get(url);
|
||||
let res = req.send().await?;
|
||||
|
||||
let mut file = File::create(destination).await?;
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
file.write_all(&chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
13
src-tauri/src/util/os.rs
Normal file
13
src-tauri/src/util/os.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const FILE_OPENING_PROGRAM: &str = "explorer";
|
||||
#[cfg(target_os = "linux")]
|
||||
const FILE_OPENING_PROGRAM: &str = "xdg-open";
|
||||
#[cfg(target_os = "macos")]
|
||||
const FILE_OPENING_PROGRAM: &str = "open";
|
||||
|
||||
pub fn open_dir_in_os(dir: String) -> Result<(), std::io::Error> {
|
||||
Command::new(FILE_OPENING_PROGRAM).arg(dir).spawn()?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
use zip::write::FileOptions;
|
||||
|
||||
|
@ -13,7 +14,7 @@ pub fn append_dir_contents_to_zip(
|
|||
internal_folder: &str,
|
||||
) -> zip::result::ZipResult<()> {
|
||||
if !dir.exists() {
|
||||
Result::<(), ()>::Err(());
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
let iter = WalkDir::new(dir).into_iter().filter_map(|e| e.ok());
|
||||
|
@ -76,3 +77,13 @@ pub fn append_file_to_zip(
|
|||
|
||||
Result::Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_and_delete_zip_file(
|
||||
zip_path: &PathBuf,
|
||||
extract_dir: &PathBuf,
|
||||
) -> Result<(), zip_extract::ZipExtractError> {
|
||||
let archive: Vec<u8> = std::fs::read(zip_path)?;
|
||||
zip_extract::extract(Cursor::new(archive), extract_dir, true)?;
|
||||
std::fs::remove_file(zip_path)?;
|
||||
Result::Ok(())
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
progressTracker.proceed();
|
||||
progressTracker.proceed();
|
||||
} else if (jobType === "updateGame") {
|
||||
// TODO - update data dir
|
||||
progressTracker.init([
|
||||
{
|
||||
status: "queued",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</ul>
|
||||
<p class="mb-3">
|
||||
You can either update the game to this new version (no save data will be
|
||||
lost) or you can change your active version to match
|
||||
lost) or you can rollback your active version to match
|
||||
</p>
|
||||
<div
|
||||
class="justify-center items-center space-y-4 sm:flex sm:space-y-0 sm:space-x-4"
|
||||
|
|
Loading…
Reference in a new issue