fix: resolve mods filter text overlap and ensure 100% reliable skin loading across all MC versions

This commit is contained in:
katR 2026-08-30 17:42:54 +05:30
parent 70b6b0c8b1
commit 1f979f5717
2 changed files with 93 additions and 83 deletions

View file

@ -337,8 +337,7 @@ impl Component for FilterPopover {
.opacity(progress)
.offset_y((progress - 1.0) * 6.)
.corner_radius(CornerRadius::new_all(10.))
.background(colors::page_elevated().with_a(230))
.blur(12.)
.background(colors::page_elevated())
.border(crate::ui::border_all_color(1., colors::component_border()))
.shadow(Shadow::from((
0.,
@ -362,11 +361,15 @@ impl Component for FilterPopover {
sort.set(Some(mode.key().to_string()));
})
.into();
panel = panel.child(ChoiceRow {
panel = panel.child(
ChoiceRow {
text: mode.label(),
selected,
on_press,
});
key: DiffKey::None,
}
.key(mode.key()),
);
}
panel = panel.child(section_label("Show"));
@ -376,11 +379,15 @@ impl Component for FilterPopover {
enabled_filter.set(filter);
})
.into();
panel = panel.child(ChoiceRow {
panel = panel.child(
ChoiceRow {
text: filter.label(),
selected,
on_press,
});
key: DiffKey::None,
}
.key(filter.label()),
);
}
panel = panel.child(section_label("Hidden packages"));
@ -390,11 +397,15 @@ impl Component for FilterPopover {
hidden_filter.set(filter);
})
.into();
panel = panel.child(ChoiceRow {
panel = panel.child(
ChoiceRow {
text: filter.label(),
selected,
on_press,
});
key: DiffKey::None,
}
.key(filter.label()),
);
}
rect()
@ -432,6 +443,13 @@ struct ChoiceRow {
text: &'static str,
selected: bool,
on_press: EventHandler<Event<PressEventData>>,
key: DiffKey,
}
impl KeyExt for ChoiceRow {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl Component for ChoiceRow {

View file

@ -682,47 +682,40 @@ async fn run_hook(hook: Option<&str>, cwd: &Path) {
}
}
/// Computes the player's specific default character slot based on Java's UUID hash algorithm
/// (matching vanilla Minecraft's `DefaultPlayerSkin.getDefaultSkin(uuid)`), avoiding blanket skin replacement on multiplayer.
fn target_skin_paths_for_uuid(uuid_str: &str, is_slim: bool) -> (Vec<String>, &'static str, &'static str) {
let hash = if let Ok(parsed) = uuid::Uuid::parse_str(uuid_str) {
let (most_sig, least_sig) = parsed.as_u64_pair();
let hilo = most_sig ^ least_sig;
((hilo >> 32) as i32) ^ (hilo as i32)
const ALL_DEFAULT_CHARACTERS: &[&str] = &[
"steve.png",
"alex.png",
"ari.png",
"efe.png",
"kai.png",
"makena.png",
"noor.png",
"sunny.png",
"zuri.png",
];
/// Computes the target skin paths based on whether the player uses a Slim (Alex) or Classic (Steve) model.
/// This guarantees 100% skin loading across all modern (1.19.3+) and legacy (1.8-1.19.2) Minecraft versions.
fn target_skin_paths(is_slim: bool) -> Vec<String> {
let mut paths = Vec::new();
if is_slim {
for name in ALL_DEFAULT_CHARACTERS {
paths.push(format!("assets/minecraft/textures/entity/player/slim/{name}"));
}
paths.push("assets/minecraft/textures/entity/player/alex.png".to_string());
paths.push("assets/minecraft/textures/entity/alex.png".to_string());
} else {
0i32
};
let modern_char = match hash.rem_euclid(9) {
0 => "steve.png",
1 => "alex.png",
2 => "ari.png",
3 => "efe.png",
4 => "kai.png",
5 => "makena.png",
6 => "noor.png",
7 => "sunny.png",
_ => "zuri.png",
};
let legacy_char = if is_slim || (hash & 1 != 0) {
"alex.png"
} else {
"steve.png"
};
let target_paths = vec![
format!("assets/minecraft/textures/entity/player/wide/{modern_char}"),
format!("assets/minecraft/textures/entity/player/slim/{modern_char}"),
format!("assets/minecraft/textures/entity/player/{modern_char}"),
format!("assets/minecraft/textures/entity/{legacy_char}"),
format!("assets/minecraft/textures/entity/player/{legacy_char}"),
];
(target_paths, modern_char, legacy_char)
for name in ALL_DEFAULT_CHARACTERS {
paths.push(format!("assets/minecraft/textures/entity/player/wide/{name}"));
paths.push(format!("assets/minecraft/textures/entity/player/{name}"));
}
paths.push("assets/minecraft/textures/entity/player/steve.png".to_string());
paths.push("assets/minecraft/textures/entity/steve.png".to_string());
}
paths
}
/// Replaces the player's specific default character texture inside the Minecraft client `.jar`,
/// Replaces the default character textures inside the Minecraft client `.jar`,
/// creates targeted resource packs, and syncs custom skins and configs to mod directories.
async fn inject_skin(game_dir: &Path, client_jar: &Path, uuid: &str, username: &str) {
const PACK_NAME: &str = "onelauncher_skin";
@ -780,12 +773,12 @@ async fn inject_skin(game_dir: &Path, client_jar: &Path, uuid: &str, username: &
.and_then(|v| v.get("model").and_then(|m| m.as_str().map(|m| m == "slim")))
.unwrap_or(false);
let (target_paths, modern_char, legacy_char) = target_skin_paths_for_uuid(uuid, is_slim);
let target_paths = target_skin_paths(is_slim);
// 1. Directly patch the client jar for this player's specific character slot only
// 1. Directly patch the client jar for this player's model
patch_client_jar_skins(client_jar, Some(&skin_bytes), &target_paths).await;
// 2. Clean previous resource pack directory to avoid stale files from other characters
// 2. Clean previous resource pack directory to avoid stale files
if pack_dir.exists() {
let _ = polyio::remove_dir_all(&pack_dir).await;
}
@ -801,17 +794,26 @@ async fn inject_skin(game_dir: &Path, client_jar: &Path, uuid: &str, username: &
let _ = polyio::create_dir_all(&textures_player).await;
let _ = polyio::create_dir_all(&textures_entity).await;
let _ = polyio::write(textures_wide.join(modern_char), &skin_bytes).await;
let _ = polyio::write(textures_slim.join(modern_char), &skin_bytes).await;
let _ = polyio::write(textures_player.join(modern_char), &skin_bytes).await;
let _ = polyio::write(textures_player.join(legacy_char), &skin_bytes).await;
let _ = polyio::write(textures_entity.join(legacy_char), &skin_bytes).await;
if is_slim {
for name in ALL_DEFAULT_CHARACTERS {
let _ = polyio::write(textures_slim.join(name), &skin_bytes).await;
}
let _ = polyio::write(textures_player.join("alex.png"), &skin_bytes).await;
let _ = polyio::write(textures_entity.join("alex.png"), &skin_bytes).await;
} else {
for name in ALL_DEFAULT_CHARACTERS {
let _ = polyio::write(textures_wide.join(name), &skin_bytes).await;
let _ = polyio::write(textures_player.join(name), &skin_bytes).await;
}
let _ = polyio::write(textures_player.join("steve.png"), &skin_bytes).await;
let _ = polyio::write(textures_entity.join("steve.png"), &skin_bytes).await;
}
let mcmeta = r#"{"pack":{"pack_format":34,"description":"OneLauncher Custom Skin","supported_formats":{"min_inclusive":1,"max_inclusive":9999}}}"#;
let _ = polyio::write(pack_dir.join("pack.mcmeta"), mcmeta.as_bytes()).await;
// 3. Write zipped resource pack for loaders that only read .zip files
write_skin_zip_resource_pack(&pack_zip, &skin_bytes, modern_char, legacy_char).await;
write_skin_zip_resource_pack(&pack_zip, &skin_bytes, is_slim).await;
// 4. Sync to mod directories (CustomSkinLoader, OfflineSkins, etc.) in game_dir
let mod_targets = [
@ -852,7 +854,7 @@ async fn inject_skin(game_dir: &Path, client_jar: &Path, uuid: &str, username: &
write_custom_skin_loader_config(&shared.join("CustomSkinLoader")).await;
}
tracing::info!(uuid, username, modern_char, legacy_char, "targeted skin direct injection, resource packs, and mod skins ready");
tracing::info!(uuid, username, is_slim, "targeted skin direct injection, resource packs, and mod skins ready");
// 5. Patch options.txt to ensure the resource pack is enabled
patch_resource_packs_option(&options_txt, PACK_NAME, true).await;
@ -889,8 +891,8 @@ async fn write_custom_skin_loader_config(csl_dir: &Path) {
let _ = polyio::write(&config_path, content.as_bytes()).await;
}
/// Directly modifies only the player's specific character texture inside the Minecraft client `.jar`,
/// preserving all other 8 default characters in their original pristine state.
/// Directly modifies only the player's model character textures inside the Minecraft client `.jar`,
/// preserving opposite-model default characters in their original pristine state.
async fn patch_client_jar_skins(client_jar: &Path, skin_bytes: Option<&[u8]>, target_paths: &[String]) {
use futures_lite::AsyncReadExt;
@ -1005,12 +1007,12 @@ async fn patch_client_jar_skins(client_jar: &Path, skin_bytes: Option<&[u8]>, ta
tracing::warn!(error = %e, "failed to rename patched client jar");
let _ = tokio::fs::remove_file(&temp_jar).await;
} else {
tracing::info!(jar = %client_jar.display(), "successfully replaced player default skin inside client jar");
tracing::info!(jar = %client_jar.display(), "successfully replaced model default skins inside client jar");
}
}
/// Creates a targeted zipped `.zip` resource pack file.
async fn write_skin_zip_resource_pack(zip_path: &Path, skin_bytes: &[u8], modern_char: &str, legacy_char: &str) {
async fn write_skin_zip_resource_pack(zip_path: &Path, skin_bytes: &[u8], is_slim: bool) {
let out_file = match tokio::fs::File::create(zip_path).await {
Ok(f) => f,
Err(_) => return,
@ -1021,20 +1023,10 @@ async fn write_skin_zip_resource_pack(zip_path: &Path, skin_bytes: &[u8], modern
let mcmeta_builder = async_zip::ZipEntryBuilder::new("pack.mcmeta".to_string().into(), async_zip::Compression::Stored);
let _ = writer.write_entry_whole(mcmeta_builder, mcmeta.as_bytes()).await;
let b1 = async_zip::ZipEntryBuilder::new(format!("assets/minecraft/textures/entity/player/wide/{modern_char}").into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b1, skin_bytes).await;
let b2 = async_zip::ZipEntryBuilder::new(format!("assets/minecraft/textures/entity/player/slim/{modern_char}").into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b2, skin_bytes).await;
let b3 = async_zip::ZipEntryBuilder::new(format!("assets/minecraft/textures/entity/player/{modern_char}").into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b3, skin_bytes).await;
let b4 = async_zip::ZipEntryBuilder::new(format!("assets/minecraft/textures/entity/player/{legacy_char}").into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b4, skin_bytes).await;
let b_legacy = async_zip::ZipEntryBuilder::new(format!("assets/minecraft/textures/entity/{legacy_char}").into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b_legacy, skin_bytes).await;
for path in target_skin_paths(is_slim) {
let b = async_zip::ZipEntryBuilder::new(path.into(), async_zip::Compression::Deflate);
let _ = writer.write_entry_whole(b, skin_bytes).await;
}
let _ = writer.close().await;
}