diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a6c4b23..20af0c22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -312,7 +312,7 @@ add_subdirectory(dPhysics) add_subdirectory(dServer) # Create a list of common libraries shared between all binaries -set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "MariaDB::ConnCpp" "magic_enum") +set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum") # Add platform specific common libraries if(UNIX) diff --git a/dDatabase/GameDatabase/Database.h b/dDatabase/GameDatabase/Database.h index 65b04722..cd0e93e3 100644 --- a/dDatabase/GameDatabase/Database.h +++ b/dDatabase/GameDatabase/Database.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "GameDatabase.h" diff --git a/dDatabase/GameDatabase/ITables/ILeaderboard.h b/dDatabase/GameDatabase/ITables/ILeaderboard.h index 84d44eb2..fc4164bc 100644 --- a/dDatabase/GameDatabase/ITables/ILeaderboard.h +++ b/dDatabase/GameDatabase/ITables/ILeaderboard.h @@ -3,12 +3,44 @@ #include #include +#include +#include class ILeaderboard { public: + struct Entry { + uint32_t charId{}; + uint32_t lastPlayedTimestamp{}; + float primaryScore{}; + float secondaryScore{}; + uint32_t tertiaryScore{}; + uint32_t numWins{}; + uint32_t numTimesPlayed{}; + uint32_t ranking{}; + std::string name{}; + }; + + struct Score { + auto operator<=>(const Score& rhs) const = default; + + float primaryScore{ 0.0f }; + float secondaryScore{ 0.0f }; + float tertiaryScore{ 0.0f }; + }; + // Get the donation total for the given activity id. virtual std::optional GetDonationTotal(const uint32_t activityId) = 0; + + virtual std::vector GetDescendingLeaderboard(const uint32_t activityId) = 0; + virtual std::vector GetAscendingLeaderboard(const uint32_t activityId) = 0; + virtual std::vector GetNsLeaderboard(const uint32_t activityId) = 0; + virtual std::vector GetAgsLeaderboard(const uint32_t activityId) = 0; + virtual std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) = 0; + + virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0; + virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0; + virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0; }; #endif //!__ILEADERBOARD__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 4d11622d..4e7d19a4 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -114,6 +114,14 @@ class MySQLDatabase : public GameDatabase { void RemoveBehavior(const int32_t characterId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; std::optional GetProperties(const IProperty::PropertyLookup& params) override; + std::vector GetDescendingLeaderboard(const uint32_t activityId) override; + std::vector GetAscendingLeaderboard(const uint32_t activityId) override; + std::vector GetNsLeaderboard(const uint32_t activityId) override; + std::vector GetAgsLeaderboard(const uint32_t activityId) override; + void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override; + void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override; + std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override; + void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; void DeleteUgcBuild(const LWOOBJID bigId) override; private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp index 22403abb..a6734030 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp @@ -1,5 +1,9 @@ #include "MySQLDatabase.h" +#include "Game.h" +#include "Logger.h" +#include "dConfig.h" + std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityId) { auto donation_total = ExecuteSelect("SELECT SUM(primaryScore) as donation_total FROM leaderboard WHERE game_id = ?;", activityId); @@ -9,3 +13,75 @@ std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityI return donation_total->getUInt("donation_total"); } + +std::vector ProcessQuery(UniqueResultSet& rows) { + std::vector entries; + entries.reserve(rows->rowsCount()); + + while (rows->next()) { + auto& entry = entries.emplace_back(); + + entry.charId = rows->getUInt("character_id"); + entry.lastPlayedTimestamp = rows->getUInt("lp_unix"); + entry.primaryScore = rows->getFloat("primaryScore"); + entry.secondaryScore = rows->getFloat("secondaryScore"); + entry.tertiaryScore = rows->getFloat("tertiaryScore"); + entry.numWins = rows->getUInt("numWins"); + entry.numTimesPlayed = rows->getUInt("timesPlayed"); + entry.name = rows->getString("char_name"); + // entry.ranking is never set because its calculated in leaderboard in code. + } + + return entries; +} + +std::vector MySQLDatabase::GetDescendingLeaderboard(const uint32_t activityId) { + auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;", activityId); + return ProcessQuery(leaderboard); +} + +std::vector MySQLDatabase::GetAscendingLeaderboard(const uint32_t activityId) { + auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore ASC, secondaryscore ASC, tertiaryScore ASC, last_played ASC;", activityId); + return ProcessQuery(leaderboard); +} + +std::vector MySQLDatabase::GetAgsLeaderboard(const uint32_t activityId) { + auto query = Game::config->GetValue("classic_survival_scoring") != "1" ? + "SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;" : + "SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY secondaryscore DESC, primaryscore DESC, tertiaryScore DESC, last_played ASC;"; + auto leaderboard = ExecuteSelect(query, activityId); + return ProcessQuery(leaderboard); +} + +std::vector MySQLDatabase::GetNsLeaderboard(const uint32_t activityId) { + auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore ASC, tertiaryScore DESC, last_played ASC;", activityId); + return ProcessQuery(leaderboard); +} + +void MySQLDatabase::SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) { + ExecuteInsert("INSERT leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, character_id = ?, game_id = ?;", + score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId); +} + +void MySQLDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) { + ExecuteInsert("UPDATE leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;", + score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId); +} + +std::optional MySQLDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) { + std::optional toReturn = std::nullopt; + auto res = ExecuteSelect("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId); + if (res->next()) { + toReturn = ILeaderboard::Score{ + .primaryScore = res->getFloat("primaryScore"), + .secondaryScore = res->getFloat("secondaryScore"), + .tertiaryScore = res->getFloat("tertiaryScore") + }; + } + + return toReturn; +} + +void MySQLDatabase::IncrementNumWins(const uint32_t playerId, const uint32_t gameId) { + ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 2135f05a..c2a3950a 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -91,6 +91,14 @@ class TestSQLDatabase : public GameDatabase { void RemoveBehavior(const int32_t behaviorId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; std::optional GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; + std::vector GetDescendingLeaderboard(const uint32_t activityId) override { return {}; }; + std::vector GetAscendingLeaderboard(const uint32_t activityId) override { return {}; }; + std::vector GetNsLeaderboard(const uint32_t activityId) override { return {}; }; + std::vector GetAgsLeaderboard(const uint32_t activityId) override { return {}; }; + void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override {}; + void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override {}; + std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override { return {}; }; + void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override {}; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {}; }; diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index 347bd68e..f3a27919 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -1,5 +1,6 @@ #include "LeaderboardManager.h" +#include #include #include @@ -72,197 +73,191 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write0(); } -void Leaderboard::QueryToLdf(std::unique_ptr& rows) { - Clear(); - if (rows->rowsCount() == 0) return; +// Takes the resulting query from a leaderboard lookup and converts it to the LDF we need +// to send it to a client. +void QueryToLdf(Leaderboard& leaderboard, const std::vector& leaderboardEntries) { + using enum Leaderboard::Type; + leaderboard.Clear(); + if (leaderboardEntries.empty()) return; - this->entries.reserve(rows->rowsCount()); - while (rows->next()) { + for (const auto& leaderboardEntry : leaderboardEntries) { constexpr int32_t MAX_NUM_DATA_PER_ROW = 9; - this->entries.push_back(std::vector()); - auto& entry = this->entries.back(); + auto& entry = leaderboard.PushBackEntry(); entry.reserve(MAX_NUM_DATA_PER_ROW); - entry.push_back(new LDFData(u"CharacterID", rows->getInt("character_id"))); - entry.push_back(new LDFData(u"LastPlayed", rows->getUInt64("lastPlayed"))); - entry.push_back(new LDFData(u"NumPlayed", rows->getInt("timesPlayed"))); - entry.push_back(new LDFData(u"name", GeneralUtils::ASCIIToUTF16(rows->getString("name").c_str()))); - entry.push_back(new LDFData(u"RowNumber", rows->getInt("ranking"))); - switch (leaderboardType) { - case Type::ShootingGallery: - entry.push_back(new LDFData(u"Score", rows->getInt("primaryScore"))); + entry.push_back(new LDFData(u"CharacterID", leaderboardEntry.charId)); + entry.push_back(new LDFData(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp)); + entry.push_back(new LDFData(u"NumPlayed", leaderboardEntry.numTimesPlayed)); + entry.push_back(new LDFData(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name))); + entry.push_back(new LDFData(u"RowNumber", leaderboardEntry.ranking)); + switch (leaderboard.GetLeaderboardType()) { + case ShootingGallery: + entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore)); // Score:1 - entry.push_back(new LDFData(u"Streak", rows->getInt("secondaryScore"))); + entry.push_back(new LDFData(u"Streak", leaderboardEntry.secondaryScore)); // Streak:1 - entry.push_back(new LDFData(u"HitPercentage", (rows->getInt("tertiaryScore") / 100.0f))); + entry.push_back(new LDFData(u"HitPercentage", (leaderboardEntry.tertiaryScore / 100.0f))); // HitPercentage:3 between 0 and 1 break; - case Type::Racing: - entry.push_back(new LDFData(u"BestTime", rows->getDouble("primaryScore"))); + case Racing: + entry.push_back(new LDFData(u"BestTime", leaderboardEntry.primaryScore)); // BestLapTime:3 - entry.push_back(new LDFData(u"BestLapTime", rows->getDouble("secondaryScore"))); + entry.push_back(new LDFData(u"BestLapTime", leaderboardEntry.secondaryScore)); // BestTime:3 entry.push_back(new LDFData(u"License", 1)); // License:1 - 1 if player has completed mission 637 and 0 otherwise - entry.push_back(new LDFData(u"NumWins", rows->getInt("numWins"))); + entry.push_back(new LDFData(u"NumWins", leaderboardEntry.numWins)); // NumWins:1 break; - case Type::UnusedLeaderboard4: - entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore"))); + case UnusedLeaderboard4: + entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore)); // Points:1 break; - case Type::MonumentRace: - entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore"))); + case MonumentRace: + entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore)); // Time:1(?) break; - case Type::FootRace: - entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore"))); + case FootRace: + entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore)); // Time:1 break; - case Type::Survival: - entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore"))); + case Survival: + entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore)); // Points:1 - entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore"))); + entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore)); // Time:1 break; - case Type::SurvivalNS: - entry.push_back(new LDFData(u"Wave", rows->getInt("primaryScore"))); + case SurvivalNS: + entry.push_back(new LDFData(u"Wave", leaderboardEntry.primaryScore)); // Wave:1 - entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore"))); + entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore)); // Time:1 break; - case Type::Donations: - entry.push_back(new LDFData(u"Score", rows->getInt("primaryScore"))); + case Donations: + entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore)); // Score:1 break; - case Type::None: - // This type is included here simply to resolve a compiler warning on mac about unused enum types - break; + case None: + [[fallthrough]]; default: break; } } } -const std::string_view Leaderboard::GetOrdering(Leaderboard::Type leaderboardType) { - // Use a switch case and return desc for all 3 columns if higher is better and asc if lower is better +std::vector FilterTo10(const std::vector& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType) { + std::vector toReturn; + + int32_t index = 0; + // for friends and top, we dont need to find this players index. + if (infoType == Leaderboard::InfoType::MyStanding || infoType == Leaderboard::InfoType::Friends) { + for (; index < leaderboard.size(); index++) { + if (leaderboard[index].charId == relatedPlayer) break; + } + } + + if (leaderboard.size() < 10) { + toReturn.assign(leaderboard.begin(), leaderboard.end()); + index = 0; + } else if (index < 10) { + toReturn.assign(leaderboard.begin(), leaderboard.begin() + 10); // get the top 10 since we are in the top 10 + index = 0; + } else if (index > leaderboard.size() - 10) { + toReturn.assign(leaderboard.end() - 10, leaderboard.end()); // get the bottom 10 since we are in the bottom 10 + index = leaderboard.size() - 10; + } else { + toReturn.assign(leaderboard.begin() + index - 5, leaderboard.begin() + index + 5); // get the 5 above and below + index -= 5; + } + + int32_t i = index; + for (auto& entry : toReturn) { + entry.ranking = ++i; + } + + return toReturn; +} + +std::vector FilterWeeklies(const std::vector& leaderboard) { + // Filter the leaderboard to only include entries from the last week + const auto currentTime = std::chrono::system_clock::now(); + auto epochTime = currentTime.time_since_epoch().count(); + constexpr auto SECONDS_IN_A_WEEK = 60 * 60 * 24 * 7; // if you think im taking leap seconds into account thats cute. + + std::vector weeklyLeaderboard; + for (const auto& entry : leaderboard) { + if (epochTime - entry.lastPlayedTimestamp < SECONDS_IN_A_WEEK) { + weeklyLeaderboard.push_back(entry); + } + } + + return weeklyLeaderboard; +} + +std::vector FilterFriends(const std::vector& leaderboard, const uint32_t relatedPlayer) { + // Filter the leaderboard to only include friends of the player + auto friendOfPlayer = Database::Get()->GetFriendsList(relatedPlayer); + std::vector friendsLeaderboard; + for (const auto& entry : leaderboard) { + const auto res = std::ranges::find_if(friendOfPlayer, [&entry, relatedPlayer](const FriendData& data) { + return entry.charId == data.friendID || entry.charId == relatedPlayer; + }); + if (res != friendOfPlayer.cend()) { + friendsLeaderboard.push_back(entry); + } + } + + return friendsLeaderboard; +} + +std::vector ProcessLeaderboard( + const std::vector& leaderboard, + const bool weekly, + const Leaderboard::InfoType infoType, + const uint32_t relatedPlayer) { + std::vector toReturn; + + if (infoType == Leaderboard::InfoType::Friends) { + const auto friendsLeaderboard = FilterFriends(leaderboard, relatedPlayer); + toReturn = FilterTo10(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType); + } else { + toReturn = FilterTo10(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType); + } + + return toReturn; +} + +void Leaderboard::SetupLeaderboard(bool weekly) { + const auto leaderboardType = LeaderboardManager::GetLeaderboardType(gameID); + std::vector leaderboardRes; + switch (leaderboardType) { + case Type::SurvivalNS: + leaderboardRes = Database::Get()->GetNsLeaderboard(gameID); + break; + case Type::Survival: + leaderboardRes = Database::Get()->GetAgsLeaderboard(gameID); + break; case Type::Racing: + [[fallthrough]]; case Type::MonumentRace: - return "primaryScore ASC, secondaryScore ASC, tertiaryScore ASC"; - case Type::Survival: - return Game::config->GetValue("classic_survival_scoring") == "1" ? - "secondaryScore DESC, primaryScore DESC, tertiaryScore DESC" : - "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC"; - case Type::SurvivalNS: - return "primaryScore DESC, secondaryScore ASC, tertiaryScore DESC"; + leaderboardRes = Database::Get()->GetAscendingLeaderboard(gameID); + break; case Type::ShootingGallery: + [[fallthrough]]; case Type::FootRace: - case Type::UnusedLeaderboard4: + [[fallthrough]]; case Type::Donations: + [[fallthrough]]; case Type::None: + [[fallthrough]]; default: - return "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC"; + leaderboardRes = Database::Get()->GetDescendingLeaderboard(gameID); + break; } -} -void Leaderboard::SetupLeaderboard(bool weekly, uint32_t resultStart, uint32_t resultEnd) { - resultStart++; - resultEnd++; - // We need everything except 1 column so i'm selecting * from leaderboard - const std::string queryBase = - R"QUERY( - WITH leaderboardsRanked AS ( - SELECT leaderboard.*, charinfo.name, - RANK() OVER - ( - ORDER BY %s, UNIX_TIMESTAMP(last_played) ASC, id DESC - ) AS ranking - FROM leaderboard JOIN charinfo on charinfo.id = leaderboard.character_id - WHERE game_id = ? %s - ), - myStanding AS ( - SELECT - ranking as myRank - FROM leaderboardsRanked - WHERE id = ? - ), - lowestRanking AS ( - SELECT MAX(ranking) AS lowestRank - FROM leaderboardsRanked - ) - SELECT leaderboardsRanked.*, character_id, UNIX_TIMESTAMP(last_played) as lastPlayed, leaderboardsRanked.name, leaderboardsRanked.ranking FROM leaderboardsRanked, myStanding, lowestRanking - WHERE leaderboardsRanked.ranking - BETWEEN - LEAST(GREATEST(CAST(myRank AS SIGNED) - 5, %i), CAST(lowestRanking.lowestRank AS SIGNED) - 9) - AND - LEAST(GREATEST(myRank + 5, %i), lowestRanking.lowestRank) - ORDER BY ranking ASC; - )QUERY"; - - std::string friendsFilter = - R"QUERY( - AND ( - character_id IN ( - SELECT fr.requested_player FROM ( - SELECT CASE - WHEN player_id = ? THEN friend_id - WHEN friend_id = ? THEN player_id - END AS requested_player - FROM friends - ) AS fr - JOIN charinfo AS ci - ON ci.id = fr.requested_player - WHERE fr.requested_player IS NOT NULL - ) - OR character_id = ? - ) - )QUERY"; - - std::string weeklyFilter = " AND UNIX_TIMESTAMP(last_played) BETWEEN UNIX_TIMESTAMP(date_sub(now(),INTERVAL 1 WEEK)) AND UNIX_TIMESTAMP(now()) "; - - std::string filter; - // Setup our filter based on the query type - if (this->infoType == InfoType::Friends) filter += friendsFilter; - if (this->weekly) filter += weeklyFilter; - const auto orderBase = GetOrdering(this->leaderboardType); - - // For top query, we want to just rank all scores, but for all others we need the scores around a specific player - std::string baseLookup; - if (this->infoType == InfoType::Top) { - baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " ORDER BY "; - baseLookup += orderBase.data(); - } else { - baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " AND character_id = "; - baseLookup += std::to_string(static_cast(this->relatedPlayer)); - } - baseLookup += " LIMIT 1"; - LOG_DEBUG("query is %s", baseLookup.c_str()); - std::unique_ptr baseQuery(Database::Get()->CreatePreppedStmt(baseLookup)); - baseQuery->setInt(1, this->gameID); - std::unique_ptr baseResult(baseQuery->executeQuery()); - - if (!baseResult->next()) return; // In this case, there are no entries in the leaderboard for this game. - - uint32_t relatedPlayerLeaderboardId = baseResult->getInt("id"); - - // Create and execute the actual save here. Using a heap allocated buffer to avoid stack overflow - constexpr uint16_t STRING_LENGTH = 4096; - std::unique_ptr lookupBuffer = std::make_unique(STRING_LENGTH); - int32_t res = snprintf(lookupBuffer.get(), STRING_LENGTH, queryBase.c_str(), orderBase.data(), filter.c_str(), resultStart, resultEnd); - DluAssert(res != -1); - std::unique_ptr query(Database::Get()->CreatePreppedStmt(lookupBuffer.get())); - LOG_DEBUG("Query is %s vars are %i %i %i", lookupBuffer.get(), this->gameID, this->relatedPlayer, relatedPlayerLeaderboardId); - query->setInt(1, this->gameID); - if (this->infoType == InfoType::Friends) { - query->setInt(2, this->relatedPlayer); - query->setInt(3, this->relatedPlayer); - query->setInt(4, this->relatedPlayer); - query->setInt(5, relatedPlayerLeaderboardId); - } else { - query->setInt(2, relatedPlayerLeaderboardId); - } - std::unique_ptr result(query->executeQuery()); - QueryToLdf(result); + const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer); + + QueryToLdf(*this, processedLeaderboard); } void Leaderboard::Send(const LWOOBJID targetID) const { @@ -272,129 +267,41 @@ void Leaderboard::Send(const LWOOBJID targetID) const { } } -std::string FormatInsert(const Leaderboard::Type& type, const Score& score, const bool useUpdate) { - std::string insertStatement; - if (useUpdate) { - insertStatement = - R"QUERY( - UPDATE leaderboard - SET primaryScore = %f, secondaryScore = %f, tertiaryScore = %f, - timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?; - )QUERY"; - } else { - insertStatement = - R"QUERY( - INSERT leaderboard SET - primaryScore = %f, secondaryScore = %f, tertiaryScore = %f, - character_id = ?, game_id = ?; - )QUERY"; - } - - constexpr uint16_t STRING_LENGTH = 400; - // Then fill in our score - char finishedQuery[STRING_LENGTH]; - int32_t res = snprintf(finishedQuery, STRING_LENGTH, insertStatement.c_str(), score.GetPrimaryScore(), score.GetSecondaryScore(), score.GetTertiaryScore()); - DluAssert(res != -1); - return finishedQuery; -} - void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) { const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId); - std::unique_ptr query(Database::Get()->CreatePreppedStmt("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;")); - query->setInt(1, playerID); - query->setInt(2, activityId); - std::unique_ptr myScoreResult(query->executeQuery()); - - std::string saveQuery("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;"); - Score newScore(primaryScore, secondaryScore, tertiaryScore); - if (myScoreResult->next()) { - Score oldScore; - bool lowerScoreBetter = false; - switch (leaderboardType) { - // Higher score better - case Leaderboard::Type::ShootingGallery: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); - oldScore.SetTertiaryScore(myScoreResult->getInt("tertiaryScore")); - break; - } - case Leaderboard::Type::FootRace: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - break; - } - case Leaderboard::Type::Survival: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); - break; - } - case Leaderboard::Type::SurvivalNS: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); - break; - } - case Leaderboard::Type::UnusedLeaderboard4: - case Leaderboard::Type::Donations: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - newScore.SetPrimaryScore(oldScore.GetPrimaryScore() + newScore.GetPrimaryScore()); - break; - } - case Leaderboard::Type::Racing: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); - - // For wins we dont care about the score, just the time, so zero out the tertiary. - // Wins are updated later. - oldScore.SetTertiaryScore(0); - newScore.SetTertiaryScore(0); - lowerScoreBetter = true; - break; - } - case Leaderboard::Type::MonumentRace: { - oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); - lowerScoreBetter = true; - // Do score checking here - break; - } - case Leaderboard::Type::None: - default: - LOG("Unknown leaderboard type %i for game %i. Cannot save score!", leaderboardType, activityId); - return; - } + const auto oldScore = Database::Get()->GetPlayerScore(playerID, activityId); + + ILeaderboard::Score newScore{ .primaryScore = primaryScore, .secondaryScore = secondaryScore, .tertiaryScore = tertiaryScore }; + if (oldScore.has_value()) { + bool lowerScoreBetter = leaderboardType == Leaderboard::Type::Racing || leaderboardType == Leaderboard::Type::MonumentRace; bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore; // Nimbus station has a weird leaderboard where we need a custom scoring system if (leaderboardType == Leaderboard::Type::SurvivalNS) { - newHighScore = newScore.GetPrimaryScore() > oldScore.GetPrimaryScore() || - (newScore.GetPrimaryScore() == oldScore.GetPrimaryScore() && newScore.GetSecondaryScore() < oldScore.GetSecondaryScore()); + newHighScore = newScore.primaryScore > oldScore->primaryScore || + (newScore.primaryScore == oldScore->primaryScore && newScore.secondaryScore < oldScore->secondaryScore); } else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") { - Score oldScoreFlipped(oldScore.GetSecondaryScore(), oldScore.GetPrimaryScore()); - Score newScoreFlipped(newScore.GetSecondaryScore(), newScore.GetPrimaryScore()); + ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore}; + ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore}; newHighScore = newScoreFlipped > oldScoreFlipped; } + if (newHighScore) { - saveQuery = FormatInsert(leaderboardType, newScore, true); + Database::Get()->UpdateScore(playerID, activityId, newScore); } } else { - saveQuery = FormatInsert(leaderboardType, newScore, false); + Database::Get()->SaveScore(playerID, activityId, newScore); } - LOG("save query %s %i %i", saveQuery.c_str(), playerID, activityId); - std::unique_ptr saveStatement(Database::Get()->CreatePreppedStmt(saveQuery)); - saveStatement->setInt(1, playerID); - saveStatement->setInt(2, activityId); - saveStatement->execute(); // track wins separately if (leaderboardType == Leaderboard::Type::Racing && tertiaryScore != 0.0f) { - std::unique_ptr winUpdate(Database::Get()->CreatePreppedStmt("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;")); - winUpdate->setInt(1, playerID); - winUpdate->setInt(2, activityId); - winUpdate->execute(); + Database::Get()->IncrementNumWins(playerID, activityId); } } -void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart, const uint32_t resultEnd) { +void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID) { Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID)); - leaderboard.SetupLeaderboard(weekly, resultStart, resultEnd); + leaderboard.SetupLeaderboard(weekly); leaderboard.Send(targetID); } diff --git a/dGame/LeaderboardManager.h b/dGame/LeaderboardManager.h index 527ae02d..af879573 100644 --- a/dGame/LeaderboardManager.h +++ b/dGame/LeaderboardManager.h @@ -9,46 +9,10 @@ #include "dCommonVars.h" #include "LDFFormat.h" -namespace sql { - class ResultSet; -}; - namespace RakNet { class BitStream; }; -class Score { -public: - Score() { - primaryScore = 0; - secondaryScore = 0; - tertiaryScore = 0; - } - Score(const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0) { - this->primaryScore = primaryScore; - this->secondaryScore = secondaryScore; - this->tertiaryScore = tertiaryScore; - } - bool operator<(const Score& rhs) const { - return primaryScore < rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore < rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore < rhs.tertiaryScore); - } - bool operator>(const Score& rhs) const { - return primaryScore > rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore > rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore > rhs.tertiaryScore); - } - void SetPrimaryScore(const float score) { primaryScore = score; } - float GetPrimaryScore() const { return primaryScore; } - - void SetSecondaryScore(const float score) { secondaryScore = score; } - float GetSecondaryScore() const { return secondaryScore; } - - void SetTertiaryScore(const float score) { tertiaryScore = score; } - float GetTertiaryScore() const { return tertiaryScore; } -private: - float primaryScore; - float secondaryScore; - float tertiaryScore; -}; - using GameID = uint32_t; class Leaderboard { @@ -79,7 +43,7 @@ class Leaderboard { /** * @brief Resets the leaderboard state and frees its allocated memory - * + * */ void Clear(); @@ -96,20 +60,16 @@ class Leaderboard { * @param resultStart The index to start the leaderboard at. Zero indexed. * @param resultEnd The index to end the leaderboard at. Zero indexed. */ - void SetupLeaderboard(bool weekly, uint32_t resultStart = 0, uint32_t resultEnd = 10); + void SetupLeaderboard(bool weekly); /** * Sends the leaderboard to the client specified by targetID. */ void Send(const LWOOBJID targetID) const; - // Helper function to get the columns, ordering and insert format for a leaderboard - static const std::string_view GetOrdering(Type leaderboardType); -private: - // Takes the resulting query from a leaderboard lookup and converts it to the LDF we need - // to send it to a client. - void QueryToLdf(std::unique_ptr& rows); + +private: using LeaderboardEntry = std::vector; using LeaderboardEntries = std::vector; @@ -119,10 +79,18 @@ class Leaderboard { InfoType infoType; Leaderboard::Type leaderboardType; bool weekly; +public: + LeaderboardEntry& PushBackEntry() { + return entries.emplace_back(); + } + + Type GetLeaderboardType() const { + return leaderboardType; + } }; namespace LeaderboardManager { - void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart = 0, const uint32_t resultEnd = 10); + void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID); void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index be034e46..8e94cee3 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -1691,7 +1691,7 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream bool weekly = inStream.ReadBit(); - LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID(), resultsStart, resultsEnd); + LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID()); } void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream& inStream, Entity* entity) { diff --git a/dScripts/ActivityManager.cpp b/dScripts/ActivityManager.cpp index 0f251dbf..8ba4834e 100644 --- a/dScripts/ActivityManager.cpp +++ b/dScripts/ActivityManager.cpp @@ -121,7 +121,7 @@ void ActivityManager::GetLeaderboardData(Entity* self, const LWOOBJID playerID, auto* sac = self->GetComponent(); uint32_t gameID = sac != nullptr ? sac->GetActivityID() : self->GetLOT(); // Save the new score to the leaderboard and show the leaderboard to the player - LeaderboardManager::SendLeaderboard(activityID, Leaderboard::InfoType::MyStanding, false, playerID, self->GetObjectID(), 0, numResults); + LeaderboardManager::SendLeaderboard(activityID, Leaderboard::InfoType::MyStanding, false, playerID, self->GetObjectID()); } void ActivityManager::ActivityTimerStart(Entity* self, const std::string& timerName, const float_t updateInterval,