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] 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); }