From 912d0da76e1dc9d9c1c55b65ba56b97fa91cf6cd Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Wed, 15 Apr 2026 23:11:59 +0300 Subject: [PATCH] feat: TRUE support for slim skins, produce a PCK from a skin --- src-tauri/src/lib.rs | 23 ++++---- src/components/views/SkinsView.tsx | 90 +++++++++++++++++++++--------- src/hooks/useSkinSync.ts | 58 +++++++++++++------ src/services/TauriService.ts | 4 ++ 4 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0ffcf86..6561944 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1033,18 +1033,14 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S let username = config.username; let _ = fs::write(instance_dir.join("username.txt"), &username); ensure_server_list(&instance_dir, servers); - if let Some(skin_data) = config.skin_base64 { - use base64::{Engine as _, engine::general_purpose}; - let base64_str = skin_data.split(',').nth(1).unwrap_or(&skin_data); - if let Ok(bytes) = general_purpose::STANDARD.decode(base64_str) { - let skin_dir = instance_dir.join("Common").join("res").join("mob"); - let _ = fs::create_dir_all(&skin_dir); - let _ = fs::write(skin_dir.join("char.png"), bytes); - } + let skin_pck_path = root.join("Skin.pck"); + if skin_pck_path.exists() { + let skin_dlc_dir = instance_dir.join("Windows64Media").join("DLC").join("Custom Skins"); + let _ = fs::create_dir_all(&skin_dlc_dir); + let _ = fs::copy(&skin_pck_path, skin_dlc_dir.join("Skin.pck")); } let _ = perform_dlc_sync(&app, &instance_dir)?; - let game_exe = instance_dir.join("Minecraft.Client.exe"); if !game_exe.exists() { return Err("Game executable not found in instance folder.".into()); @@ -1344,6 +1340,13 @@ fn delete_screenshot(path: String) -> Result<(), String> { fs::remove_file(path).map_err(|e| e.to_string()) } +#[tauri::command] +async fn save_global_skin_pck(app: AppHandle, pck_data: Vec) -> Result<(), String> { + let app_dir = get_app_dir(&app); + let _ = fs::write(app_dir.join("Skin.pck"), pck_data); + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -1373,7 +1376,7 @@ pub fn run() { } } }) - .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, get_screenshots, delete_screenshot]) + .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, get_screenshots, delete_screenshot, save_global_skin_pck]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index fc0a424..a014cfc 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -8,23 +8,24 @@ interface SavedSkin { id: string; name: string; url: string; + isSlim?: boolean; } const DEFAULT_SKINS: SavedSkin[] = [ - { id: 'default', name: 'Default Steve', url: '/images/Default.png' }, - { id: 'neoapps', name: 'neoapps', url: '/Skins/neoapps.png' }, - { id: 'justneki', name: 'JustNeki', url: '/Skins/JustNeki.png' }, - { id: 'kayjann', name: 'KayJann', url: '/Skins/KayJann.png' }, - { id: 'leon', name: 'Leon', url: '/Skins/Leon.png' }, - { id: 'mr_anilex', name: 'mr_anilex', url: '/Skins/mr_anilex.png' }, - { id: 'peter', name: 'Peter', url: '/Skins/Peter.png' }, - { id: 'piebot', name: 'piebot', url: '/Skins/piebot.png' } + { id: 'default', name: 'Default Steve', url: '/images/Default.png', isSlim: false }, + { id: 'neoapps', name: 'neoapps', url: '/Skins/neoapps.png', isSlim: false }, + { id: 'justneki', name: 'JustNeki', url: '/Skins/JustNeki.png', isSlim: false }, + { id: 'kayjann', name: 'KayJann', url: '/Skins/KayJann.png', isSlim: false }, + { id: 'leon', name: 'Leon', url: '/Skins/Leon.png', isSlim: false }, + { id: 'mr_anilex', name: 'mr_anilex', url: '/Skins/mr_anilex.png', isSlim: false }, + { id: 'peter', name: 'Peter', url: '/Skins/Peter.png', isSlim: false }, + { id: 'piebot', name: 'piebot', url: '/Skins/piebot.png', isSlim: false } ]; const SkinsView = memo(function SkinsView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); - const { skinUrl, setSkinUrl } = useSkin(); + const { skinUrl, setSkinUrl, setSkinIsSlim } = useSkin(); const [focusIndex, setFocusIndex] = useState(null); const containerRef = useRef(null); @@ -48,30 +49,43 @@ const SkinsView = memo(function SkinsView() { const [showImportModal, setShowImportModal] = useState(false); const [modalFocusIndex, setModalFocusIndex] = useState(0); - const [importMode, setImportMode] = useState<'file' | 'username' | null>(null); + const [importMode, setImportMode] = useState<'file' | 'username' | 'model' | null>(null); const [importUsername, setImportUsername] = useState(''); const [isImporting, setIsImporting] = useState(false); const [importError, setImportError] = useState(''); + const [pendingSkin, setPendingSkin] = useState<{ url: string, defaultName: string } | null>(null); const processSkinImage = (url: string, defaultName: string) => { + setPendingSkin({ url, defaultName }); + setImportMode('model'); + setModalFocusIndex(0); + }; + + const handleFinalizeImport = (isSlim: boolean) => { + if (!pendingSkin) return; const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { const cvs = document.createElement("canvas"); - cvs.width = 64; - cvs.height = 32; + cvs.width = img.width; + cvs.height = img.height; const ctx = cvs.getContext("2d"); if (ctx) { - ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32); + ctx.drawImage(img, 0, 0); const base64String = cvs.toDataURL("image/png"); const newId = Date.now().toString(); - const newSkin = { id: newId, name: defaultName, url: base64String }; + const newSkin = { id: newId, name: pendingSkin.defaultName, url: base64String, isSlim }; setSavedSkins(prev => [...prev, newSkin]); setSkinUrl(base64String); + setSkinIsSlim(isSlim); setActiveSkinId(newId); } }; - img.src = url; + img.src = pendingSkin.url; + + setShowImportModal(false); + setImportMode(null); + setPendingSkin(null); }; const handleFetchUsername = async () => { @@ -83,10 +97,6 @@ const SkinsView = memo(function SkinsView() { const [base64Raw, exactName] = await TauriService.fetchSkin(importUsername.trim()); const skinBase64 = `data:image/png;base64,${base64Raw}`; processSkinImage(skinBase64, exactName.substring(0, 16)); - - setShowImportModal(false); - setImportMode(null); - setImportUsername(''); } catch (e: any) { setImportError(typeof e === 'string' ? e : (e.message || 'Failed to fetch skin')); } finally { @@ -130,7 +140,7 @@ const SkinsView = memo(function SkinsView() { setShowImportModal(false); setModalFocusIndex(0); } - } else { + } else if (importMode === 'username') { if (modalFocusIndex === 0 || modalFocusIndex === 1) handleFetchUsername(); else if (modalFocusIndex === 2) { playBackSound(); @@ -139,6 +149,15 @@ const SkinsView = memo(function SkinsView() { setImportError(''); setModalFocusIndex(0); } + } else if (importMode === 'model') { + if (modalFocusIndex === 0) handleFinalizeImport(false); + else if (modalFocusIndex === 1) handleFinalizeImport(true); + else if (modalFocusIndex === 2) { + playBackSound(); + setImportMode(null); + setPendingSkin(null); + setModalFocusIndex(0); + } } } return; @@ -212,14 +231,13 @@ const SkinsView = memo(function SkinsView() { }; reader.readAsDataURL(file); e.target.value = ''; - setShowImportModal(false); - setImportMode(null); }; const handleSkinSelect = (skin: SavedSkin) => { playPressSound(); setActiveSkinId(skin.id); setSkinUrl(skin.url); + setSkinIsSlim(skin.isSlim || false); }; const isDefaultSkin = (id: string | null) => DEFAULT_SKINS.some(d => d.id === id); @@ -230,6 +248,7 @@ const SkinsView = memo(function SkinsView() { const updatedSkins = savedSkins.filter(s => s.id !== activeSkinId); setSavedSkins(updatedSkins); setSkinUrl('/images/Default.png'); + setSkinIsSlim(false); setActiveSkinId('default'); }; @@ -296,8 +315,9 @@ const SkinsView = memo(function SkinsView() { const isFocused = focusIndex === idx; return (
setFocusIndex(idx)} className="flex flex-col items-center gap-1 w-32 outline-none"> -
+
{isActive && Active} + {skin.isSlim && Slim}
handleSkinSelect(skin)} @@ -352,7 +372,7 @@ const SkinsView = memo(function SkinsView() { From Username
- ) : ( + ) : importMode === 'username' ? (
- )} + ) : importMode === 'model' ? ( +
+ Choose the player model type for this skin: + + +
+ ) : null}