mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 17:11:00 +00:00
Merge diamond into main (#175)
Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
pub enum BridgeAction {
|
||||
Play,
|
||||
OpenContainer,
|
||||
OpenSettings,
|
||||
SwitchProton,
|
||||
InstallDriver,
|
||||
SetAudioBackend,
|
||||
}
|
||||
|
||||
pub fn launch_bridge(
|
||||
instance_path: String,
|
||||
action: BridgeAction,
|
||||
extra_args: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use jni::objects::{JObject, JValue};
|
||||
use jni::sys::jint;
|
||||
use jni::JNIEnv;
|
||||
use wry::prelude::{dispatch, find_class};
|
||||
fn start_bridge(
|
||||
env: &mut JNIEnv,
|
||||
activity: &JObject,
|
||||
action: &str,
|
||||
instance_path: &str,
|
||||
extra_args: &[String],
|
||||
) -> jni::errors::Result<()> {
|
||||
let bridge_class =
|
||||
find_class(env, activity, "dev.lcehub.emerald.LauncherBridgeActivity".to_string())?;
|
||||
let intent_class = env.find_class("android/content/Intent")?;
|
||||
let intent = env.new_object(
|
||||
intent_class,
|
||||
"(Landroid/content/Context;Ljava/lang/Class;)V", //neo: i hate smali so much
|
||||
&[(&activity).into(), (&bridge_class).into()],
|
||||
)?;
|
||||
|
||||
let extra_action = env.new_string("launcher_action")?;
|
||||
let action_str = env.new_string(action)?;
|
||||
env.call_method(
|
||||
&intent,
|
||||
"putExtra",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
|
||||
&[(&extra_action).into(), (&action_str).into()],
|
||||
)?;
|
||||
|
||||
let extra_path = env.new_string("instance_path")?;
|
||||
let path_str = env.new_string(instance_path)?;
|
||||
env.call_method(
|
||||
&intent,
|
||||
"putExtra",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
|
||||
&[(&extra_path).into(), (&path_str).into()],
|
||||
)?;
|
||||
|
||||
let extra_args_key = env.new_string("extra_args")?;
|
||||
let extra_args_json =
|
||||
env.new_string(&serde_json::to_string(extra_args).unwrap_or_else(|_| "[]".into()))?;
|
||||
env.call_method(
|
||||
&intent,
|
||||
"putExtra",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
|
||||
&[(&extra_args_key).into(), (&extra_args_json).into()],
|
||||
)?;
|
||||
|
||||
env.call_method(
|
||||
&intent,
|
||||
"addFlags",
|
||||
"(I)Landroid/content/Intent;",
|
||||
&[JValue::Int(0x10000000 as jint)], //neo: FLAG_ACTIVITY_NEW_TASK
|
||||
)?;
|
||||
|
||||
env.call_method(
|
||||
activity,
|
||||
"startActivity",
|
||||
"(Landroid/content/Intent;)V",
|
||||
&[(&intent).into()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let action_str = match action {
|
||||
BridgeAction::Play => "play",
|
||||
BridgeAction::OpenContainer => "open",
|
||||
BridgeAction::OpenSettings => "settings",
|
||||
BridgeAction::SwitchProton => "switch_proton",
|
||||
BridgeAction::InstallDriver => "install_driver",
|
||||
BridgeAction::SetAudioBackend => "set_audio_backend",
|
||||
}
|
||||
.to_string();
|
||||
dispatch(move |env, activity, _webview| {
|
||||
if let Err(e) = start_bridge(env, activity, &action_str, &instance_path, &extra_args) {
|
||||
eprintln!("[android_bridge] failed to start activity: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (instance_path, action, extra_args);
|
||||
Err("Only supported on Android".into())
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ pub fn get_external_palettes(app: AppHandle) -> Vec<ThemePalette> {
|
||||
palettes
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn import_theme(app: AppHandle) -> Result<String, String> {
|
||||
let file = rfd::FileDialog::new()
|
||||
@@ -54,6 +55,7 @@ pub fn import_theme(app: AppHandle) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn export_settings(app: AppHandle) -> Result<(), String> {
|
||||
let config = config::load_config_raw(app.clone());
|
||||
@@ -72,6 +74,7 @@ pub fn export_settings(app: AppHandle) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn import_settings(app: AppHandle) -> Result<String, String> {
|
||||
let file = rfd::FileDialog::new()
|
||||
|
||||
@@ -429,7 +429,7 @@ fn build_empty_chunk_nbt(chunk_x: i32, chunk_z: i32) -> Vec<u8> {
|
||||
out.extend_from_slice(&0i64.to_be_bytes());
|
||||
out.extend_from_slice(b"\x01\x00\x10TerrainPopulated\x01");
|
||||
out.extend_from_slice(b"\x09\x00\x08Entities\x0a\x00\x00\x00\x00");
|
||||
out.extend_from_slice(b"\x09\x00\x0cTileEntities\x0a\x00\x00\x00\x00");
|
||||
out.extend_from_slice(b"\x09\x00\x0cTileEntities\x0a\x00\x00\x00\x00"); //neo: someone send help
|
||||
out.push(0x00);
|
||||
out.push(0x00);
|
||||
out
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn stream_download(
|
||||
lock.insert(instance_id.to_string(), token);
|
||||
}
|
||||
|
||||
let response = reqwest::get(url).await.map_err(|e| e.to_string())?;
|
||||
let response = reqwest::Client::new().get(url).header(reqwest::header::USER_AGENT, "Emerald-Launcher").send().await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
{ state.tokens.lock().await.remove(instance_id); }
|
||||
return Err(format!("Download failed: {}", response.status()));
|
||||
@@ -181,7 +181,18 @@ pub async fn download_and_install(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let status = std::process::Command::new("unzip")
|
||||
.args(["-o", zip_path.to_str().unwrap(), "-d", instance_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !status.success() {
|
||||
return Err("Extraction failed".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), not(target_os = "android")))]
|
||||
{
|
||||
let mut cmd = std::process::Command::new("tar");
|
||||
cmd.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()]);
|
||||
@@ -291,6 +302,7 @@ pub async fn check_game_update(
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.head(&url)
|
||||
.header(reqwest::header::USER_AGENT, "Emerald-Launcher")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::fs;
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn pick_folder() -> Result<String, String> {
|
||||
let folder = rfd::FileDialog::new()
|
||||
@@ -12,6 +13,7 @@ pub fn pick_folder() -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn pick_file(title: String, filters: Vec<String>) -> Result<String, String> {
|
||||
let mut dialog = rfd::FileDialog::new().set_title(&title);
|
||||
@@ -26,6 +28,7 @@ pub fn pick_file(title: String, filters: Vec<String>) -> Result<String, String>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn save_file_dialog(title: String, filename: String, filters: Vec<String>) -> Result<String, String> {
|
||||
let mut dialog = rfd::FileDialog::new().set_title(&title).set_file_name(&filename);
|
||||
|
||||
+188
-14
@@ -24,6 +24,52 @@ 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>,
|
||||
mut extra_args: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
extra_args.extend(load_instance_args(&app, &instance_id));
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = state;
|
||||
let mut servers = servers;
|
||||
let working_dir = util::get_instance_working_dir(&app, &instance_id);
|
||||
if !working_dir.join("Minecraft.Client.exe").exists() {
|
||||
return Err("Game executable not found in instance folder.".into());
|
||||
}
|
||||
|
||||
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 result = crate::android_runtime::launch_bridge(
|
||||
working_dir.to_string_lossy().to_string(),
|
||||
crate::android_runtime::BridgeAction::Play,
|
||||
extra_args,
|
||||
);
|
||||
if result.is_ok() {
|
||||
playtime::start_session(&app, &instance_id);
|
||||
}
|
||||
result
|
||||
}
|
||||
#[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,
|
||||
@@ -155,9 +201,15 @@ pub async fn launch_game(
|
||||
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 is_intel = std::env::consts::ARCH == "x86_64";
|
||||
let wine_binary = if is_intel {
|
||||
macos::find_executable_recursive(&toolkit_dir, "wine")
|
||||
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine64"))
|
||||
} else {
|
||||
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()
|
||||
@@ -231,7 +283,11 @@ pub async fn launch_game(
|
||||
return handle_game_exit(&app, &state, result);
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
|
||||
#[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();
|
||||
@@ -291,8 +347,39 @@ pub fn check_game_installed(app: AppHandle, instance_id: String) -> bool {
|
||||
#[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>);
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = std::fs::create_dir_all(&folder);
|
||||
let _ = crate::android_runtime::launch_bridge(
|
||||
folder.to_string_lossy().to_string(),
|
||||
crate::android_runtime::BridgeAction::OpenContainer,
|
||||
Vec::new(),
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
if folder.exists() {
|
||||
let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn open_container_settings(app: AppHandle, instance_id: String) -> Result<(), String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let folder = util::get_instance_working_dir(&app, &instance_id);
|
||||
crate::android_runtime::launch_bridge(
|
||||
folder.to_string_lossy().to_string(),
|
||||
crate::android_runtime::BridgeAction::OpenSettings,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (app, instance_id);
|
||||
Err("Only supported on Android".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +418,21 @@ pub fn get_instance_path(app: AppHandle, instance_id: String) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn load_instance_args(app: &AppHandle, instance_id: &str) -> Vec<String> {
|
||||
let config_val = config::load_config_raw(app.clone());
|
||||
config_val
|
||||
.instance_launch_args
|
||||
.and_then(|m| m.get(instance_id).cloned())
|
||||
.map(|entry| entry.args)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_instance_args_schema(app: AppHandle, instance_id: String) -> Option<String> {
|
||||
let dir = util::get_instance_working_dir(&app, &instance_id);
|
||||
fs::read_to_string(dir.join("Arguments.Schema.json")).ok()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_playtime(app: AppHandle, instance_id: String) -> PlaytimeResponse {
|
||||
playtime::get_playtime(&app, &instance_id)
|
||||
@@ -341,6 +443,7 @@ pub fn get_playtime_daily(app: AppHandle, instance_id: String, days: u64) -> Vec
|
||||
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);
|
||||
@@ -374,6 +477,7 @@ pub fn backup_instance(app: AppHandle, instance_id: String) -> Result<(), String
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn restore_instance(app: AppHandle) -> Result<String, String> {
|
||||
let file = rfd::FileDialog::new()
|
||||
@@ -568,6 +672,7 @@ fn apply_launch_env_vars(cmd: &mut tokio::process::Command, config: &AppConfig)
|
||||
const MAX_LOG_BYTES: usize = 1024 * 1024;
|
||||
struct GameRunResult {
|
||||
log: String,
|
||||
exit_code: i32,
|
||||
}
|
||||
|
||||
fn spawn_log_reader<R>(mut reader: R, log: Arc<Mutex<Vec<u8>>>) -> tokio::task::JoinHandle<()> where R: AsyncRead + Unpin + Send + 'static, {
|
||||
@@ -607,11 +712,13 @@ async fn run_game_and_capture(
|
||||
let mut lock = state.child.lock().await;
|
||||
*lock = Some(child);
|
||||
}
|
||||
let exit_code;
|
||||
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() {
|
||||
if let Some(status) = c.try_wait().map_err(|e| e.to_string())? {
|
||||
exit_code = status.code().unwrap_or(1);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -629,14 +736,21 @@ async fn run_game_and_capture(
|
||||
}
|
||||
let bytes = log.lock().await.clone();
|
||||
let log_str = String::from_utf8_lossy(&bytes).to_string();
|
||||
Ok(Some(GameRunResult { log: log_str }))
|
||||
Ok(Some(GameRunResult { log: log_str, exit_code }))
|
||||
}
|
||||
|
||||
fn game_exited_ok(log: &str) -> bool {
|
||||
log.lines()
|
||||
.rev()
|
||||
.take(3)
|
||||
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
|
||||
fn game_exited_ok(result: &GameRunResult) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
result.log.lines()
|
||||
.rev()
|
||||
.take(3)
|
||||
.any(|line| line.contains("AppPolicyGetProcessTerminationMethod"))
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
result.exit_code == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_game_exit(
|
||||
@@ -644,7 +758,7 @@ fn handle_game_exit(
|
||||
state: &State<'_, GameState>,
|
||||
result: GameRunResult,
|
||||
) -> Result<(), String> {
|
||||
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result.log) {
|
||||
if state.manual_stop.swap(false, Ordering::SeqCst) || game_exited_ok(&result) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = app.emit("game-log", result.log);
|
||||
@@ -669,3 +783,63 @@ async fn perform_instance_sync(app: &AppHandle, instance_id: &str) -> Result<(),
|
||||
perform_dlc_sync(app, &target_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn switch_proton(app: AppHandle, version: String) -> Result<(), String> {
|
||||
let instance_id = {
|
||||
let config_val = config::load_config_raw(app.clone());
|
||||
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
|
||||
};
|
||||
crate::android_runtime::launch_bridge(
|
||||
instance_id,
|
||||
crate::android_runtime::BridgeAction::SwitchProton,
|
||||
vec![version],
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn switch_proton(_app: AppHandle, _version: String) -> Result<(), String> {
|
||||
Err("Only supported on Android".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn install_latest_driver(app: AppHandle) -> Result<(), String> {
|
||||
let instance_id = {
|
||||
let config_val = config::load_config_raw(app.clone());
|
||||
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
|
||||
};
|
||||
crate::android_runtime::launch_bridge(
|
||||
instance_id,
|
||||
crate::android_runtime::BridgeAction::InstallDriver,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn install_latest_driver(_app: AppHandle) -> Result<(), String> {
|
||||
Err("Only supported on Android".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn set_audio_backend(app: AppHandle, backend: String) -> Result<(), String> {
|
||||
let instance_id = {
|
||||
let config_val = config::load_config_raw(app.clone());
|
||||
config_val.profile.unwrap_or_else(|| "legacy_evolved".into())
|
||||
};
|
||||
crate::android_runtime::launch_bridge(
|
||||
instance_id,
|
||||
crate::android_runtime::BridgeAction::SetAudioBackend,
|
||||
vec![backend],
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn set_audio_backend(_app: AppHandle, _backend: String) -> Result<(), String> {
|
||||
Err("Only supported on Android".into())
|
||||
}
|
||||
|
||||
@@ -1285,6 +1285,7 @@ static MODERN_DIRECT_MAP: once_cell::sync::Lazy<HashMap<&'static str, LegacyBloc
|
||||
"lava_cauldron",
|
||||
LegacyBlockState { id: 118, data: 0 },
|
||||
);
|
||||
//neo: m.insert("help", neo {thisis: verytiring});
|
||||
m
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub enum NbtValue {
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct NbtCompound {
|
||||
pub name: String,
|
||||
pub tags: Vec<(String, NbtValue)>,
|
||||
pub tags: Vec<(String, NbtValue)>, //neo: if you came here asking about this, dont. please. dont ask why im storing a tuple in a vec. or why im manually resizing a vec.
|
||||
}
|
||||
|
||||
impl NbtCompound {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//neo: thanks Huckle for the Intel macOS support reference code lol
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::io::Write;
|
||||
@@ -55,6 +56,14 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
|
||||
assets: Vec<GithubAsset>,
|
||||
}
|
||||
|
||||
let is_intel = std::env::consts::ARCH == "x86_64";
|
||||
|
||||
let repo = if is_intel {
|
||||
"Gcenx/macOS_Wine_builds"
|
||||
} else {
|
||||
"Gcenx/game-porting-toolkit"
|
||||
};
|
||||
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"resolving",
|
||||
@@ -64,7 +73,7 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let release_text = client
|
||||
.get("https://api.github.com/repos/Gcenx/game-porting-toolkit/releases/latest")
|
||||
.get(format!("https://api.github.com/repos/{}/releases/latest", repo))
|
||||
.header("User-Agent", "Emerald-Legacy-Launcher")
|
||||
.send()
|
||||
.await
|
||||
@@ -80,7 +89,15 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz"))
|
||||
.find(|a| {
|
||||
let is_archive =
|
||||
a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz");
|
||||
if is_intel {
|
||||
is_archive && a.name.contains("staging")
|
||||
} else {
|
||||
is_archive
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| "No compatible runtime asset found in latest release.".to_string())?;
|
||||
|
||||
let runtime_dir = macos::get_macos_runtime_dir(&app);
|
||||
@@ -171,9 +188,14 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
|
||||
}
|
||||
|
||||
fs::create_dir_all(&prefix_dir).map_err(|e| e.to_string())?;
|
||||
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_binary = if is_intel {
|
||||
macos::find_executable_recursive(&toolkit_dir, "wine")
|
||||
.or_else(|| macos::find_executable_recursive(&toolkit_dir, "wine64"))
|
||||
} else {
|
||||
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()
|
||||
@@ -213,6 +235,181 @@ pub async fn setup_macos_runtime(window: tauri::Window, app: AppHandle) -> Resul
|
||||
return Err("Wine prefix initialization failed".into());
|
||||
}
|
||||
|
||||
if is_intel {
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"dxvk",
|
||||
"Fetching latest DXVK…".into(),
|
||||
None,
|
||||
);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DxvkAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DxvkRelease {
|
||||
assets: Vec<DxvkAsset>,
|
||||
}
|
||||
|
||||
let dxvk_release_text = client
|
||||
.get("https://api.github.com/repos/Gcenx/DXVK-macOS/releases/latest")
|
||||
.header("User-Agent", "Emerald-Legacy-Launcher")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.error_for_status()
|
||||
.map_err(|e| e.to_string())?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let dxvk_release: DxvkRelease =
|
||||
serde_json::from_str(&dxvk_release_text).map_err(|e| e.to_string())?;
|
||||
let dxvk_asset = dxvk_release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| {
|
||||
let is_archive =
|
||||
a.name.ends_with(".tar.xz") || a.name.ends_with(".tar.gz");
|
||||
is_archive && !a.name.contains("builtin")
|
||||
})
|
||||
.ok_or_else(|| "No compatible DXVK asset found.".to_string())?;
|
||||
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"dxvk_downloading",
|
||||
"Downloading DXVK…".into(),
|
||||
Some(0.0),
|
||||
);
|
||||
|
||||
let dxvk_archive_path = runtime_dir.join(format!("dxvk_{}", dxvk_asset.name));
|
||||
let dxvk_response = client
|
||||
.get(&dxvk_asset.browser_download_url)
|
||||
.header("User-Agent", "Emerald-Legacy-Launcher")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.error_for_status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let dxvk_total = dxvk_response.content_length().unwrap_or(0) as f64;
|
||||
let mut dxvk_file = fs::File::create(&dxvk_archive_path).map_err(|e| e.to_string())?;
|
||||
let mut dxvk_downloaded = 0.0;
|
||||
let mut dxvk_last_percent: i64 = -1;
|
||||
let mut dxvk_stream = dxvk_response.bytes_stream();
|
||||
while let Some(chunk) = dxvk_stream.next().await {
|
||||
let chunk = chunk.map_err(|e| e.to_string())?;
|
||||
dxvk_file.write_all(&chunk).map_err(|e| e.to_string())?;
|
||||
dxvk_downloaded += chunk.len() as f64;
|
||||
if dxvk_total > 0.0 {
|
||||
let percent = (dxvk_downloaded / dxvk_total * 100.0).clamp(0.0, 100.0);
|
||||
let rounded = percent.floor() as i64;
|
||||
if rounded != dxvk_last_percent {
|
||||
dxvk_last_percent = rounded;
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"dxvk_downloading",
|
||||
format!("Downloading DXVK… {}%", rounded),
|
||||
Some(percent),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(dxvk_file);
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"dxvk_extracting",
|
||||
"Extracting DXVK…".into(),
|
||||
None,
|
||||
);
|
||||
|
||||
let dxvk_dir = runtime_dir.join("dxvk");
|
||||
fs::create_dir_all(&dxvk_dir).map_err(|e| e.to_string())?;
|
||||
let status = Command::new("tar")
|
||||
.args([
|
||||
"-xf",
|
||||
dxvk_archive_path
|
||||
.to_str()
|
||||
.ok_or_else(|| "Invalid DXVK archive path".to_string())?,
|
||||
"-C",
|
||||
dxvk_dir
|
||||
.to_str()
|
||||
.ok_or_else(|| "Invalid DXVK extraction path".to_string())?,
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = fs::remove_file(&dxvk_archive_path);
|
||||
if !status.success() {
|
||||
return Err(format!("DXVK extraction failed with status: {:?}", status));
|
||||
}
|
||||
|
||||
macos::emit_macos_setup_progress(
|
||||
&window,
|
||||
"dxvk_patching",
|
||||
"Patching runtime with DXVK…".into(),
|
||||
None,
|
||||
);
|
||||
|
||||
let system32 = prefix_dir.join("drive_c/windows/system32");
|
||||
let syswow64 = prefix_dir.join("drive_c/windows/syswow64");
|
||||
let _ = fs::create_dir_all(&system32);
|
||||
let _ = fs::create_dir_all(&syswow64);
|
||||
let copy_dir = |src_dir: &std::path::Path, dst_dir: &std::path::Path| -> Result<(), String> {
|
||||
if !src_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in fs::read_dir(src_dir).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("dll") {
|
||||
let dest = dst_dir.join(path.file_name().unwrap());
|
||||
fs::copy(&path, &dest).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let dxvk_x64 = dxvk_dir.join("x64");
|
||||
let dxvk_x32 = dxvk_dir.join("x32");
|
||||
copy_dir(&dxvk_x64, &system32)?;
|
||||
copy_dir(&dxvk_x32, &syswow64)?;
|
||||
for dll_name in &["d3d11", "dxgi"] {
|
||||
let mut reg_cmd = Command::new(&wine_binary);
|
||||
reg_cmd
|
||||
.args(["reg", "add", "HKCU\\Software\\Wine\\DllOverrides"])
|
||||
.arg("/v")
|
||||
.arg(dll_name)
|
||||
.arg("/t")
|
||||
.arg("REG_SZ")
|
||||
.arg("/d")
|
||||
.arg("native,builtin")
|
||||
.arg("/f");
|
||||
reg_cmd
|
||||
.env("WINEPREFIX", &prefix_dir)
|
||||
.env("WINEDEBUG", "-all")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
wine_bin_dir.to_string_lossy(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
),
|
||||
)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
let _ = reg_cmd.status();
|
||||
}
|
||||
|
||||
let _ = fs::remove_dir_all(&dxvk_dir);
|
||||
}
|
||||
|
||||
macos::emit_macos_setup_progress(&window, "done", "Setup complete.".into(), Some(100.0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn add_to_steam(
|
||||
let (exe_str, launch_options, start_dir) = if in_flatpak {
|
||||
(
|
||||
"/usr/bin/flatpak".to_string(),
|
||||
format!("run io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher \"{}\"", instance_id),
|
||||
format!("run io.github.Emerald_Legacy_Launcher.Emerald_Legacy_Launcher \"{}\"", instance_id), //neo: yes i'm hardcoding it lmfao
|
||||
std::env::var("HOME").unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -152,6 +152,32 @@ fn extract_archive(zip_tmp: &Path, dest_dir: &Path) -> Result<(Vec<String>, bool
|
||||
extract_ok = st.success();
|
||||
}
|
||||
Ok((files, extract_ok))
|
||||
} else if cfg!(target_os = "android") {
|
||||
let unzip_list = std::process::Command::new("unzip")
|
||||
.args(["-l", zip_str])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !unzip_list.status.success() {
|
||||
return Err(format!("Failed to list contents of {}", zip_str));
|
||||
}
|
||||
let listing = String::from_utf8_lossy(&unzip_list.stdout);
|
||||
let files: Vec<String> = listing.lines()
|
||||
.filter_map(|l| {
|
||||
let mut parts = l.trim().split_whitespace();
|
||||
let size_str = parts.next()?;
|
||||
size_str.parse::<u64>().ok()?;
|
||||
parts.next()?;
|
||||
parts.next()?;
|
||||
Some(parts.collect::<Vec<&str>>().join(" "))
|
||||
})
|
||||
.filter(|l| !l.ends_with('/') && !l.contains('*'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("unzip")
|
||||
.args(["-o", zip_str, "-d", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok((files, st.success()))
|
||||
} else {
|
||||
let st = std::process::Command::new("tar")
|
||||
.args(["-xf", zip_str, "-C", dest_dir.to_str().unwrap()])
|
||||
|
||||
@@ -37,6 +37,9 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
|
||||
extra_launch_args: None,
|
||||
launch_prefix: None,
|
||||
launch_env_vars: None,
|
||||
instance_launch_args: None,
|
||||
android_runner: None,
|
||||
android_audio_backend: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use tauri::plugin::{Builder, PluginHandle, TauriPlugin};
|
||||
use tauri::Wry;
|
||||
pub struct LceAuthState(pub PluginHandle<Wry>);
|
||||
pub fn init() -> TauriPlugin<Wry> {
|
||||
Builder::new("emerald-lce-auth")
|
||||
.setup(|app, api| {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let handle = api.register_android_plugin("com.emerald.legacy", "LceAuthPlugin")?;
|
||||
app.manage(LceAuthState(handle));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_lce_auth(app: tauri::AppHandle) -> Result<String, String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let state = app.state::<LceAuthState>();
|
||||
state
|
||||
.0
|
||||
.run_mobile_plugin_async("startAuth", ())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = app;
|
||||
Err("LCE Online auth is only supported on Android".into()) //neo: erm, rust-side only. its supported on desktop on the typescript side.
|
||||
}
|
||||
}
|
||||
+86
-44
@@ -1,3 +1,4 @@
|
||||
#![cfg_attr(target_os = "android", allow(dead_code, unused))]
|
||||
mod types;
|
||||
mod state;
|
||||
mod config;
|
||||
@@ -6,6 +7,9 @@ mod playtime;
|
||||
mod platform;
|
||||
mod networking;
|
||||
mod workshop_server;
|
||||
mod lce_auth;
|
||||
#[cfg(target_os = "android")]
|
||||
mod android_runtime;
|
||||
mod commands;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -30,25 +34,34 @@ use state::{DownloadState, GameState, ProxyGuard};
|
||||
fn webview_deep_link_interceptor() -> impl tauri::plugin::Plugin<tauri::Wry> { tauri::plugin::Builder::<tauri::Wry>::new("emerald-deep-link-interceptor").on_navigation(|webview, url| { if url.scheme() == "emerald" || url.scheme() == "emeraldlauncher" { let _ = webview.app_handle().emit("deep-link", vec![url.to_string()]); false } else { true }}).build()}
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(webview_deep_link_interceptor())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
let urls: Vec<String> = args
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
a.starts_with("emerald://")
|
||||
|| a.starts_with("emeraldlauncher://")
|
||||
|| a.starts_with("discord-1482504445152460871://")
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
let _ = app.emit("deep-link", urls);
|
||||
}
|
||||
}))
|
||||
.plugin(lce_auth::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_gamepad::init())
|
||||
.plugin(tauri_plugin_opener::init());
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
let urls: Vec<String> = args
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
a.starts_with("emerald://")
|
||||
|| a.starts_with("emeraldlauncher://")
|
||||
|| a.starts_with("discord-1482504445152460871://")
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
let _ = app.emit("deep-link", urls);
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_drpc::init());
|
||||
}
|
||||
builder
|
||||
.manage(DownloadState {
|
||||
tokens: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
@@ -61,9 +74,6 @@ pub fn run() {
|
||||
cancel_tokens: Arc::new(Mutex::new(HashMap::new())),
|
||||
local_port: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
.plugin(tauri_plugin_gamepad::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_drpc::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
macos_setup::setup_macos_runtime,
|
||||
dlc::list_git_directory,
|
||||
@@ -75,12 +85,17 @@ pub fn run() {
|
||||
config_cmds::load_config,
|
||||
download::download_and_install,
|
||||
game::open_instance_folder,
|
||||
game::open_container_settings,
|
||||
download::cancel_download,
|
||||
runners::get_available_runners,
|
||||
config_cmds::get_external_palettes,
|
||||
#[cfg(desktop)]
|
||||
config_cmds::import_theme,
|
||||
#[cfg(desktop)]
|
||||
config_cmds::export_settings,
|
||||
#[cfg(desktop)]
|
||||
config_cmds::import_settings,
|
||||
#[cfg(desktop)]
|
||||
file_dialogs::pick_folder,
|
||||
download::download_runner,
|
||||
game::delete_instance,
|
||||
@@ -97,7 +112,9 @@ pub fn run() {
|
||||
macos_setup::check_macos_runtime_installed,
|
||||
macos_setup::check_macos_runtime_installed_fast,
|
||||
skin::download_logo,
|
||||
#[cfg(desktop)]
|
||||
file_dialogs::pick_file,
|
||||
#[cfg(desktop)]
|
||||
file_dialogs::save_file_dialog,
|
||||
file_dialogs::write_binary_file,
|
||||
file_dialogs::read_binary_file,
|
||||
@@ -107,7 +124,10 @@ pub fn run() {
|
||||
game::get_instance_path,
|
||||
game::get_playtime,
|
||||
game::get_playtime_daily,
|
||||
game::get_instance_args_schema,
|
||||
#[cfg(desktop)]
|
||||
game::backup_instance,
|
||||
#[cfg(desktop)]
|
||||
game::restore_instance,
|
||||
commands::console2lce::import_world,
|
||||
commands::console2lce::import_lce_save,
|
||||
@@ -119,45 +139,67 @@ pub fn run() {
|
||||
relay::stop_proxy,
|
||||
relay::stop_all_proxies,
|
||||
relay::join_game,
|
||||
lce_auth::start_lce_auth,
|
||||
plugins::get_plugins_dir,
|
||||
plugins::list_directory,
|
||||
plugins::create_plugin_dir,
|
||||
plugins::remove_plugin_dir,
|
||||
game::switch_proton,
|
||||
game::install_latest_driver,
|
||||
game::set_audio_backend,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
let config = config::load_config_raw(app_handle.clone());
|
||||
if config.start_fullscreen.unwrap_or(false) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.set_fullscreen(true);
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let config = config::load_config_raw(app_handle.clone());
|
||||
if config.start_fullscreen.unwrap_or(false) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.set_fullscreen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 && !args[1].starts_with('-') {
|
||||
let first = &args[1];
|
||||
let is_deep_link = first.starts_with("emerald://")
|
||||
|| first.starts_with("emeraldlauncher://")
|
||||
|| first.starts_with("discord-1482504445152460871://");
|
||||
if !is_deep_link {
|
||||
let instance_id = first.clone();
|
||||
let app_handle_clone = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Some(window) = app_handle_clone.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
let state = app_handle_clone.state::<GameState>();
|
||||
match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await {
|
||||
Ok(_) => app_handle_clone.exit(0),
|
||||
Err(e) => {
|
||||
let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}"));
|
||||
eprintln!("Auto-launch error: {}", e);
|
||||
app_handle_clone.exit(1);
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let handle = app_handle.clone();
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::Focused(true) = event {
|
||||
playtime::finish_active_session(&handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 && !args[1].starts_with('-') {
|
||||
let first = &args[1];
|
||||
let is_deep_link = first.starts_with("emerald://")
|
||||
|| first.starts_with("emeraldlauncher://")
|
||||
|| first.starts_with("discord-1482504445152460871://");
|
||||
if !is_deep_link {
|
||||
let instance_id = first.clone();
|
||||
let app_handle_clone = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Some(window) = app_handle_clone.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
let state = app_handle_clone.state::<GameState>();
|
||||
match game::launch_game(app_handle_clone.clone(), state, instance_id, Vec::new(), vec![]).await {
|
||||
Ok(_) => app_handle_clone.exit(0),
|
||||
Err(e) => {
|
||||
let _ = app_handle_clone.emit("backend-error", format!("Auto-launch: {e}"));
|
||||
eprintln!("Auto-launch error: {}", e);
|
||||
app_handle_clone.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -50,6 +50,9 @@ pub fn is_macos_runtime_installed(app: &AppHandle) -> bool {
|
||||
if !toolkit_dir.exists() {
|
||||
return false;
|
||||
}
|
||||
let candidates = ["Game Porting Toolkit.app"];
|
||||
candidates.iter().any(|name| find_executable_recursive(&toolkit_dir, name).is_some())
|
||||
if find_executable_recursive(&toolkit_dir, "Game Porting Toolkit.app").is_some() {
|
||||
return true;
|
||||
}
|
||||
find_executable_recursive(&toolkit_dir, "wine").is_some()
|
||||
|| find_executable_recursive(&toolkit_dir, "wine64").is_some()
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ pub struct PlaytimeData {
|
||||
pub sessions: HashMap<String, Vec<PlaytimeSession>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ActiveSession {
|
||||
instance_id: String,
|
||||
start: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaytimeResponse {
|
||||
@@ -54,6 +60,34 @@ pub fn record_session(app: &AppHandle, instance_id: &str, start: u64, end: u64)
|
||||
save(app, &data);
|
||||
}
|
||||
|
||||
const MAX_SESSION_SECONDS: u64 = 12 * 60 * 60;
|
||||
fn active_session_path(app: &AppHandle) -> PathBuf {
|
||||
util::get_app_dir(app).join("active_session.json")
|
||||
}
|
||||
|
||||
pub fn start_session(app: &AppHandle, instance_id: &str) {
|
||||
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let active = ActiveSession { instance_id: instance_id.to_string(), start };
|
||||
let path = active_session_path(app);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(content) = serde_json::to_string(&active) {
|
||||
let _ = std::fs::write(&path, content);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_active_session(app: &AppHandle) {
|
||||
let path = active_session_path(app);
|
||||
let Ok(content) = std::fs::read_to_string(&path) else { return };
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let Ok(active) = serde_json::from_str::<ActiveSession>(&content) else { return };
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
if now > active.start && now - active.start <= MAX_SESSION_SECONDS {
|
||||
record_session(app, &active.instance_id, active.start, now);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_playtime(app: &AppHandle, instance_id: &str) -> PlaytimeResponse {
|
||||
let data = load(app);
|
||||
let sessions = data.sessions.get(instance_id);
|
||||
|
||||
@@ -32,6 +32,13 @@ pub struct CustomizationEntry {
|
||||
pub panorama: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstanceLaunchArgs {
|
||||
pub values: std::collections::HashMap<String, serde_json::Value>,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppConfig {
|
||||
@@ -59,6 +66,9 @@ pub struct AppConfig {
|
||||
pub extra_launch_args: Option<Vec<String>>,
|
||||
pub launch_prefix: Option<String>,
|
||||
pub launch_env_vars: Option<std::collections::HashMap<String, String>>,
|
||||
pub instance_launch_args: Option<std::collections::HashMap<String, InstanceLaunchArgs>>,
|
||||
pub android_runner: Option<String>,
|
||||
pub android_audio_backend: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main";
|
||||
const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main"; //neo: more hardcoding!
|
||||
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| reqwest::Client::new());
|
||||
pub struct Guard {
|
||||
cancel: Option<CancellationToken>,
|
||||
@@ -34,7 +34,7 @@ pub async fn start(app: AppHandle) -> CancellationToken {
|
||||
}
|
||||
|
||||
async fn serve(app: AppHandle, cancel: CancellationToken) {
|
||||
let listener = match TcpListener::bind("127.0.0.1:5582").await {
|
||||
let listener = match TcpListener::bind("127.0.0.1:5582").await { //neo: dont say a word about this weird port.
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
let _ = app.emit("backend-error", format!("Workshop server failed to bind: {e}"));
|
||||
|
||||
Reference in New Issue
Block a user