Merge pull request #1107 from EmosewaMC/first-draft-leaderboard-re-write

feat: Leaderboards
This commit is contained in:
Gie "Max" Vanommeslaeghe 2023-07-24 00:29:17 +02:00 committed by GitHub
commit d1316cfc9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 529 additions and 531 deletions

View File

@ -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.

View File

@ -1,5 +1,8 @@
#include "LeaderboardManager.h"
#include <sstream>
#include <utility>
#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<LeaderboardEntry> entries,
LWOOBJID relatedPlayer, LeaderboardType leaderboardType) {
this->relatedPlayer = relatedPlayer;
namespace LeaderboardManager {
std::map<GameID, Leaderboard::Type> 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<uint32_t>(leaderboardSize);
// Doing this all in 1 call so there is no possbility of a dangling pointer.
bitStream->WriteAlignedBytes(reinterpret_cast<const unsigned char*>(GeneralUtils::ASCIIToUTF16(leaderboard.str()).c_str()), leaderboardSize * sizeof(char16_t));
if (leaderboardSize > 0) bitStream->Write<uint16_t>(0);
bitStream->Write0();
bitStream->Write0();
}
std::vector<LeaderboardEntry> Leaderboard::GetEntries() {
return entries;
void Leaderboard::QueryToLdf(std::unique_ptr<sql::ResultSet>& 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<LDFBaseData*>());
auto& entry = this->entries.back();
entry.reserve(MAX_NUM_DATA_PER_ROW);
entry.push_back(new LDFData<uint64_t>(u"CharacterID", rows->getInt("character_id")));
entry.push_back(new LDFData<uint64_t>(u"LastPlayed", rows->getUInt64("lastPlayed")));
entry.push_back(new LDFData<int32_t>(u"NumPlayed", rows->getInt("timesPlayed")));
entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(rows->getString("name").c_str())));
entry.push_back(new LDFData<uint64_t>(u"RowNumber", rows->getInt("ranking")));
switch (leaderboardType) {
case Type::ShootingGallery:
entry.push_back(new LDFData<int32_t>(u"Score", rows->getInt("primaryScore")));
// Score:1
entry.push_back(new LDFData<int32_t>(u"Streak", rows->getInt("secondaryScore")));
// Streak:1
entry.push_back(new LDFData<float>(u"HitPercentage", (rows->getInt("tertiaryScore") / 100.0f)));
// HitPercentage:3 between 0 and 1
break;
case Type::Racing:
entry.push_back(new LDFData<float>(u"BestTime", rows->getDouble("primaryScore")));
// BestLapTime:3
entry.push_back(new LDFData<float>(u"BestLapTime", rows->getDouble("secondaryScore")));
// BestTime:3
entry.push_back(new LDFData<int32_t>(u"License", 1));
// License:1 - 1 if player has completed mission 637 and 0 otherwise
entry.push_back(new LDFData<int32_t>(u"NumWins", rows->getInt("numWins")));
// NumWins:1
break;
case Type::UnusedLeaderboard4:
entry.push_back(new LDFData<int32_t>(u"Points", rows->getInt("primaryScore")));
// Points:1
break;
case Type::MonumentRace:
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("primaryScore")));
// Time:1(?)
break;
case Type::FootRace:
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("primaryScore")));
// Time:1
break;
case Type::Survival:
entry.push_back(new LDFData<int32_t>(u"Points", rows->getInt("primaryScore")));
// Points:1
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("secondaryScore")));
// Time:1
break;
case Type::SurvivalNS:
entry.push_back(new LDFData<int32_t>(u"Wave", rows->getInt("primaryScore")));
// Wave:1
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("secondaryScore")));
// Time:1
break;
case Type::Donations:
entry.push_back(new LDFData<int32_t>(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<uint32_t>(this->relatedPlayer));
}
baseLookup += " LIMIT 1";
Game::logger->LogDebug("LeaderboardManager", "query is %s", baseLookup.c_str());
std::unique_ptr<sql::PreparedStatement> baseQuery(Database::CreatePreppedStmt(baseLookup));
baseQuery->setInt(1, this->gameID);
std::unique_ptr<sql::ResultSet> 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<char[]> lookupBuffer = std::make_unique<char[]>(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<sql::PreparedStatement> 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<sql::ResultSet> 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<sql::PreparedStatement> query(Database::CreatePreppedStmt(lookup));
query->setInt(1, playerID);
query->setInt(2, activityId);
std::unique_ptr<sql::ResultSet> 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<sql::PreparedStatement> 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<sql::PreparedStatement> 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<LeaderboardEntry> 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<CDActivitiesTable>();
std::vector<CDActivities> activities = activitiesTable->Query([=](const CDActivities& entry) {
return (entry.ActivityID == gameID);
std::vector<CDActivities> activities = activitiesTable->Query([gameID](const CDActivities& entry) {
return entry.ActivityID == gameID;
});
for (const auto& activity : activities) {
return static_cast<LeaderboardType>(activity.leaderboardType);
}
return LeaderboardType::None;
auto type = !activities.empty() ? static_cast<Leaderboard::Type>(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;";

View File

@ -1,80 +1,134 @@
#pragma once
#ifndef __LEADERBOARDMANAGER__H__
#define __LEADERBOARDMANAGER__H__
#include <map>
#include <memory>
#include <string_view>
#include <vector>
#include <climits>
#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<LeaderboardEntry> entries,
LWOOBJID relatedPlayer = LWOOBJID_EMPTY, LeaderboardType = None);
std::vector<LeaderboardEntry> 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<LeaderboardEntry> 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<sql::ResultSet>& rows);
using LeaderboardEntry = std::vector<LDFBaseData*>;
using LeaderboardEntries = std::vector<LeaderboardEntry>;
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<GameID, Leaderboard::Type> leaderboardCache;
};
#endif //!__LEADERBOARDMANAGER__H__

View File

@ -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<float>(player.raceTime), static_cast<float>(player.bestLapTime), static_cast<float>(player.finished == 1));
// Entire race time
missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME);

View File

@ -36,7 +36,7 @@ ScriptedActivityComponent::ScriptedActivityComponent(Entity* parent, int activit
for (CDActivities activity : activities) {
m_ActivityInfo = activity;
if (static_cast<LeaderboardType>(activity.leaderboardType) == LeaderboardType::Racing && Game::config->GetValue("solo_racing") == "1") {
if (static_cast<Leaderboard::Type>(activity.leaderboardType) == Leaderboard::Type::Racing && Game::config->GetValue("solo_racing") == "1") {
m_ActivityInfo.minTeamSize = 1;
m_ActivityInfo.minTeams = 1;
}

View File

@ -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<uint32_t>(leaderboardString.size());
for (const auto c : leaderboardString) {
bitStream.Write<uint16_t>(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<Leaderboard::InfoType>(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) {

View File

@ -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<ScriptedActivityComponent>();
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<float>(finish));
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard",
scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(),

View File

@ -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<ScriptedActivityComponent>();
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<ScriptedActivityComponent>();
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<ScriptedActivityComponent>();
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,

View File

@ -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;

View File

@ -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);

View File

@ -378,6 +378,7 @@ void BaseWavesServer::GameOver(Entity* self, bool won) {
}
StopActivity(self, playerID, wave, time, score);
SaveScore(self, playerID, wave, time);
}
}

View File

@ -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<float>(param1), static_cast<float>(param2), static_cast<float>(param3));
}
}
}

View File

@ -150,7 +150,7 @@ void SGCannon::OnMessageBoxResponse(Entity* self, Entity* sender, int32_t button
if (IsPlayerInActivity(self, player->GetObjectID())) return;
self->SetNetworkVar<bool>(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<LWOOBJID>(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<uint32_t>(MissesVariable);
auto fired = self->GetVar<uint32_t>(ShotsFiredVariable);
@ -564,6 +564,9 @@ void SGCannon::StopGame(Entity* self, bool cancel) {
LootGenerator::Instance().GiveActivityLoot(player, self, GetGameID(self), self->GetVar<uint32_t>(TotalScoreVariable));
SaveScore(self, player->GetObjectID(),
static_cast<float>(self->GetVar<uint32_t>(TotalScoreVariable)), static_cast<float>(self->GetVar<uint32_t>(MaxStreakVariable)), percentage);
StopActivity(self, player->GetObjectID(), self->GetVar<uint32_t>(TotalScoreVariable), self->GetVar<uint32_t>(MaxStreakVariable), percentage);
self->SetNetworkVar<bool>(AudioFinalWaveDoneVariable, true);
@ -578,17 +581,6 @@ void SGCannon::StopGame(Entity* self, bool cancel) {
self->SetNetworkVar<std::u16string>(u"UI_Rewards",
GeneralUtils::to_u16string(self->GetVar<uint32_t>(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());

View File

@ -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();