From 0a32333019d40dd31261e3bad7f819b6dcdf4b99 Mon Sep 17 00:00:00 2001 From: /home/neo <158327205+neoapps-dev@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:07:52 +0300 Subject: [PATCH] 1.6.0 drop 1 (#132) --- RELEASE_NOTES.md | 32 ++----- src-tauri/src/commands/workshop.rs | 112 +++++++++++++++++------ src-tauri/src/config.rs | 1 + src-tauri/src/lib.rs | 2 + src-tauri/src/networking/relay.rs | 51 ++++++++--- src-tauri/src/types.rs | 1 + src/components/views/ModelEditorView.tsx | 2 +- src/components/views/PckEditorView.tsx | 18 ++-- src/components/views/SettingsView.tsx | 44 ++------- src/components/views/WorkshopView.tsx | 36 +++++--- src/data/splashes.ts | 5 +- src/hooks/useDiscordRPC.ts | 5 +- src/hooks/useGameManager.ts | 5 +- src/hooks/useLceOnlineNotifications.ts | 37 +++++--- 14 files changed, 207 insertions(+), 144 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3a6bb64..b3f0347 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,25 +1,11 @@ ![Release Artwork](https://raw.githubusercontent.com/LCE-Hub/LCE-Emerald-Launcher/refs/heads/main/RELEASE_ARTWORK.png) -- Fix fresh installation defaulting to Fullscreen -- Add a "Download to custom path" button to Versions menu -- Fix workshop black screen on server plugins category -- New logo for hellish ends -- Update neoLegacy download link -- (soon!) Partnership with BluerNetwork -- Skip Intro on keypress or click -- Disable skin remodeling for neoLegacy -- Download retry support -- Bring back the custom titlebar on non-macOS systems, and show on fullscreen regardless of OS -- Fix workshop showing extra stuff that don't exist for plugins -- Official external DLC support, used for neoLegacy to make the size smaller (700MiB -> 300MiB) -- Add logos to credits menu -- Partnership with [Minecraft: Moon Edition](https://www.youtube.com/watch?v=6_ZSUjZfBr0) -- Parallel downloads and redesigned download overlay notification -- A little bit of UI polish -- Community Guides for Developer Tools -- (BIG!) End partnership with LCELive. And migration to LCEOnline -- Workshop dependencies support -- Workshop required versions support -- New Model Editor developer tool -- New fun easter egg! try changing your username to `TOUHOU` -- And much much more you should see yourself! +- Fix custom path downloading overwriting non-empty directories ([#127](https://github.com/LCE-Hub/LCE-Emerald-Launcher/issues/127)) +- Add support for version channels in Workshop +- Fix LCEOnline Auth on Linux and macOS +- Add `unzip` fallback for Workshop items on Linux +- Fix critical bug in Workshop that deleted worlds and DLCs (thanks LeGamer!) +- Fix random disconnects on Windows +- Show multiplayer status on Discord RPC +- Fix skipping intro, fullscreen and legacy mode not importing in Settings (thanks Ryuzaki!) +- Remove controls menu from Settings diff --git a/src-tauri/src/commands/workshop.rs b/src-tauri/src/commands/workshop.rs index 45243bc..876f9a4 100644 --- a/src-tauri/src/commands/workshop.rs +++ b/src-tauri/src/commands/workshop.rs @@ -42,7 +42,6 @@ pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) - } let bytes = response.bytes().await.map_err(|e| e.to_string())?; fs::write(&zip_tmp, &bytes).map_err(|e| e.to_string())?; - let dest_dir = if placeholder.is_empty() { instance_dir.clone() } else { @@ -55,35 +54,96 @@ pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) - }; fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?; + let (extracted_files, extract_ok) = if cfg!(target_os = "linux") { + let bsdtar_list = std::process::Command::new("bsdtar") + .args(["-tf", zip_tmp.to_str().unwrap()]) + .output(); + if let Ok(out) = bsdtar_list { + if out.status.success() { + let listing = String::from_utf8_lossy(&out.stdout); + let files: Vec = listing.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty() && !l.ends_with('/')) + .map(|l| dest_dir.join(l).to_string_lossy().to_string()) + .collect(); + let st = std::process::Command::new("bsdtar") + .args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()]) + .status() + .map_err(|e| e.to_string())?; + (files, st.success()) + } else { + (Vec::new(), false) + } + } else { + (Vec::new(), false) + } + } else { + (Vec::new(), false) + }; + #[cfg(target_os = "linux")] - { - let status = std::process::Command::new("bsdtar") - .args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()]) + let (extracted_files, extract_ok) = if !extract_ok { + let unzip_list = std::process::Command::new("unzip") + .args(["-l", zip_tmp.to_str().unwrap()]) + .output() + .map_err(|e| e.to_string())?; + if !unzip_list.status.success() { + let _ = fs::remove_dir_all(&tmp_dir); + return Err(format!("Failed to list contents of {}", zip_name)); + } + let listing = String::from_utf8_lossy(&unzip_list.stdout); + let files: Vec = listing.lines() + .filter_map(|l| { + let mut parts = l.trim().split_whitespace(); + let size_str = parts.next()?; + size_str.parse::().ok()?; + parts.next()?; + parts.next()?; + Some(parts.collect::>().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_tmp.to_str().unwrap(), "-d", dest_dir.to_str().unwrap()]) .status() .map_err(|e| e.to_string())?; - if !status.success() { - let _ = fs::remove_dir_all(&tmp_dir); - return Err(format!("Extraction failed for {}", zip_name)); - } - } + (files, st.success()) + } else { + (extracted_files, extract_ok) + }; + #[cfg(not(target_os = "linux"))] - { - let status = std::process::Command::new("tar") + let (extracted_files, extract_ok) = { + let st = std::process::Command::new("tar") .args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()]) - .status() + .output() .map_err(|e| e.to_string())?; - if !status.success() { - let _ = fs::remove_dir_all(&tmp_dir); - return Err(format!("Extraction failed for {}", zip_name)); - } + let listing = std::process::Command::new("tar") + .args(["-tf", zip_tmp.to_str().unwrap()]) + .output() + .map_err(|e| e.to_string())?; + let listing_str = String::from_utf8_lossy(&listing.stdout); + let files: Vec = listing_str.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty() && !l.ends_with('/')) + .map(|l| dest_dir.join(l).to_string_lossy().to_string()) + .collect(); + (files, st.status.success()) + }; + + if !extract_ok { + let _ = fs::remove_dir_all(&tmp_dir); + return Err(format!("Extraction failed for {}", zip_name)); } - let dest_str = dest_dir.to_string_lossy().to_string(); - if !workshop_files.contains(&dest_str) { - workshop_files.push(dest_str.clone()); - } - if !pkg_dirs.contains(&dest_str) { - pkg_dirs.push(dest_str); + for f in &extracted_files { + if !workshop_files.contains(f) { + workshop_files.push(f.clone()); + } + if !pkg_dirs.contains(f) { + pkg_dirs.push(f.clone()); + } } } @@ -116,11 +176,9 @@ pub async fn workshop_uninstall(app: AppHandle, instance_id: String, package_id: .unwrap_or_default(); if let Some(pkg) = packages.iter().find(|p| p.id == package_id) { - for dir in &pkg.dirs { - let path = PathBuf::from(dir); - if path.is_dir() { - let _ = fs::remove_dir_all(&path); - } else if path.is_file() { + for file in &pkg.dirs { + let path = PathBuf::from(file); + if path.is_file() { let _ = fs::remove_file(&path); } } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 0903963..160a726 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -31,6 +31,7 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig { music_vol: Some(50), sfx_vol: Some(100), legacy_mode: Some(false), + skip_intro: Some(false), mangohud_enabled: None, saved_servers: None, extra_launch_args: None, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 636f89a..e2852c6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,10 +26,12 @@ use commands::workshop; use networking::relay; use networking::stun; use state::{DownloadState, GameState, ProxyGuard}; +fn webview_deep_link_interceptor() -> impl tauri::plugin::Plugin { tauri::plugin::Builder::::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() .plugin(tauri_plugin_deep_link::init()) + .plugin(webview_deep_link_interceptor()) .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { let urls: Vec = args .iter() diff --git a/src-tauri/src/networking/relay.rs b/src-tauri/src/networking/relay.rs index adbc7e6..f4773c5 100644 --- a/src-tauri/src/networking/relay.rs +++ b/src-tauri/src/networking/relay.rs @@ -1,6 +1,8 @@ use tauri::State; +use tauri::webview::cookie::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; +use tokio::time::sleep; use tokio_util::sync::CancellationToken; use crate::state::ProxyGuard; const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it @@ -10,7 +12,7 @@ async fn read_line(stream: &mut TcpStream) -> Result { loop { stream.read_exact(&mut byte).await.map_err(|e| e.to_string())?; if byte[0] == b'\n' { break; } - buf.push(byte[0]); + if byte[0] != b'\r' { buf.push(byte[0]); } } String::from_utf8(buf).map_err(|e| e.to_string()) } @@ -32,18 +34,23 @@ async fn run_host_relay( .map_err(|e| format!("Proxy connect failed: {}", e))?; write_line(&mut host_conn, &format!("HOST {} 0", auth_token)).await?; - let game_stream = loop { - match TcpStream::connect(format!("127.0.0.1:{}", game_port)).await { - Ok(s) => break s, - Err(_) => { - tokio::select! { - _ = cancel.cancelled() => return Err("Cancelled".into()), - _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {} + let game_stream = tokio::time::timeout( + std::time::Duration::from_secs(30), + async { + loop { + match TcpStream::connect(format!("127.0.0.1:{}", game_port)).await { + Ok(s) => return Ok::<_, String>(s), + Err(_) => { + tokio::select! { + _ = cancel.cancelled() => return Err("Cancelled".into()), + _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {} + } + } } } } - }; - + ).await + .map_err(|_| "Timed out waiting for game to start".to_string())??; let client_line = read_line(&mut host_conn).await?; let client_parts: Vec<&str> = client_line.split_whitespace().collect(); if client_parts.len() < 2 || client_parts[0] != "CLIENT" { @@ -167,7 +174,9 @@ pub async fn start_host_relay( let session_id = "__host__".to_string(); { let mut tokens = proxy_state.cancel_tokens.lock().await; - tokens.insert(session_id.clone(), cancel.clone()); + if let Some(old) = tokens.insert(session_id.clone(), cancel.clone()) { + old.cancel(); + } } let result = run_host_relay(&proxy_state, &addr, &auth_token, game_port, cancel).await; @@ -205,11 +214,12 @@ pub async fn start_relay_proxy( #[tauri::command] pub async fn stop_proxy(proxy_state: State<'_, ProxyGuard>, session_id: String) -> Result<(), String> { let mut tokens = proxy_state.cancel_tokens.lock().await; - if let Some(token) = tokens.remove(&session_id) { + let found = tokens.remove(&session_id); + if let Some(token) = found { token.cancel(); + let mut port = proxy_state.local_port.lock().await; + *port = None; } - let mut port = proxy_state.local_port.lock().await; - *port = None; Ok(()) } @@ -241,5 +251,18 @@ pub async fn join_game( ip: host_ip, port: host_port, }; + #[cfg(target_os = "windows")] + { + let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10); + loop { + if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", host_port)).await.is_ok() { + break; + } + if tokio::time::Instant::now() >= deadline { + return Err("Timed out waiting for relay proxy".into()); + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + } //neo: workaround for Windows having a race condition where the game is launched before the relay proxy crate::commands::game::launch_game(app, game_state, instance_id, vec![server], vec![]).await } diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 04d5eef..f51e828 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -53,6 +53,7 @@ pub struct AppConfig { pub music_vol: Option, pub sfx_vol: Option, pub legacy_mode: Option, + pub skip_intro: Option, pub mangohud_enabled: Option, pub saved_servers: Option>, pub extra_launch_args: Option>, diff --git a/src/components/views/ModelEditorView.tsx b/src/components/views/ModelEditorView.tsx index ba2aca5..6ea5cbf 100644 --- a/src/components/views/ModelEditorView.tsx +++ b/src/components/views/ModelEditorView.tsx @@ -281,7 +281,7 @@ export default function ModelEditorView() { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (document.activeElement?.tagName === "INPUT") return; - if (e.key === "Escape" || e.key === "Backspace") { + if (e.key === "Escape") { playBackSound(); setActiveView("devtools"); } diff --git a/src/components/views/PckEditorView.tsx b/src/components/views/PckEditorView.tsx index 102bd63..584b0e0 100644 --- a/src/components/views/PckEditorView.tsx +++ b/src/components/views/PckEditorView.tsx @@ -128,7 +128,13 @@ export default function PckEditorView() { setExpandedFolders(next); }; - type TreeNode = { name: string; path: string; isFolder: boolean; children: TreeNode[]; asset?: PCKAsset }; + type TreeNode = { + name: string; + path: string; + isFolder: boolean; + children: TreeNode[]; + asset?: PCKAsset; + }; const renderTree = (nodes: TreeNode[], depth = 0) => { return nodes.map((node) => { @@ -448,7 +454,7 @@ export default function PckEditorView() { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (document.activeElement?.tagName === "INPUT") return; - if (e.key === "Escape" || e.key === "Backspace") { + if (e.key === "Escape") { if (isEditingProperty) { setIsEditingProperty(null); return; @@ -768,18 +774,14 @@ export default function PckEditorView() {
- -