#include "stdafx.h" #include "WindowsLeaderboardManager.h" #include #include #include "../Network/json.hpp" using json = nlohmann::json; #include "../../Minecraft.h" #include "../../StatsCounter.h" #include "../Windows64_Launcher.h" #include "../Windows64_Minecraft.h" #include "../Network/WinsockNetLayer.h" #include "../../../Minecraft.World/Stats.h" #include "../../../Minecraft.World/StringHelpers.h" #include "../../../Minecraft.World/net.minecraft.world.item.h" #include "../../../Minecraft.World/net.minecraft.world.level.tile.h" wchar_t g_Win64LeaderboardURL[256] = L"leaderboard.mclegacyedition.xyz"; namespace { static int ClampDifficulty(int d) { if (d < 0) return 0; if (d > 3) return 3; return d; } static unsigned long ClampToULong(unsigned long long v) { if (v > static_cast(ULONG_MAX)) return ULONG_MAX; return static_cast(v); } static unsigned int CombinePigmanKills(StatsCounter* stats, unsigned int difficulty) { if (!stats) return 0; return stats->getValue(Stats::killsZombiePigman, difficulty) + stats->getValue(Stats::killsNetherZombiePigman, difficulty); } static PlayerUID HexToUid(const std::string& hex) { if (hex.empty()) return INVALID_XUID; unsigned long long raw = 0; sscanf_s(hex.c_str(), "%llX", &raw); return static_cast(raw); } static void ParseServerEntries(const std::string& jsonText, std::vector& outScores) { if (jsonText.empty()) return; try { json arr = json::parse(jsonText); if (!arr.is_array()) return; for (auto& entry : arr) { LeaderboardManager::ReadScore score = {}; score.m_rank = 1; score.m_idsErrorMessage = 0; if (entry.contains("uuid") && entry["uuid"].is_string()) score.m_uid = HexToUid(entry["uuid"].get()); if (entry.contains("username") && entry["username"].is_string()) score.m_name = convStringToWstring(entry["username"].get()); if (entry.contains("totalScore") && entry["totalScore"].is_number()) score.m_totalScore = ClampToULong( static_cast(entry["totalScore"].get())); if (entry.contains("stats") && entry["stats"].is_array()) { auto& sa = entry["stats"]; unsigned int count = static_cast( (std::min)(sa.size(), LeaderboardManager::ReadScore::STATSDATA_MAX)); score.m_statsSize = static_cast(count); for (unsigned int i = 0; i < count; ++i) if (sa[i].is_number()) score.m_statsData[i] = static_cast(sa[i].get()); } outScores.push_back(score); } } catch (...) {} } static void AssignRanks(std::vector& scores) { for (size_t i = 0; i < scores.size(); ++i) scores[i].m_rank = static_cast(i + 1); } static bool BuildLocalScore(LeaderboardManager::ReadScore& out, StatsCounter* stats, LeaderboardManager::EStatsType type, unsigned int diff) { ZeroMemory(&out, sizeof(out)); out.m_rank = 1; out.m_idsErrorMessage = 0; unsigned long long total = 0; auto col = [&](unsigned int idx, unsigned int val) { if (idx >= LeaderboardManager::ReadScore::STATSDATA_MAX) return; out.m_statsData[idx] = val; total += val; }; switch (type) { case LeaderboardManager::eStatsType_Travelling: out.m_statsSize = 4; col(0, stats->getValue(Stats::walkOneM, diff)); col(1, stats->getValue(Stats::fallOneM, diff)); col(2, stats->getValue(Stats::minecartOneM, diff)); col(3, stats->getValue(Stats::boatOneM, diff)); break; case LeaderboardManager::eStatsType_Mining: out.m_statsSize = 7; col(0, stats->getValue(Stats::blocksMined[Tile::dirt_Id], diff)); col(1, stats->getValue(Stats::blocksMined[Tile::cobblestone_Id], diff)); col(2, stats->getValue(Stats::blocksMined[Tile::sand_Id], diff)); col(3, stats->getValue(Stats::blocksMined[Tile::stone_Id], diff)); col(4, stats->getValue(Stats::blocksMined[Tile::gravel_Id], diff)); col(5, stats->getValue(Stats::blocksMined[Tile::clay_Id], diff)); col(6, stats->getValue(Stats::blocksMined[Tile::obsidian_Id], diff)); break; case LeaderboardManager::eStatsType_Farming: out.m_statsSize = 6; col(0, stats->getValue(Stats::itemsCollected[Item::egg_Id], diff)); col(1, stats->getValue(Stats::blocksMined[Tile::wheat_Id], diff)); col(2, stats->getValue(Stats::blocksMined[Tile::mushroom_brown_Id], diff)); col(3, stats->getValue(Stats::blocksMined[Tile::reeds_Id], diff)); col(4, stats->getValue(Stats::cowsMilked, diff)); col(5, stats->getValue(Stats::itemsCollected[Tile::pumpkin_Id], diff)); break; case LeaderboardManager::eStatsType_Kills: out.m_statsSize = 7; col(0, stats->getValue(Stats::killsZombie, diff)); col(1, stats->getValue(Stats::killsSkeleton, diff)); col(2, stats->getValue(Stats::killsCreeper, diff)); col(3, stats->getValue(Stats::killsSpider, diff)); col(4, stats->getValue(Stats::killsSpiderJockey, diff)); col(5, CombinePigmanKills(stats, diff)); col(6, stats->getValue(Stats::killsSlime, diff)); break; default: return false; } out.m_totalScore = ClampToULong(total); return true; } } LeaderboardManager* LeaderboardManager::m_instance = new WindowsLeaderboardManager(); bool WindowsLeaderboardManager::WriteStats(unsigned int viewCount, ViewIn views) { return false; // Since WriteStats is useless we just do this, exact same thing is done in DurangoLeaderboardManager } bool WindowsLeaderboardManager::SendStats(EStatsType type, int diff) { if (!Windows64Launcher::IsInOfflineMode || Windows64Minecraft::IsOfflineMode) { Minecraft* mc = Minecraft::GetInstance(); const int pad = ProfileManager.GetPrimaryPad(); if (mc && pad >= 0 && pad < XUSER_MAX_COUNT) { StatsCounter* stats = mc->stats[pad]; if (stats) { LeaderboardManager::ReadScore localScore = {}; if (BuildLocalScore(localScore, stats, type, diff)) { json statsArr = json::array(); for (unsigned int i = 0; i < localScore.m_statsSize; ++i) statsArr.push_back(localScore.m_statsData[i]); json submitBody; submitBody["type"] = static_cast(type); submitBody["difficulty"] = static_cast(diff); submitBody["stats"] = statsArr; submitBody["totalScore"] = static_cast(localScore.m_totalScore); sendScores(submitBody.dump()); } } } } } 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, startIndex, myUID, readCount)) return false; return ReadNetworkStats(callback, difficulty, type, eFM_Friends, startIndex, readCount); } 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 ReadNetworkStats(callback, difficulty, type, eFM_MyScore, 1, readCount); } 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; return ReadNetworkStats(callback, difficulty, type, eFM_TopRank, startIndex, readCount); } bool WindowsLeaderboardManager::ReadNetworkStats(LeaderboardReadListener* callback, int difficulty, EStatsType type, EFilterMode filterMode, unsigned int startIndex, unsigned int readCount) { if (!callback) return false; unsigned int diff = static_cast(ClampDifficulty(difficulty)); if (type == eStatsType_Kills && diff == 0) diff = 1; // Submit live score only when authenticated. if (!Windows64Launcher::IsInOfflineMode || Windows64Minecraft::IsOfflineMode) { WindowsLeaderboardManager::SendStats(type, diff); } std::vector allRows; if (filterMode == eFM_Friends) { unsigned int limit = (readCount > 0) ? (std::min)(readCount, 100u) : 64u; unsigned int offset = (startIndex > 1) ? (startIndex - 1) : 0; ParseServerEntries(fetchFriends(type, diff, offset, limit), allRows); } else if (filterMode == eFM_MyScore) { unsigned int limit = (readCount > 0) ? (std::min)(readCount, 100u) : 64u; unsigned int offset = (startIndex > 1) ? (startIndex - 1) : 0; ParseServerEntries(fetchMyscore(type, diff, offset, limit), allRows); } else { unsigned int limit = (readCount > 0) ? (std::min)(readCount, 100u) : 64u; unsigned int offset = (startIndex > 1) ? (startIndex - 1) : 0; ParseServerEntries(fetchOverall(type, diff, offset, limit), allRows); } std::sort(allRows.begin(), allRows.end(), [](const LeaderboardManager::ReadScore& a, const LeaderboardManager::ReadScore& b) { if (a.m_totalScore != b.m_totalScore) return a.m_totalScore > b.m_totalScore; const wchar_t* na = a.m_name.empty() ? L"" : a.m_name.c_str(); const wchar_t* nb = b.m_name.empty() ? L"" : b.m_name.c_str(); int cmp = _wcsicmp(na, nb); if (cmp != 0) return cmp < 0; return _wcsicmp(na, nb) < 0; }); AssignRanks(allRows); if (allRows.empty()) { ReadView empty = {}; callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, empty); return true; } size_t pageStart = 0; size_t pageCount = allRows.size(); if (filterMode == eFM_TopRank) { if (startIndex > 1) pageStart = (std::min)(static_cast(startIndex - 1), allRows.size()); pageCount = allRows.size() - pageStart; if (readCount > 0) pageCount = (std::min)(pageCount, static_cast(readCount)); } if (pageStart >= allRows.size()) { ReadView empty = {}; callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, empty); return true; } pageCount = (std::min)(pageCount, allRows.size() - pageStart); std::vector page( allRows.begin() + static_cast(pageStart), allRows.begin() + static_cast(pageStart + pageCount)); if (page.empty()) { ReadView empty = {}; callback->OnStatsReadComplete(eStatsReturn_NoResults, 0, empty); return true; } ReadView view = {}; view.m_numQueries = static_cast(page.size()); view.m_queries = page.data(); int totalResults = (filterMode == eFM_MyScore) ? static_cast(page.size()) : static_cast(allRows.size()); callback->OnStatsReadComplete(eStatsReturn_Success, totalResults, view); return true; } int WindowsLeaderboardManager::sendScores(const std::string& _data) { std::vector headers; #if defined(MINECRAFT_SERVER_BUILD) std::string authToken = ServerAuth::GetAuthenticationToken(); #else std::string authToken; if (Windows64Minecraft::IsExternalLauncher()) { authToken = Windows64Minecraft::GetAuthenticationTicket(); } else { authToken = Windows64Launcher::GetAuthenticationToken(); } #endif headers.push_back(L"Content-Type: application/json"); headers.push_back(L"Authorization: " + std::wstring(authToken.begin(), authToken.end())); HttpResponse response = WinsockNetLayer::DoWinHttpRequest(g_Win64LeaderboardURL, L"/sendscores", L"POST", _data, headers); return response.status; } std::string WindowsLeaderboardManager::fetchOverall(int type, int difficulty, int offset, int limit) { std::vector headers; std::wstring path = L"/overall?type=" + std::to_wstring(type) + L"&difficulty=" + std::to_wstring(difficulty) + L"&offset=" + std::to_wstring(offset) + L"&limit=" + std::to_wstring(limit); HttpResponse response = WinsockNetLayer::DoWinHttpRequest(g_Win64LeaderboardURL, path, L"GET", "", headers); return response.body; } std::string WindowsLeaderboardManager::fetchFriends(int type, int difficulty, int offset, int limit) { std::vector headers; #if defined(MINECRAFT_SERVER_BUILD) std::string authToken = ServerAuth::GetAuthenticationToken(); #else std::string authToken; if (Windows64Minecraft::IsExternalLauncher()) { authToken = Windows64Minecraft::GetAuthenticationTicket(); } else { authToken = Windows64Launcher::GetAuthenticationToken(); } #endif headers.push_back(L"Authorization: " + std::wstring(authToken.begin(), authToken.end())); std::wstring path = L"/friends?type=" + std::to_wstring(type) + L"&difficulty=" + std::to_wstring(difficulty) + L"&offset=" + std::to_wstring(offset) + L"&limit=" + std::to_wstring(limit); HttpResponse response = WinsockNetLayer::DoWinHttpRequest(g_Win64LeaderboardURL, path, L"GET", "", headers); return response.body; } std::string WindowsLeaderboardManager::fetchMyscore(int type, int difficulty, int offset, int limit) { std::vector headers; #if defined(MINECRAFT_SERVER_BUILD) std::string authToken = ServerAuth::GetAuthenticationToken(); #else std::string authToken; if (Windows64Minecraft::IsExternalLauncher()) { authToken = Windows64Minecraft::GetAuthenticationTicket(); } else { authToken = Windows64Launcher::GetAuthenticationToken(); } #endif headers.push_back(L"Authorization: " + std::wstring(authToken.begin(), authToken.end())); std::wstring path = L"/myscore?type=" + std::to_wstring(type) + L"&difficulty=" + std::to_wstring(difficulty) + L"&offset=" + std::to_wstring(offset) + L"&limit=" + std::to_wstring(limit); HttpResponse response = WinsockNetLayer::DoWinHttpRequest(g_Win64LeaderboardURL, path, L"GET", "", headers); return response.body; }