From a8826b83d3de41f9b264506b02af71ea5b437b51 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sun, 11 Oct 2026 09:13:55 +0530 Subject: [PATCH] fix(skins): fix skin lookup across directories, reliable TCP flush, modern userProperties, and optimize compile time --- .cargo/config.toml | 4 + Cargo.toml | 8 +- .../oneclient_app/src/hooks/queries/skin.rs | 36 ++-- packages/oneclient_common/src/paths.rs | 21 ++ packages/oneclient_core/src/game/launch.rs | 19 +- .../oneclient_core/src/game/skin_server.rs | 199 ++++++++---------- 6 files changed, 154 insertions(+), 133 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index eb8e347c..cefa8528 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,7 @@ +[target.x86_64-unknown-linux-gnu] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + # Linux and Macos by default have 8MB of stack storage. # Windows has 1MB, it isn't enough, so it's changed to 8MB also. [target.x86_64-pc-windows-msvc] diff --git a/Cargo.toml b/Cargo.toml index c3750939..6d614ef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,8 +151,8 @@ freya-skia-safe = "=0.103.2" # required for the SKSL player renderer image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]} [profile.release] -codegen-units = 1 -debug = "line-tables-only" +codegen-units = 16 +debug = false lto = "thin" opt-level = 3 panic = "abort" @@ -174,5 +174,9 @@ lto = false strip = false [profile.dev] +opt-level = 0 debug = 0 strip = "debuginfo" + +[profile.dev.package."*"] +opt-level = 2 diff --git a/packages/oneclient_app/src/hooks/queries/skin.rs b/packages/oneclient_app/src/hooks/queries/skin.rs index 6c92ff60..b9a59212 100644 --- a/packages/oneclient_app/src/hooks/queries/skin.rs +++ b/packages/oneclient_app/src/hooks/queries/skin.rs @@ -150,9 +150,6 @@ pub async fn save_account_skin( .await .map_err(|e| e.to_string())?; - let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; - let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; - let img = image::load_from_memory(skin_bytes) .map_err(|e| format!("Invalid image format: {e}"))? .to_rgba8(); @@ -177,17 +174,20 @@ pub async fn save_account_skin( .write_to(&mut cursor, image::ImageFormat::Png) .map_err(|e| format!("Failed to encode PNG: {e}"))?; - polyio::write(&skin_path, &png_bytes) - .await - .map_err(|e| format!("Failed to write skin file: {e}"))?; - let meta = SkinMetadata { is_slim }; let meta_json = serde_json::to_string_pretty(&meta) .map_err(|e| format!("Failed to serialize metadata: {e}"))?; - polyio::write(&meta_path, meta_json.as_bytes()) - .await - .map_err(|e| format!("Failed to write skin metadata: {e}"))?; + for dir in oneclient_common::paths::all_skins_dirs() { + let _ = polyio::create_dir_all(&dir).await; + let _ = polyio::write(&dir.join(format!("{uuid}.png")), &png_bytes).await; + let _ = polyio::write(&dir.join(format!("{uuid}.json")), meta_json.as_bytes()).await; + let _ = polyio::write(&dir.join(format!("{uuid}.meta.json")), meta_json.as_bytes()).await; + if let Some(uname) = username.filter(|u| !u.is_empty()) { + let _ = polyio::write(&dir.join(format!("{uname}.png")), &png_bytes).await; + let _ = polyio::write(&dir.join(format!("{uname}.json")), meta_json.as_bytes()).await; + } + } if let Some(uname) = username.filter(|u| !u.is_empty()) { let _ = sync_custom_skin_loader(uname, &png_bytes).await; @@ -199,14 +199,14 @@ pub async fn save_account_skin( } pub async fn delete_account_skin(uuid: &str, username: Option<&str>) -> Result<(), String> { - let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; - let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; - - if skin_path.exists() { - let _ = polyio::remove_file(&skin_path).await; - } - if meta_path.exists() { - let _ = polyio::remove_file(&meta_path).await; + for dir in oneclient_common::paths::all_skins_dirs() { + let _ = polyio::remove_file(&dir.join(format!("{uuid}.png"))).await; + let _ = polyio::remove_file(&dir.join(format!("{uuid}.json"))).await; + let _ = polyio::remove_file(&dir.join(format!("{uuid}.meta.json"))).await; + if let Some(uname) = username.filter(|u| !u.is_empty()) { + let _ = polyio::remove_file(&dir.join(format!("{uname}.png"))).await; + let _ = polyio::remove_file(&dir.join(format!("{uname}.json"))).await; + } } if let Some(uname) = username.filter(|u| !u.is_empty()) { diff --git a/packages/oneclient_common/src/paths.rs b/packages/oneclient_common/src/paths.rs index 99331506..14ec57a0 100644 --- a/packages/oneclient_common/src/paths.rs +++ b/packages/oneclient_common/src/paths.rs @@ -116,6 +116,27 @@ pub fn skins_dir() -> PathsResult { Ok(data_dir()?.join("skins")) } +/// Returns all candidate skins directories across current data dir, release, dev, and legacy paths +pub fn all_skins_dirs() -> Vec { + let mut dirs = Vec::new(); + if let Ok(dir) = skins_dir() { + dirs.push(dir); + } + if let Some(base) = BaseDirs::new() { + let data = base.data_dir(); + for candidate in [ + data.join("Polyfrost").join("OneClient").join("skins"), + data.join("oneclient-dev").join("skins"), + data.join("oneclient").join("skins"), + ] { + if !dirs.contains(&candidate) { + dirs.push(candidate); + } + } + } + dirs +} + pub fn skin_file_path(uuid: &str) -> PathsResult { Ok(skins_dir()?.join(format!("{uuid}.png"))) } diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index 497f91d4..114983d3 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -317,7 +317,14 @@ async fn start( let mods_dir = paths::cluster_mods_dir(&cluster.folder_name)?; if account.is_offline() { - crate::game::skin_server::disable_incompatible_offline_mods(&[mods_dir.clone(), cwd.join("mods")]).await; + let mut scan_dirs = vec![mods_dir.clone(), cwd.join("mods")]; + if let Ok(shared_mods) = paths::shared_mods_dir() { + scan_dirs.push(shared_mods); + } + if let Ok(shared_mc) = paths::shared_minecraft_dir() { + scan_dirs.push(shared_mc.join("mods")); + } + crate::game::skin_server::disable_incompatible_offline_mods(&scan_dirs).await; if cluster.mc_loader == oneclient_common::domain::GameLoader::Fabric && version.id == "1.21.11" { @@ -423,12 +430,18 @@ async fn start( && let Some(user_props) = crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await { + let mut replaced = false; for i in 0..mc_args.len() { - if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() && mc_args[i + 1] == "{}" { - mc_args[i + 1] = user_props; + if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() { + mc_args[i + 1] = user_props.clone(); + replaced = true; break; } } + if !replaced { + mc_args.push("--userProperties".to_string()); + mc_args.push(user_props); + } } arguments::append_profile_game_arguments( &mut mc_args, diff --git a/packages/oneclient_core/src/game/skin_server.rs b/packages/oneclient_core/src/game/skin_server.rs index afd07ce6..2b8b2325 100644 --- a/packages/oneclient_core/src/game/skin_server.rs +++ b/packages/oneclient_core/src/game/skin_server.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant}; use base64::Engine; use oneclient_common::paths::{ - authlib_injector_path, shared_minecraft_dir, skin_file_path, skin_meta_path, skins_dir, + authlib_injector_path, shared_minecraft_dir, }; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; @@ -185,9 +185,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std:: tracing::info!(method, uri, "skin_server incoming request"); if method != "GET" && method != "HEAD" && method != "POST" { - let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - stream.write_all(response.as_bytes()).await?; - return Ok(()); + return send_no_content(stream).await; } let path = uri.split('?').next().unwrap_or(uri); @@ -200,16 +198,10 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std:: "implementationName": "onelauncher-skin-server", "implementationVersion": env!("CARGO_PKG_VERSION") }, - "skinDomains": ["127.0.0.1", "localhost"], + "skinDomains": ["127.0.0.1", "localhost", ".minecraft.net", ".mojang.com"], "signaturePublickey": SKIN_SERVER_PUBLIC_KEY }).to_string(); - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(header.as_bytes()).await?; - return Ok(()); + return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await; } // 2. Public keys endpoint (for Minecraft 1.19+ profile signature verification) @@ -219,13 +211,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std:: "profilePropertyKeys": [{"publicKey": key_b64}], "playerCertificateKeys": [{"publicKey": key_b64}] }).to_string(); - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(header.as_bytes()).await?; - return Ok(()); + return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await; } // 3. Profiles by names endpoint (bulk profile lookup) @@ -243,13 +229,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std:: } } let body = serde_json::to_string(&profiles).unwrap_or_else(|_| "[]".to_string()); - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(header.as_bytes()).await?; - return Ok(()); + return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await; } // 4. Profile request: /sessionserver/session/minecraft/profile/ or /session/minecraft/profile/ @@ -262,50 +242,58 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std:: // Check if this is one of our local accounts with a custom skin if let Some(json_body) = build_profile_response(uuid_str, port).await { - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - json_body.len(), - json_body - ); - stream.write_all(header.as_bytes()).await?; - return Ok(()); + return send_response(stream, "200 OK", "application/json; charset=utf-8", json_body.as_bytes()).await; } // For other players on the server, check Mojang session server so official players keep their skins if let Some(mojang_body) = fetch_mojang_profile(uuid_str).await { - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - mojang_body.len(), - mojang_body - ); - stream.write_all(header.as_bytes()).await?; - return Ok(()); + return send_response(stream, "200 OK", "application/json; charset=utf-8", mojang_body.as_bytes()).await; } // Offline / unknown player: return 204 No Content -> vanilla Minecraft renders default Steve/Alex - let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - stream.write_all(response.as_bytes()).await?; - return Ok(()); + return send_no_content(stream).await; } - // 5. Texture request: /textures/.png - if let Some(texture_raw) = path.strip_prefix("/textures/") { + // 5. Texture request: /textures/.png or /texture/ + let texture_opt = path + .strip_prefix("/textures/") + .or_else(|| path.strip_prefix("/texture/")); + + if let Some(texture_raw) = texture_opt { let texture_id = texture_raw.trim_matches('/').trim_end_matches(".png"); if let Some(png_bytes) = load_skin_png(texture_id).await { - let header = format!( - "HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - png_bytes.len() - ); - stream.write_all(header.as_bytes()).await?; - if method == "GET" { - stream.write_all(&png_bytes).await?; - } - return Ok(()); + let body_to_send = if method == "HEAD" { &[][..] } else { &png_bytes[..] }; + return send_response(stream, "200 OK", "image/png", body_to_send).await; } } + send_no_content(stream).await +} + +async fn send_response( + mut stream: TcpStream, + status: &str, + content_type: &str, + body: &[u8], +) -> Result<(), std::io::Error> { + let header = format!( + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(header.as_bytes()).await?; + if !body.is_empty() { + stream.write_all(body).await?; + } + let _ = stream.flush().await; + let _ = stream.shutdown().await; + Ok(()) +} + +async fn send_no_content(mut stream: TcpStream) -> Result<(), std::io::Error> { let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; stream.write_all(response.as_bytes()).await?; + let _ = stream.flush().await; + let _ = stream.shutdown().await; Ok(()) } @@ -332,53 +320,32 @@ pub fn to_undashed_uuid(s: &str) -> String { async fn find_skin_path(uuid_or_name: &str) -> Option { let dashed = to_dashed_uuid(uuid_or_name); let undashed = to_undashed_uuid(uuid_or_name); + let lower = uuid_or_name.to_lowercase(); - if let Ok(p) = skin_file_path(&dashed) - && p.exists() - { - return Some(p); - } - if let Ok(p) = skin_file_path(&undashed) - && p.exists() - { - return Some(p); - } - if let Ok(p) = skin_file_path(uuid_or_name) - && p.exists() - { - return Some(p); - } + let mut keys = vec![dashed.clone(), undashed.clone(), lower.clone(), uuid_or_name.to_string()]; if let Some(accounts) = REGISTERED_ACCOUNTS.get() { let read = accounts.read(); if let Some(reg) = read .get(&dashed) .or_else(|| read.get(&undashed)) - .or_else(|| read.get(&uuid_or_name.to_lowercase())) + .or_else(|| read.get(&lower)) { - if let Ok(p) = skin_file_path(®.uuid) - && p.exists() - { - return Some(p); - } - if let Ok(p) = skin_file_path(®.undashed_uuid) - && p.exists() - { - return Some(p); - } - if let Ok(dir) = skins_dir() { - let name_path = dir.join(format!("{}.png", reg.username)); - if name_path.exists() { - return Some(name_path); - } - } + keys.push(reg.uuid.clone()); + keys.push(reg.undashed_uuid.clone()); + keys.push(reg.username.clone()); + keys.push(reg.username.to_lowercase()); } } - if let Ok(dir) = skins_dir() { - let name_path = dir.join(format!("{uuid_or_name}.png")); - if name_path.exists() { - return Some(name_path); + let search_dirs = oneclient_common::paths::all_skins_dirs(); + + for dir in &search_dirs { + for key in &keys { + let p1 = dir.join(format!("{key}.png")); + if p1.is_file() { + return Some(p1); + } } } @@ -401,17 +368,21 @@ async fn resolve_username(uuid_input: &str) -> String { } } - // Try reading from auth.json - if let Ok(auth_p) = oneclient_common::paths::auth_file() - && let Ok(content) = polyio::read_to_string(&auth_p).await - && let Ok(val) = serde_json::from_str::(&content) - && let Some(users) = val.get("users").and_then(|u| u.as_object()) - { - for (id, user) in users { - if (id == &dashed || id.replace('-', "").to_lowercase() == undashed) - && let Some(name) = user.get("username").and_then(|n| n.as_str()) + // Try reading from auth.json across all data directory candidates + for skin_dir in oneclient_common::paths::all_skins_dirs() { + if let Some(data_dir) = skin_dir.parent() { + let auth_p = data_dir.join("auth.json"); + if let Ok(content) = polyio::read_to_string(&auth_p).await + && let Ok(val) = serde_json::from_str::(&content) + && let Some(users) = val.get("users").and_then(|u| u.as_object()) { - return name.to_string(); + for (id, user) in users { + if (id == &dashed || id.replace('-', "").to_lowercase() == undashed) + && let Some(name) = user.get("username").and_then(|n| n.as_str()) + { + return name.to_string(); + } + } } } } @@ -423,13 +394,21 @@ async fn read_skin_metadata(uuid_or_name: &str) -> bool { let dashed = to_dashed_uuid(uuid_or_name); let undashed = to_undashed_uuid(uuid_or_name); let lower = uuid_or_name.to_lowercase(); - for key in [&dashed, &undashed, &lower] { - if let Ok(meta_p) = skin_meta_path(key) - && meta_p.exists() - && let Ok(s) = polyio::read_to_string(&meta_p).await - && let Ok(m) = serde_json::from_str::(&s) - { - return m.is_slim; + let keys = [&dashed, &undashed, &lower, &uuid_or_name.to_string()]; + + let search_dirs = oneclient_common::paths::all_skins_dirs(); + + for dir in &search_dirs { + for key in &keys { + for ext in ["json", "meta.json"] { + let meta_p = dir.join(format!("{key}.{ext}")); + if meta_p.is_file() + && let Ok(s) = polyio::read_to_string(&meta_p).await + && let Ok(m) = serde_json::from_str::(&s) + { + return m.is_slim; + } + } } } false @@ -523,11 +502,11 @@ pub async fn build_user_properties(uuid_input: &str, port: u16) -> Option