mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-20 04:27:29 +00:00
695 lines
25 KiB
Rust
695 lines
25 KiB
Rust
use std::fs;
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio;
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::Arc;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use tauri::{AppHandle, Emitter, Manager, State};
|
|
use tauri_plugin_opener::OpenerExt;
|
|
use tokio::io::AsyncRead;
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio::sync::Mutex;
|
|
use crate::commands::runners;
|
|
use crate::config;
|
|
use crate::types::AppConfig;
|
|
#[cfg(target_os = "macos")]
|
|
use crate::platform::macos;
|
|
#[cfg(unix)]
|
|
use crate::platform::linux;
|
|
use crate::playtime::{self, PlaytimeResponse, PlaytimeDayEntry};
|
|
use crate::state::GameState;
|
|
use crate::types::McServer;
|
|
use crate::util;
|
|
use crate::workshop_server;
|
|
#[tauri::command]
|
|
#[allow(non_snake_case)]
|
|
pub async fn launch_game(
|
|
app: AppHandle,
|
|
state: State<'_, GameState>,
|
|
instance_id: String,
|
|
servers: Vec<McServer>,
|
|
extra_args: Vec<String>,
|
|
) -> Result<(), String> {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
let _ = (app, state, instance_id, servers, extra_args);
|
|
return Err("Launching the game is not implemented on Android. -neo".into());
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
launch_game_desktop(app, state, instance_id, servers, extra_args).await
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
async fn launch_game_desktop(
|
|
app: AppHandle,
|
|
state: State<'_, GameState>,
|
|
instance_id: String,
|
|
mut servers: Vec<McServer>,
|
|
extra_args: Vec<String>,
|
|
) -> Result<(), String> {
|
|
state.manual_stop.store(false, Ordering::SeqCst);
|
|
perform_instance_sync(&app, &instance_id).await?;
|
|
let working_dir = util::get_instance_working_dir(&app, &instance_id);
|
|
let config_val = config::load_config_raw(app.clone());
|
|
let lce_online = McServer { name: "LCEOnline Game".into(), ip: "127.0.0.1".into(), port: 61000 };
|
|
if !servers.iter().any(|s| s.ip == lce_online.ip && s.port == lce_online.port) {
|
|
servers.push(lce_online);
|
|
}
|
|
if let Some(ref saved) = config_val.saved_servers {
|
|
for s in saved {
|
|
if !servers.iter().any(|existing| existing.ip == s.ip && existing.port == s.port) {
|
|
servers.push(s.clone());
|
|
}
|
|
}
|
|
}
|
|
ensure_server_list(&working_dir, servers);
|
|
let ws_cancel = workshop_server::start(app.clone()).await;
|
|
let _ws_guard = workshop_server::Guard::new(ws_cancel.clone());
|
|
{
|
|
let mut lock = state.workshop_cancel.lock().await;
|
|
*lock = Some(ws_cancel);
|
|
}
|
|
|
|
let game_exe = working_dir.join("Minecraft.Client.exe");
|
|
if !game_exe.exists() {
|
|
return Err("Game executable not found in instance folder.".into());
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Some(ref runner_id) = config_val.linux_runner {
|
|
let runners_list = runners::get_available_runners(app.clone());
|
|
if let Some(runner) = runners_list.into_iter().find(|r| r.id == *runner_id) {
|
|
let is_proton = runner.r#type == "proton";
|
|
let program = if is_proton {
|
|
PathBuf::from(&runner.path).join("proton").to_string_lossy().to_string()
|
|
} else {
|
|
runner.path.clone()
|
|
};
|
|
let mut args: Vec<String> = Vec::new();
|
|
if is_proton {
|
|
args.push("run".to_string());
|
|
}
|
|
let compat_data = if is_proton {
|
|
let cd = working_dir.join("proton_prefix");
|
|
fs::create_dir_all(&cd).map_err(|e| e.to_string())?;
|
|
Some(cd)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mangohud = config_val.mangohud_enabled.unwrap_or(false);
|
|
let (prog, runner_args): (&str, &[&str]) = if mangohud {
|
|
("mangohud", &[&program])
|
|
} else {
|
|
(&program, &[])
|
|
};
|
|
|
|
let mut all_args: Vec<String> = Vec::new();
|
|
for a in runner_args {
|
|
all_args.push(a.to_string());
|
|
}
|
|
all_args.extend(args.clone());
|
|
all_args.push(game_exe.to_string_lossy().to_string());
|
|
all_args.extend(extra_args.clone());
|
|
let (final_prog, final_args) = apply_launch_prefix(prog, all_args, &config_val);
|
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
|
for a in &final_args {
|
|
cmd.arg(a);
|
|
}
|
|
|
|
if is_proton {
|
|
let cd = compat_data.as_ref().unwrap();
|
|
if std::env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").is_err() {
|
|
cmd.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", "");
|
|
}
|
|
cmd.env("STEAM_COMPAT_DATA_PATH", cd.to_str().unwrap());
|
|
if std::env::var("SteamAppId").is_err() {
|
|
cmd.env("SteamAppId", "480");
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
cmd.process_group(0);
|
|
cmd.env_remove("LD_PRELOAD");
|
|
cmd.env_remove("PYTHONPATH");
|
|
cmd.env_remove("PYTHONHOME");
|
|
cmd.env_remove("LD_LIBRARY_PATH");
|
|
cmd.env_remove("QT_PLUGIN_PATH");
|
|
}
|
|
|
|
apply_launch_env_vars(&mut cmd, &config_val);
|
|
cmd.current_dir(&working_dir);
|
|
cmd.env("WINEDEBUG", "+debugstr");
|
|
let playtime_start = std::time::Instant::now();
|
|
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
|
return Ok(());
|
|
};
|
|
|
|
let duration = playtime_start.elapsed();
|
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
|
let start = now - duration.as_secs();
|
|
playtime::record_session(&app, &instance_id, start, now);
|
|
|
|
return handle_game_exit(&app, &state, result);
|
|
}
|
|
}
|
|
Err("No Linux runner selected in settings.".into())
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let runtime_dir = macos::get_macos_runtime_dir(&app);
|
|
let toolkit_dir = runtime_dir.join("toolkit");
|
|
let prefix_dir = runtime_dir.join("prefix");
|
|
if !toolkit_dir.exists() || !prefix_dir.exists() {
|
|
return Err("macOS Compatibility is not set up. Open Settings and run Setup macOS Compatibility.".into());
|
|
}
|
|
|
|
let gptk_no_hud = macos::find_executable_recursive(&toolkit_dir, "gameportingtoolkit-no-hud")
|
|
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "gameportingtoolkit"));
|
|
|
|
let wine_binary = macos::find_executable_recursive(&toolkit_dir, "wine64")
|
|
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine"))
|
|
.ok_or_else(|| "Unable to locate wine binary inside runtime.".to_string())?;
|
|
|
|
let wine_bin_dir = wine_binary
|
|
.parent()
|
|
.map(|pp| pp.to_path_buf())
|
|
.ok_or_else(|| "Unable to locate wine bin directory inside runtime.".to_string())?;
|
|
|
|
let (mac_prog, mut mac_args): (String, Vec<String>) = if let Some(ref wrapper) = gptk_no_hud {
|
|
let win_path = util::unix_path_to_wine_z_path(&game_exe);
|
|
(wrapper.to_string_lossy().to_string(), vec![
|
|
prefix_dir.to_string_lossy().to_string(),
|
|
win_path,
|
|
])
|
|
} else {
|
|
(wine_binary.to_string_lossy().to_string(), vec![
|
|
game_exe.to_string_lossy().to_string(),
|
|
])
|
|
};
|
|
|
|
mac_args.extend(extra_args.clone());
|
|
|
|
let (final_prog, final_args) = apply_launch_prefix(&mac_prog, mac_args, &config_val);
|
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
|
for a in &final_args {
|
|
cmd.arg(a);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
cmd.process_group(0);
|
|
|
|
apply_launch_env_vars(&mut cmd, &config_val);
|
|
cmd.current_dir(&working_dir);
|
|
cmd.env("WINEPREFIX", &prefix_dir);
|
|
cmd.env("WINEDEBUG", "+debugstr");
|
|
let perf_boost = config_val.apple_silicon_performance_boost.unwrap_or(false);
|
|
if perf_boost {
|
|
#[cfg(target_arch = "aarch64")]
|
|
{
|
|
cmd.env("WINE_MSYNC", "1");
|
|
cmd.env("MVK_ALLOW_METAL_FENCES", "1");
|
|
}
|
|
#[cfg(not(target_arch = "aarch64"))]
|
|
{
|
|
cmd.env("WINEESYNC", "1");
|
|
}
|
|
} else {
|
|
cmd.env("WINEESYNC", "1");
|
|
}
|
|
cmd.env("WINEDLLOVERRIDES", "winemenubuilder.exe=d;mscoree,mshtml=");
|
|
cmd.env("MTL_HUD_ENABLED", "0");
|
|
cmd.env("MVK_CONFIG_RESUME_LOST_DEVICE", "1");
|
|
cmd.env(
|
|
"PATH",
|
|
format!(
|
|
"{}:{}",
|
|
wine_bin_dir.to_string_lossy(),
|
|
std::env::var("PATH").unwrap_or_default()
|
|
),
|
|
);
|
|
cmd.stdin(std::process::Stdio::null());
|
|
|
|
let playtime_start = std::time::Instant::now();
|
|
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
|
return Ok(());
|
|
};
|
|
|
|
let duration = playtime_start.elapsed();
|
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
|
let start = now - duration.as_secs();
|
|
playtime::record_session(&app, &instance_id, start, now);
|
|
|
|
return handle_game_exit(&app, &state, result);
|
|
}
|
|
|
|
#[cfg(all(
|
|
not(target_os = "macos"),
|
|
not(target_os = "linux"),
|
|
not(target_os = "android")
|
|
))]
|
|
{
|
|
let exe_str = game_exe.to_string_lossy().to_string();
|
|
let all_args: Vec<String> = extra_args.clone();
|
|
let (final_prog, final_args) = apply_launch_prefix(&exe_str, all_args, &config_val);
|
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
|
for a in &final_args {
|
|
cmd.arg(a);
|
|
}
|
|
#[cfg(unix)]
|
|
cmd.process_group(0);
|
|
apply_launch_env_vars(&mut cmd, &config_val);
|
|
cmd.current_dir(&working_dir);
|
|
let playtime_start = std::time::Instant::now();
|
|
let Some(result) = run_game_and_capture(&state, cmd).await? else {
|
|
return Ok(());
|
|
};
|
|
let duration = playtime_start.elapsed();
|
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
|
let start = now - duration.as_secs();
|
|
playtime::record_session(&app, &instance_id, start, now);
|
|
return handle_game_exit(&app, &state, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn stop_game(
|
|
app: AppHandle,
|
|
instance_id: String,
|
|
state: State<'_, GameState>,
|
|
) -> Result<(), String> {
|
|
state.manual_stop.store(true, Ordering::SeqCst);
|
|
let mut lock = state.child.lock().await;
|
|
if let Some(mut child) = lock.take() {
|
|
#[cfg(unix)]
|
|
linux::kill_process_tree(&app, &instance_id);
|
|
let _ = child.kill().await;
|
|
}
|
|
drop(lock);
|
|
|
|
let mut lock = state.workshop_cancel.lock().await;
|
|
if let Some(cancel) = lock.take() {
|
|
cancel.cancel();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[allow(non_snake_case)]
|
|
pub fn check_game_installed(app: AppHandle, instance_id: String) -> bool {
|
|
util::get_instance_working_dir(&app, &instance_id)
|
|
.join("Minecraft.Client.exe")
|
|
.exists()
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[allow(non_snake_case)]
|
|
pub fn open_instance_folder(app: AppHandle, instance_id: String) {
|
|
let folder = util::get_instance_working_dir(&app, &instance_id);
|
|
if folder.exists() {
|
|
let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>);
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[allow(non_snake_case)]
|
|
pub fn delete_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
|
|
let config_val = config::load_config_raw(app.clone());
|
|
if let Some(ref editions) = config_val.custom_editions {
|
|
if let Some(edition) = editions.iter().find(|e| e.id == instance_id) {
|
|
if edition.path.is_some() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
if let Some(ref custom_paths) = config_val.custom_paths {
|
|
if custom_paths.contains_key(&instance_id) {
|
|
return Ok(());
|
|
}
|
|
}
|
|
let dir = util::get_app_dir(&app).join("instances").join(&instance_id);
|
|
if dir.exists() {
|
|
let _ = fs::remove_dir_all(dir);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sync_dlc(app: AppHandle, instance_id: String) -> Result<(), String> {
|
|
perform_instance_sync(&app, &instance_id).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn get_instance_path(app: AppHandle, instance_id: String) -> String {
|
|
util::get_instance_working_dir(&app, &instance_id)
|
|
.to_string_lossy()
|
|
.to_string()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn get_playtime(app: AppHandle, instance_id: String) -> PlaytimeResponse {
|
|
playtime::get_playtime(&app, &instance_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn get_playtime_daily(app: AppHandle, instance_id: String, days: u64) -> Vec<PlaytimeDayEntry> {
|
|
playtime::get_playtime_daily(&app, &instance_id, days)
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
#[tauri::command]
|
|
pub fn backup_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
|
|
let instance_dir = util::get_instance_working_dir(&app, &instance_id);
|
|
if !instance_dir.exists() {
|
|
return Err("Instance not found".into());
|
|
}
|
|
|
|
let backup_name = format!("{}_backup.tar.gz", instance_id);
|
|
let file = rfd::FileDialog::new()
|
|
.set_title("Save Instance Backup")
|
|
.set_file_name(&backup_name)
|
|
.add_filter("Tar Gzip Archive", &["tar.gz"])
|
|
.save_file();
|
|
|
|
if let Some(path) = file {
|
|
let parent = instance_dir.parent().ok_or("Invalid instance path")?;
|
|
let dir_name = instance_dir.file_name().ok_or("Invalid dir name")?;
|
|
|
|
let status = std::process::Command::new("tar")
|
|
.args(["-czf", path.to_str().unwrap(), "-C", parent.to_str().unwrap(), dir_name.to_str().unwrap()])
|
|
.status()
|
|
.map_err(|e| format!("Failed to create backup: {}", e))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Backup command failed".into())
|
|
}
|
|
} else {
|
|
Err("CANCELED".into())
|
|
}
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
#[tauri::command]
|
|
pub fn restore_instance(app: AppHandle) -> Result<String, String> {
|
|
let file = rfd::FileDialog::new()
|
|
.set_title("Restore Instance Backup")
|
|
.add_filter("Tar Gzip Archive", &["tar.gz"])
|
|
.add_filter("Zip Archive", &["zip"])
|
|
.pick_file();
|
|
|
|
if let Some(path) = file {
|
|
let instances_dir = util::get_app_dir(&app).join("instances");
|
|
std::fs::create_dir_all(&instances_dir).map_err(|e| e.to_string())?;
|
|
|
|
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
|
|
let status = if ext == "zip" {
|
|
std::process::Command::new("unzip")
|
|
.arg("-o")
|
|
.arg(path.to_str().unwrap())
|
|
.arg("-d")
|
|
.arg(instances_dir.to_str().unwrap())
|
|
.status()
|
|
.map_err(|e| format!("Failed to extract backup: {}", e))?
|
|
} else {
|
|
std::process::Command::new("tar")
|
|
.args(["-xzf", path.to_str().unwrap(), "-C", instances_dir.to_str().unwrap()])
|
|
.status()
|
|
.map_err(|e| format!("Failed to extract backup: {}", e))?
|
|
};
|
|
|
|
if status.success() {
|
|
let extracted: Vec<_> = std::fs::read_dir(&instances_dir)
|
|
.map_err(|e| e.to_string())?
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path().is_dir() && e.path().join("Minecraft.Client.exe").exists())
|
|
.collect();
|
|
|
|
if let Some(new_instance) = extracted.into_iter().next() {
|
|
let id = new_instance.file_name().to_string_lossy().to_string();
|
|
Ok(id)
|
|
} else {
|
|
Ok("restored".into())
|
|
}
|
|
} else {
|
|
Err("Restore command failed".into())
|
|
}
|
|
} else {
|
|
Err("CANCELED".into())
|
|
}
|
|
}
|
|
|
|
pub fn ensure_server_list(instance_dir: &PathBuf, servers: Vec<McServer>) {
|
|
let servers_db = instance_dir.join("servers.db");
|
|
let mut all_servers = Vec::new();
|
|
if let Ok(content) = fs::read(&servers_db) {
|
|
if content.len() >= 12 && &content[0..4] == b"MCSV" {
|
|
let count = u32::from_le_bytes(content[8..12].try_into().unwrap_or([0; 4]));
|
|
let mut pos = 12;
|
|
for _ in 0..count {
|
|
if pos + 2 > content.len() { break; }
|
|
let ip_len = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2])) as usize;
|
|
pos += 2;
|
|
if pos + ip_len > content.len() { break; }
|
|
let ip = String::from_utf8_lossy(&content[pos..pos+ip_len]).to_string();
|
|
pos += ip_len;
|
|
if pos + 2 > content.len() { break; }
|
|
let port = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2]));
|
|
pos += 2;
|
|
if pos + 2 > content.len() { break; }
|
|
let name_len = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2])) as usize;
|
|
pos += 2;
|
|
if pos + name_len > content.len() { break; }
|
|
let name = String::from_utf8_lossy(&content[pos..pos+name_len]).to_string();
|
|
pos += name_len;
|
|
all_servers.push(McServer { name, ip, port });
|
|
}
|
|
}
|
|
}
|
|
|
|
for s in servers {
|
|
all_servers.push(s);
|
|
}
|
|
|
|
let mut unique_servers = Vec::new();
|
|
let mut seen: std::collections::HashSet<(String, u16)> = std::collections::HashSet::new();
|
|
for s in all_servers {
|
|
let key = (s.ip.clone(), s.port);
|
|
if seen.insert(key) {
|
|
unique_servers.push(s);
|
|
}
|
|
}
|
|
|
|
let mut file_content = Vec::new();
|
|
file_content.extend_from_slice(b"MCSV");
|
|
file_content.extend_from_slice(&1u32.to_le_bytes());
|
|
file_content.extend_from_slice(&(unique_servers.len() as u32).to_le_bytes());
|
|
for server in unique_servers {
|
|
let ip_bytes = server.ip.as_bytes();
|
|
let name_bytes = server.name.as_bytes();
|
|
file_content.extend_from_slice(&(ip_bytes.len() as u16).to_le_bytes());
|
|
file_content.extend_from_slice(ip_bytes);
|
|
file_content.extend_from_slice(&server.port.to_le_bytes());
|
|
file_content.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
|
|
file_content.extend_from_slice(name_bytes);
|
|
}
|
|
let _ = fs::create_dir_all(instance_dir);
|
|
let _ = fs::write(&servers_db, file_content);
|
|
}
|
|
|
|
fn perform_dlc_sync(app: &AppHandle, instance_dir: &PathBuf) -> Result<(), String> {
|
|
let mut dlc_src = None;
|
|
let root = util::get_app_dir(app);
|
|
use tauri::path::BaseDirectory;
|
|
if let Ok(p) = app.path().resolve("resources/DLC", BaseDirectory::Resource) {
|
|
if p.exists() {
|
|
dlc_src = Some(p);
|
|
} else {
|
|
if let Ok(p2) = app.path().resolve("DLC", BaseDirectory::Resource) {
|
|
if p2.exists() { dlc_src = Some(p2); }
|
|
}
|
|
}
|
|
}
|
|
|
|
if dlc_src.is_none() {
|
|
let current = std::env::current_dir().unwrap_or_default();
|
|
let p3 = current.join("src-tauri").join("resources").join("DLC");
|
|
let p4 = current.join("resources").join("DLC");
|
|
if p3.exists() { dlc_src = Some(p3); }
|
|
else if p4.exists() { dlc_src = Some(p4); }
|
|
}
|
|
|
|
if dlc_src.is_none() {
|
|
let p5 = root.join("DLC");
|
|
if p5.exists() { dlc_src = Some(p5); }
|
|
}
|
|
|
|
match dlc_src {
|
|
Some(src) => {
|
|
let dlc_dest = instance_dir.join("Windows64Media").join("DLC");
|
|
let _ = fs::create_dir_all(&dlc_dest);
|
|
if let Ok(entries) = fs::read_dir(&src) {
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name();
|
|
let dest_path = dlc_dest.join(&name);
|
|
if !dest_path.exists() {
|
|
if let Err(e) = if entry.path().is_dir() {
|
|
util::copy_dir_all(entry.path(), &dest_path)
|
|
} else {
|
|
fs::copy(entry.path(), &dest_path).map(|_| ())
|
|
} {
|
|
let _ = app.emit("backend-error", format!("DLC Sync: Failed to copy {:?}: {}", entry.path(), e));
|
|
eprintln!("[DLC Sync] Failed to copy {:?} to {:?}: {}", entry.path(), dest_path, e);
|
|
} else {
|
|
println!("[DLC Sync] Copied to {:?}", dest_path);
|
|
}
|
|
} else {
|
|
println!("[DLC Sync] Skipping {:?}: Already exists in instance", name);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
},
|
|
None => {
|
|
println!("[DLC Sync] Skipping sync: No DLC source found.");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_launch_prefix(program: &str, args: Vec<String>, config: &AppConfig) -> (String, Vec<String>) {
|
|
if let Some(ref prefix) = config.launch_prefix {
|
|
let trimmed = prefix.trim();
|
|
if !trimmed.is_empty() {
|
|
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
|
if !parts.is_empty() {
|
|
let mut all_args: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
|
|
all_args.push(program.to_string());
|
|
all_args.extend(args);
|
|
return (parts[0].to_string(), all_args);
|
|
}
|
|
}
|
|
}
|
|
(program.to_string(), args)
|
|
}
|
|
|
|
fn apply_launch_env_vars(cmd: &mut tokio::process::Command, config: &AppConfig) {
|
|
if let Some(ref env_vars) = config.launch_env_vars {
|
|
for (k, v) in env_vars {
|
|
cmd.env(k, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
const MAX_LOG_BYTES: usize = 1024 * 1024;
|
|
struct GameRunResult {
|
|
log: String,
|
|
}
|
|
|
|
fn spawn_log_reader<R>(mut reader: R, log: Arc<Mutex<Vec<u8>>>) -> tokio::task::JoinHandle<()> where R: AsyncRead + Unpin + Send + 'static, {
|
|
tokio::spawn(async move {
|
|
let mut buf = [0u8; 8192];
|
|
loop {
|
|
match reader.read(&mut buf).await {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(n) => {
|
|
let mut guard = log.lock().await;
|
|
guard.extend_from_slice(&buf[..n]);
|
|
if guard.len() > MAX_LOG_BYTES {
|
|
let remove = guard.len() - MAX_LOG_BYTES;
|
|
guard.drain(..remove);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async fn run_game_and_capture(
|
|
state: &State<'_, GameState>,
|
|
mut cmd: tokio::process::Command,
|
|
) -> Result<Option<GameRunResult>, String> {
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
let mut child = cmd.spawn().map_err(|e| e.to_string())?;
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let mut handles = Vec::new();
|
|
if let Some(stdout) = child.stdout.take() {
|
|
handles.push(spawn_log_reader(stdout, log.clone()));
|
|
}
|
|
if let Some(stderr) = child.stderr.take() {
|
|
handles.push(spawn_log_reader(stderr, log.clone()));
|
|
}
|
|
{
|
|
let mut lock = state.child.lock().await;
|
|
*lock = Some(child);
|
|
}
|
|
loop {
|
|
{
|
|
let mut lock = state.child.lock().await;
|
|
if let Some(ref mut c) = *lock {
|
|
if c.try_wait().map_err(|e| e.to_string())?.is_some() {
|
|
break;
|
|
}
|
|
} else {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
}
|
|
{
|
|
let mut lock = state.child.lock().await;
|
|
*lock = None;
|
|
}
|
|
for handle in handles {
|
|
let _ = handle.await;
|
|
}
|
|
let bytes = log.lock().await.clone();
|
|
let log_str = String::from_utf8_lossy(&bytes).to_string();
|
|
Ok(Some(GameRunResult { log: log_str }))
|
|
}
|
|
|
|
fn game_exited_ok(log: &str) -> bool {
|
|
log.lines()
|
|
.rev()
|
|
.take(3)
|
|
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
|
|
}
|
|
|
|
fn handle_game_exit(
|
|
app: &AppHandle,
|
|
state: &State<'_, GameState>,
|
|
result: GameRunResult,
|
|
) -> Result<(), String> {
|
|
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result.log) {
|
|
return Ok(());
|
|
}
|
|
let _ = app.emit("game-log", result.log);
|
|
Err("The game exited unexpectedly. Check the crash log for details.".into())
|
|
}
|
|
|
|
async fn perform_instance_sync(app: &AppHandle, instance_id: &str) -> Result<(), String> {
|
|
let target_dir = util::get_instance_working_dir(app, instance_id);
|
|
if !target_dir.exists() {
|
|
return Err("Instance directory not found".into());
|
|
}
|
|
|
|
let config_val = config::load_config_raw(app.clone());
|
|
let _ = fs::write(target_dir.join("username.txt"), &config_val.username);
|
|
let skin_pck_path = util::get_app_dir(app).join("Skin.pck");
|
|
if skin_pck_path.exists() {
|
|
let skin_dlc_dir = target_dir.join("Windows64Media").join("DLC").join("Custom Skins");
|
|
let _ = fs::create_dir_all(&skin_dlc_dir);
|
|
let _ = fs::copy(&skin_pck_path, skin_dlc_dir.join("Skin.pck"));
|
|
}
|
|
|
|
perform_dlc_sync(app, &target_dir)?;
|
|
Ok(())
|
|
}
|