From 79d0517d760434941e29a0cd8583d731fbb676f8 Mon Sep 17 00:00:00 2001 From: IsraelDXPP Date: Tue, 3 Mar 2026 11:44:21 -0700 Subject: [PATCH 1/7] SkinChanger System (Beta xd) --- Minecraft.Client/Common/UI/UIController.cpp | 2 + .../Common/UI/UIScene_SkinSelectMenu.cpp | 136 +++++++++++++++++- .../Common/UI/UIScene_SkinSelectMenu.h | 3 + 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index 9e4a32024..ee9ee299f 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -1013,6 +1013,8 @@ void UIController::handleKeyPress(unsigned int iPad, unsigned int key) case ACTION_MENU_LEFT_SCROLL: kbDown = KMInput.IsKeyDown('Q'); kbPressed = KMInput.IsKeyPressed('Q'); kbReleased = KMInput.IsKeyReleased('Q'); break; case ACTION_MENU_RIGHT_SCROLL: kbDown = KMInput.IsKeyDown('E'); kbPressed = KMInput.IsKeyPressed('E'); kbReleased = KMInput.IsKeyReleased('E'); break; case ACTION_MENU_QUICK_MOVE: kbDown = KMInput.IsKeyDown(VK_SHIFT); kbPressed = KMInput.IsKeyPressed(VK_SHIFT); kbReleased = KMInput.IsKeyReleased(VK_SHIFT); break; + case ACTION_MENU_X: kbDown = KMInput.IsKeyDown('X'); kbPressed = KMInput.IsKeyPressed('X'); kbReleased = KMInput.IsKeyReleased('X'); break; + case ACTION_MENU_Y: kbDown = KMInput.IsKeyDown('Y'); kbPressed = KMInput.IsKeyPressed('Y'); kbReleased = KMInput.IsKeyReleased('Y'); break; } pressed = pressed || kbPressed; released = released || kbReleased; diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index d4f26ae71..db852294a 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -7,6 +7,8 @@ #elif defined __PSVITA__ #include #endif +#include +#include // Necesario para GetOpenFileNameW #define SKIN_SELECT_PACK_DEFAULT 0 #define SKIN_SELECT_PACK_FAVORITES 1 @@ -53,6 +55,7 @@ UIScene_SkinSelectMenu::UIScene_SkinSelectMenu(int iPad, void *initData, UILayer m_bSlidingSkins = false; m_bAnimatingMove = false; m_bSkinIndexChanged = false; + m_bUsingCustomSkin = (!GET_IS_DLC_SKIN_FROM_BITMASK(m_originalSkinId) && GET_UGC_SKIN_ID_FROM_BITMASK(m_originalSkinId) != 0); m_currentNavigation = eSkinNavigation_Skin; @@ -225,6 +228,14 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr InputActionOK(iPad); } break; + case ACTION_MENU_X: + if (pressed && !repeat) + { + LoadExternalSkin(); + handled = true; + return; + } + break; case ACTION_MENU_UP: case ACTION_MENU_DOWN: if(pressed) @@ -265,6 +276,7 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr m_skinIndex = getPreviousSkinIndex(m_skinIndex); //handleSkinIndexChanged(); + m_bUsingCustomSkin = false; m_bSlidingSkins = true; m_bAnimatingMove = true; @@ -300,6 +312,7 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr m_skinIndex = getNextSkinIndex(m_skinIndex); //handleSkinIndexChanged(); + m_bUsingCustomSkin = false; m_bSlidingSkins = true; m_bAnimatingMove = true; @@ -402,6 +415,15 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad) { ui.AnimateKeyPress(iPad, ACTION_MENU_OK, false, true, false); + if (m_bUsingCustomSkin) + { + ui.PlayUISFX(eSFX_Press); + m_currentSkinPath = app.GetPlayerSkinName(iPad); + m_originalSkinId = app.GetPlayerSkinId(iPad); + setCharacterSelected(true); + return; + } + // if the profile data has been changed, then force a profile write // It seems we're allowed to break the 5 minute rule if it's the result of a user action switch(m_packIndex) @@ -646,7 +668,19 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_controlSkinNamePlate.setVisible( false ); - if( m_currentPack != NULL ) + if (m_bUsingCustomSkin) + { + m_selectedSkinPath = m_currentSkinPath; + m_selectedCapePath = L""; + m_vAdditionalSkinBoxes = NULL; + skinName = L"Custom Skin"; + skinOrigin = L"External PNG"; + setCharacterSelected(true); + setCharacterLocked(false); + m_characters[eCharacter_Current].setVisible(true); + m_controlSkinNamePlate.setVisible(true); + } + else if( m_currentPack != NULL ) { skinFile = m_currentPack->getSkinFile(m_skinIndex); m_selectedSkinPath = skinFile->getPath(); @@ -752,6 +786,16 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_labelSkinName.setLabel(skinName); m_labelSkinOrigin.setLabel(skinOrigin); + if (m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_bUsingCustomSkin) + { + m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Muestra la ayuda visual + } + + if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) + { + setCharacterSelected(true); // Activa el icono de seleccionado + } + if(m_vAdditionalSkinBoxes && m_vAdditionalSkinBoxes->size()!=0) { @@ -1734,4 +1778,92 @@ int UIScene_SkinSelectMenu::PSNSignInReturned(void* pParam, bool bContinue, int } return 0; } -#endif // __PSVITA__ \ No newline at end of file +#endif // __PSVITA__ + +void UIScene_SkinSelectMenu::LoadExternalSkin() +{ + OPENFILENAMEW ofn; + wchar_t szFile[MAX_PATH]; + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = NULL; + ofn.lpstrFile = szFile; + ofn.lpstrFile[0] = L'\0'; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrFilter = L"PNG Files\0*.png\0All Files\0*.*\0"; + ofn.nFilterIndex = 1; + + // IMPORTANTE: OFN_NOCHANGEDIR evita que el Explorador cambie la carpeta de trabajo. + // Si la carpeta cambia, el juego NO podrá guardar mundos (Assertion failed en STO_SaveGame.cpp). + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; + + if (GetOpenFileNameW(&ofn) == TRUE) + { + std::ifstream file(ofn.lpstrFile, std::ios::binary | std::ios::ate); + if (file.is_open()) + { + std::streamsize size = file.tellg(); + if (size <= 0 || size > 2 * 1024 * 1024) { // Limite 2MB + file.close(); + ui.PlayUISFX(eSFX_CraftFail); + return; + } + file.seekg(0, std::ios::beg); + + // MEMORIA: El motor AddMemoryTextureFile NO copia los datos, usa el puntero directamente. + // Si usas un vector o buffer local, la skin se corromperá al salir de la función. + PBYTE persistentBuffer = new (std::nothrow) BYTE[(size_t)size]; + if (!persistentBuffer || !file.read((char*)persistentBuffer, size)) { + if(persistentBuffer) delete[] persistentBuffer; + file.close(); + return; + } + + // Validar Cabecera PNG y Dimensiones (64x32 o 64x64) + if (size > 24 && (unsigned char)persistentBuffer[0] == 0x89 && persistentBuffer[1] == 'P') + { + unsigned int w = (unsigned char)persistentBuffer[16] << 24 | (unsigned char)persistentBuffer[17] << 16 | (unsigned char)persistentBuffer[18] << 8 | (unsigned char)persistentBuffer[19]; + unsigned int h = (unsigned char)persistentBuffer[20] << 24 | (unsigned char)persistentBuffer[21] << 16 | (unsigned char)persistentBuffer[22] << 8 | (unsigned char)persistentBuffer[23]; + + if ((w == 64 && h == 32) || (w == 64 && h == 64)) + { + // CACHE BUSTING: Si usas el mismo nombre "custom.png", el cache del motor no se refresca. + // Usamos un contador para generar IDs únicos (ugcskin00000101.png, 102, etc). + static int s_ugcSkinLoadCount = 0; + s_ugcSkinLoadCount++; + + wchar_t textureNameBuf[256]; + // Formato ugcskinXXXXXXXX.png es obligatorio para que getSkinIdFromPath genere un ID válido. + // Ajustamos el ID para que los bits bajos (0-4) sean 0, respetando el bitmask de UGC. + swprintf(textureNameBuf, 256, L"ugcskin%08X.png", 0x100 + (s_ugcSkinLoadCount << 5)); + wstring textureName = textureNameBuf; + + // Limpieza opcional del buffer anterior + if (m_bUsingCustomSkin && !m_selectedSkinPath.empty()) { + app.RemoveMemoryTextureFile(m_selectedSkinPath); + } + + // 1. Cargar datos en el sistema de archivos en memoria + app.AddMemoryTextureFile(textureName, persistentBuffer, (DWORD)size); + + // 2. Aplicar la skin al jugador local + app.SetPlayerSkin(m_iPad, textureName); + + // 3. Forzar refresco de la UI + m_bUsingCustomSkin = true; + m_selectedSkinPath = textureName; + m_currentSkinPath = textureName; + m_selectedCapePath = L""; + handleSkinIndexChanged(); + + file.close(); + return; + } + } + delete[] persistentBuffer; // Borrar si falló la validación + ui.PlayUISFX(eSFX_CraftFail); + file.close(); + } + } +} \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h index e8d760967..1db9aa265 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h @@ -136,9 +136,12 @@ class UIScene_SkinSelectMenu : public UIScene // TODO: This should be pure virtual in this class virtual wstring getMoviePath(); + bool m_bUsingCustomSkin; // Flag para saber si hay una skin externa activa + public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); + void LoadExternalSkin(); // Función principal del explorador virtual void customDraw(IggyCustomDrawCallbackRegion *region); From ecb47a15ef165471c2ad5b4917e0042790d3c5ac Mon Sep 17 00:00:00 2001 From: IsraelProyects <150495544+IsraelDXPP@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:46:23 -0700 Subject: [PATCH 2/7] Update UIScene_SkinSelectMenu.cpp English cm --- Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index db852294a..c1d60d277 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -8,7 +8,7 @@ #include #endif #include -#include // Necesario para GetOpenFileNameW +#include // Required for GetOpenFileNameW #define SKIN_SELECT_PACK_DEFAULT 0 #define SKIN_SELECT_PACK_FAVORITES 1 @@ -1866,4 +1866,4 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() file.close(); } } -} \ No newline at end of file +} From 9a0cfd0243431b41358e6d865d2231ea48f11a3c Mon Sep 17 00:00:00 2001 From: IsraelProyects <150495544+IsraelDXPP@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:00:22 -0700 Subject: [PATCH 3/7] Update UIScene_SkinSelectMenu.cpp English // --- Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index c1d60d277..cfd1efded 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -788,12 +788,12 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() if (m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_bUsingCustomSkin) { - m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Muestra la ayuda visual + m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Show visual help } if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) { - setCharacterSelected(true); // Activa el icono de seleccionado + setCharacterSelected(true); // Activates the selected icon automatically } From d249e2528febc525e740202e003660be7b1b730d Mon Sep 17 00:00:00 2001 From: IsraelDXPP Date: Wed, 4 Mar 2026 07:19:48 -0700 Subject: [PATCH 4/7] Improve and clarify English comments for better understanding --- .../Common/UI/UIScene_SkinSelectMenu.cpp | 34 +++++++++---------- .../Common/UI/UIScene_SkinSelectMenu.h | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index db852294a..d5126f365 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -8,7 +8,7 @@ #include #endif #include -#include // Necesario para GetOpenFileNameW +#include // Required for GetOpenFileNameW #define SKIN_SELECT_PACK_DEFAULT 0 #define SKIN_SELECT_PACK_FAVORITES 1 @@ -788,12 +788,12 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() if (m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_bUsingCustomSkin) { - m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Muestra la ayuda visual + m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Display visual aid for custom skin } if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) { - setCharacterSelected(true); // Activa el icono de seleccionado + setCharacterSelected(true); // Enable selection checkmark } @@ -1794,8 +1794,8 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() ofn.lpstrFilter = L"PNG Files\0*.png\0All Files\0*.*\0"; ofn.nFilterIndex = 1; - // IMPORTANTE: OFN_NOCHANGEDIR evita que el Explorador cambie la carpeta de trabajo. - // Si la carpeta cambia, el juego NO podrá guardar mundos (Assertion failed en STO_SaveGame.cpp). + // IMPORTANT: OFN_NOCHANGEDIR prevents the File Explorer from changing the current working directory. + // If the directory changes, the game will FAIL TO SAVE worlds (Assertion failed in STO_SaveGame.cpp). ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; if (GetOpenFileNameW(&ofn) == TRUE) @@ -1811,8 +1811,8 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() } file.seekg(0, std::ios::beg); - // MEMORIA: El motor AddMemoryTextureFile NO copia los datos, usa el puntero directamente. - // Si usas un vector o buffer local, la skin se corromperá al salir de la función. + // MEMORY MANAGEMENT: AddMemoryTextureFile does NOT copy the buffer; it uses the pointer directly. + // If you use a local vector or stack buffer, the skin will corrupt/crash as soon as the function returns. PBYTE persistentBuffer = new (std::nothrow) BYTE[(size_t)size]; if (!persistentBuffer || !file.read((char*)persistentBuffer, size)) { if(persistentBuffer) delete[] persistentBuffer; @@ -1820,7 +1820,7 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() return; } - // Validar Cabecera PNG y Dimensiones (64x32 o 64x64) + // Validate PNG Header and Dimensions (64x32 or 64x64) if (size > 24 && (unsigned char)persistentBuffer[0] == 0x89 && persistentBuffer[1] == 'P') { unsigned int w = (unsigned char)persistentBuffer[16] << 24 | (unsigned char)persistentBuffer[17] << 16 | (unsigned char)persistentBuffer[18] << 8 | (unsigned char)persistentBuffer[19]; @@ -1828,29 +1828,29 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() if ((w == 64 && h == 32) || (w == 64 && h == 64)) { - // CACHE BUSTING: Si usas el mismo nombre "custom.png", el cache del motor no se refresca. - // Usamos un contador para generar IDs únicos (ugcskin00000101.png, 102, etc). + // CACHE BUSTING: If we use the same filename (e.g., "custom.png"), the engine's texture cache won't refresh. + // We use a counter to generate unique texture names (ugcskin00000100.png, 120, etc). static int s_ugcSkinLoadCount = 0; s_ugcSkinLoadCount++; wchar_t textureNameBuf[256]; - // Formato ugcskinXXXXXXXX.png es obligatorio para que getSkinIdFromPath genere un ID válido. - // Ajustamos el ID para que los bits bajos (0-4) sean 0, respetando el bitmask de UGC. + // Standard ugcskinXXXXXXXX.png naming is required for correct internal ID generation via getSkinIdFromPath. + // We shift the counter by 5 bits to align with the UGC bitmask (lower 5 bits are reserved for default skins). swprintf(textureNameBuf, 256, L"ugcskin%08X.png", 0x100 + (s_ugcSkinLoadCount << 5)); wstring textureName = textureNameBuf; - // Limpieza opcional del buffer anterior + // Optional: Remove previous custom texture from memory to avoid leaks if (m_bUsingCustomSkin && !m_selectedSkinPath.empty()) { app.RemoveMemoryTextureFile(m_selectedSkinPath); } - // 1. Cargar datos en el sistema de archivos en memoria + // 1. Add texture data to the in-memory filesystem app.AddMemoryTextureFile(textureName, persistentBuffer, (DWORD)size); - // 2. Aplicar la skin al jugador local + // 2. Apply the skin path to the local player character app.SetPlayerSkin(m_iPad, textureName); - // 3. Forzar refresco de la UI + // 3. Force UI refresh and update internal state m_bUsingCustomSkin = true; m_selectedSkinPath = textureName; m_currentSkinPath = textureName; @@ -1861,7 +1861,7 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() return; } } - delete[] persistentBuffer; // Borrar si falló la validación + delete[] persistentBuffer; // Cleanup if validation or loading failed ui.PlayUISFX(eSFX_CraftFail); file.close(); } diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h index 1db9aa265..101cec2b3 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h @@ -136,12 +136,12 @@ class UIScene_SkinSelectMenu : public UIScene // TODO: This should be pure virtual in this class virtual wstring getMoviePath(); - bool m_bUsingCustomSkin; // Flag para saber si hay una skin externa activa + bool m_bUsingCustomSkin; // Flag indicating if a custom external skin is active public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); - void LoadExternalSkin(); // Función principal del explorador + void LoadExternalSkin(); // Main function for selecting and loading an external skin via file picker virtual void customDraw(IggyCustomDrawCallbackRegion *region); From 9406eece549e5a45c5185cdf9633e5df1d89040e Mon Sep 17 00:00:00 2001 From: IsraelDXPP Date: Wed, 4 Mar 2026 07:44:46 -0700 Subject: [PATCH 5/7] Fixed comments --- .../Common/UI/UIScene_SkinSelectMenu.cpp | 32 +++++++++---------- .../Common/UI/UIScene_SkinSelectMenu.h | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index cfd1efded..8fe43a4c4 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -788,12 +788,12 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() if (m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_bUsingCustomSkin) { - m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Show visual help + m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Display visual aid for custom skin } if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) { - setCharacterSelected(true); // Activates the selected icon automatically + setCharacterSelected(true); // Enable selection checkmark } @@ -1794,8 +1794,8 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() ofn.lpstrFilter = L"PNG Files\0*.png\0All Files\0*.*\0"; ofn.nFilterIndex = 1; - // IMPORTANTE: OFN_NOCHANGEDIR evita que el Explorador cambie la carpeta de trabajo. - // Si la carpeta cambia, el juego NO podrá guardar mundos (Assertion failed en STO_SaveGame.cpp). + // IMPORTANT: OFN_NOCHANGEDIR prevents the File Explorer from changing the current working directory. + // If the directory changes, the game will FAIL TO SAVE worlds (Assertion failed in STO_SaveGame.cpp). ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; if (GetOpenFileNameW(&ofn) == TRUE) @@ -1811,8 +1811,8 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() } file.seekg(0, std::ios::beg); - // MEMORIA: El motor AddMemoryTextureFile NO copia los datos, usa el puntero directamente. - // Si usas un vector o buffer local, la skin se corromperá al salir de la función. + // MEMORY MANAGEMENT: AddMemoryTextureFile does NOT copy the buffer; it uses the pointer directly. + // If you use a local vector or stack buffer, the skin will corrupt/crash as soon as the function returns. PBYTE persistentBuffer = new (std::nothrow) BYTE[(size_t)size]; if (!persistentBuffer || !file.read((char*)persistentBuffer, size)) { if(persistentBuffer) delete[] persistentBuffer; @@ -1820,7 +1820,7 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() return; } - // Validar Cabecera PNG y Dimensiones (64x32 o 64x64) + // Validate PNG Header and Dimensions (64x32 or 64x64) if (size > 24 && (unsigned char)persistentBuffer[0] == 0x89 && persistentBuffer[1] == 'P') { unsigned int w = (unsigned char)persistentBuffer[16] << 24 | (unsigned char)persistentBuffer[17] << 16 | (unsigned char)persistentBuffer[18] << 8 | (unsigned char)persistentBuffer[19]; @@ -1828,29 +1828,29 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() if ((w == 64 && h == 32) || (w == 64 && h == 64)) { - // CACHE BUSTING: Si usas el mismo nombre "custom.png", el cache del motor no se refresca. - // Usamos un contador para generar IDs únicos (ugcskin00000101.png, 102, etc). + // CACHE BUSTING: If we use the same filename (e.g., "custom.png"), the engine's texture cache won't refresh. + // We use a counter to generate unique texture names (ugcskin00000100.png, 120, etc). static int s_ugcSkinLoadCount = 0; s_ugcSkinLoadCount++; wchar_t textureNameBuf[256]; - // Formato ugcskinXXXXXXXX.png es obligatorio para que getSkinIdFromPath genere un ID válido. - // Ajustamos el ID para que los bits bajos (0-4) sean 0, respetando el bitmask de UGC. + // Standard ugcskinXXXXXXXX.png naming is required for correct internal ID generation via getSkinIdFromPath. + // We shift the counter by 5 bits to align with the UGC bitmask (lower 5 bits are reserved for default skins). swprintf(textureNameBuf, 256, L"ugcskin%08X.png", 0x100 + (s_ugcSkinLoadCount << 5)); wstring textureName = textureNameBuf; - // Limpieza opcional del buffer anterior + // Optional: Remove previous custom texture from memory to avoid leaks if (m_bUsingCustomSkin && !m_selectedSkinPath.empty()) { app.RemoveMemoryTextureFile(m_selectedSkinPath); } - // 1. Cargar datos en el sistema de archivos en memoria + // 1. Add texture data to the in-memory filesystem app.AddMemoryTextureFile(textureName, persistentBuffer, (DWORD)size); - // 2. Aplicar la skin al jugador local + // 2. Apply the skin path to the local player character app.SetPlayerSkin(m_iPad, textureName); - // 3. Forzar refresco de la UI + // 3. Force UI refresh and update internal state m_bUsingCustomSkin = true; m_selectedSkinPath = textureName; m_currentSkinPath = textureName; @@ -1861,7 +1861,7 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() return; } } - delete[] persistentBuffer; // Borrar si falló la validación + delete[] persistentBuffer; // Cleanup if validation or loading failed ui.PlayUISFX(eSFX_CraftFail); file.close(); } diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h index 1db9aa265..101cec2b3 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h @@ -136,12 +136,12 @@ class UIScene_SkinSelectMenu : public UIScene // TODO: This should be pure virtual in this class virtual wstring getMoviePath(); - bool m_bUsingCustomSkin; // Flag para saber si hay una skin externa activa + bool m_bUsingCustomSkin; // Flag indicating if a custom external skin is active public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); - void LoadExternalSkin(); // Función principal del explorador + void LoadExternalSkin(); // Main function for selecting and loading an external skin via file picker virtual void customDraw(IggyCustomDrawCallbackRegion *region); From a0fc7d68451b0e3643d4b2f04e426a3c3a01872b Mon Sep 17 00:00:00 2001 From: IsraelDXPP Date: Wed, 4 Mar 2026 12:15:42 -0700 Subject: [PATCH 6/7] za --- Minecraft.Client/Common/Consoles_App.cpp | 41 ++++++- .../Common/UI/UIScene_SkinSelectMenu.cpp | 111 +++++++++++++++--- .../Common/UI/UIScene_SkinSelectMenu.h | 2 +- 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index b476ca909..f028fc679 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -1,5 +1,6 @@  #include "stdafx.h" +#include #include "..\..\Minecraft.World\net.minecraft.world.entity.item.h" #include "..\..\Minecraft.World\net.minecraft.world.entity.player.h" #include "..\..\Minecraft.World\net.minecraft.world.level.tile.entity.h" @@ -60,9 +61,6 @@ #include "UI\UI.h" #include "UI\UIScene_PauseMenu.h" #endif -#ifdef __PS3__ -#include -#endif #ifdef __ORBIS__ #include #endif @@ -9344,6 +9342,43 @@ wstring CMinecraftApp::getSkinPathFromId(DWORD skinId) else { swprintf(chars, 256, L"ugcskin%08X.png",ugcSkinIndex); + + // --- PERSISTENCE: Check if we need to load this skin from disk --- + // Iterate through all possible active players (max 4) to find if this skin belongs to one of them. + for (int i = 0; i < 4; ++i) + { + if (ProfileManager.IsSignedIn(i)) // Check if player is signed in + { + wstring username = ProfileManager.GetDisplayName(i); + if (username.empty()) username = L"default"; + + WCHAR expectedSkinPath[MAX_PATH]; + swprintf(expectedSkinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); + + // If the file is not already in memory textures, load it. + // Note: app.IsFileInMemoryTextures check is internal to app.AddMemoryTextureFile normally, + // but we check here to avoid redundant file I/O. + if (!app.IsFileInMemoryTextures(chars)) + { + std::ifstream file(expectedSkinPath, std::ios::binary | std::ios::ate); + if (file.is_open()) + { + std::streamsize size = file.tellg(); + if (size > 0 && size <= 2 * 1024 * 1024) + { + file.seekg(0, std::ios::beg); + PBYTE buffer = new (std::nothrow) BYTE[(size_t)size]; + if (buffer && file.read((char*)buffer, size)) + { + app.AddMemoryTextureFile(chars, buffer, (DWORD)size); + } + else if (buffer) delete[] buffer; + } + file.close(); + } + } + } + } } } return chars; diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index 8fe43a4c4..9f2ef7ca2 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -55,7 +55,45 @@ UIScene_SkinSelectMenu::UIScene_SkinSelectMenu(int iPad, void *initData, UILayer m_bSlidingSkins = false; m_bAnimatingMove = false; m_bSkinIndexChanged = false; - m_bUsingCustomSkin = (!GET_IS_DLC_SKIN_FROM_BITMASK(m_originalSkinId) && GET_UGC_SKIN_ID_FROM_BITMASK(m_originalSkinId) != 0); + + m_customSkinTextureName = L""; + m_bUsingCustomSkin = false; + + // --- PERSISTENCE: Check for existing custom skin on disk --- + wstring username = ProfileManager.GetDisplayName(iPad); + if (username.empty()) username = L"default"; + WCHAR skinPath[MAX_PATH]; + swprintf(skinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); + + std::ifstream file(skinPath, std::ios::binary | std::ios::ate); + if (file.is_open()) + { + std::streamsize size = file.tellg(); + if (size > 0 && size <= 2 * 1024 * 1024) + { + file.seekg(0, std::ios::beg); + PBYTE buffer = new (std::nothrow) BYTE[(size_t)size]; + if (buffer && file.read((char*)buffer, size)) + { + static int s_persistentLoadCount = 0; + s_persistentLoadCount++; + wchar_t nameBuf[256]; + swprintf(nameBuf, 256, L"ugcskin%08X.png", 0x300 + (s_persistentLoadCount << 5)); + m_customSkinTextureName = nameBuf; + app.AddMemoryTextureFile(m_customSkinTextureName, buffer, (DWORD)size); + m_bUsingCustomSkin = true; + } + else if (buffer) delete[] buffer; + } + file.close(); + } + + // If the player is already wearing a custom skin, override with the current one + if (!GET_IS_DLC_SKIN_FROM_BITMASK(m_originalSkinId) && GET_UGC_SKIN_ID_FROM_BITMASK(m_originalSkinId) != 0) + { + m_customSkinTextureName = m_currentSkinPath; + m_bUsingCustomSkin = true; + } m_currentNavigation = eSkinNavigation_Skin; @@ -229,7 +267,8 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr } break; case ACTION_MENU_X: - if (pressed && !repeat) + // Only allow changing the external skin if we are on the first slot of the default pack + if (pressed && !repeat && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) { LoadExternalSkin(); handled = true; @@ -276,7 +315,6 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr m_skinIndex = getPreviousSkinIndex(m_skinIndex); //handleSkinIndexChanged(); - m_bUsingCustomSkin = false; m_bSlidingSkins = true; m_bAnimatingMove = true; @@ -312,7 +350,6 @@ void UIScene_SkinSelectMenu::handleInput(int iPad, int key, bool repeat, bool pr m_skinIndex = getNextSkinIndex(m_skinIndex); //handleSkinIndexChanged(); - m_bUsingCustomSkin = false; m_bSlidingSkins = true; m_bAnimatingMove = true; @@ -415,15 +452,23 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad) { ui.AnimateKeyPress(iPad, ACTION_MENU_OK, false, true, false); - if (m_bUsingCustomSkin) + // If we are specifically on the custom skin slot (0) and we have a custom skin loaded, + // apply it and set the active flag to true. + if (!m_customSkinTextureName.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) { ui.PlayUISFX(eSFX_Press); + app.SetPlayerSkin(iPad, m_customSkinTextureName); m_currentSkinPath = app.GetPlayerSkinName(iPad); m_originalSkinId = app.GetPlayerSkinId(iPad); + m_bUsingCustomSkin = true; setCharacterSelected(true); return; } + // If the user selects a DIFFERENT skin (DLC, Default, etc.), clear the active flag + // but KEEP m_customSkinTextureName so it stays in the menu. + m_bUsingCustomSkin = false; + // if the profile data has been changed, then force a profile write // It seems we're allowed to break the 5 minute rule if it's the result of a user action switch(m_packIndex) @@ -668,14 +713,23 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_controlSkinNamePlate.setVisible( false ); - if (m_bUsingCustomSkin) + // --- PERSISTENCE: Custom Skin UI Display --- + // Show the "Custom Skin" info if we have a custom skin loaded and are on slot 0. + if (!m_customSkinTextureName.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) { - m_selectedSkinPath = m_currentSkinPath; + m_selectedSkinPath = m_customSkinTextureName; m_selectedCapePath = L""; m_vAdditionalSkinBoxes = NULL; skinName = L"Custom Skin"; skinOrigin = L"External PNG"; - setCharacterSelected(true); + + // If the player is currently wearing THE custom skin, show the checkmark. + // We use m_currentSkinPath (what the player is actually wearing) to decide. + if (m_selectedSkinPath.compare(app.GetPlayerSkinName(m_iPad)) == 0) + { + setCharacterSelected(true); + } + setCharacterLocked(false); m_characters[eCharacter_Current].setVisible(true); m_controlSkinNamePlate.setVisible(true); @@ -786,11 +840,6 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_labelSkinName.setLabel(skinName); m_labelSkinOrigin.setLabel(skinOrigin); - if (m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_bUsingCustomSkin) - { - m_labelSkinOrigin.setLabel(L"X: Custom Skin"); // Display visual aid for custom skin - } - if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) { setCharacterSelected(true); // Enable selection checkmark @@ -888,7 +937,15 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() switch(m_packIndex) { case SKIN_SELECT_PACK_DEFAULT: - backupTexture = getTextureId(nextIndex); + if (nextIndex == 0 && !m_customSkinTextureName.empty()) + { + otherSkinPath = m_customSkinTextureName; + backupTexture = TN_MOB_CHAR; + } + else + { + backupTexture = getTextureId(nextIndex); + } break; case SKIN_SELECT_PACK_FAVORITES: if(uiCurrentFavoriteC>0) @@ -959,7 +1016,15 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() switch(m_packIndex) { case SKIN_SELECT_PACK_DEFAULT: - backupTexture = getTextureId(previousIndex); + if (previousIndex == 0 && !m_customSkinTextureName.empty()) + { + otherSkinPath = m_customSkinTextureName; + backupTexture = TN_MOB_CHAR; + } + else + { + backupTexture = getTextureId(previousIndex); + } break; case SKIN_SELECT_PACK_FAVORITES: if(uiCurrentFavoriteC>0) @@ -1847,11 +1912,27 @@ void UIScene_SkinSelectMenu::LoadExternalSkin() // 1. Add texture data to the in-memory filesystem app.AddMemoryTextureFile(textureName, persistentBuffer, (DWORD)size); + // --- PERSISTENCE: Save the custom skin to disk --- + // Create the 'CustomSkin' directory if it doesn't exist, and save as 'custom_skin_%username%.png' + CreateDirectoryW(L"CustomSkin", NULL); + wstring username = ProfileManager.GetDisplayName(m_iPad); + if (username.empty()) username = L"default"; + WCHAR skinPath[MAX_PATH]; + swprintf(skinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); + + std::ofstream outFile(skinPath, std::ios::binary); + if (outFile.is_open()) + { + outFile.write((char*)persistentBuffer, size); + outFile.close(); + } + // 2. Apply the skin path to the local player character app.SetPlayerSkin(m_iPad, textureName); // 3. Force UI refresh and update internal state m_bUsingCustomSkin = true; + m_customSkinTextureName = textureName; m_selectedSkinPath = textureName; m_currentSkinPath = textureName; m_selectedCapePath = L""; diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h index 101cec2b3..af756bca6 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h @@ -102,7 +102,7 @@ class UIScene_SkinSelectMenu : public UIScene DLCPack *m_currentPack; DWORD m_packIndex, m_skinIndex; DWORD m_originalSkinId; - wstring m_currentSkinPath, m_selectedSkinPath, m_selectedCapePath; + wstring m_currentSkinPath, m_selectedSkinPath, m_selectedCapePath, m_customSkinTextureName; vector *m_vAdditionalSkinBoxes; bool m_bSlidingSkins, m_bAnimatingMove; From 672df271235dea162dcf7b26a81d3eb55688be41 Mon Sep 17 00:00:00 2001 From: IsraelDXPP Date: Thu, 5 Mar 2026 10:09:49 -0700 Subject: [PATCH 7/7] Some changes were reversed and a warning was fixed. --- Minecraft.Client/Common/Consoles_App.cpp | 84 +++--- .../Common/UI/UIScene_SkinSelectMenu.cpp | 259 +++++++++--------- .../Common/UI/UIScene_SkinSelectMenu.h | 6 +- 3 files changed, 177 insertions(+), 172 deletions(-) diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index f028fc679..5d9d99ee1 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -9296,6 +9296,13 @@ void CMinecraftApp::SetAnimOverrideBitmask(DWORD dwSkinID,unsigned int uiAnimOve DWORD CMinecraftApp::getSkinIdFromPath(const wstring &skin) { + if (skin.find(L"custom_skin_") != wstring::npos) + { + // If the texture name is our new custom_skin_ format, return a fixed UGC ID (0x100). + // This prevents the engine from parsing non-hex characters and defaulting to 0. + return MAKE_SKIN_BITMASK(false, 0x100); + } + bool dlcSkin = false; unsigned int skinId = 0; @@ -9322,62 +9329,71 @@ DWORD CMinecraftApp::getSkinIdFromPath(const wstring &skin) wstring CMinecraftApp::getSkinPathFromId(DWORD skinId) { - // 4J Stu - This function maps the encoded DWORD we store in the player profile - // to a filename that is stored as a memory texture and shared between systems in game + // Maps the encoded DWORD stored in the player profile to a texture filename + // that the engine can look up in the in-memory texture filesystem. wchar_t chars[256]; - if( GET_IS_DLC_SKIN_FROM_BITMASK(skinId) ) + if (GET_IS_DLC_SKIN_FROM_BITMASK(skinId)) { - // 4J Stu - DLC skins are numbered using decimal rather than hex to make it easier to number manually + // DLC skins use decimal numbering to make manual numbering easier. swprintf(chars, 256, L"dlcskin%08d.png", GET_DLC_SKIN_ID_FROM_BITMASK(skinId)); - } else { - DWORD ugcSkinIndex = GET_UGC_SKIN_ID_FROM_BITMASK(skinId); + DWORD ugcSkinIndex = GET_UGC_SKIN_ID_FROM_BITMASK(skinId); DWORD defaultSkinIndex = GET_DEFAULT_SKIN_ID_FROM_BITMASK(skinId); - if( ugcSkinIndex == 0 ) + if (ugcSkinIndex == 0) { - swprintf(chars, 256, L"defskin%08X.png",defaultSkinIndex); + swprintf(chars, 256, L"defskin%08X.png", defaultSkinIndex); } else { - swprintf(chars, 256, L"ugcskin%08X.png",ugcSkinIndex); - - // --- PERSISTENCE: Check if we need to load this skin from disk --- - // Iterate through all possible active players (max 4) to find if this skin belongs to one of them. + // For custom (UGC) skins we use a player-readable name: custom_skin_{gamertag}.png. + // Check every signed-in slot and load the skin from disk if it isn't in memory yet. for (int i = 0; i < 4; ++i) { - if (ProfileManager.IsSignedIn(i)) // Check if player is signed in - { - wstring username = ProfileManager.GetDisplayName(i); - if (username.empty()) username = L"default"; + if (!ProfileManager.IsSignedIn(i)) + continue; + + char* gamertag = ProfileManager.GetGamertag(i); + const char* tag = (gamertag && gamertag[0]) ? gamertag : "Player"; - WCHAR expectedSkinPath[MAX_PATH]; - swprintf(expectedSkinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); + // The in-memory texture name for this player's custom skin. + wchar_t texName[128]; + swprintf(texName, 128, L"custom_skin_%hs.png", tag); - // If the file is not already in memory textures, load it. - // Note: app.IsFileInMemoryTextures check is internal to app.AddMemoryTextureFile normally, - // but we check here to avoid redundant file I/O. - if (!app.IsFileInMemoryTextures(chars)) + if (!app.IsFileInMemoryTextures(texName)) + { + // Disk path: CustomSkin/custom_skin_{gamertag}.png + wchar_t diskPath[MAX_PATH]; + swprintf(diskPath, MAX_PATH, L"CustomSkin/custom_skin_%hs.png", tag); + + File skinFile(diskPath); + if (skinFile.exists()) { - std::ifstream file(expectedSkinPath, std::ios::binary | std::ios::ate); - if (file.is_open()) + DWORD size = (DWORD)skinFile.length(); + if (size > 0 && size <= 2 * 1024 * 1024) { - std::streamsize size = file.tellg(); - if (size > 0 && size <= 2 * 1024 * 1024) + PBYTE buffer = new (std::nothrow) BYTE[(size_t)size]; + if (buffer) { - file.seekg(0, std::ios::beg); - PBYTE buffer = new (std::nothrow) BYTE[(size_t)size]; - if (buffer && file.read((char*)buffer, size)) - { - app.AddMemoryTextureFile(chars, buffer, (DWORD)size); - } - else if (buffer) delete[] buffer; + FileInputStream fis(skinFile); + byteArray b; + b.data = buffer; + b.length = size; + if (fis.read(b, 0, size) == size) + app.AddMemoryTextureFile(texName, buffer, size); + else + delete[] buffer; + fis.close(); } - file.close(); } } } + + // Return the texture name for the first signed-in player slot — + // this matches what LoadExternalSkin registers on behalf of m_iPad. + wcscpy_s(chars, 256, texName); + break; } } } diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp index 9f2ef7ca2..9a8b2478a 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp @@ -9,6 +9,7 @@ #endif #include #include // Required for GetOpenFileNameW +#include #define SKIN_SELECT_PACK_DEFAULT 0 #define SKIN_SELECT_PACK_FAVORITES 1 @@ -56,42 +57,53 @@ UIScene_SkinSelectMenu::UIScene_SkinSelectMenu(int iPad, void *initData, UILayer m_bAnimatingMove = false; m_bSkinIndexChanged = false; - m_customSkinTextureName = L""; + m_customSkinPath = L""; m_bUsingCustomSkin = false; - // --- PERSISTENCE: Check for existing custom skin on disk --- - wstring username = ProfileManager.GetDisplayName(iPad); - if (username.empty()) username = L"default"; - WCHAR skinPath[MAX_PATH]; - swprintf(skinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); + // Try to load a previously saved custom skin from disk. + // The file is named after the player's gamertag so each player has their own skin. + char* gamertag = ProfileManager.GetGamertag(iPad); + const char* tag = (gamertag && gamertag[0]) ? gamertag : "Player"; - std::ifstream file(skinPath, std::ios::binary | std::ios::ate); - if (file.is_open()) + std::array diskPathBuf = {}; + swprintf(diskPathBuf.data(), diskPathBuf.size(), L"CustomSkin/custom_skin_%hs.png", tag); + + File skinFile(diskPathBuf.data()); + if (skinFile.exists()) { - std::streamsize size = file.tellg(); + DWORD size = (DWORD)skinFile.length(); if (size > 0 && size <= 2 * 1024 * 1024) { - file.seekg(0, std::ios::beg); PBYTE buffer = new (std::nothrow) BYTE[(size_t)size]; - if (buffer && file.read((char*)buffer, size)) + if (buffer) { - static int s_persistentLoadCount = 0; - s_persistentLoadCount++; - wchar_t nameBuf[256]; - swprintf(nameBuf, 256, L"ugcskin%08X.png", 0x300 + (s_persistentLoadCount << 5)); - m_customSkinTextureName = nameBuf; - app.AddMemoryTextureFile(m_customSkinTextureName, buffer, (DWORD)size); - m_bUsingCustomSkin = true; + FileInputStream fis(skinFile); + byteArray b; + b.data = buffer; + b.length = size; + if (fis.read(b, 0, size) == size) + { + std::array texNameBuf = {}; + swprintf(texNameBuf.data(), texNameBuf.size(), L"custom_skin_%hs.png", tag); + wstring texName = texNameBuf.data(); + + app.AddMemoryTextureFile(texName, buffer, size); + m_customSkinPath = texName; + m_bUsingCustomSkin = true; + } + else + { + delete[] buffer; + } + fis.close(); } - else if (buffer) delete[] buffer; } - file.close(); } - // If the player is already wearing a custom skin, override with the current one + // If the profile already tracks a custom skin name, prefer that over the disk load. if (!GET_IS_DLC_SKIN_FROM_BITMASK(m_originalSkinId) && GET_UGC_SKIN_ID_FROM_BITMASK(m_originalSkinId) != 0) { - m_customSkinTextureName = m_currentSkinPath; + m_customSkinPath = m_currentSkinPath; m_bUsingCustomSkin = true; } @@ -454,10 +466,10 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad) // If we are specifically on the custom skin slot (0) and we have a custom skin loaded, // apply it and set the active flag to true. - if (!m_customSkinTextureName.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) + if (!m_customSkinPath.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) { ui.PlayUISFX(eSFX_Press); - app.SetPlayerSkin(iPad, m_customSkinTextureName); + app.SetPlayerSkin(iPad, m_customSkinPath); m_currentSkinPath = app.GetPlayerSkinName(iPad); m_originalSkinId = app.GetPlayerSkinId(iPad); m_bUsingCustomSkin = true; @@ -465,8 +477,8 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad) return; } - // If the user selects a DIFFERENT skin (DLC, Default, etc.), clear the active flag - // but KEEP m_customSkinTextureName so it stays in the menu. + // If the user selects a different skin (DLC, Default, etc.), clear the active flag + // but KEEP m_customSkinPath so the custom skin stays available in the menu. m_bUsingCustomSkin = false; // if the profile data has been changed, then force a profile write @@ -666,7 +678,10 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad) void UIScene_SkinSelectMenu::customDraw(IggyCustomDrawCallbackRegion *region) { int characterId = -1; - swscanf((wchar_t*)region->name,L"Character%d",&characterId); + if (swscanf((wchar_t*)region->name,L"Character%d",&characterId) != 1) + { + characterId = -1; + } if (characterId == -1) { app.DebugPrintf("Invalid character to render found\n"); @@ -713,19 +728,18 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_controlSkinNamePlate.setVisible( false ); - // --- PERSISTENCE: Custom Skin UI Display --- // Show the "Custom Skin" info if we have a custom skin loaded and are on slot 0. - if (!m_customSkinTextureName.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) + if (!m_customSkinPath.empty() && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0) { - m_selectedSkinPath = m_customSkinTextureName; + m_selectedSkinPath = m_customSkinPath; m_selectedCapePath = L""; m_vAdditionalSkinBoxes = NULL; skinName = L"Custom Skin"; skinOrigin = L"External PNG"; - - // If the player is currently wearing THE custom skin, show the checkmark. - // We use m_currentSkinPath (what the player is actually wearing) to decide. - if (m_selectedSkinPath.compare(app.GetPlayerSkinName(m_iPad)) == 0) + + // If the player is wearing this custom skin, show the "Selected" checkmark + // We also check m_bUsingCustomSkin because the session sequence number in the memory path might differ. + if (m_bUsingCustomSkin || m_selectedSkinPath.compare(app.GetPlayerSkinName(m_iPad)) == 0) { setCharacterSelected(true); } @@ -840,7 +854,7 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() m_labelSkinName.setLabel(skinName); m_labelSkinOrigin.setLabel(skinOrigin); - if (m_selectedSkinPath.compare(m_currentSkinPath) == 0) + if (m_selectedSkinPath.compare(m_currentSkinPath) == 0 || (m_bUsingCustomSkin && m_packIndex == SKIN_SELECT_PACK_DEFAULT && m_skinIndex == 0)) { setCharacterSelected(true); // Enable selection checkmark } @@ -937,9 +951,9 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() switch(m_packIndex) { case SKIN_SELECT_PACK_DEFAULT: - if (nextIndex == 0 && !m_customSkinTextureName.empty()) + if (nextIndex == 0 && !m_customSkinPath.empty()) { - otherSkinPath = m_customSkinTextureName; + otherSkinPath = m_customSkinPath; backupTexture = TN_MOB_CHAR; } else @@ -1016,9 +1030,9 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged() switch(m_packIndex) { case SKIN_SELECT_PACK_DEFAULT: - if (previousIndex == 0 && !m_customSkinTextureName.empty()) + if (previousIndex == 0 && !m_customSkinPath.empty()) { - otherSkinPath = m_customSkinTextureName; + otherSkinPath = m_customSkinPath; backupTexture = TN_MOB_CHAR; } else @@ -1845,106 +1859,81 @@ int UIScene_SkinSelectMenu::PSNSignInReturned(void* pParam, bool bContinue, int } #endif // __PSVITA__ +// We include array at the top if not present, but using std::array here: void UIScene_SkinSelectMenu::LoadExternalSkin() { OPENFILENAMEW ofn; - wchar_t szFile[MAX_PATH]; + std::array szFile = {}; ZeroMemory(&ofn, sizeof(ofn)); - ofn.lStructSize = sizeof(ofn); - ofn.hwndOwner = NULL; - ofn.lpstrFile = szFile; - ofn.lpstrFile[0] = L'\0'; - ofn.nMaxFile = MAX_PATH; - ofn.lpstrFilter = L"PNG Files\0*.png\0All Files\0*.*\0"; - ofn.nFilterIndex = 1; - - // IMPORTANT: OFN_NOCHANGEDIR prevents the File Explorer from changing the current working directory. - // If the directory changes, the game will FAIL TO SAVE worlds (Assertion failed in STO_SaveGame.cpp). - ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; - - if (GetOpenFileNameW(&ofn) == TRUE) + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = szFile.data(); + ofn.nMaxFile = (DWORD)szFile.size(); + ofn.lpstrFilter = L"PNG Files\0*.png\0All Files\0*.*\0"; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; + + if (!GetOpenFileNameW(&ofn)) + return; + + File srcFile(ofn.lpstrFile); + DWORD size = (DWORD)srcFile.length(); + if (size == 0 || size > 2 * 1024 * 1024) // invalid size { - std::ifstream file(ofn.lpstrFile, std::ios::binary | std::ios::ate); - if (file.is_open()) - { - std::streamsize size = file.tellg(); - if (size <= 0 || size > 2 * 1024 * 1024) { // Limite 2MB - file.close(); - ui.PlayUISFX(eSFX_CraftFail); - return; - } - file.seekg(0, std::ios::beg); - - // MEMORY MANAGEMENT: AddMemoryTextureFile does NOT copy the buffer; it uses the pointer directly. - // If you use a local vector or stack buffer, the skin will corrupt/crash as soon as the function returns. - PBYTE persistentBuffer = new (std::nothrow) BYTE[(size_t)size]; - if (!persistentBuffer || !file.read((char*)persistentBuffer, size)) { - if(persistentBuffer) delete[] persistentBuffer; - file.close(); - return; - } - - // Validate PNG Header and Dimensions (64x32 or 64x64) - if (size > 24 && (unsigned char)persistentBuffer[0] == 0x89 && persistentBuffer[1] == 'P') - { - unsigned int w = (unsigned char)persistentBuffer[16] << 24 | (unsigned char)persistentBuffer[17] << 16 | (unsigned char)persistentBuffer[18] << 8 | (unsigned char)persistentBuffer[19]; - unsigned int h = (unsigned char)persistentBuffer[20] << 24 | (unsigned char)persistentBuffer[21] << 16 | (unsigned char)persistentBuffer[22] << 8 | (unsigned char)persistentBuffer[23]; - - if ((w == 64 && h == 32) || (w == 64 && h == 64)) - { - // CACHE BUSTING: If we use the same filename (e.g., "custom.png"), the engine's texture cache won't refresh. - // We use a counter to generate unique texture names (ugcskin00000100.png, 120, etc). - static int s_ugcSkinLoadCount = 0; - s_ugcSkinLoadCount++; - - wchar_t textureNameBuf[256]; - // Standard ugcskinXXXXXXXX.png naming is required for correct internal ID generation via getSkinIdFromPath. - // We shift the counter by 5 bits to align with the UGC bitmask (lower 5 bits are reserved for default skins). - swprintf(textureNameBuf, 256, L"ugcskin%08X.png", 0x100 + (s_ugcSkinLoadCount << 5)); - wstring textureName = textureNameBuf; - - // Optional: Remove previous custom texture from memory to avoid leaks - if (m_bUsingCustomSkin && !m_selectedSkinPath.empty()) { - app.RemoveMemoryTextureFile(m_selectedSkinPath); - } - - // 1. Add texture data to the in-memory filesystem - app.AddMemoryTextureFile(textureName, persistentBuffer, (DWORD)size); - - // --- PERSISTENCE: Save the custom skin to disk --- - // Create the 'CustomSkin' directory if it doesn't exist, and save as 'custom_skin_%username%.png' - CreateDirectoryW(L"CustomSkin", NULL); - wstring username = ProfileManager.GetDisplayName(m_iPad); - if (username.empty()) username = L"default"; - WCHAR skinPath[MAX_PATH]; - swprintf(skinPath, 256, L"CustomSkin\\custom_skin_%ls.png", username.c_str()); - - std::ofstream outFile(skinPath, std::ios::binary); - if (outFile.is_open()) - { - outFile.write((char*)persistentBuffer, size); - outFile.close(); - } - - // 2. Apply the skin path to the local player character - app.SetPlayerSkin(m_iPad, textureName); - - // 3. Force UI refresh and update internal state - m_bUsingCustomSkin = true; - m_customSkinTextureName = textureName; - m_selectedSkinPath = textureName; - m_currentSkinPath = textureName; - m_selectedCapePath = L""; - handleSkinIndexChanged(); - - file.close(); - return; - } - } - delete[] persistentBuffer; // Cleanup if validation or loading failed - ui.PlayUISFX(eSFX_CraftFail); - file.close(); - } + ui.PlayUISFX(eSFX_CraftFail); + return; } -} + + FileInputStream fis(srcFile); + PBYTE buffer = new (std::nothrow) BYTE[size]; + if (!buffer || fis.read({ buffer, size }, 0, size) != size) // read error + { + delete[] buffer; + ui.PlayUISFX(eSFX_CraftFail); + return; + } + fis.close(); + + // validate PNG header + if (size <= 24 || buffer[0] != 0x89 || buffer[1] != 'P') + { + delete[] buffer; + ui.PlayUISFX(eSFX_CraftFail); + return; + } + + unsigned int w = (buffer[16] << 24) | (buffer[17] << 16) | (buffer[18] << 8) | buffer[19]; + unsigned int h = (buffer[20] << 24) | (buffer[21] << 16) | (buffer[22] << 8) | buffer[23]; + if (!((w == 64 && h == 32) || (w == 64 && h == 64))) // invalid dimensions + { + delete[] buffer; + ui.PlayUISFX(eSFX_CraftFail); + return; + } + + char* gamertag = ProfileManager.GetGamertag(m_iPad); + std::array texNameBuf = {}; + swprintf(texNameBuf.data(), texNameBuf.size(), L"custom_skin_%hs.png", gamertag ? gamertag : "Player"); + wstring textureName = texNameBuf.data(); + + if (m_bUsingCustomSkin && !m_customSkinPath.empty()) + app.RemoveMemoryTextureFile(m_customSkinPath); + + app.AddMemoryTextureFile(textureName, buffer, size); // register in memory + + // save to disk + std::array diskPathBuf = {}; + swprintf(diskPathBuf.data(), diskPathBuf.size(), L"CustomSkin/custom_skin_%hs.png", gamertag ? gamertag : "Player"); + File(L"CustomSkin").mkdirs(); + FileOutputStream(File(diskPathBuf.data())).write({ buffer, size }, 0, size); + + app.SetPlayerSkin(m_iPad, textureName); // apply to player + + // update state + m_bUsingCustomSkin = true; + m_customSkinPath = textureName; + m_selectedSkinPath = textureName; + m_currentSkinPath = textureName; + m_selectedCapePath = L""; + + handleSkinIndexChanged(); +} \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h index af756bca6..e99723d40 100644 --- a/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.h @@ -102,10 +102,11 @@ class UIScene_SkinSelectMenu : public UIScene DLCPack *m_currentPack; DWORD m_packIndex, m_skinIndex; DWORD m_originalSkinId; - wstring m_currentSkinPath, m_selectedSkinPath, m_selectedCapePath, m_customSkinTextureName; + wstring m_currentSkinPath, m_selectedSkinPath, m_selectedCapePath; + wstring m_customSkinPath; // Persistent path for the loaded custom skin — not overwritten on scroll vector *m_vAdditionalSkinBoxes; - bool m_bSlidingSkins, m_bAnimatingMove; + bool m_bSlidingSkins, m_bAnimatingMove, m_bUsingCustomSkin; ESkinSelectNavigation m_currentNavigation; bool m_bNoSkinsToShow; @@ -136,7 +137,6 @@ class UIScene_SkinSelectMenu : public UIScene // TODO: This should be pure virtual in this class virtual wstring getMoviePath(); - bool m_bUsingCustomSkin; // Flag indicating if a custom external skin is active public: // INPUT