From 1d78fdb33a1bef52ecb57f2ba7a8ce7a85528eaa Mon Sep 17 00:00:00 2001 From: Fireblade <72758695+Firebladedoge229@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:26:04 -0400 Subject: [PATCH 1/4] feat. leaderboard + fix for building on linux --- Minecraft.Client/Common/Consoles_App.cpp | 7 + .../UI/IUIScene_AbstractContainerMenu.cpp | 2 +- .../Common/UI/UIScene_LeaderboardsMenu.cpp | 8 +- .../Common/UI/UIScene_LoadMenu.cpp | 11 + Minecraft.Client/Extrax64Stubs.cpp | 207 ++++++-- .../WindowsLeaderboardManager.cpp | 491 +++++++++++++++++- .../Leaderboards/WindowsLeaderboardManager.h | 30 +- 7 files changed, 688 insertions(+), 68 deletions(-) diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 0ca50199..02f56258 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -954,6 +954,13 @@ void CMinecraftApp::InitGameSettings() SetDefaultOptions(pProfileSettings,i,false); #endif + + Minecraft* minecraft = Minecraft::GetInstance(); + if (minecraft != nullptr && minecraft->stats[i] != nullptr) + { + minecraft->stats[i]->clear(); + minecraft->stats[i]->parse(GameSettingsA[i]); + } } } diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp index 1c08827b..2b54b2eb 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp @@ -15,7 +15,7 @@ #include "../../Minecraft.h" #include "../../Options.h" #include "../../Minecraft.World/Level.h" -#include "../../MultiplayerLevel.h" +#include "../../MultiPlayerLevel.h" #include "../../../Minecraft.World/Enchantment.h" #ifdef __ORBIS__ #include diff --git a/Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp index ad27fa9a..57f11f45 100644 --- a/Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp @@ -424,6 +424,10 @@ void UIScene_LeaderboardsMenu::handleInput(int iPad, int key, bool repeat, bool void UIScene_LeaderboardsMenu::ReadStats(int startIndex) { + // load before so real stats cant get overwritten afterwards + m_labelInfo.setLabel(app.GetString(IDS_LEADERBOARD_LOADING)); + m_labelInfo.setVisible(true); + //If startIndex == -1, then use default values if( startIndex == -1 ) { @@ -490,10 +494,6 @@ void UIScene_LeaderboardsMenu::ReadStats(int startIndex) } break; } - - //Show the loading message - m_labelInfo.setLabel(app.GetString(IDS_LEADERBOARD_LOADING)); - m_labelInfo.setVisible(true); } bool UIScene_LeaderboardsMenu::OnStatsReadComplete(LeaderboardManager::eStatsReturn retIn, int numResults, LeaderboardManager::ViewOut results) diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp index 3a5fb128..a98a5dbf 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp @@ -517,6 +517,17 @@ void UIScene_LoadMenu::tick() m_MoreOptionsParams.bNaturalRegeneration = app.GetGameHostOption(uiHostOptions, eGameHostOption_NaturalRegeneration); m_MoreOptionsParams.bDoDaylightCycle = app.GetGameHostOption(uiHostOptions, eGameHostOption_DoDaylightCycle); + unsigned int saveDifficulty = app.GetGameHostOption(uiHostOptions, eGameHostOption_Difficulty); + if (saveDifficulty <= 3) + { + m_CurrentDifficulty = static_cast(saveDifficulty); + app.SetGameSettings(m_iPad, eGameSetting_Difficulty, m_CurrentDifficulty); + + WCHAR difficultyLabel[256]; + swprintf((WCHAR*)difficultyLabel, 256, L"%ls: %ls", app.GetString(IDS_SLIDER_DIFFICULTY), app.GetString(m_iDifficultyTitleSettingA[m_CurrentDifficulty])); + m_sliderDifficulty.init(difficultyLabel, eControl_Difficulty, 0, 3, m_CurrentDifficulty); + } + bool cheatsOn = m_MoreOptionsParams.bHostPrivileges; if (!cheatsOn) { diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index d606f810..5bdb155c 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -576,6 +576,126 @@ DWORD XEnableGuestSignin(BOOL fEnable) { return 0; } #ifdef _WINDOWS64 static void* profileData[4]; static bool s_bProfileIsFullVersion; +static unsigned int s_profileDataBytesPerPad = 0; + +static void Win64_GetProfileDataPath(int iQuadrant, char* outPath, size_t outPathSize) +{ + if (outPath == nullptr || outPathSize == 0) + return; + + outPath[0] = '\0'; + GetModuleFileNameA(nullptr, outPath, static_cast(outPathSize)); + char* lastSlash = strrchr(outPath, '\\'); + char* lastForwardSlash = strrchr(outPath, '/'); + if (lastForwardSlash != nullptr && (lastSlash == nullptr || lastForwardSlash > lastSlash)) + lastSlash = lastForwardSlash; + if (lastSlash != nullptr) + *(lastSlash + 1) = '\0'; + + char profileFileName[32] = {}; + sprintf_s(profileFileName, "profile%d.dat", iQuadrant); + strncat_s(outPath, outPathSize, profileFileName, _TRUNCATE); +} + +static bool Win64_LoadProfileDataBlob(int iQuadrant, void* dstData, unsigned int dataSize) +{ + if (dstData == nullptr || dataSize == 0) + return false; + + char filePath[MAX_PATH] = {}; + Win64_GetProfileDataPath(iQuadrant, filePath, MAX_PATH); + + FILE* profileFile = nullptr; + if (fopen_s(&profileFile, filePath, "rb") != 0 || profileFile == nullptr) + return false; + + fseek(profileFile, 0, SEEK_END); + long fileSize = ftell(profileFile); + rewind(profileFile); + + if (fileSize != static_cast(dataSize)) + { + fclose(profileFile); + return false; + } + + const size_t bytesRead = fread(dstData, 1, dataSize, profileFile); + fclose(profileFile); + + return bytesRead == dataSize; +} + +static void Win64_SaveProfileDataBlob(int iQuadrant, const void* srcData, unsigned int dataSize) +{ + if (srcData == nullptr || dataSize == 0) + return; + + char filePath[MAX_PATH] = {}; + Win64_GetProfileDataPath(iQuadrant, filePath, MAX_PATH); + + FILE* profileFile = nullptr; + if (fopen_s(&profileFile, filePath, "wb") != 0 || profileFile == nullptr) + return; + + fwrite(srcData, 1, dataSize, profileFile); + fclose(profileFile); +} + +static void Win64_ApplyDefaultProfileGameSettings(void* profileBytes) +{ + if (profileBytes == nullptr) + return; + + // Set some sane initial values! + GAME_SETTINGS* pGameSettings = static_cast(profileBytes); + pGameSettings->ucMenuSensitivity = 100; //eGameSetting_Sensitivity_InMenu + pGameSettings->ucInterfaceOpacity = 80; //eGameSetting_Sensitivity_InMenu + pGameSettings->usBitmaskValues |= 0x0200; //eGameSetting_DisplaySplitscreenGamertags - on + pGameSettings->usBitmaskValues |= 0x0400; //eGameSetting_Hints - on + pGameSettings->usBitmaskValues |= 0x1000; //eGameSetting_Autosave - 2 + pGameSettings->usBitmaskValues |= 0x8000; //eGameSetting_Tooltips - on + pGameSettings->uiBitmaskValues = 0L; // reset + pGameSettings->uiBitmaskValues |= GAMESETTING_CLOUDS; //eGameSetting_Clouds - on + pGameSettings->uiBitmaskValues |= GAMESETTING_ONLINE; //eGameSetting_GameSetting_Online - on + pGameSettings->uiBitmaskValues |= GAMESETTING_FRIENDSOFFRIENDS; //eGameSetting_GameSetting_FriendsOfFriends - on + pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYUPDATEMSG; //eGameSetting_DisplayUpdateMessage (counter) + pGameSettings->uiBitmaskValues &= ~GAMESETTING_BEDROCKFOG; //eGameSetting_BedrockFog - off + pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYHUD; //eGameSetting_DisplayHUD - on + pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYHAND; //eGameSetting_DisplayHand - on + pGameSettings->uiBitmaskValues |= GAMESETTING_CUSTOMSKINANIM; //eGameSetting_CustomSkinAnim - on + pGameSettings->uiBitmaskValues |= GAMESETTING_DEATHMESSAGES; //eGameSetting_DeathMessages - on + pGameSettings->uiBitmaskValues |= (GAMESETTING_UISIZE & 0x00000800); // uisize 2 + pGameSettings->uiBitmaskValues |= (GAMESETTING_UISIZE_SPLITSCREEN & 0x00004000); // splitscreen ui size 3 + pGameSettings->uiBitmaskValues |= GAMESETTING_ANIMATEDCHARACTER; //eGameSetting_AnimatedCharacter - on + + // TU12 + // favorite skins added, but only set in TU12 - set to FFs + for (int skinIndex = 0; skinIndex < MAX_FAVORITE_SKINS; ++skinIndex) + { + pGameSettings->uiFavoriteSkinA[skinIndex] = 0xFFFFFFFF; + } + pGameSettings->ucCurrentFavoriteSkinPos = 0; + // Added a bitmask in TU13 to enable/disable display of the Mash-up pack worlds in the saves list + pGameSettings->uiMashUpPackWorldsDisplay = 0xFFFFFFFF; + + // PS3DEC13 + pGameSettings->uiBitmaskValues &= ~GAMESETTING_PS3EULAREAD; //eGameSetting_PS3_EULA_Read - off + + // PS3 1.05 - added Greek + pGameSettings->ucLanguage = MINECRAFT_LANGUAGE_DEFAULT; // use the system language + + // PS Vita - network mode added + pGameSettings->uiBitmaskValues &= ~GAMESETTING_PSVITANETWORKMODEADHOC; //eGameSetting_PSVita_NetworkModeAdhoc - off + + // Tutorials for most menus, and a few other things + pGameSettings->ucTutorialCompletion[0] = 0xFF; + pGameSettings->ucTutorialCompletion[1] = 0xFF; + pGameSettings->ucTutorialCompletion[2] = 0xF; + + // Has gone halfway through the tutorial + pGameSettings->ucTutorialCompletion[28] |= 1 << 0; +} + void C_4JProfile::Initialise(DWORD dwTitleID, DWORD dwOfferID, unsigned short usProfileVersion, @@ -585,60 +705,24 @@ void C_4JProfile::Initialise(DWORD dwTitleID, int iGameDefinedDataSizeX4, unsigned int* puiGameDefinedDataChangedBitmask) { + s_profileDataBytesPerPad = static_cast(iGameDefinedDataSizeX4 / XUSER_MAX_COUNT); + if (s_profileDataBytesPerPad == 0) + s_profileDataBytesPerPad = static_cast(iGameDefinedDataSizeX4 / 4); + for (int i = 0; i < 4; i++) { - profileData[i] = new byte[iGameDefinedDataSizeX4 / 4]; - ZeroMemory(profileData[i], sizeof(byte) * iGameDefinedDataSizeX4 / 4); + profileData[i] = new byte[s_profileDataBytesPerPad]; + ZeroMemory(profileData[i], sizeof(byte) * s_profileDataBytesPerPad); - // Set some sane initial values! - GAME_SETTINGS* pGameSettings = static_cast(profileData[i]); - pGameSettings->ucMenuSensitivity = 100; //eGameSetting_Sensitivity_InMenu - pGameSettings->ucInterfaceOpacity = 80; //eGameSetting_Sensitivity_InMenu - pGameSettings->usBitmaskValues |= 0x0200; //eGameSetting_DisplaySplitscreenGamertags - on - pGameSettings->usBitmaskValues |= 0x0400; //eGameSetting_Hints - on - pGameSettings->usBitmaskValues |= 0x1000; //eGameSetting_Autosave - 2 - pGameSettings->usBitmaskValues |= 0x8000; //eGameSetting_Tooltips - on - pGameSettings->uiBitmaskValues = 0L; // reset - pGameSettings->uiBitmaskValues |= GAMESETTING_CLOUDS; //eGameSetting_Clouds - on - pGameSettings->uiBitmaskValues |= GAMESETTING_ONLINE; //eGameSetting_GameSetting_Online - on - pGameSettings->uiBitmaskValues |= GAMESETTING_FRIENDSOFFRIENDS; //eGameSetting_GameSetting_FriendsOfFriends - on - pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYUPDATEMSG; //eGameSetting_DisplayUpdateMessage (counter) - pGameSettings->uiBitmaskValues &= ~GAMESETTING_BEDROCKFOG; //eGameSetting_BedrockFog - off - pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYHUD; //eGameSetting_DisplayHUD - on - pGameSettings->uiBitmaskValues |= GAMESETTING_DISPLAYHAND; //eGameSetting_DisplayHand - on - pGameSettings->uiBitmaskValues |= GAMESETTING_CUSTOMSKINANIM; //eGameSetting_CustomSkinAnim - on - pGameSettings->uiBitmaskValues |= GAMESETTING_DEATHMESSAGES; //eGameSetting_DeathMessages - on - pGameSettings->uiBitmaskValues |= (GAMESETTING_UISIZE & 0x00000800); // uisize 2 - pGameSettings->uiBitmaskValues |= (GAMESETTING_UISIZE_SPLITSCREEN & 0x00004000); // splitscreen ui size 3 - pGameSettings->uiBitmaskValues |= GAMESETTING_ANIMATEDCHARACTER; //eGameSetting_AnimatedCharacter - on - - // TU12 - // favorite skins added, but only set in TU12 - set to FFs - for (int i = 0; i < MAX_FAVORITE_SKINS; i++) + if (!Win64_LoadProfileDataBlob(i, profileData[i], s_profileDataBytesPerPad)) { - pGameSettings->uiFavoriteSkinA[i] = 0xFFFFFFFF; + Win64_ApplyDefaultProfileGameSettings(profileData[i]); } - pGameSettings->ucCurrentFavoriteSkinPos = 0; - // Added a bitmask in TU13 to enable/disable display of the Mash-up pack worlds in the saves list - pGameSettings->uiMashUpPackWorldsDisplay = 0xFFFFFFFF; + } - // PS3DEC13 - pGameSettings->uiBitmaskValues &= ~GAMESETTING_PS3EULAREAD; //eGameSetting_PS3_EULA_Read - off - - // PS3 1.05 - added Greek - pGameSettings->ucLanguage = MINECRAFT_LANGUAGE_DEFAULT; // use the system language - - // PS Vita - network mode added - pGameSettings->uiBitmaskValues &= ~GAMESETTING_PSVITANETWORKMODEADHOC; //eGameSetting_PSVita_NetworkModeAdhoc - off - - - // Tutorials for most menus, and a few other things - pGameSettings->ucTutorialCompletion[0] = 0xFF; - pGameSettings->ucTutorialCompletion[1] = 0xFF; - pGameSettings->ucTutorialCompletion[2] = 0xF; - - // Has gone halfway through the tutorial - pGameSettings->ucTutorialCompletion[28] |= 1 << 0; + if (puiGameDefinedDataChangedBitmask != nullptr) + { + *puiGameDefinedDataChangedBitmask = (1u << XUSER_MAX_COUNT) - 1u; } } void C_4JProfile::SetTrialTextStringTable(CXuiStringTable * pStringTable, int iAccept, int iReject) {} @@ -730,8 +814,31 @@ int C_4JProfile::SetOldProfileVersionCallback(int(*Func)(LPVOID, unsigned ch C_4JProfile::PROFILESETTINGS ProfileSettingsA[XUSER_MAX_COUNT]; C_4JProfile::PROFILESETTINGS* C_4JProfile::GetDashboardProfileSettings(int iPad) { return &ProfileSettingsA[iPad]; } -void C_4JProfile::WriteToProfile(int iQuadrant, bool bGameDefinedDataChanged, bool bOverride5MinuteLimitOnProfileWrites) {} -void C_4JProfile::ForceQueuedProfileWrites(int iPad) {} +void C_4JProfile::WriteToProfile(int iQuadrant, bool bGameDefinedDataChanged, bool bOverride5MinuteLimitOnProfileWrites) +{ + if (s_profileDataBytesPerPad == 0) + return; + + if (iQuadrant == XUSER_INDEX_ANY) + { + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (profileData[i] != nullptr) + Win64_SaveProfileDataBlob(i, profileData[i], s_profileDataBytesPerPad); + } + return; + } + + if (iQuadrant < 0 || iQuadrant >= XUSER_MAX_COUNT) + return; + + if (profileData[iQuadrant] != nullptr) + Win64_SaveProfileDataBlob(iQuadrant, profileData[iQuadrant], s_profileDataBytesPerPad); +} +void C_4JProfile::ForceQueuedProfileWrites(int iPad) +{ + WriteToProfile(iPad, true, true); +} void* C_4JProfile::GetGameDefinedProfileData(int iQuadrant) { // 4J Stu - Don't reset the options when we call this!! diff --git a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp index cc197ce5..ef77144d 100644 --- a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp +++ b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp @@ -2,4 +2,493 @@ #include "WindowsLeaderboardManager.h" -LeaderboardManager *LeaderboardManager::m_instance = new WindowsLeaderboardManager(); //Singleton instance of the LeaderboardManager \ No newline at end of file +#include + +#include "../../Minecraft.h" +#include "../../StatsCounter.h" + +#include "../../../Minecraft.World/StringHelpers.h" +#include "../../../Minecraft.World/Stats.h" +#include "../../../Minecraft.World/net.minecraft.world.item.h" +#include "../../../Minecraft.World/net.minecraft.world.level.tile.h" + +namespace +{ + static const DWORD kLeaderboardCacheMagic = 0x31424C57; // file identifier: WLB1 + static const DWORD kLeaderboardCacheVersion = 1; + static const unsigned int kDifficultyCount = 4; + static const unsigned int kPersistedBoardCount = + static_cast(LeaderboardManager::eStatsType_MAX) * kDifficultyCount; + + struct PersistedLeaderboardRow + { + BYTE valid; + BYTE statsSize; + WORD reserved; + DWORD totalScore; + ULONGLONG uid; + DWORD statsData[LeaderboardManager::ReadScore::STATSDATA_MAX]; + wchar_t name[XUSER_NAME_SIZE + 1]; + }; + + struct PersistedLeaderboardCache + { + DWORD magic; + DWORD version; + PersistedLeaderboardRow rows[kPersistedBoardCount]; + }; + + static int ClampDifficulty(int difficulty) + { + if (difficulty < 0) + return 0; + if (difficulty > 3) + return 3; + return difficulty; + } + + static unsigned long ClampToULong(unsigned long long value) + { + const unsigned long long maxValue = static_cast(ULONG_MAX); + if (value > maxValue) + return ULONG_MAX; + return static_cast(value); + } + + static unsigned int CombinePigmanKills(StatsCounter* stats, unsigned int difficulty) + { + if (stats == nullptr) + return 0; + + const unsigned int zombiePigman = stats->getValue(Stats::killsZombiePigman, difficulty); + const unsigned int netherPigman = stats->getValue(Stats::killsNetherZombiePigman, difficulty); + return zombiePigman + netherPigman; + } + + static void BuildLeaderboardCachePath(char* outPath, size_t outPathSize) + { + if (outPath == nullptr || outPathSize == 0) + return; + + outPath[0] = '\0'; + GetModuleFileNameA(nullptr, outPath, static_cast(outPathSize)); + + char* lastSlash = strrchr(outPath, '\\'); + char* lastForwardSlash = strrchr(outPath, '/'); + if (lastForwardSlash != nullptr && (lastSlash == nullptr || lastForwardSlash > lastSlash)) + lastSlash = lastForwardSlash; + + if (lastSlash != nullptr) + *(lastSlash + 1) = '\0'; + + strncat_s(outPath, outPathSize, "leaderboards-cache.dat", _TRUNCATE); + } + + static void InitLeaderboardCache(PersistedLeaderboardCache& cache) + { + ZeroMemory(&cache, sizeof(cache)); + cache.magic = kLeaderboardCacheMagic; + cache.version = kLeaderboardCacheVersion; + } + + static bool LoadLeaderboardCache(PersistedLeaderboardCache& outCache) + { + InitLeaderboardCache(outCache); + + char filePath[MAX_PATH] = {}; + BuildLeaderboardCachePath(filePath, MAX_PATH); + + FILE* f = nullptr; + if (fopen_s(&f, filePath, "rb") != 0 || f == nullptr) + return false; + + PersistedLeaderboardCache loaded = {}; + const size_t read = fread(&loaded, 1, sizeof(loaded), f); + fclose(f); + + if (read != sizeof(loaded)) + return false; + + if (loaded.magic != kLeaderboardCacheMagic || loaded.version != kLeaderboardCacheVersion) + return false; + + outCache = loaded; + return true; + } + + static void SaveLeaderboardCache(const PersistedLeaderboardCache& cache) + { + char filePath[MAX_PATH] = {}; + BuildLeaderboardCachePath(filePath, MAX_PATH); + + FILE* f = nullptr; + if (fopen_s(&f, filePath, "wb") != 0 || f == nullptr) + return; + + fwrite(&cache, 1, sizeof(cache), f); + fclose(f); + } + + static int GetPersistedRowIndex(LeaderboardManager::EStatsType type, unsigned int difficulty) + { + if (type < 0 || type >= LeaderboardManager::eStatsType_MAX) + return -1; + + unsigned int clampedDifficulty = (difficulty > 3) ? 3 : difficulty; + if (type == LeaderboardManager::eStatsType_Kills && clampedDifficulty == 0) + clampedDifficulty = 1; + + return static_cast(type) * static_cast(kDifficultyCount) + static_cast(clampedDifficulty); + } + + static void RecomputeTotalScore(LeaderboardManager::ReadScore& score) + { + unsigned long long total = 0; + const unsigned int columnCount = std::min( + static_cast(score.m_statsSize), + LeaderboardManager::ReadScore::STATSDATA_MAX); + + for (unsigned int i = 0; i < columnCount; ++i) + total += score.m_statsData[i]; + + score.m_totalScore = ClampToULong(total); + } + + static bool ScoreHasAnyData(const LeaderboardManager::ReadScore& score) + { + if (score.m_totalScore > 0) + return true; + + const unsigned int columnCount = std::min( + static_cast(score.m_statsSize), + LeaderboardManager::ReadScore::STATSDATA_MAX); + + for (unsigned int i = 0; i < columnCount; ++i) + { + if (score.m_statsData[i] > 0) + return true; + } + + return false; + } + + static void ApplyPersistedRow(const PersistedLeaderboardRow& row, LeaderboardManager::ReadScore& outScore) + { + if (row.valid == 0) + return; + + ZeroMemory(&outScore.m_statsData, sizeof(outScore.m_statsData)); + + outScore.m_uid = static_cast(row.uid); + outScore.m_rank = 1; + outScore.m_idsErrorMessage = 0; + outScore.m_statsSize = static_cast(std::min( + static_cast(row.statsSize), + LeaderboardManager::ReadScore::STATSDATA_MAX)); + outScore.m_totalScore = row.totalScore; + outScore.m_name = row.name; + + for (unsigned int i = 0; i < outScore.m_statsSize; ++i) + outScore.m_statsData[i] = row.statsData[i]; + } + + static void PersistRow(PersistedLeaderboardRow& row, const LeaderboardManager::ReadScore& score) + { + row.valid = 1; + row.statsSize = static_cast(std::min( + static_cast(score.m_statsSize), + LeaderboardManager::ReadScore::STATSDATA_MAX)); + row.reserved = 0; + row.totalScore = score.m_totalScore; + row.uid = static_cast(score.m_uid); + + ZeroMemory(row.statsData, sizeof(row.statsData)); + for (unsigned int i = 0; i < row.statsSize; ++i) + row.statsData[i] = score.m_statsData[i]; + + ZeroMemory(row.name, sizeof(row.name)); + if (!score.m_name.empty()) + wcsncpy_s(row.name, score.m_name.c_str(), _TRUNCATE); + } + + static bool BuildReadScoreFromRegisterScore(const LeaderboardManager::RegisterScore& source, + LeaderboardManager::ReadScore& outScore) + { + ZeroMemory(&outScore, sizeof(outScore)); + + PlayerUID uid = INVALID_XUID; + ProfileManager.GetXUID(source.m_iPad, &uid, true); + outScore.m_uid = uid; + outScore.m_rank = 1; + outScore.m_idsErrorMessage = 0; + outScore.m_name = convStringToWstring(ProfileManager.GetGamertag(source.m_iPad)); + + switch (source.m_commentData.m_statsType) + { + case LeaderboardManager::eStatsType_Travelling: + outScore.m_statsSize = 4; + outScore.m_statsData[0] = source.m_commentData.m_travelling.m_walked; + outScore.m_statsData[1] = source.m_commentData.m_travelling.m_fallen; + outScore.m_statsData[2] = source.m_commentData.m_travelling.m_minecart; + outScore.m_statsData[3] = source.m_commentData.m_travelling.m_boat; + break; + + case LeaderboardManager::eStatsType_Mining: + outScore.m_statsSize = 7; + outScore.m_statsData[0] = source.m_commentData.m_mining.m_dirt; + outScore.m_statsData[1] = source.m_commentData.m_mining.m_cobblestone; + outScore.m_statsData[2] = source.m_commentData.m_mining.m_sand; + outScore.m_statsData[3] = source.m_commentData.m_mining.m_stone; + outScore.m_statsData[4] = source.m_commentData.m_mining.m_gravel; + outScore.m_statsData[5] = source.m_commentData.m_mining.m_clay; + outScore.m_statsData[6] = source.m_commentData.m_mining.m_obsidian; + break; + + case LeaderboardManager::eStatsType_Farming: + outScore.m_statsSize = 6; + outScore.m_statsData[0] = source.m_commentData.m_farming.m_eggs; + outScore.m_statsData[1] = source.m_commentData.m_farming.m_wheat; + outScore.m_statsData[2] = source.m_commentData.m_farming.m_mushroom; + outScore.m_statsData[3] = source.m_commentData.m_farming.m_sugarcane; + outScore.m_statsData[4] = source.m_commentData.m_farming.m_milk; + outScore.m_statsData[5] = source.m_commentData.m_farming.m_pumpkin; + break; + + case LeaderboardManager::eStatsType_Kills: + outScore.m_statsSize = 7; + outScore.m_statsData[0] = source.m_commentData.m_kills.m_zombie; + outScore.m_statsData[1] = source.m_commentData.m_kills.m_skeleton; + outScore.m_statsData[2] = source.m_commentData.m_kills.m_creeper; + outScore.m_statsData[3] = source.m_commentData.m_kills.m_spider; + outScore.m_statsData[4] = source.m_commentData.m_kills.m_spiderJockey; + outScore.m_statsData[5] = source.m_commentData.m_kills.m_zombiePigman; + outScore.m_statsData[6] = source.m_commentData.m_kills.m_slime; + break; + + default: + return false; + } + + RecomputeTotalScore(outScore); + if (source.m_score > 0) + outScore.m_totalScore = std::max(outScore.m_totalScore, static_cast(source.m_score)); + + return true; + } +} + +LeaderboardManager *LeaderboardManager::m_instance = new WindowsLeaderboardManager(); // Singleton instance of the LeaderboardManager + +bool WindowsLeaderboardManager::WriteStats(unsigned int viewCount, ViewIn views) +{ + PersistedLeaderboardCache cache = {}; + LoadLeaderboardCache(cache); + + if (views != nullptr) + { + for (unsigned int i = 0; i < viewCount; ++i) + { + ReadScore score = {}; + if (!BuildReadScoreFromRegisterScore(views[i], score)) + continue; + + unsigned int difficulty = static_cast(ClampDifficulty(views[i].m_difficulty)); + if (views[i].m_commentData.m_statsType == eStatsType_Kills && difficulty == 0) + difficulty = 1; + + const int index = GetPersistedRowIndex(views[i].m_commentData.m_statsType, difficulty); + if (index < 0 || index >= static_cast(kPersistedBoardCount)) + continue; + + PersistRow(cache.rows[index], score); + } + + delete[] views; + } + + SaveLeaderboardCache(cache); + return true; +} + +bool WindowsLeaderboardManager::ReadStats_Friends(LeaderboardReadListener* callback, int difficulty, + EStatsType type, PlayerUID myUID, unsigned int startIndex, unsigned int readCount) +{ + if (!LeaderboardManager::ReadStats_Friends(callback, difficulty, type, myUID, startIndex, readCount)) + return false; + + return ReadLocalStats(callback, difficulty, type, myUID); +} + +bool WindowsLeaderboardManager::ReadStats_MyScore(LeaderboardReadListener* callback, int difficulty, + EStatsType type, PlayerUID myUID, unsigned int readCount) +{ + if (!LeaderboardManager::ReadStats_MyScore(callback, difficulty, type, myUID, readCount)) + return false; + + return ReadLocalStats(callback, difficulty, type, myUID); +} + +bool WindowsLeaderboardManager::ReadStats_TopRank(LeaderboardReadListener* callback, int difficulty, + EStatsType type, unsigned int startIndex, unsigned int readCount) +{ + if (!LeaderboardManager::ReadStats_TopRank(callback, difficulty, type, startIndex, readCount)) + return false; + + PlayerUID uid = INVALID_XUID; + ProfileManager.GetXUID(ProfileManager.GetPrimaryPad(), &uid, true); + return ReadLocalStats(callback, difficulty, type, uid); +} + +bool WindowsLeaderboardManager::ReadLocalStats(LeaderboardReadListener* callback, int difficulty, + EStatsType type, PlayerUID uid) +{ + if (callback == nullptr) + return false; + + ReadView view = {}; + ReadScore row = {}; + + if (BuildLocalReadScore(row, difficulty, type, uid)) + { + view.m_numQueries = 1; + view.m_queries = &row; + + callback->OnStatsReadComplete(eStatsReturn_Success, 1, view); + } + else + { + view.m_numQueries = 0; + view.m_queries = nullptr; + + callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, view); + } + + return true; +} + +bool WindowsLeaderboardManager::BuildLocalReadScore(ReadScore& outScore, int difficulty, + EStatsType type, PlayerUID uid) +{ + const int primaryPad = ProfileManager.GetPrimaryPad(); + if (primaryPad < 0 || primaryPad >= XUSER_MAX_COUNT) + return false; + + Minecraft* minecraft = Minecraft::GetInstance(); + if (minecraft == nullptr) + return false; + + StatsCounter* stats = minecraft->stats[primaryPad]; + if (stats == nullptr) + return false; + + ZeroMemory(&outScore, sizeof(outScore)); + + outScore.m_uid = uid; + outScore.m_rank = 1; + outScore.m_idsErrorMessage = 0; + + char* gamertag = ProfileManager.GetGamertag(primaryPad); + if (gamertag != nullptr && gamertag[0] != 0) + outScore.m_name = convStringToWstring(gamertag); + else + outScore.m_name = L"Player"; + + unsigned int diff = static_cast(ClampDifficulty(difficulty)); + if (type == eStatsType_Kills && diff == 0) + diff = 1; + unsigned long long totalScore = 0; + + auto setColumn = [&](unsigned int index, unsigned int value) + { + if (index >= ReadScore::STATSDATA_MAX) + return; + + outScore.m_statsData[index] = value; + totalScore += value; + }; + + switch (type) + { + case eStatsType_Travelling: + outScore.m_statsSize = 4; + setColumn(0, stats->getValue(Stats::walkOneM, diff)); + setColumn(1, stats->getValue(Stats::fallOneM, diff)); + setColumn(2, stats->getValue(Stats::minecartOneM, diff)); + setColumn(3, stats->getValue(Stats::boatOneM, diff)); + break; + + case eStatsType_Mining: + outScore.m_statsSize = 7; + setColumn(0, stats->getValue(Stats::blocksMined[Tile::dirt_Id], diff)); + setColumn(1, stats->getValue(Stats::blocksMined[Tile::cobblestone_Id], diff)); + setColumn(2, stats->getValue(Stats::blocksMined[Tile::sand_Id], diff)); + setColumn(3, stats->getValue(Stats::blocksMined[Tile::stone_Id], diff)); + setColumn(4, stats->getValue(Stats::blocksMined[Tile::gravel_Id], diff)); + setColumn(5, stats->getValue(Stats::blocksMined[Tile::clay_Id], diff)); + setColumn(6, stats->getValue(Stats::blocksMined[Tile::obsidian_Id], diff)); + break; + + case eStatsType_Farming: + outScore.m_statsSize = 6; + setColumn(0, stats->getValue(Stats::itemsCollected[Item::egg_Id], diff)); + setColumn(1, stats->getValue(Stats::blocksMined[Tile::wheat_Id], diff)); + setColumn(2, stats->getValue(Stats::blocksMined[Tile::mushroom_brown_Id], diff)); + setColumn(3, stats->getValue(Stats::blocksMined[Tile::reeds_Id], diff)); + setColumn(4, stats->getValue(Stats::cowsMilked, diff)); + setColumn(5, stats->getValue(Stats::itemsCollected[Tile::pumpkin_Id], diff)); + break; + + case eStatsType_Kills: + outScore.m_statsSize = 7; + setColumn(0, stats->getValue(Stats::killsZombie, diff)); + setColumn(1, stats->getValue(Stats::killsSkeleton, diff)); + setColumn(2, stats->getValue(Stats::killsCreeper, diff)); + setColumn(3, stats->getValue(Stats::killsSpider, diff)); + setColumn(4, stats->getValue(Stats::killsSpiderJockey, diff)); + setColumn(5, CombinePigmanKills(stats, diff)); + setColumn(6, stats->getValue(Stats::killsSlime, diff)); + break; + + default: + return false; + } + + outScore.m_totalScore = ClampToULong(totalScore); + + PersistedLeaderboardCache cache = {}; + LoadLeaderboardCache(cache); + + const int persistedIndex = GetPersistedRowIndex(type, diff); + if (persistedIndex >= 0 && persistedIndex < static_cast(kPersistedBoardCount)) + { + PersistedLeaderboardRow& persistedRow = cache.rows[persistedIndex]; + const bool currentHasData = ScoreHasAnyData(outScore); + + if (persistedRow.valid != 0) + { + if (!currentHasData) + { + ApplyPersistedRow(persistedRow, outScore); + } + else if (persistedRow.statsSize == outScore.m_statsSize) + { + const unsigned int columnCount = std::min( + static_cast(outScore.m_statsSize), + ReadScore::STATSDATA_MAX); + + for (unsigned int i = 0; i < columnCount; ++i) + outScore.m_statsData[i] = (std::max)(outScore.m_statsData[i], persistedRow.statsData[i]); + + RecomputeTotalScore(outScore); + + if (outScore.m_name.empty() && persistedRow.name[0] != 0) + outScore.m_name = persistedRow.name; + } + } + + PersistRow(persistedRow, outScore); + SaveLeaderboardCache(cache); + } + + return true; +} diff --git a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.h b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.h index a66bc9cc..ec59653b 100644 --- a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.h +++ b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.h @@ -5,32 +5,38 @@ class WindowsLeaderboardManager : public LeaderboardManager { public: - virtual void Tick() {} + virtual void Tick() override {} //Open a session - virtual bool OpenSession() { return true; } + virtual bool OpenSession() override { return true; } //Close a session - virtual void CloseSession() {} + virtual void CloseSession() override {} //Delete a session - virtual void DeleteSession() {} + virtual void DeleteSession() override {} //Write the given stats //This is called synchronously and will not free any memory allocated for views when it is done + virtual bool WriteStats(unsigned int viewCount, ViewIn views) override; - virtual bool WriteStats(unsigned int viewCount, ViewIn views) { return false; } - - virtual bool ReadStats_Friends(LeaderboardReadListener *callback, int difficulty, EStatsType type, PlayerUID myUID) { return false; } - virtual bool ReadStats_MyScore(LeaderboardReadListener *callback, int difficulty, EStatsType type, PlayerUID myUID, unsigned int readCount) { return false; } - virtual bool ReadStats_TopRank(LeaderboardReadListener *callback, int difficulty, EStatsType type, unsigned int startIndex, unsigned int readCount) { return false; } + virtual bool ReadStats_Friends(LeaderboardReadListener* callback, int difficulty, EStatsType type, + PlayerUID myUID, unsigned int startIndex, unsigned int readCount) override; + virtual bool ReadStats_MyScore(LeaderboardReadListener* callback, int difficulty, EStatsType type, + PlayerUID myUID, unsigned int readCount) override; + virtual bool ReadStats_TopRank(LeaderboardReadListener* callback, int difficulty, EStatsType type, + unsigned int startIndex, unsigned int readCount) override; //Perform a flush of the stats - virtual void FlushStats() {} + virtual void FlushStats() override {} //Cancel the current operation - virtual void CancelOperation() {} + virtual void CancelOperation() override {} //Is the leaderboard manager idle. - virtual bool isIdle() { return true; } + virtual bool isIdle() override { return true; } + +private: + bool ReadLocalStats(LeaderboardReadListener* callback, int difficulty, EStatsType type, PlayerUID uid); + bool BuildLocalReadScore(ReadScore& outScore, int difficulty, EStatsType type, PlayerUID uid); }; From ddcb092552f74b30900b9ff22b6bfff57e838cf3 Mon Sep 17 00:00:00 2001 From: Fireblade <72758695+Firebladedoge229@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:12:38 -0400 Subject: [PATCH 2/4] fix. multiple players on leaderboard --- .../WindowsLeaderboardManager.cpp | 375 +++++++++++++++--- 1 file changed, 322 insertions(+), 53 deletions(-) diff --git a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp index ef77144d..8f3d8047 100644 --- a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp +++ b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp @@ -3,6 +3,7 @@ #include "WindowsLeaderboardManager.h" #include +#include #include "../../Minecraft.h" #include "../../StatsCounter.h" @@ -15,10 +16,11 @@ namespace { static const DWORD kLeaderboardCacheMagic = 0x31424C57; // file identifier: WLB1 - static const DWORD kLeaderboardCacheVersion = 1; + static const DWORD kLeaderboardCacheVersion = 2; static const unsigned int kDifficultyCount = 4; static const unsigned int kPersistedBoardCount = static_cast(LeaderboardManager::eStatsType_MAX) * kDifficultyCount; + static const unsigned int kMaxRowsPerBoard = 64; struct PersistedLeaderboardRow { @@ -31,11 +33,17 @@ namespace wchar_t name[XUSER_NAME_SIZE + 1]; }; + struct PersistedLeaderboardBoard + { + DWORD rowCount; + PersistedLeaderboardRow rows[kMaxRowsPerBoard]; + }; + struct PersistedLeaderboardCache { DWORD magic; DWORD version; - PersistedLeaderboardRow rows[kPersistedBoardCount]; + PersistedLeaderboardBoard boards[kPersistedBoardCount]; }; static int ClampDifficulty(int difficulty) @@ -102,18 +110,43 @@ namespace if (fopen_s(&f, filePath, "rb") != 0 || f == nullptr) return false; - PersistedLeaderboardCache loaded = {}; - const size_t read = fread(&loaded, 1, sizeof(loaded), f); + if (fseek(f, 0, SEEK_END) != 0) + { + fclose(f); + return false; + } + + const long fileSize = ftell(f); + if (fileSize <= 0 || fseek(f, 0, SEEK_SET) != 0) + { + fclose(f); + return false; + } + + if (static_cast(fileSize) == sizeof(PersistedLeaderboardCache)) + { + PersistedLeaderboardCache loaded = {}; + const size_t read = fread(&loaded, 1, sizeof(loaded), f); + fclose(f); + + if (read != sizeof(loaded)) + return false; + + if (loaded.magic != kLeaderboardCacheMagic || loaded.version != kLeaderboardCacheVersion) + return false; + + outCache = loaded; + for (unsigned int boardIndex = 0; boardIndex < kPersistedBoardCount; ++boardIndex) + { + if (outCache.boards[boardIndex].rowCount > kMaxRowsPerBoard) + outCache.boards[boardIndex].rowCount = kMaxRowsPerBoard; + } + + return true; + } + fclose(f); - - if (read != sizeof(loaded)) - return false; - - if (loaded.magic != kLeaderboardCacheMagic || loaded.version != kLeaderboardCacheVersion) - return false; - - outCache = loaded; - return true; + return false; } static void SaveLeaderboardCache(const PersistedLeaderboardCache& cache) @@ -129,7 +162,7 @@ namespace fclose(f); } - static int GetPersistedRowIndex(LeaderboardManager::EStatsType type, unsigned int difficulty) + static int GetPersistedBoardIndex(LeaderboardManager::EStatsType type, unsigned int difficulty) { if (type < 0 || type >= LeaderboardManager::eStatsType_MAX) return -1; @@ -141,6 +174,11 @@ namespace return static_cast(type) * static_cast(kDifficultyCount) + static_cast(clampedDifficulty); } + static unsigned int GetBoardRowCount(const PersistedLeaderboardBoard& board) + { + return (board.rowCount > kMaxRowsPerBoard) ? kMaxRowsPerBoard : board.rowCount; + } + static void RecomputeTotalScore(LeaderboardManager::ReadScore& score) { unsigned long long total = 0; @@ -211,6 +249,154 @@ namespace wcsncpy_s(row.name, score.m_name.c_str(), _TRUNCATE); } + static int FindMatchingRowIndex(const PersistedLeaderboardBoard& board, const LeaderboardManager::ReadScore& score) + { + const unsigned int rowCount = GetBoardRowCount(board); + + if (!score.m_name.empty()) + { + for (unsigned int rowIndex = 0; rowIndex < rowCount; ++rowIndex) + { + const PersistedLeaderboardRow& row = board.rows[rowIndex]; + if (row.valid == 0 || row.name[0] == 0) + continue; + + if (_wcsicmp(row.name, score.m_name.c_str()) == 0) + return static_cast(rowIndex); + } + + return -1; + } + + if (score.m_uid != INVALID_XUID) + { + for (unsigned int rowIndex = 0; rowIndex < rowCount; ++rowIndex) + { + const PersistedLeaderboardRow& row = board.rows[rowIndex]; + if (row.valid == 0) + continue; + + if (static_cast(row.uid) == score.m_uid) + return static_cast(rowIndex); + } + } + + return -1; + } + + static int FindWritableRowIndex(PersistedLeaderboardBoard& board, const LeaderboardManager::ReadScore& score) + { + if (board.rowCount > kMaxRowsPerBoard) + board.rowCount = kMaxRowsPerBoard; + + const int matchingRowIndex = FindMatchingRowIndex(board, score); + if (matchingRowIndex >= 0) + return matchingRowIndex; + + const unsigned int rowCount = GetBoardRowCount(board); + for (unsigned int rowIndex = 0; rowIndex < rowCount; ++rowIndex) + { + if (board.rows[rowIndex].valid == 0) + return static_cast(rowIndex); + } + + if (rowCount < kMaxRowsPerBoard) + { + board.rowCount = rowCount + 1; + return static_cast(rowCount); + } + + unsigned int replaceIndex = 0; + DWORD lowestScore = ULONG_MAX; + for (unsigned int rowIndex = 0; rowIndex < kMaxRowsPerBoard; ++rowIndex) + { + if (board.rows[rowIndex].valid == 0) + return static_cast(rowIndex); + + if (board.rows[rowIndex].totalScore <= lowestScore) + { + lowestScore = board.rows[rowIndex].totalScore; + replaceIndex = rowIndex; + } + } + + return static_cast(replaceIndex); + } + + static void MergeScoreWithPersistedRow(const PersistedLeaderboardRow& persistedRow, LeaderboardManager::ReadScore& inOutScore) + { + if (persistedRow.valid == 0) + return; + + const bool currentHasData = ScoreHasAnyData(inOutScore); + if (!currentHasData) + { + ApplyPersistedRow(persistedRow, inOutScore); + return; + } + + if (persistedRow.statsSize == inOutScore.m_statsSize) + { + const unsigned int columnCount = std::min( + static_cast(inOutScore.m_statsSize), + LeaderboardManager::ReadScore::STATSDATA_MAX); + + for (unsigned int i = 0; i < columnCount; ++i) + inOutScore.m_statsData[i] = (std::max)(inOutScore.m_statsData[i], persistedRow.statsData[i]); + + RecomputeTotalScore(inOutScore); + } + + if (inOutScore.m_name.empty() && persistedRow.name[0] != 0) + inOutScore.m_name = persistedRow.name; + } + + static bool RowMatchesPlayer(const LeaderboardManager::ReadScore& row, const LeaderboardManager::ReadScore& player) + { + if (!player.m_name.empty() && !row.m_name.empty()) + return _wcsicmp(row.m_name.c_str(), player.m_name.c_str()) == 0; + + if (player.m_uid != INVALID_XUID) + return row.m_uid == player.m_uid; + + return false; + } + + static bool CompareRowsForRank(const LeaderboardManager::ReadScore& left, const LeaderboardManager::ReadScore& right) + { + if (left.m_totalScore != right.m_totalScore) + return left.m_totalScore > right.m_totalScore; + + const wchar_t* leftName = left.m_name.empty() ? L"" : left.m_name.c_str(); + const wchar_t* rightName = right.m_name.empty() ? L"" : right.m_name.c_str(); + const int nameCompare = _wcsicmp(leftName, rightName); + if (nameCompare != 0) + return nameCompare < 0; + + return left.m_uid < right.m_uid; + } + + static void CollectScoresFromBoard(const PersistedLeaderboardBoard& board, + std::vector& outScores) + { + const unsigned int rowCount = GetBoardRowCount(board); + outScores.reserve(outScores.size() + rowCount); + + for (unsigned int rowIndex = 0; rowIndex < rowCount; ++rowIndex) + { + const PersistedLeaderboardRow& row = board.rows[rowIndex]; + if (row.valid == 0) + continue; + + LeaderboardManager::ReadScore score = {}; + ApplyPersistedRow(row, score); + if (score.m_statsSize == 0) + continue; + + outScores.push_back(score); + } + } + static bool BuildReadScoreFromRegisterScore(const LeaderboardManager::RegisterScore& source, LeaderboardManager::ReadScore& outScore) { @@ -221,7 +407,12 @@ namespace outScore.m_uid = uid; outScore.m_rank = 1; outScore.m_idsErrorMessage = 0; - outScore.m_name = convStringToWstring(ProfileManager.GetGamertag(source.m_iPad)); + + char* gamertag = ProfileManager.GetGamertag(source.m_iPad); + if (gamertag != nullptr && gamertag[0] != 0) + outScore.m_name = convStringToWstring(gamertag); + else + outScore.m_name = L"Player"; switch (source.m_commentData.m_statsType) { @@ -296,11 +487,16 @@ bool WindowsLeaderboardManager::WriteStats(unsigned int viewCount, ViewIn views) if (views[i].m_commentData.m_statsType == eStatsType_Kills && difficulty == 0) difficulty = 1; - const int index = GetPersistedRowIndex(views[i].m_commentData.m_statsType, difficulty); - if (index < 0 || index >= static_cast(kPersistedBoardCount)) + const int boardIndex = GetPersistedBoardIndex(views[i].m_commentData.m_statsType, difficulty); + if (boardIndex < 0 || boardIndex >= static_cast(kPersistedBoardCount)) continue; - PersistRow(cache.rows[index], score); + PersistedLeaderboardBoard& board = cache.boards[boardIndex]; + const int rowIndex = FindWritableRowIndex(board, score); + if (rowIndex < 0 || rowIndex >= static_cast(kMaxRowsPerBoard)) + continue; + + PersistRow(board.rows[rowIndex], score); } delete[] views; @@ -346,23 +542,112 @@ bool WindowsLeaderboardManager::ReadLocalStats(LeaderboardReadListener* callback return false; ReadView view = {}; - ReadScore row = {}; + ReadScore localScore = {}; - if (BuildLocalReadScore(row, difficulty, type, uid)) - { - view.m_numQueries = 1; - view.m_queries = &row; - - callback->OnStatsReadComplete(eStatsReturn_Success, 1, view); - } - else + if (!BuildLocalReadScore(localScore, difficulty, type, uid)) { view.m_numQueries = 0; view.m_queries = nullptr; - callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, view); + return true; } + unsigned int diff = static_cast(ClampDifficulty(difficulty)); + if (type == eStatsType_Kills && diff == 0) + diff = 1; + + std::vector allRows; + PersistedLeaderboardCache cache = {}; + LoadLeaderboardCache(cache); + + const int boardIndex = GetPersistedBoardIndex(type, diff); + if (boardIndex >= 0 && boardIndex < static_cast(kPersistedBoardCount)) + CollectScoresFromBoard(cache.boards[boardIndex], allRows); + + bool hasLocalRow = false; + for (const ReadScore& row : allRows) + { + if (RowMatchesPlayer(row, localScore)) + { + hasLocalRow = true; + break; + } + } + + if (!hasLocalRow) + allRows.push_back(localScore); + + if (allRows.empty()) + { + view.m_numQueries = 0; + view.m_queries = nullptr; + callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, view); + return true; + } + + std::sort(allRows.begin(), allRows.end(), CompareRowsForRank); + for (size_t i = 0; i < allRows.size(); ++i) + allRows[i].m_rank = static_cast(i + 1); + + size_t pageStart = 0; + size_t pageCount = allRows.size(); + + if (m_eFilterMode == eFM_TopRank) + { + if (m_startIndex > 0) + pageStart = std::min(static_cast(m_startIndex - 1), allRows.size()); + + pageCount = allRows.size() - pageStart; + if (m_readCount > 0) + pageCount = std::min(pageCount, static_cast(m_readCount)); + } + else if (m_eFilterMode == eFM_MyScore && m_readCount > 0 && allRows.size() > m_readCount) + { + size_t playerIndex = 0; + for (size_t i = 0; i < allRows.size(); ++i) + { + if (RowMatchesPlayer(allRows[i], localScore)) + { + playerIndex = i; + break; + } + } + + const size_t windowSize = static_cast(m_readCount); + const size_t halfWindow = windowSize / 2; + pageStart = (playerIndex > halfWindow) ? (playerIndex - halfWindow) : 0; + + const size_t maxStart = allRows.size() - windowSize; + if (pageStart > maxStart) + pageStart = maxStart; + + pageCount = windowSize; + } + + std::vector pageRows; + if (pageStart < allRows.size() && pageCount > 0) + { + pageRows.insert( + pageRows.end(), + allRows.begin() + static_cast(pageStart), + allRows.begin() + static_cast(pageStart + pageCount)); + } + + if (pageRows.empty()) + { + view.m_numQueries = 0; + view.m_queries = nullptr; + callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, view); + return true; + } + + view.m_numQueries = static_cast(pageRows.size()); + view.m_queries = pageRows.data(); + callback->OnStatsReadComplete( + eStatsReturn_Success, + static_cast(allRows.size()), + view); + return true; } @@ -458,35 +743,19 @@ bool WindowsLeaderboardManager::BuildLocalReadScore(ReadScore& outScore, int dif PersistedLeaderboardCache cache = {}; LoadLeaderboardCache(cache); - const int persistedIndex = GetPersistedRowIndex(type, diff); - if (persistedIndex >= 0 && persistedIndex < static_cast(kPersistedBoardCount)) + const int boardIndex = GetPersistedBoardIndex(type, diff); + if (boardIndex >= 0 && boardIndex < static_cast(kPersistedBoardCount)) { - PersistedLeaderboardRow& persistedRow = cache.rows[persistedIndex]; - const bool currentHasData = ScoreHasAnyData(outScore); + PersistedLeaderboardBoard& board = cache.boards[boardIndex]; - if (persistedRow.valid != 0) - { - if (!currentHasData) - { - ApplyPersistedRow(persistedRow, outScore); - } - else if (persistedRow.statsSize == outScore.m_statsSize) - { - const unsigned int columnCount = std::min( - static_cast(outScore.m_statsSize), - ReadScore::STATSDATA_MAX); + const int matchingRowIndex = FindMatchingRowIndex(board, outScore); + if (matchingRowIndex >= 0 && matchingRowIndex < static_cast(kMaxRowsPerBoard)) + MergeScoreWithPersistedRow(board.rows[matchingRowIndex], outScore); - for (unsigned int i = 0; i < columnCount; ++i) - outScore.m_statsData[i] = (std::max)(outScore.m_statsData[i], persistedRow.statsData[i]); + const int writeRowIndex = FindWritableRowIndex(board, outScore); + if (writeRowIndex >= 0 && writeRowIndex < static_cast(kMaxRowsPerBoard)) + PersistRow(board.rows[writeRowIndex], outScore); - RecomputeTotalScore(outScore); - - if (outScore.m_name.empty() && persistedRow.name[0] != 0) - outScore.m_name = persistedRow.name; - } - } - - PersistRow(persistedRow, outScore); SaveLeaderboardCache(cache); } From 3c731f9cea380ac4ed699d62c30e8fb0fa13b779 Mon Sep 17 00:00:00 2001 From: Fireblade <72758695+Firebladedoge229@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:20:25 -0400 Subject: [PATCH 3/4] fix. leaderboard filtration --- .../WindowsLeaderboardManager.cpp | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp index 8f3d8047..091c05c5 100644 --- a/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp +++ b/Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp @@ -589,10 +589,25 @@ bool WindowsLeaderboardManager::ReadLocalStats(LeaderboardReadListener* callback for (size_t i = 0; i < allRows.size(); ++i) allRows[i].m_rank = static_cast(i + 1); + size_t playerIndex = 0; + for (size_t i = 0; i < allRows.size(); ++i) + { + if (RowMatchesPlayer(allRows[i], localScore)) + { + playerIndex = i; + break; + } + } + size_t pageStart = 0; size_t pageCount = allRows.size(); - if (m_eFilterMode == eFM_TopRank) + if (m_eFilterMode == eFM_MyScore) + { + pageStart = playerIndex; + pageCount = 1; + } + else if (m_eFilterMode == eFM_TopRank) { if (m_startIndex > 0) pageStart = std::min(static_cast(m_startIndex - 1), allRows.size()); @@ -601,28 +616,6 @@ bool WindowsLeaderboardManager::ReadLocalStats(LeaderboardReadListener* callback if (m_readCount > 0) pageCount = std::min(pageCount, static_cast(m_readCount)); } - else if (m_eFilterMode == eFM_MyScore && m_readCount > 0 && allRows.size() > m_readCount) - { - size_t playerIndex = 0; - for (size_t i = 0; i < allRows.size(); ++i) - { - if (RowMatchesPlayer(allRows[i], localScore)) - { - playerIndex = i; - break; - } - } - - const size_t windowSize = static_cast(m_readCount); - const size_t halfWindow = windowSize / 2; - pageStart = (playerIndex > halfWindow) ? (playerIndex - halfWindow) : 0; - - const size_t maxStart = allRows.size() - windowSize; - if (pageStart > maxStart) - pageStart = maxStart; - - pageCount = windowSize; - } std::vector pageRows; if (pageStart < allRows.size() && pageCount > 0) @@ -643,9 +636,12 @@ bool WindowsLeaderboardManager::ReadLocalStats(LeaderboardReadListener* callback view.m_numQueries = static_cast(pageRows.size()); view.m_queries = pageRows.data(); + const int totalResults = (m_eFilterMode == eFM_MyScore) + ? static_cast(pageRows.size()) + : static_cast(allRows.size()); callback->OnStatsReadComplete( eStatsReturn_Success, - static_cast(allRows.size()), + totalResults, view); return true; From a5eedcdc9c18b89a1c75abf08a27a99aa36727a1 Mon Sep 17 00:00:00 2001 From: Fireblade <72758695+Firebladedoge229@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:15:38 -0400 Subject: [PATCH 4/4] hey man i dont think xbox live is a thing anymore nor does it really apply to us --- .../Windows64Media/loc/stringsPlatformSpecific.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Minecraft.Client/Windows64Media/loc/stringsPlatformSpecific.xml b/Minecraft.Client/Windows64Media/loc/stringsPlatformSpecific.xml index d8b57da7..d690dd11 100644 --- a/Minecraft.Client/Windows64Media/loc/stringsPlatformSpecific.xml +++ b/Minecraft.Client/Windows64Media/loc/stringsPlatformSpecific.xml @@ -75,7 +75,7 @@ When in flying mode, you can hold down{*CONTROLLER_ACTION_JUMP*} to move up and{ - Invite Xbox LIVE Party + Invite Party @@ -95,11 +95,11 @@ When in flying mode, you can hold down{*CONTROLLER_ACTION_JUMP*} to move up and{ - Connection to Xbox LIVE was lost. Exiting to the main menu. + Connection to the server was lost. Exiting to the main menu. - Connection to Xbox LIVE was lost. + Connection to the server was lost. @@ -152,7 +152,7 @@ Would you like to unlock the full game? - Guest players cannot unlock the full game. Please sign in with an Xbox LIVE user ID. + Guest players cannot unlock the full game. Please sign in with a correct user ID. @@ -176,11 +176,11 @@ Would you like to unlock the full game? - Failed to join the game as one or more players are not allowed to play multiplayer games on Xbox LIVE. + Failed to join the game as one or more players are not allowed to play multiplayer games online. - Failed to create an online game as one or more players are not allowed to play multiplayer games on Xbox LIVE. Uncheck the "Online Game" box to start an offline game. + Failed to create a game as one or more players are not allowed to play multiplayer games online. Uncheck the "Online Game" box to start an offline game.