Merge diamond into main (#175)

Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
/home/neo
2026-08-17 21:42:38 +03:00
committed by GitHub
co-authored by str1k3r
parent 816997803a
commit d6f622c913
128 changed files with 5747 additions and 1513 deletions
+104
View File
@@ -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())
}
}
+3
View File
@@ -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()
+1 -1
View File
@@ -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
+14 -2
View File
@@ -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())?;
+3
View File
@@ -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
View File
@@ -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
});
+1 -1
View File
@@ -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 {
+202 -5
View File
@@ -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(())
}
+1 -1
View File
@@ -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 {
+26
View File
@@ -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()])
+3
View File
@@ -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,
}
}
+35
View File
@@ -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
View File
@@ -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!())
+5 -2
View File
@@ -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()
}
+34
View File
@@ -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);
+10
View File
@@ -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)]
+2 -2
View File
@@ -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}"));