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 + if (!ProfileManager.IsSignedIn(i)) + continue; + + char* gamertag = ProfileManager.GetGamertag(i); + const char* tag = (gamertag && gamertag[0]) ? gamertag : "Player"; + + // 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 (!app.IsFileInMemoryTextures(texName)) { - wstring username = ProfileManager.GetDisplayName(i); - if (username.empty()) username = L"default"; + // Disk path: CustomSkin/custom_skin_{gamertag}.png + wchar_t diskPath[MAX_PATH]; + swprintf(diskPath, MAX_PATH, L"CustomSkin/custom_skin_%hs.png", tag); - 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)) + 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; + 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; - // 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)) + return; - if (GetOpenFileNameW(&ofn) == TRUE) + 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 @@ private: 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 @@ protected: // 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