diff --git a/CMakeVariables.txt b/CMakeVariables.txt index d3c8b36..02173ba 100644 --- a/CMakeVariables.txt +++ b/CMakeVariables.txt @@ -1,6 +1,6 @@ PROJECT_VERSION_MAJOR=1 -PROJECT_VERSION_MINOR=0 -PROJECT_VERSION_PATCH=4 +PROJECT_VERSION_MINOR=1 +PROJECT_VERSION_PATCH=0 # LICENSE LICENSE=AGPL-3.0 # The network version. diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index edc39f7..a4c6ec1 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -1,5 +1,8 @@ #include "LeaderboardManager.h" + +#include #include + #include "Database.h" #include "EntityManager.h" #include "Character.h" @@ -10,461 +13,400 @@ #include "CDClientManager.h" #include "GeneralUtils.h" #include "Entity.h" +#include "LDFFormat.h" +#include "DluAssert.h" #include "CDActivitiesTable.h" +#include "Metrics.hpp" -Leaderboard::Leaderboard(uint32_t gameID, uint32_t infoType, bool weekly, std::vector entries, - LWOOBJID relatedPlayer, LeaderboardType leaderboardType) { - this->relatedPlayer = relatedPlayer; +namespace LeaderboardManager { + std::map leaderboardCache; +} + +Leaderboard::Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type leaderboardType) { this->gameID = gameID; this->weekly = weekly; this->infoType = infoType; - this->entries = std::move(entries); this->leaderboardType = leaderboardType; + this->relatedPlayer = relatedPlayer; } -std::u16string Leaderboard::ToString() const { - std::string leaderboard; +Leaderboard::~Leaderboard() { + Clear(); +} - leaderboard += "ADO.Result=7:1\n"; - leaderboard += "Result.Count=1:1\n"; - leaderboard += "Result[0].Index=0:RowNumber\n"; - leaderboard += "Result[0].RowCount=1:" + std::to_string(entries.size()) + "\n"; +void Leaderboard::Clear() { + for (auto& entry : entries) for (auto ldfData : entry) delete ldfData; +} - auto index = 0; - for (const auto& entry : entries) { - leaderboard += "Result[0].Row[" + std::to_string(index) + "].LastPlayed=8:" + std::to_string(entry.lastPlayed) + "\n"; - leaderboard += "Result[0].Row[" + std::to_string(index) + "].CharacterID=8:" + std::to_string(entry.playerID) + "\n"; - leaderboard += "Result[0].Row[" + std::to_string(index) + "].NumPlayed=1:1\n"; - leaderboard += "Result[0].Row[" + std::to_string(index) + "].RowNumber=8:" + std::to_string(entry.placement) + "\n"; - leaderboard += "Result[0].Row[" + std::to_string(index) + "].Time=1:" + std::to_string(entry.time) + "\n"; +inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) { + leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString(); +} - // Only these minigames have a points system - if (leaderboardType == Survival || leaderboardType == ShootingGallery) { - leaderboard += "Result[0].Row[" + std::to_string(index) + "].Points=1:" + std::to_string(entry.score) + "\n"; - } else if (leaderboardType == SurvivalNS) { - leaderboard += "Result[0].Row[" + std::to_string(index) + "].Wave=1:" + std::to_string(entry.score) + "\n"; +void Leaderboard::Serialize(RakNet::BitStream* bitStream) const { + bitStream->Write(gameID); + bitStream->Write(infoType); + + std::ostringstream leaderboard; + + leaderboard << "ADO.Result=7:1"; // Unused in 1.10.64, but is in captures + leaderboard << "\nResult.Count=1:1"; // number of results, always 1 + if (!this->entries.empty()) leaderboard << "\nResult[0].Index=0:RowNumber"; // "Primary key". Live doesn't include this if there are no entries. + leaderboard << "\nResult[0].RowCount=1:" << entries.size(); + + int32_t rowNumber = 0; + for (auto& entry : entries) { + for (auto* data : entry) { + WriteLeaderboardRow(leaderboard, rowNumber, data); } - - leaderboard += "Result[0].Row[" + std::to_string(index) + "].name=0:" + entry.playerName + "\n"; - index++; + rowNumber++; } - return GeneralUtils::UTF8ToUTF16(leaderboard); + // Serialize the thing to a BitStream + uint32_t leaderboardSize = leaderboard.tellp(); + bitStream->Write(leaderboardSize); + // Doing this all in 1 call so there is no possbility of a dangling pointer. + bitStream->WriteAlignedBytes(reinterpret_cast(GeneralUtils::ASCIIToUTF16(leaderboard.str()).c_str()), leaderboardSize * sizeof(char16_t)); + if (leaderboardSize > 0) bitStream->Write(0); + bitStream->Write0(); + bitStream->Write0(); } -std::vector Leaderboard::GetEntries() { - return entries; +void Leaderboard::QueryToLdf(std::unique_ptr& rows) { + Clear(); + if (rows->rowsCount() == 0) return; + + this->entries.reserve(rows->rowsCount()); + while (rows->next()) { + constexpr int32_t MAX_NUM_DATA_PER_ROW = 9; + this->entries.push_back(std::vector()); + auto& entry = this->entries.back(); + 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"))); + // Score:1 + entry.push_back(new LDFData(u"Streak", rows->getInt("secondaryScore"))); + // Streak:1 + entry.push_back(new LDFData(u"HitPercentage", (rows->getInt("tertiaryScore") / 100.0f))); + // HitPercentage:3 between 0 and 1 + break; + case Type::Racing: + entry.push_back(new LDFData(u"BestTime", rows->getDouble("primaryScore"))); + // BestLapTime:3 + entry.push_back(new LDFData(u"BestLapTime", rows->getDouble("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"))); + // NumWins:1 + break; + case Type::UnusedLeaderboard4: + entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore"))); + // Points:1 + break; + case Type::MonumentRace: + entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore"))); + // Time:1(?) + break; + case Type::FootRace: + entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore"))); + // Time:1 + break; + case Type::Survival: + entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore"))); + // Points:1 + entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore"))); + // Time:1 + break; + case Type::SurvivalNS: + entry.push_back(new LDFData(u"Wave", rows->getInt("primaryScore"))); + // Wave:1 + entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore"))); + // Time:1 + break; + case Type::Donations: + entry.push_back(new LDFData(u"Points", rows->getInt("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; + default: + break; + } + } } -uint32_t Leaderboard::GetGameID() const { - return gameID; +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 + switch (leaderboardType) { + case Type::Racing: + 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"; + case Type::ShootingGallery: + case Type::FootRace: + case Type::UnusedLeaderboard4: + case Type::Donations: + case Type::None: + default: + return "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC"; + } } -uint32_t Leaderboard::GetInfoType() const { - return infoType; +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), lowestRanking.lowestRank - 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"; + Game::logger->LogDebug("LeaderboardManager", "query is %s", baseLookup.c_str()); + std::unique_ptr baseQuery(Database::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::CreatePreppedStmt(lookupBuffer.get())); + Game::logger->LogDebug("LeaderboardManager", "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); } -void Leaderboard::Send(LWOOBJID targetID) const { +void Leaderboard::Send(const LWOOBJID targetID) const { auto* player = Game::entityManager->GetEntity(relatedPlayer); if (player != nullptr) { GameMessages::SendActivitySummaryLeaderboardData(targetID, this, player->GetSystemAddress()); } } -void LeaderboardManager::SaveScore(LWOOBJID playerID, uint32_t gameID, uint32_t score, uint32_t time) { - const auto* player = Game::entityManager->GetEntity(playerID); - if (player == nullptr) - return; +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"; + } - auto* character = player->GetCharacter(); - if (character == nullptr) - return; + 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; +} - auto* select = Database::CreatePreppedStmt("SELECT time, score FROM leaderboard WHERE character_id = ? AND game_id = ?;"); +void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) { + const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId); + auto* lookup = "SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;"; - select->setUInt64(1, character->GetID()); - select->setInt(2, gameID); - - auto any = false; - auto* result = select->executeQuery(); - auto leaderboardType = GetLeaderboardType(gameID); - - // Check if the new score is a high score - while (result->next()) { - any = true; - - const auto storedTime = result->getInt(1); - const auto storedScore = result->getInt(2); - auto highscore = true; - bool classicSurvivalScoring = Game::config->GetValue("classic_survival_scoring") == "1"; + std::unique_ptr query(Database::CreatePreppedStmt(lookup)); + 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) { - case ShootingGallery: - if (score <= storedScore) - highscore = false; + // Higher score better + case Leaderboard::Type::ShootingGallery: { + oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); + oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); + oldScore.SetTertiaryScore(myScoreResult->getInt("tertiaryScore")); break; - case Racing: - if (time >= storedTime) - highscore = false; - break; - case MonumentRace: - if (time >= storedTime) - highscore = false; - break; - case FootRace: - if (time <= storedTime) - highscore = false; - break; - case Survival: - if (classicSurvivalScoring) { - if (time <= storedTime) { // Based on time (LU live) - highscore = false; - } - } else { - if (score <= storedScore) // Based on score (DLU) - highscore = false; - } - break; - case SurvivalNS: - if (!(score > storedScore || (time < storedTime && score >= storedScore))) - highscore = false; - break; - default: - highscore = false; } + 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")); + break; + } + case Leaderboard::Type::Racing: { + oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore")); + oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore")); - if (!highscore) { - delete select; - delete result; + // 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: + Game::logger->Log("LeaderboardManager", "Unknown leaderboard type %i for game %i. Cannot save score!", leaderboardType, activityId); return; } - } - - delete select; - delete result; - - if (any) { - auto* statement = Database::CreatePreppedStmt("UPDATE leaderboard SET time = ?, score = ?, last_played=SYSDATE() WHERE character_id = ? AND game_id = ?;"); - statement->setInt(1, time); - statement->setInt(2, score); - statement->setUInt64(3, character->GetID()); - statement->setInt(4, gameID); - statement->execute(); - - delete statement; + 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()); + } 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()); + newHighScore = newScoreFlipped > oldScoreFlipped; + } + if (newHighScore) { + saveQuery = FormatInsert(leaderboardType, newScore, true); + } } else { - // Note: last_played will be set to SYSDATE() by default when inserting into leaderboard - auto* statement = Database::CreatePreppedStmt("INSERT INTO leaderboard (character_id, game_id, time, score) VALUES (?, ?, ?, ?);"); - statement->setUInt64(1, character->GetID()); - statement->setInt(2, gameID); - statement->setInt(3, time); - statement->setInt(4, score); - statement->execute(); - - delete statement; + saveQuery = FormatInsert(leaderboardType, newScore, false); + } + Game::logger->Log("LeaderboardManager", "save query %s %i %i", saveQuery.c_str(), playerID, activityId); + std::unique_ptr saveStatement(Database::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::CreatePreppedStmt("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;")); + winUpdate->setInt(1, playerID); + winUpdate->setInt(2, activityId); + winUpdate->execute(); } } -Leaderboard* LeaderboardManager::GetLeaderboard(uint32_t gameID, InfoType infoType, bool weekly, LWOOBJID playerID) { - auto leaderboardType = GetLeaderboardType(gameID); - - std::string query; - bool classicSurvivalScoring = Game::config->GetValue("classic_survival_scoring") == "1"; - switch (infoType) { - case InfoType::Standings: - switch (leaderboardType) { - case ShootingGallery: - query = standingsScoreQuery; // Shooting gallery is based on the highest score. - break; - case FootRace: - query = standingsTimeQuery; // The higher your time, the better for FootRace. - break; - case Survival: - query = classicSurvivalScoring ? standingsTimeQuery : standingsScoreQuery; - break; - case SurvivalNS: - query = standingsScoreQueryAsc; // BoNS is scored by highest wave (score) first, then time. - break; - default: - query = standingsTimeQueryAsc; // MonumentRace and Racing are based on the shortest time. - } - break; - case InfoType::Friends: - switch (leaderboardType) { - case ShootingGallery: - query = friendsScoreQuery; // Shooting gallery is based on the highest score. - break; - case FootRace: - query = friendsTimeQuery; // The higher your time, the better for FootRace. - break; - case Survival: - query = classicSurvivalScoring ? friendsTimeQuery : friendsScoreQuery; - break; - case SurvivalNS: - query = friendsScoreQueryAsc; // BoNS is scored by highest wave (score) first, then time. - break; - default: - query = friendsTimeQueryAsc; // MonumentRace and Racing are based on the shortest time. - } - break; - - default: - switch (leaderboardType) { - case ShootingGallery: - query = topPlayersScoreQuery; // Shooting gallery is based on the highest score. - break; - case FootRace: - query = topPlayersTimeQuery; // The higher your time, the better for FootRace. - break; - case Survival: - query = classicSurvivalScoring ? topPlayersTimeQuery : topPlayersScoreQuery; - break; - case SurvivalNS: - query = topPlayersScoreQueryAsc; // BoNS is scored by highest wave (score) first, then time. - break; - default: - query = topPlayersTimeQueryAsc; // MonumentRace and Racing are based on the shortest time. - } - } - - auto* statement = Database::CreatePreppedStmt(query); - statement->setUInt(1, gameID); - - // Only the standings and friends leaderboards require the character ID to be set - if (infoType == Standings || infoType == Friends) { - auto characterID = 0; - - const auto* player = Game::entityManager->GetEntity(playerID); - if (player != nullptr) { - auto* character = player->GetCharacter(); - if (character != nullptr) - characterID = character->GetID(); - } - - statement->setUInt64(2, characterID); - } - - auto* res = statement->executeQuery(); - - std::vector entries{}; - - uint32_t index = 0; - while (res->next()) { - LeaderboardEntry entry; - entry.playerID = res->getUInt64(4); - entry.playerName = res->getString(5); - entry.time = res->getUInt(1); - entry.score = res->getUInt(2); - entry.placement = res->getUInt(3); - entry.lastPlayed = res->getUInt(6); - - entries.push_back(entry); - index++; - } - - delete res; - delete statement; - - return new Leaderboard(gameID, infoType, weekly, entries, playerID, leaderboardType); +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) { + Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID)); + leaderboard.SetupLeaderboard(weekly, resultStart, resultEnd); + leaderboard.Send(targetID); } -void LeaderboardManager::SendLeaderboard(uint32_t gameID, InfoType infoType, bool weekly, LWOOBJID targetID, - LWOOBJID playerID) { - const auto* leaderboard = LeaderboardManager::GetLeaderboard(gameID, infoType, weekly, playerID); - leaderboard->Send(targetID); - delete leaderboard; -} +Leaderboard::Type LeaderboardManager::GetLeaderboardType(const GameID gameID) { + auto lookup = leaderboardCache.find(gameID); + if (lookup != leaderboardCache.end()) return lookup->second; -LeaderboardType LeaderboardManager::GetLeaderboardType(uint32_t gameID) { auto* activitiesTable = CDClientManager::Instance().GetTable(); - std::vector activities = activitiesTable->Query([=](const CDActivities& entry) { - return (entry.ActivityID == gameID); + std::vector activities = activitiesTable->Query([gameID](const CDActivities& entry) { + return entry.ActivityID == gameID; }); - - for (const auto& activity : activities) { - return static_cast(activity.leaderboardType); - } - - return LeaderboardType::None; + auto type = !activities.empty() ? static_cast(activities.at(0).leaderboardType) : Leaderboard::Type::None; + leaderboardCache.insert_or_assign(gameID, type); + return type; } - -const std::string LeaderboardManager::topPlayersScoreQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -"RANK() OVER ( ORDER BY l.score DESC, l.time DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -"INNER JOIN charinfo c ON l.character_id = c.id " -"WHERE l.game_id = ? " -"ORDER BY leaderboard_rank) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales LIMIT 11;"; - -const std::string LeaderboardManager::friendsScoreQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, f.friend_id, f.player_id, " -" RANK() OVER ( ORDER BY l.score DESC, l.time DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" INNER JOIN friends f ON f.player_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -" personal_values AS ( " -" SELECT id as related_player_id, " -" GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE leaderboard_vales.id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank AND (player_id = related_player_id OR friend_id = related_player_id);"; - -const std::string LeaderboardManager::standingsScoreQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -" RANK() OVER ( ORDER BY l.score DESC, l.time DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -"personal_values AS ( " -" SELECT GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank;"; - -const std::string LeaderboardManager::topPlayersScoreQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -"RANK() OVER ( ORDER BY l.score DESC, l.time ASC, last_played ) leaderboard_rank " -" FROM leaderboard l " -"INNER JOIN charinfo c ON l.character_id = c.id " -"WHERE l.game_id = ? " -"ORDER BY leaderboard_rank) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales LIMIT 11;"; - -const std::string LeaderboardManager::friendsScoreQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, f.friend_id, f.player_id, " -" RANK() OVER ( ORDER BY l.score DESC, l.time ASC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" INNER JOIN friends f ON f.player_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -" personal_values AS ( " -" SELECT id as related_player_id, " -" GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE leaderboard_vales.id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank AND (player_id = related_player_id OR friend_id = related_player_id);"; - -const std::string LeaderboardManager::standingsScoreQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -" RANK() OVER ( ORDER BY l.score DESC, l.time ASC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -"personal_values AS ( " -" SELECT GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank;"; - -const std::string LeaderboardManager::topPlayersTimeQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -"RANK() OVER ( ORDER BY l.time DESC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -"INNER JOIN charinfo c ON l.character_id = c.id " -"WHERE l.game_id = ? " -"ORDER BY leaderboard_rank) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales LIMIT 11;"; - -const std::string LeaderboardManager::friendsTimeQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, f.friend_id, f.player_id, " -" RANK() OVER ( ORDER BY l.time DESC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" INNER JOIN friends f ON f.player_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -" personal_values AS ( " -" SELECT id as related_player_id, " -" GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE leaderboard_vales.id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank AND (player_id = related_player_id OR friend_id = related_player_id);"; - -const std::string LeaderboardManager::standingsTimeQuery = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -" RANK() OVER ( ORDER BY l.time DESC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -"personal_values AS ( " -" SELECT GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank;"; - -const std::string LeaderboardManager::topPlayersTimeQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -"RANK() OVER ( ORDER BY l.time ASC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -"INNER JOIN charinfo c ON l.character_id = c.id " -"WHERE l.game_id = ? " -"ORDER BY leaderboard_rank) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales LIMIT 11;"; - -const std::string LeaderboardManager::friendsTimeQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, f.friend_id, f.player_id, " -" RANK() OVER ( ORDER BY l.time ASC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" INNER JOIN friends f ON f.player_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -" personal_values AS ( " -" SELECT id as related_player_id, " -" GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE leaderboard_vales.id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank AND (player_id = related_player_id OR friend_id = related_player_id);"; - -const std::string LeaderboardManager::standingsTimeQueryAsc = -"WITH leaderboard_vales AS ( " -" SELECT l.time, l.score, UNIX_TIMESTAMP(l.last_played) last_played, c.name, c.id, " -" RANK() OVER ( ORDER BY l.time ASC, l.score DESC, last_played ) leaderboard_rank " -" FROM leaderboard l " -" INNER JOIN charinfo c ON l.character_id = c.id " -" WHERE l.game_id = ? " -" ORDER BY leaderboard_rank), " -"personal_values AS ( " -" SELECT GREATEST(CAST(leaderboard_rank AS SIGNED) - 5, 1) AS min_rank, " -" GREATEST(leaderboard_rank + 5, 11) AS max_rank " -" FROM leaderboard_vales WHERE id = ? LIMIT 1) " -"SELECT time, score, leaderboard_rank, id, name, last_played " -"FROM leaderboard_vales, personal_values " -"WHERE leaderboard_rank BETWEEN min_rank AND max_rank;"; diff --git a/dGame/LeaderboardManager.h b/dGame/LeaderboardManager.h index cabdf2d..e2ce3f9 100644 --- a/dGame/LeaderboardManager.h +++ b/dGame/LeaderboardManager.h @@ -1,80 +1,134 @@ -#pragma once +#ifndef __LEADERBOARDMANAGER__H__ +#define __LEADERBOARDMANAGER__H__ + +#include +#include +#include #include -#include + +#include "Singleton.h" #include "dCommonVars.h" +#include "LDFFormat.h" -struct LeaderboardEntry { - uint64_t playerID; - std::string playerName; - uint32_t time; - uint32_t score; - uint32_t placement; - time_t lastPlayed; +namespace sql { + class ResultSet; }; -enum InfoType : uint32_t { - Top, // Top 11 all time players - Standings, // Ranking of the current player - Friends // Ranking between friends +namespace RakNet { + class BitStream; }; -enum LeaderboardType : uint32_t { - ShootingGallery, - Racing, - MonumentRace, - FootRace, - Survival = 5, - SurvivalNS = 6, - None = UINT_MAX +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 { public: - Leaderboard(uint32_t gameID, uint32_t infoType, bool weekly, std::vector entries, - LWOOBJID relatedPlayer = LWOOBJID_EMPTY, LeaderboardType = None); - std::vector GetEntries(); - [[nodiscard]] std::u16string ToString() const; - [[nodiscard]] uint32_t GetGameID() const; - [[nodiscard]] uint32_t GetInfoType() const; - void Send(LWOOBJID targetID) const; + + // Enums for leaderboards + enum InfoType : uint32_t { + Top, // Top 11 all time players + MyStanding, // Ranking of the current player + Friends // Ranking between friends + }; + + enum Type : uint32_t { + ShootingGallery, + Racing, + MonumentRace, + FootRace, + UnusedLeaderboard4, // There is no 4 defined anywhere in the cdclient, but it takes a Score. + Survival, + SurvivalNS, + Donations, + None + }; + Leaderboard() = delete; + Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type = None); + + ~Leaderboard(); + + /** + * @brief Resets the leaderboard state and frees its allocated memory + * + */ + void Clear(); + + /** + * Serialize the Leaderboard to a BitStream + * + * Expensive! Leaderboards are very string intensive so be wary of performatnce calling this method. + */ + void Serialize(RakNet::BitStream* bitStream) const; + + /** + * Builds the leaderboard from the database based on the associated gameID + * + * @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); + + /** + * 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: - std::vector entries{}; + // 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); + + using LeaderboardEntry = std::vector; + using LeaderboardEntries = std::vector; + + LeaderboardEntries entries; LWOOBJID relatedPlayer; - uint32_t gameID; - uint32_t infoType; - LeaderboardType leaderboardType; + GameID gameID; + InfoType infoType; + Leaderboard::Type leaderboardType; bool weekly; }; -class LeaderboardManager { -public: - static LeaderboardManager* Instance() { - if (address == nullptr) - address = new LeaderboardManager; - return address; - } - static void SendLeaderboard(uint32_t gameID, InfoType infoType, bool weekly, LWOOBJID targetID, - LWOOBJID playerID = LWOOBJID_EMPTY); - static Leaderboard* GetLeaderboard(uint32_t gameID, InfoType infoType, bool weekly, LWOOBJID playerID = LWOOBJID_EMPTY); - static void SaveScore(LWOOBJID playerID, uint32_t gameID, uint32_t score, uint32_t time); - static LeaderboardType GetLeaderboardType(uint32_t gameID); -private: - static LeaderboardManager* address; +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); - // Modified 12/12/2021: Existing queries were renamed to be more descriptive. - static const std::string topPlayersScoreQuery; - static const std::string friendsScoreQuery; - static const std::string standingsScoreQuery; - static const std::string topPlayersScoreQueryAsc; - static const std::string friendsScoreQueryAsc; - static const std::string standingsScoreQueryAsc; + void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0); - // Added 12/12/2021: Queries dictated by time are needed for certain minigames. - static const std::string topPlayersTimeQuery; - static const std::string friendsTimeQuery; - static const std::string standingsTimeQuery; - static const std::string topPlayersTimeQueryAsc; - static const std::string friendsTimeQueryAsc; - static const std::string standingsTimeQueryAsc; + Leaderboard::Type GetLeaderboardType(const GameID gameID); + extern std::map leaderboardCache; }; +#endif //!__LEADERBOARDMANAGER__H__ diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index d3fb709..5de2444 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -23,6 +23,7 @@ #include "dConfig.h" #include "Loot.h" #include "eMissionTaskType.h" +#include "LeaderboardManager.h" #include "dZoneManager.h" #include "CDActivitiesTable.h" @@ -367,9 +368,7 @@ void RacingControlComponent::HandleMessageBoxResponse(Entity* player, int32_t bu } if (id == "rewardButton") { - if (data->collectedRewards) { - return; - } + if (data->collectedRewards) return; data->collectedRewards = true; @@ -839,6 +838,7 @@ void RacingControlComponent::Update(float deltaTime) { "Completed time %llu, %llu", raceTime, raceTime * 1000); + LeaderboardManager::SaveScore(playerEntity->GetObjectID(), m_ActivityID, static_cast(player.raceTime), static_cast(player.bestLapTime), static_cast(player.finished == 1)); // Entire race time missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME); diff --git a/dGame/dComponents/ScriptedActivityComponent.cpp b/dGame/dComponents/ScriptedActivityComponent.cpp index 23c65ae..81b3e9a 100644 --- a/dGame/dComponents/ScriptedActivityComponent.cpp +++ b/dGame/dComponents/ScriptedActivityComponent.cpp @@ -36,7 +36,7 @@ ScriptedActivityComponent::ScriptedActivityComponent(Entity* parent, int activit for (CDActivities activity : activities) { m_ActivityInfo = activity; - if (static_cast(activity.leaderboardType) == LeaderboardType::Racing && Game::config->GetValue("solo_racing") == "1") { + if (static_cast(activity.leaderboardType) == Leaderboard::Type::Racing && Game::config->GetValue("solo_racing") == "1") { m_ActivityInfo.minTeamSize = 1; m_ActivityInfo.minTeams = 1; } diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 05f4784..8d8085a 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -1645,20 +1645,7 @@ void GameMessages::SendActivitySummaryLeaderboardData(const LWOOBJID& objectID, bitStream.Write(objectID); bitStream.Write(eGameMessageType::SEND_ACTIVITY_SUMMARY_LEADERBOARD_DATA); - bitStream.Write(leaderboard->GetGameID()); - bitStream.Write(leaderboard->GetInfoType()); - - // Leaderboard is written back as LDF string - const auto leaderboardString = leaderboard->ToString(); - bitStream.Write(leaderboardString.size()); - for (const auto c : leaderboardString) { - bitStream.Write(c); - } - if (!leaderboardString.empty()) bitStream.Write(uint16_t(0)); - - bitStream.Write0(); - bitStream.Write0(); - + leaderboard->Serialize(&bitStream); SEND_PACKET; } @@ -1666,8 +1653,8 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream int32_t gameID = 0; if (inStream->ReadBit()) inStream->Read(gameID); - int32_t queryType = 1; - if (inStream->ReadBit()) inStream->Read(queryType); + Leaderboard::InfoType queryType = Leaderboard::InfoType::MyStanding; + if (inStream->ReadBit()) inStream->Read(queryType); int32_t resultsEnd = 10; if (inStream->ReadBit()) inStream->Read(resultsEnd); @@ -1680,9 +1667,7 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream bool weekly = inStream->ReadBit(); - const auto* leaderboard = LeaderboardManager::GetLeaderboard(gameID, (InfoType)queryType, weekly, entity->GetObjectID()); - SendActivitySummaryLeaderboardData(entity->GetObjectID(), leaderboard, sysAddr); - delete leaderboard; + LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID(), resultsStart, resultsEnd); } void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream* inStream, Entity* entity) { diff --git a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp index 199e4c6..b47260f 100644 --- a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp +++ b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp @@ -68,15 +68,12 @@ void NpcAgCourseStarter::OnMessageBoxResponse(Entity* self, Entity* sender, int3 } } -void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, - int32_t param3) { +void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) { auto* scriptedActivityComponent = self->GetComponent(); - if (scriptedActivityComponent == nullptr) - return; + if (scriptedActivityComponent == nullptr) return; auto* data = scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID()); - if (data == nullptr) - return; + if (data == nullptr) return; if (args == "course_cancel") { GameMessages::SendNotifyClientObject(self->GetObjectID(), u"cancel_timer", 0, 0, @@ -96,8 +93,7 @@ void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std } Game::entityManager->SerializeEntity(self); - LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), - 0, (uint32_t)finish); + LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), static_cast(finish)); GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard", scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(), diff --git a/dScripts/ActivityManager.cpp b/dScripts/ActivityManager.cpp index d15ad85..24e661e 100644 --- a/dScripts/ActivityManager.cpp +++ b/dScripts/ActivityManager.cpp @@ -73,24 +73,25 @@ void ActivityManager::StopActivity(Entity* self, const LWOOBJID playerID, const LootGenerator::Instance().GiveActivityLoot(player, self, gameID, CalculateActivityRating(self, playerID)); - // Save the new score to the leaderboard and show the leaderboard to the player - LeaderboardManager::SaveScore(playerID, gameID, score, value1); - const auto* leaderboard = LeaderboardManager::GetLeaderboard(gameID, InfoType::Standings, - false, player->GetObjectID()); - GameMessages::SendActivitySummaryLeaderboardData(self->GetObjectID(), leaderboard, player->GetSystemAddress()); - delete leaderboard; - - // Makes the leaderboard show up for the player - GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard", - gameID, 0, playerID, "", - player->GetSystemAddress()); - if (sac != nullptr) { sac->PlayerRemove(player->GetObjectID()); } } } +void ActivityManager::SaveScore(Entity* self, const LWOOBJID playerID, const float primaryScore, const float secondaryScore, const float tertiaryScore) const { + auto* player = Game::entityManager->GetEntity(playerID); + if (!player) return; + + 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::SaveScore(playerID, gameID, primaryScore, secondaryScore, tertiaryScore); + + // Makes the leaderboard show up for the player + GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard", gameID, 0, playerID, "", player->GetSystemAddress()); +} + bool ActivityManager::TakeActivityCost(const Entity* self, const LWOOBJID playerID) { auto* sac = self->GetComponent(); if (sac == nullptr) @@ -117,7 +118,10 @@ uint32_t ActivityManager::GetActivityID(const Entity* self) { } void ActivityManager::GetLeaderboardData(Entity* self, const LWOOBJID playerID, const uint32_t activityID, uint32_t numResults) { - LeaderboardManager::SendLeaderboard(activityID, Standings, false, self->GetObjectID(), 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); } void ActivityManager::ActivityTimerStart(Entity* self, const std::string& timerName, const float_t updateInterval, diff --git a/dScripts/ActivityManager.h b/dScripts/ActivityManager.h index 640cf4b..a2202bf 100644 --- a/dScripts/ActivityManager.h +++ b/dScripts/ActivityManager.h @@ -18,6 +18,7 @@ public: static bool TakeActivityCost(const Entity* self, LWOOBJID playerID); static uint32_t GetActivityID(const Entity* self); void StopActivity(Entity* self, LWOOBJID playerID, uint32_t score, uint32_t value1 = 0, uint32_t value2 = 0, bool quit = false); + void SaveScore(Entity* self, const LWOOBJID playerID, const float primaryScore, const float secondaryScore = 0.0f, const float tertiaryScore = 0.0f) const; virtual uint32_t CalculateActivityRating(Entity* self, LWOOBJID playerID); static void GetLeaderboardData(Entity* self, LWOOBJID playerID, uint32_t activityID, uint32_t numResults = 0); // void FreezePlayer(Entity *self, const LWOOBJID playerID, const bool state) const; diff --git a/dScripts/BaseSurvivalServer.cpp b/dScripts/BaseSurvivalServer.cpp index 8859dc0..0e8d043 100644 --- a/dScripts/BaseSurvivalServer.cpp +++ b/dScripts/BaseSurvivalServer.cpp @@ -8,6 +8,8 @@ #include "eMissionState.h" #include "MissionComponent.h" #include "Character.h" +#include "Game.h" +#include "dConfig.h" void BaseSurvivalServer::SetGameVariables(Entity* self) { this->constants = std::move(GetConstants()); @@ -354,6 +356,7 @@ void BaseSurvivalServer::GameOver(Entity* self) { const auto score = GetActivityValue(self, playerID, 0); const auto time = GetActivityValue(self, playerID, 1); + SaveScore(self, playerID, score, time); GameMessages::SendNotifyClientZoneObject(self->GetObjectID(), u"Update_ScoreBoard", time, 0, playerID, std::to_string(score), UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dScripts/BaseWavesServer.cpp b/dScripts/BaseWavesServer.cpp index 4add13e..1090d8c 100644 --- a/dScripts/BaseWavesServer.cpp +++ b/dScripts/BaseWavesServer.cpp @@ -378,6 +378,7 @@ void BaseWavesServer::GameOver(Entity* self, bool won) { } StopActivity(self, playerID, wave, time, score); + SaveScore(self, playerID, wave, time); } } diff --git a/dScripts/ai/ACT/FootRace/BaseFootRaceManager.cpp b/dScripts/ai/ACT/FootRace/BaseFootRaceManager.cpp index 45b2da0..c02bf56 100644 --- a/dScripts/ai/ACT/FootRace/BaseFootRaceManager.cpp +++ b/dScripts/ai/ACT/FootRace/BaseFootRaceManager.cpp @@ -1,6 +1,7 @@ #include "BaseFootRaceManager.h" #include "EntityManager.h" #include "Character.h" +#include "Entity.h" void BaseFootRaceManager::OnStartup(Entity* self) { // TODO: Add to FootRaceStarter group @@ -40,6 +41,7 @@ void BaseFootRaceManager::OnFireEventServerSide(Entity* self, Entity* sender, st } StopActivity(self, player->GetObjectID(), 0, param1); + SaveScore(self, player->GetObjectID(), static_cast(param1), static_cast(param2), static_cast(param3)); } } } diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index 435f092..4aa8d0c 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -150,7 +150,7 @@ void SGCannon::OnMessageBoxResponse(Entity* self, Entity* sender, int32_t button if (IsPlayerInActivity(self, player->GetObjectID())) return; self->SetNetworkVar(ClearVariable, true); StartGame(self); - } else if (button == 0 && ((identifier == u"Shooting_Gallery_Retry" || identifier == u"RePlay"))){ + } else if (button == 0 && ((identifier == u"Shooting_Gallery_Retry" || identifier == u"RePlay"))) { RemovePlayer(player->GetObjectID()); UpdatePlayer(self, player->GetObjectID(), true); } else if (button == 1 && identifier == u"Shooting_Gallery_Exit") { @@ -341,7 +341,7 @@ void SGCannon::StartGame(Entity* self) { auto* player = Game::entityManager->GetEntity(self->GetVar(PlayerIDVariable)); if (player != nullptr) { - GetLeaderboardData(self, player->GetObjectID(), GetActivityID(self)); + GetLeaderboardData(self, player->GetObjectID(), GetActivityID(self), 1); Game::logger->Log("SGCannon", "Sending ActivityStart"); GameMessages::SendActivityStart(self->GetObjectID(), player->GetSystemAddress()); @@ -436,8 +436,8 @@ void SGCannon::RemovePlayer(LWOOBJID playerID) { } } -void SGCannon::OnRequestActivityExit(Entity* self, LWOOBJID player, bool canceled){ - if (canceled){ +void SGCannon::OnRequestActivityExit(Entity* self, LWOOBJID player, bool canceled) { + if (canceled) { StopGame(self, canceled); RemovePlayer(player); } @@ -546,7 +546,7 @@ void SGCannon::StopGame(Entity* self, bool cancel) { // The player won, store all the score and send rewards if (!cancel) { - auto percentage = 0; + int32_t percentage = 0.0f; auto misses = self->GetVar(MissesVariable); auto fired = self->GetVar(ShotsFiredVariable); @@ -564,6 +564,9 @@ void SGCannon::StopGame(Entity* self, bool cancel) { LootGenerator::Instance().GiveActivityLoot(player, self, GetGameID(self), self->GetVar(TotalScoreVariable)); + SaveScore(self, player->GetObjectID(), + static_cast(self->GetVar(TotalScoreVariable)), static_cast(self->GetVar(MaxStreakVariable)), percentage); + StopActivity(self, player->GetObjectID(), self->GetVar(TotalScoreVariable), self->GetVar(MaxStreakVariable), percentage); self->SetNetworkVar(AudioFinalWaveDoneVariable, true); @@ -578,17 +581,6 @@ void SGCannon::StopGame(Entity* self, bool cancel) { self->SetNetworkVar(u"UI_Rewards", GeneralUtils::to_u16string(self->GetVar(TotalScoreVariable)) + u"_0_0_0_0_0_0" ); - - GameMessages::SendRequestActivitySummaryLeaderboardData( - player->GetObjectID(), - self->GetObjectID(), - player->GetSystemAddress(), - GetGameID(self), - 1, - 10, - 0, - false - ); } GameMessages::SendActivityStop(self->GetObjectID(), false, cancel, player->GetSystemAddress()); diff --git a/migrations/dlu/9_Update_Leaderboard_Storage.sql b/migrations/dlu/9_Update_Leaderboard_Storage.sql new file mode 100644 index 0000000..c87e350 --- /dev/null +++ b/migrations/dlu/9_Update_Leaderboard_Storage.sql @@ -0,0 +1,18 @@ +ALTER TABLE leaderboard + ADD COLUMN tertiaryScore FLOAT NOT NULL DEFAULT 0, + ADD COLUMN numWins INT NOT NULL DEFAULT 0, + ADD COLUMN timesPlayed INT NOT NULL DEFAULT 1, + MODIFY time INT NOT NULL DEFAULT 0; + +/* Can only ALTER one column at a time... */ +ALTER TABLE leaderboard CHANGE score primaryScore FLOAT NOT NULL DEFAULT 0; +ALTER TABLE leaderboard CHANGE time secondaryScore FLOAT NOT NULL DEFAULT 0 AFTER primaryScore; + +/* A bit messy, but better than going through a bunch of code fixes all to be run once. */ +UPDATE leaderboard SET + primaryScore = secondaryScore, + secondaryScore = 0 WHERE game_id IN (1, 44, 46, 47, 48, 49, 53, 103, 104, 108, 1901); + +/* Do this last so we dont update entry times erroneously */ +ALTER TABLE leaderboard + CHANGE last_played last_played TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP();