fix(skins): fix skin lookup across directories, reliable TCP flush, modern userProperties, and optimize compile time
This commit is contained in:
parent
e06775aae5
commit
a8826b83d3
6 changed files with 154 additions and 133 deletions
|
|
@ -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.
|
# Linux and Macos by default have 8MB of stack storage.
|
||||||
# Windows has 1MB, it isn't enough, so it's changed to 8MB also.
|
# Windows has 1MB, it isn't enough, so it's changed to 8MB also.
|
||||||
[target.x86_64-pc-windows-msvc]
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
|
|
||||||
|
|
@ -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"]}
|
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 16
|
||||||
debug = "line-tables-only"
|
debug = false
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
@ -174,5 +174,9 @@ lto = false
|
||||||
strip = false
|
strip = false
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
debug = 0
|
debug = 0
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 2
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,6 @@ pub async fn save_account_skin(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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)
|
let img = image::load_from_memory(skin_bytes)
|
||||||
.map_err(|e| format!("Invalid image format: {e}"))?
|
.map_err(|e| format!("Invalid image format: {e}"))?
|
||||||
.to_rgba8();
|
.to_rgba8();
|
||||||
|
|
@ -177,17 +174,20 @@ pub async fn save_account_skin(
|
||||||
.write_to(&mut cursor, image::ImageFormat::Png)
|
.write_to(&mut cursor, image::ImageFormat::Png)
|
||||||
.map_err(|e| format!("Failed to encode PNG: {e}"))?;
|
.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 = SkinMetadata { is_slim };
|
||||||
let meta_json = serde_json::to_string_pretty(&meta)
|
let meta_json = serde_json::to_string_pretty(&meta)
|
||||||
.map_err(|e| format!("Failed to serialize metadata: {e}"))?;
|
.map_err(|e| format!("Failed to serialize metadata: {e}"))?;
|
||||||
|
|
||||||
polyio::write(&meta_path, meta_json.as_bytes())
|
for dir in oneclient_common::paths::all_skins_dirs() {
|
||||||
.await
|
let _ = polyio::create_dir_all(&dir).await;
|
||||||
.map_err(|e| format!("Failed to write skin metadata: {e}"))?;
|
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()) {
|
if let Some(uname) = username.filter(|u| !u.is_empty()) {
|
||||||
let _ = sync_custom_skin_loader(uname, &png_bytes).await;
|
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> {
|
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())?;
|
for dir in oneclient_common::paths::all_skins_dirs() {
|
||||||
let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?;
|
let _ = polyio::remove_file(&dir.join(format!("{uuid}.png"))).await;
|
||||||
|
let _ = polyio::remove_file(&dir.join(format!("{uuid}.json"))).await;
|
||||||
if skin_path.exists() {
|
let _ = polyio::remove_file(&dir.join(format!("{uuid}.meta.json"))).await;
|
||||||
let _ = polyio::remove_file(&skin_path).await;
|
if let Some(uname) = username.filter(|u| !u.is_empty()) {
|
||||||
}
|
let _ = polyio::remove_file(&dir.join(format!("{uname}.png"))).await;
|
||||||
if meta_path.exists() {
|
let _ = polyio::remove_file(&dir.join(format!("{uname}.json"))).await;
|
||||||
let _ = polyio::remove_file(&meta_path).await;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(uname) = username.filter(|u| !u.is_empty()) {
|
if let Some(uname) = username.filter(|u| !u.is_empty()) {
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,27 @@ pub fn skins_dir() -> PathsResult<PathBuf> {
|
||||||
Ok(data_dir()?.join("skins"))
|
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<PathBuf> {
|
||||||
|
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<PathBuf> {
|
pub fn skin_file_path(uuid: &str) -> PathsResult<PathBuf> {
|
||||||
Ok(skins_dir()?.join(format!("{uuid}.png")))
|
Ok(skins_dir()?.join(format!("{uuid}.png")))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,14 @@ async fn start(
|
||||||
let mods_dir = paths::cluster_mods_dir(&cluster.folder_name)?;
|
let mods_dir = paths::cluster_mods_dir(&cluster.folder_name)?;
|
||||||
|
|
||||||
if account.is_offline() {
|
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
|
if cluster.mc_loader == oneclient_common::domain::GameLoader::Fabric
|
||||||
&& version.id == "1.21.11"
|
&& version.id == "1.21.11"
|
||||||
{
|
{
|
||||||
|
|
@ -423,12 +430,18 @@ async fn start(
|
||||||
&& let Some(user_props) =
|
&& let Some(user_props) =
|
||||||
crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await
|
crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await
|
||||||
{
|
{
|
||||||
|
let mut replaced = false;
|
||||||
for i in 0..mc_args.len() {
|
for i in 0..mc_args.len() {
|
||||||
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() && mc_args[i + 1] == "{}" {
|
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() {
|
||||||
mc_args[i + 1] = user_props;
|
mc_args[i + 1] = user_props.clone();
|
||||||
|
replaced = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !replaced {
|
||||||
|
mc_args.push("--userProperties".to_string());
|
||||||
|
mc_args.push(user_props);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
arguments::append_profile_game_arguments(
|
arguments::append_profile_game_arguments(
|
||||||
&mut mc_args,
|
&mut mc_args,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use oneclient_common::paths::{
|
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 parking_lot::RwLock;
|
||||||
use serde::{Deserialize, Serialize};
|
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");
|
tracing::info!(method, uri, "skin_server incoming request");
|
||||||
|
|
||||||
if method != "GET" && method != "HEAD" && method != "POST" {
|
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";
|
return send_no_content(stream).await;
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = uri.split('?').next().unwrap_or(uri);
|
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",
|
"implementationName": "onelauncher-skin-server",
|
||||||
"implementationVersion": env!("CARGO_PKG_VERSION")
|
"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
|
"signaturePublickey": SKIN_SERVER_PUBLIC_KEY
|
||||||
}).to_string();
|
}).to_string();
|
||||||
let header = format!(
|
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
|
||||||
"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(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Public keys endpoint (for Minecraft 1.19+ profile signature verification)
|
// 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}],
|
"profilePropertyKeys": [{"publicKey": key_b64}],
|
||||||
"playerCertificateKeys": [{"publicKey": key_b64}]
|
"playerCertificateKeys": [{"publicKey": key_b64}]
|
||||||
}).to_string();
|
}).to_string();
|
||||||
let header = format!(
|
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
|
||||||
"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(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Profiles by names endpoint (bulk profile lookup)
|
// 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 body = serde_json::to_string(&profiles).unwrap_or_else(|_| "[]".to_string());
|
||||||
let header = format!(
|
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
|
||||||
"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(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Profile request: /sessionserver/session/minecraft/profile/<uuid> or /session/minecraft/profile/<uuid>
|
// 4. Profile request: /sessionserver/session/minecraft/profile/<uuid> or /session/minecraft/profile/<uuid>
|
||||||
|
|
@ -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
|
// 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 {
|
if let Some(json_body) = build_profile_response(uuid_str, port).await {
|
||||||
let header = format!(
|
return send_response(stream, "200 OK", "application/json; charset=utf-8", json_body.as_bytes()).await;
|
||||||
"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(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other players on the server, check Mojang session server so official players keep their skins
|
// 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 {
|
if let Some(mojang_body) = fetch_mojang_profile(uuid_str).await {
|
||||||
let header = format!(
|
return send_response(stream, "200 OK", "application/json; charset=utf-8", mojang_body.as_bytes()).await;
|
||||||
"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(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offline / unknown player: return 204 No Content -> vanilla Minecraft renders default Steve/Alex
|
// 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";
|
return send_no_content(stream).await;
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Texture request: /textures/<id>.png
|
// 5. Texture request: /textures/<id>.png or /texture/<id>
|
||||||
if let Some(texture_raw) = path.strip_prefix("/textures/") {
|
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");
|
let texture_id = texture_raw.trim_matches('/').trim_end_matches(".png");
|
||||||
if let Some(png_bytes) = load_skin_png(texture_id).await {
|
if let Some(png_bytes) = load_skin_png(texture_id).await {
|
||||||
let header = format!(
|
let body_to_send = if method == "HEAD" { &[][..] } else { &png_bytes[..] };
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
return send_response(stream, "200 OK", "image/png", body_to_send).await;
|
||||||
png_bytes.len()
|
|
||||||
);
|
|
||||||
stream.write_all(header.as_bytes()).await?;
|
|
||||||
if method == "GET" {
|
|
||||||
stream.write_all(&png_bytes).await?;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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";
|
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?;
|
stream.write_all(response.as_bytes()).await?;
|
||||||
|
let _ = stream.flush().await;
|
||||||
|
let _ = stream.shutdown().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,53 +320,32 @@ pub fn to_undashed_uuid(s: &str) -> String {
|
||||||
async fn find_skin_path(uuid_or_name: &str) -> Option<PathBuf> {
|
async fn find_skin_path(uuid_or_name: &str) -> Option<PathBuf> {
|
||||||
let dashed = to_dashed_uuid(uuid_or_name);
|
let dashed = to_dashed_uuid(uuid_or_name);
|
||||||
let undashed = to_undashed_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)
|
let mut keys = vec![dashed.clone(), undashed.clone(), lower.clone(), uuid_or_name.to_string()];
|
||||||
&& 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(accounts) = REGISTERED_ACCOUNTS.get() {
|
if let Some(accounts) = REGISTERED_ACCOUNTS.get() {
|
||||||
let read = accounts.read();
|
let read = accounts.read();
|
||||||
if let Some(reg) = read
|
if let Some(reg) = read
|
||||||
.get(&dashed)
|
.get(&dashed)
|
||||||
.or_else(|| read.get(&undashed))
|
.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)
|
keys.push(reg.uuid.clone());
|
||||||
&& p.exists()
|
keys.push(reg.undashed_uuid.clone());
|
||||||
{
|
keys.push(reg.username.clone());
|
||||||
return Some(p);
|
keys.push(reg.username.to_lowercase());
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(dir) = skins_dir() {
|
let search_dirs = oneclient_common::paths::all_skins_dirs();
|
||||||
let name_path = dir.join(format!("{uuid_or_name}.png"));
|
|
||||||
if name_path.exists() {
|
for dir in &search_dirs {
|
||||||
return Some(name_path);
|
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
|
// Try reading from auth.json across all data directory candidates
|
||||||
if let Ok(auth_p) = oneclient_common::paths::auth_file()
|
for skin_dir in oneclient_common::paths::all_skins_dirs() {
|
||||||
&& let Ok(content) = polyio::read_to_string(&auth_p).await
|
if let Some(data_dir) = skin_dir.parent() {
|
||||||
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
let auth_p = data_dir.join("auth.json");
|
||||||
&& let Some(users) = val.get("users").and_then(|u| u.as_object())
|
if let Ok(content) = polyio::read_to_string(&auth_p).await
|
||||||
{
|
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
||||||
for (id, user) in users {
|
&& let Some(users) = val.get("users").and_then(|u| u.as_object())
|
||||||
if (id == &dashed || id.replace('-', "").to_lowercase() == undashed)
|
|
||||||
&& let Some(name) = user.get("username").and_then(|n| n.as_str())
|
|
||||||
{
|
{
|
||||||
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 dashed = to_dashed_uuid(uuid_or_name);
|
||||||
let undashed = to_undashed_uuid(uuid_or_name);
|
let undashed = to_undashed_uuid(uuid_or_name);
|
||||||
let lower = uuid_or_name.to_lowercase();
|
let lower = uuid_or_name.to_lowercase();
|
||||||
for key in [&dashed, &undashed, &lower] {
|
let keys = [&dashed, &undashed, &lower, &uuid_or_name.to_string()];
|
||||||
if let Ok(meta_p) = skin_meta_path(key)
|
|
||||||
&& meta_p.exists()
|
let search_dirs = oneclient_common::paths::all_skins_dirs();
|
||||||
&& let Ok(s) = polyio::read_to_string(&meta_p).await
|
|
||||||
&& let Ok(m) = serde_json::from_str::<SkinMetadata>(&s)
|
for dir in &search_dirs {
|
||||||
{
|
for key in &keys {
|
||||||
return m.is_slim;
|
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::<SkinMetadata>(&s)
|
||||||
|
{
|
||||||
|
return m.is_slim;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
@ -523,11 +502,11 @@ pub async fn build_user_properties(uuid_input: &str, port: u16) -> Option<String
|
||||||
|
|
||||||
let prop_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) {
|
let prop_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) {
|
||||||
format!(
|
format!(
|
||||||
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}","signature":"{sig}"}}]}}"#
|
r#"{{"textures":[{{"value":"{encoded_textures}","signature":"{sig}"}}]}}"#
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}"}}]}}"#
|
r#"{{"textures":[{{"value":"{encoded_textures}"}}]}}"#
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -829,7 +808,7 @@ mod tests {
|
||||||
let test_user = "TestPlayer";
|
let test_user = "TestPlayer";
|
||||||
register_launch_account(test_uuid, test_user);
|
register_launch_account(test_uuid, test_user);
|
||||||
|
|
||||||
if let Ok(dir) = skins_dir() {
|
if let Ok(dir) = oneclient_common::paths::skins_dir() {
|
||||||
let _ = polyio::create_dir_all(&dir).await;
|
let _ = polyio::create_dir_all(&dir).await;
|
||||||
let skin_p = dir.join(format!("{test_uuid}.png"));
|
let skin_p = dir.join(format!("{test_uuid}.png"));
|
||||||
// 1x1 png or dummy png
|
// 1x1 png or dummy png
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue