mirror of
https://github.com/DarkflameUniverse/DarkflameServer
synced 2024-08-30 18:43:58 +00:00
Merge pull request #1107 from EmosewaMC/first-draft-leaderboard-re-write
feat: Leaderboards
This commit is contained in:
commit
d1316cfc9f
@ -1,6 +1,6 @@
|
|||||||
PROJECT_VERSION_MAJOR=1
|
PROJECT_VERSION_MAJOR=1
|
||||||
PROJECT_VERSION_MINOR=0
|
PROJECT_VERSION_MINOR=1
|
||||||
PROJECT_VERSION_PATCH=4
|
PROJECT_VERSION_PATCH=0
|
||||||
# LICENSE
|
# LICENSE
|
||||||
LICENSE=AGPL-3.0
|
LICENSE=AGPL-3.0
|
||||||
# The network version.
|
# The network version.
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
#include "LeaderboardManager.h"
|
#include "LeaderboardManager.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
#include "EntityManager.h"
|
#include "EntityManager.h"
|
||||||
#include "Character.h"
|
#include "Character.h"
|
||||||
@ -10,461 +13,400 @@
|
|||||||
#include "CDClientManager.h"
|
#include "CDClientManager.h"
|
||||||
#include "GeneralUtils.h"
|
#include "GeneralUtils.h"
|
||||||
#include "Entity.h"
|
#include "Entity.h"
|
||||||
|
#include "LDFFormat.h"
|
||||||
|
#include "DluAssert.h"
|
||||||
|
|
||||||
#include "CDActivitiesTable.h"
|
#include "CDActivitiesTable.h"
|
||||||
|
#include "Metrics.hpp"
|
||||||
|
|
||||||
Leaderboard::Leaderboard(uint32_t gameID, uint32_t infoType, bool weekly, std::vector<LeaderboardEntry> entries,
|
namespace LeaderboardManager {
|
||||||
LWOOBJID relatedPlayer, LeaderboardType leaderboardType) {
|
std::map<GameID, Leaderboard::Type> leaderboardCache;
|
||||||
this->relatedPlayer = relatedPlayer;
|
}
|
||||||
|
|
||||||
|
Leaderboard::Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type leaderboardType) {
|
||||||
this->gameID = gameID;
|
this->gameID = gameID;
|
||||||
this->weekly = weekly;
|
this->weekly = weekly;
|
||||||
this->infoType = infoType;
|
this->infoType = infoType;
|
||||||
this->entries = std::move(entries);
|
|
||||||
this->leaderboardType = leaderboardType;
|
this->leaderboardType = leaderboardType;
|
||||||
|
this->relatedPlayer = relatedPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::u16string Leaderboard::ToString() const {
|
Leaderboard::~Leaderboard() {
|
||||||
std::string leaderboard;
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
leaderboard += "ADO.Result=7:1\n";
|
void Leaderboard::Clear() {
|
||||||
leaderboard += "Result.Count=1:1\n";
|
for (auto& entry : entries) for (auto ldfData : entry) delete ldfData;
|
||||||
leaderboard += "Result[0].Index=0:RowNumber\n";
|
}
|
||||||
leaderboard += "Result[0].RowCount=1:" + std::to_string(entries.size()) + "\n";
|
|
||||||
|
|
||||||
auto index = 0;
|
inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) {
|
||||||
for (const auto& entry : entries) {
|
leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString();
|
||||||
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";
|
|
||||||
|
|
||||||
// Only these minigames have a points system
|
void Leaderboard::Serialize(RakNet::BitStream* bitStream) const {
|
||||||
if (leaderboardType == Survival || leaderboardType == ShootingGallery) {
|
bitStream->Write(gameID);
|
||||||
leaderboard += "Result[0].Row[" + std::to_string(index) + "].Points=1:" + std::to_string(entry.score) + "\n";
|
bitStream->Write(infoType);
|
||||||
} else if (leaderboardType == SurvivalNS) {
|
|
||||||
leaderboard += "Result[0].Row[" + std::to_string(index) + "].Wave=1:" + std::to_string(entry.score) + "\n";
|
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);
|
||||||
}
|
}
|
||||||
|
rowNumber++;
|
||||||
leaderboard += "Result[0].Row[" + std::to_string(index) + "].name=0:" + entry.playerName + "\n";
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
void Leaderboard::QueryToLdf(std::unique_ptr<sql::ResultSet>& rows) {
|
||||||
return entries;
|
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 {
|
const std::string_view Leaderboard::GetOrdering(Leaderboard::Type leaderboardType) {
|
||||||
return gameID;
|
// 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 {
|
void Leaderboard::SetupLeaderboard(bool weekly, uint32_t resultStart, uint32_t resultEnd) {
|
||||||
return infoType;
|
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);
|
auto* player = Game::entityManager->GetEntity(relatedPlayer);
|
||||||
if (player != nullptr) {
|
if (player != nullptr) {
|
||||||
GameMessages::SendActivitySummaryLeaderboardData(targetID, this, player->GetSystemAddress());
|
GameMessages::SendActivitySummaryLeaderboardData(targetID, this, player->GetSystemAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LeaderboardManager::SaveScore(LWOOBJID playerID, uint32_t gameID, uint32_t score, uint32_t time) {
|
std::string FormatInsert(const Leaderboard::Type& type, const Score& score, const bool useUpdate) {
|
||||||
const auto* player = Game::entityManager->GetEntity(playerID);
|
std::string insertStatement;
|
||||||
if (player == nullptr)
|
if (useUpdate) {
|
||||||
return;
|
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();
|
constexpr uint16_t STRING_LENGTH = 400;
|
||||||
if (character == nullptr)
|
// Then fill in our score
|
||||||
return;
|
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());
|
std::unique_ptr<sql::PreparedStatement> query(Database::CreatePreppedStmt(lookup));
|
||||||
select->setInt(2, gameID);
|
query->setInt(1, playerID);
|
||||||
|
query->setInt(2, activityId);
|
||||||
auto any = false;
|
std::unique_ptr<sql::ResultSet> myScoreResult(query->executeQuery());
|
||||||
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::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) {
|
switch (leaderboardType) {
|
||||||
case ShootingGallery:
|
// Higher score better
|
||||||
if (score <= storedScore)
|
case Leaderboard::Type::ShootingGallery: {
|
||||||
highscore = false;
|
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
|
||||||
|
oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
|
||||||
|
oldScore.SetTertiaryScore(myScoreResult->getInt("tertiaryScore"));
|
||||||
break;
|
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) {
|
// For wins we dont care about the score, just the time, so zero out the tertiary.
|
||||||
delete select;
|
// Wins are updated later.
|
||||||
delete result;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore;
|
||||||
|
// Nimbus station has a weird leaderboard where we need a custom scoring system
|
||||||
delete select;
|
if (leaderboardType == Leaderboard::Type::SurvivalNS) {
|
||||||
delete result;
|
newHighScore = newScore.GetPrimaryScore() > oldScore.GetPrimaryScore() ||
|
||||||
|
(newScore.GetPrimaryScore() == oldScore.GetPrimaryScore() && newScore.GetSecondaryScore() < oldScore.GetSecondaryScore());
|
||||||
if (any) {
|
} else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") {
|
||||||
auto* statement = Database::CreatePreppedStmt("UPDATE leaderboard SET time = ?, score = ?, last_played=SYSDATE() WHERE character_id = ? AND game_id = ?;");
|
Score oldScoreFlipped(oldScore.GetSecondaryScore(), oldScore.GetPrimaryScore());
|
||||||
statement->setInt(1, time);
|
Score newScoreFlipped(newScore.GetSecondaryScore(), newScore.GetPrimaryScore());
|
||||||
statement->setInt(2, score);
|
newHighScore = newScoreFlipped > oldScoreFlipped;
|
||||||
statement->setUInt64(3, character->GetID());
|
}
|
||||||
statement->setInt(4, gameID);
|
if (newHighScore) {
|
||||||
statement->execute();
|
saveQuery = FormatInsert(leaderboardType, newScore, true);
|
||||||
|
}
|
||||||
delete statement;
|
|
||||||
} else {
|
} else {
|
||||||
// Note: last_played will be set to SYSDATE() by default when inserting into leaderboard
|
saveQuery = FormatInsert(leaderboardType, newScore, false);
|
||||||
auto* statement = Database::CreatePreppedStmt("INSERT INTO leaderboard (character_id, game_id, time, score) VALUES (?, ?, ?, ?);");
|
}
|
||||||
statement->setUInt64(1, character->GetID());
|
Game::logger->Log("LeaderboardManager", "save query %s %i %i", saveQuery.c_str(), playerID, activityId);
|
||||||
statement->setInt(2, gameID);
|
std::unique_ptr<sql::PreparedStatement> saveStatement(Database::CreatePreppedStmt(saveQuery));
|
||||||
statement->setInt(3, time);
|
saveStatement->setInt(1, playerID);
|
||||||
statement->setInt(4, score);
|
saveStatement->setInt(2, activityId);
|
||||||
statement->execute();
|
saveStatement->execute();
|
||||||
|
|
||||||
delete statement;
|
// 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) {
|
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) {
|
||||||
auto leaderboardType = GetLeaderboardType(gameID);
|
Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID));
|
||||||
|
leaderboard.SetupLeaderboard(weekly, resultStart, resultEnd);
|
||||||
std::string query;
|
leaderboard.Send(targetID);
|
||||||
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(uint32_t gameID, InfoType infoType, bool weekly, LWOOBJID targetID,
|
Leaderboard::Type LeaderboardManager::GetLeaderboardType(const GameID gameID) {
|
||||||
LWOOBJID playerID) {
|
auto lookup = leaderboardCache.find(gameID);
|
||||||
const auto* leaderboard = LeaderboardManager::GetLeaderboard(gameID, infoType, weekly, playerID);
|
if (lookup != leaderboardCache.end()) return lookup->second;
|
||||||
leaderboard->Send(targetID);
|
|
||||||
delete leaderboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
LeaderboardType LeaderboardManager::GetLeaderboardType(uint32_t gameID) {
|
|
||||||
auto* activitiesTable = CDClientManager::Instance().GetTable<CDActivitiesTable>();
|
auto* activitiesTable = CDClientManager::Instance().GetTable<CDActivitiesTable>();
|
||||||
std::vector<CDActivities> activities = activitiesTable->Query([=](const CDActivities& entry) {
|
std::vector<CDActivities> activities = activitiesTable->Query([gameID](const CDActivities& entry) {
|
||||||
return (entry.ActivityID == gameID);
|
return entry.ActivityID == gameID;
|
||||||
});
|
});
|
||||||
|
auto type = !activities.empty() ? static_cast<Leaderboard::Type>(activities.at(0).leaderboardType) : Leaderboard::Type::None;
|
||||||
for (const auto& activity : activities) {
|
leaderboardCache.insert_or_assign(gameID, type);
|
||||||
return static_cast<LeaderboardType>(activity.leaderboardType);
|
return type;
|
||||||
}
|
|
||||||
|
|
||||||
return LeaderboardType::None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;";
|
|
||||||
|
@ -1,80 +1,134 @@
|
|||||||
#pragma once
|
#ifndef __LEADERBOARDMANAGER__H__
|
||||||
|
#define __LEADERBOARDMANAGER__H__
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <climits>
|
|
||||||
|
#include "Singleton.h"
|
||||||
#include "dCommonVars.h"
|
#include "dCommonVars.h"
|
||||||
|
#include "LDFFormat.h"
|
||||||
|
|
||||||
struct LeaderboardEntry {
|
namespace sql {
|
||||||
uint64_t playerID;
|
class ResultSet;
|
||||||
std::string playerName;
|
|
||||||
uint32_t time;
|
|
||||||
uint32_t score;
|
|
||||||
uint32_t placement;
|
|
||||||
time_t lastPlayed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum InfoType : uint32_t {
|
namespace RakNet {
|
||||||
Top, // Top 11 all time players
|
class BitStream;
|
||||||
Standings, // Ranking of the current player
|
|
||||||
Friends // Ranking between friends
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum LeaderboardType : uint32_t {
|
class Score {
|
||||||
ShootingGallery,
|
public:
|
||||||
Racing,
|
Score() {
|
||||||
MonumentRace,
|
primaryScore = 0;
|
||||||
FootRace,
|
secondaryScore = 0;
|
||||||
Survival = 5,
|
tertiaryScore = 0;
|
||||||
SurvivalNS = 6,
|
}
|
||||||
None = UINT_MAX
|
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 {
|
class Leaderboard {
|
||||||
public:
|
public:
|
||||||
Leaderboard(uint32_t gameID, uint32_t infoType, bool weekly, std::vector<LeaderboardEntry> entries,
|
|
||||||
LWOOBJID relatedPlayer = LWOOBJID_EMPTY, LeaderboardType = None);
|
// Enums for leaderboards
|
||||||
std::vector<LeaderboardEntry> GetEntries();
|
enum InfoType : uint32_t {
|
||||||
[[nodiscard]] std::u16string ToString() const;
|
Top, // Top 11 all time players
|
||||||
[[nodiscard]] uint32_t GetGameID() const;
|
MyStanding, // Ranking of the current player
|
||||||
[[nodiscard]] uint32_t GetInfoType() const;
|
Friends // Ranking between friends
|
||||||
void Send(LWOOBJID targetID) const;
|
};
|
||||||
|
|
||||||
|
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:
|
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;
|
LWOOBJID relatedPlayer;
|
||||||
uint32_t gameID;
|
GameID gameID;
|
||||||
uint32_t infoType;
|
InfoType infoType;
|
||||||
LeaderboardType leaderboardType;
|
Leaderboard::Type leaderboardType;
|
||||||
bool weekly;
|
bool weekly;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LeaderboardManager {
|
namespace LeaderboardManager {
|
||||||
public:
|
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);
|
||||||
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;
|
|
||||||
|
|
||||||
// Modified 12/12/2021: Existing queries were renamed to be more descriptive.
|
void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0);
|
||||||
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;
|
|
||||||
|
|
||||||
// Added 12/12/2021: Queries dictated by time are needed for certain minigames.
|
Leaderboard::Type GetLeaderboardType(const GameID gameID);
|
||||||
static const std::string topPlayersTimeQuery;
|
extern std::map<GameID, Leaderboard::Type> leaderboardCache;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif //!__LEADERBOARDMANAGER__H__
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include "dConfig.h"
|
#include "dConfig.h"
|
||||||
#include "Loot.h"
|
#include "Loot.h"
|
||||||
#include "eMissionTaskType.h"
|
#include "eMissionTaskType.h"
|
||||||
|
#include "LeaderboardManager.h"
|
||||||
#include "dZoneManager.h"
|
#include "dZoneManager.h"
|
||||||
#include "CDActivitiesTable.h"
|
#include "CDActivitiesTable.h"
|
||||||
|
|
||||||
@ -367,9 +368,7 @@ void RacingControlComponent::HandleMessageBoxResponse(Entity* player, int32_t bu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (id == "rewardButton") {
|
if (id == "rewardButton") {
|
||||||
if (data->collectedRewards) {
|
if (data->collectedRewards) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data->collectedRewards = true;
|
data->collectedRewards = true;
|
||||||
|
|
||||||
@ -839,6 +838,7 @@ void RacingControlComponent::Update(float deltaTime) {
|
|||||||
"Completed time %llu, %llu",
|
"Completed time %llu, %llu",
|
||||||
raceTime, raceTime * 1000);
|
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
|
// Entire race time
|
||||||
missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME);
|
missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME);
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ ScriptedActivityComponent::ScriptedActivityComponent(Entity* parent, int activit
|
|||||||
|
|
||||||
for (CDActivities activity : activities) {
|
for (CDActivities activity : activities) {
|
||||||
m_ActivityInfo = activity;
|
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.minTeamSize = 1;
|
||||||
m_ActivityInfo.minTeams = 1;
|
m_ActivityInfo.minTeams = 1;
|
||||||
}
|
}
|
||||||
|
@ -1645,20 +1645,7 @@ void GameMessages::SendActivitySummaryLeaderboardData(const LWOOBJID& objectID,
|
|||||||
bitStream.Write(objectID);
|
bitStream.Write(objectID);
|
||||||
bitStream.Write(eGameMessageType::SEND_ACTIVITY_SUMMARY_LEADERBOARD_DATA);
|
bitStream.Write(eGameMessageType::SEND_ACTIVITY_SUMMARY_LEADERBOARD_DATA);
|
||||||
|
|
||||||
bitStream.Write(leaderboard->GetGameID());
|
leaderboard->Serialize(&bitStream);
|
||||||
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();
|
|
||||||
|
|
||||||
SEND_PACKET;
|
SEND_PACKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1666,8 +1653,8 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream
|
|||||||
int32_t gameID = 0;
|
int32_t gameID = 0;
|
||||||
if (inStream->ReadBit()) inStream->Read(gameID);
|
if (inStream->ReadBit()) inStream->Read(gameID);
|
||||||
|
|
||||||
int32_t queryType = 1;
|
Leaderboard::InfoType queryType = Leaderboard::InfoType::MyStanding;
|
||||||
if (inStream->ReadBit()) inStream->Read(queryType);
|
if (inStream->ReadBit()) inStream->Read<Leaderboard::InfoType>(queryType);
|
||||||
|
|
||||||
int32_t resultsEnd = 10;
|
int32_t resultsEnd = 10;
|
||||||
if (inStream->ReadBit()) inStream->Read(resultsEnd);
|
if (inStream->ReadBit()) inStream->Read(resultsEnd);
|
||||||
@ -1680,9 +1667,7 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream
|
|||||||
|
|
||||||
bool weekly = inStream->ReadBit();
|
bool weekly = inStream->ReadBit();
|
||||||
|
|
||||||
const auto* leaderboard = LeaderboardManager::GetLeaderboard(gameID, (InfoType)queryType, weekly, entity->GetObjectID());
|
LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID(), resultsStart, resultsEnd);
|
||||||
SendActivitySummaryLeaderboardData(entity->GetObjectID(), leaderboard, sysAddr);
|
|
||||||
delete leaderboard;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream* inStream, Entity* entity) {
|
void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream* inStream, Entity* entity) {
|
||||||
|
@ -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,
|
void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) {
|
||||||
int32_t param3) {
|
|
||||||
auto* scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>();
|
auto* scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>();
|
||||||
if (scriptedActivityComponent == nullptr)
|
if (scriptedActivityComponent == nullptr) return;
|
||||||
return;
|
|
||||||
|
|
||||||
auto* data = scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID());
|
auto* data = scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID());
|
||||||
if (data == nullptr)
|
if (data == nullptr) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (args == "course_cancel") {
|
if (args == "course_cancel") {
|
||||||
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"cancel_timer", 0, 0,
|
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);
|
Game::entityManager->SerializeEntity(self);
|
||||||
LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(),
|
LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), static_cast<float>(finish));
|
||||||
0, (uint32_t)finish);
|
|
||||||
|
|
||||||
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard",
|
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard",
|
||||||
scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(),
|
scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(),
|
||||||
|
@ -73,24 +73,25 @@ void ActivityManager::StopActivity(Entity* self, const LWOOBJID playerID, const
|
|||||||
|
|
||||||
LootGenerator::Instance().GiveActivityLoot(player, self, gameID, CalculateActivityRating(self, playerID));
|
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) {
|
if (sac != nullptr) {
|
||||||
sac->PlayerRemove(player->GetObjectID());
|
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) {
|
bool ActivityManager::TakeActivityCost(const Entity* self, const LWOOBJID playerID) {
|
||||||
auto* sac = self->GetComponent<ScriptedActivityComponent>();
|
auto* sac = self->GetComponent<ScriptedActivityComponent>();
|
||||||
if (sac == nullptr)
|
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) {
|
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,
|
void ActivityManager::ActivityTimerStart(Entity* self, const std::string& timerName, const float_t updateInterval,
|
||||||
|
@ -18,6 +18,7 @@ public:
|
|||||||
static bool TakeActivityCost(const Entity* self, LWOOBJID playerID);
|
static bool TakeActivityCost(const Entity* self, LWOOBJID playerID);
|
||||||
static uint32_t GetActivityID(const Entity* self);
|
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 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);
|
virtual uint32_t CalculateActivityRating(Entity* self, LWOOBJID playerID);
|
||||||
static void GetLeaderboardData(Entity* self, LWOOBJID playerID, uint32_t activityID, uint32_t numResults = 0);
|
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;
|
// void FreezePlayer(Entity *self, const LWOOBJID playerID, const bool state) const;
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
#include "eMissionState.h"
|
#include "eMissionState.h"
|
||||||
#include "MissionComponent.h"
|
#include "MissionComponent.h"
|
||||||
#include "Character.h"
|
#include "Character.h"
|
||||||
|
#include "Game.h"
|
||||||
|
#include "dConfig.h"
|
||||||
|
|
||||||
void BaseSurvivalServer::SetGameVariables(Entity* self) {
|
void BaseSurvivalServer::SetGameVariables(Entity* self) {
|
||||||
this->constants = std::move(GetConstants());
|
this->constants = std::move(GetConstants());
|
||||||
@ -354,6 +356,7 @@ void BaseSurvivalServer::GameOver(Entity* self) {
|
|||||||
|
|
||||||
const auto score = GetActivityValue(self, playerID, 0);
|
const auto score = GetActivityValue(self, playerID, 0);
|
||||||
const auto time = GetActivityValue(self, playerID, 1);
|
const auto time = GetActivityValue(self, playerID, 1);
|
||||||
|
SaveScore(self, playerID, score, time);
|
||||||
|
|
||||||
GameMessages::SendNotifyClientZoneObject(self->GetObjectID(), u"Update_ScoreBoard", time, 0,
|
GameMessages::SendNotifyClientZoneObject(self->GetObjectID(), u"Update_ScoreBoard", time, 0,
|
||||||
playerID, std::to_string(score), UNASSIGNED_SYSTEM_ADDRESS);
|
playerID, std::to_string(score), UNASSIGNED_SYSTEM_ADDRESS);
|
||||||
|
@ -378,6 +378,7 @@ void BaseWavesServer::GameOver(Entity* self, bool won) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StopActivity(self, playerID, wave, time, score);
|
StopActivity(self, playerID, wave, time, score);
|
||||||
|
SaveScore(self, playerID, wave, time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#include "BaseFootRaceManager.h"
|
#include "BaseFootRaceManager.h"
|
||||||
#include "EntityManager.h"
|
#include "EntityManager.h"
|
||||||
#include "Character.h"
|
#include "Character.h"
|
||||||
|
#include "Entity.h"
|
||||||
|
|
||||||
void BaseFootRaceManager::OnStartup(Entity* self) {
|
void BaseFootRaceManager::OnStartup(Entity* self) {
|
||||||
// TODO: Add to FootRaceStarter group
|
// TODO: Add to FootRaceStarter group
|
||||||
@ -40,6 +41,7 @@ void BaseFootRaceManager::OnFireEventServerSide(Entity* self, Entity* sender, st
|
|||||||
}
|
}
|
||||||
|
|
||||||
StopActivity(self, player->GetObjectID(), 0, param1);
|
StopActivity(self, player->GetObjectID(), 0, param1);
|
||||||
|
SaveScore(self, player->GetObjectID(), static_cast<float>(param1), static_cast<float>(param2), static_cast<float>(param3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ void SGCannon::OnMessageBoxResponse(Entity* self, Entity* sender, int32_t button
|
|||||||
if (IsPlayerInActivity(self, player->GetObjectID())) return;
|
if (IsPlayerInActivity(self, player->GetObjectID())) return;
|
||||||
self->SetNetworkVar<bool>(ClearVariable, true);
|
self->SetNetworkVar<bool>(ClearVariable, true);
|
||||||
StartGame(self);
|
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());
|
RemovePlayer(player->GetObjectID());
|
||||||
UpdatePlayer(self, player->GetObjectID(), true);
|
UpdatePlayer(self, player->GetObjectID(), true);
|
||||||
} else if (button == 1 && identifier == u"Shooting_Gallery_Exit") {
|
} 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));
|
auto* player = Game::entityManager->GetEntity(self->GetVar<LWOOBJID>(PlayerIDVariable));
|
||||||
if (player != nullptr) {
|
if (player != nullptr) {
|
||||||
GetLeaderboardData(self, player->GetObjectID(), GetActivityID(self));
|
GetLeaderboardData(self, player->GetObjectID(), GetActivityID(self), 1);
|
||||||
Game::logger->Log("SGCannon", "Sending ActivityStart");
|
Game::logger->Log("SGCannon", "Sending ActivityStart");
|
||||||
GameMessages::SendActivityStart(self->GetObjectID(), player->GetSystemAddress());
|
GameMessages::SendActivityStart(self->GetObjectID(), player->GetSystemAddress());
|
||||||
|
|
||||||
@ -436,8 +436,8 @@ void SGCannon::RemovePlayer(LWOOBJID playerID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SGCannon::OnRequestActivityExit(Entity* self, LWOOBJID player, bool canceled){
|
void SGCannon::OnRequestActivityExit(Entity* self, LWOOBJID player, bool canceled) {
|
||||||
if (canceled){
|
if (canceled) {
|
||||||
StopGame(self, canceled);
|
StopGame(self, canceled);
|
||||||
RemovePlayer(player);
|
RemovePlayer(player);
|
||||||
}
|
}
|
||||||
@ -546,7 +546,7 @@ void SGCannon::StopGame(Entity* self, bool cancel) {
|
|||||||
|
|
||||||
// The player won, store all the score and send rewards
|
// The player won, store all the score and send rewards
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
auto percentage = 0;
|
int32_t percentage = 0.0f;
|
||||||
auto misses = self->GetVar<uint32_t>(MissesVariable);
|
auto misses = self->GetVar<uint32_t>(MissesVariable);
|
||||||
auto fired = self->GetVar<uint32_t>(ShotsFiredVariable);
|
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));
|
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);
|
StopActivity(self, player->GetObjectID(), self->GetVar<uint32_t>(TotalScoreVariable), self->GetVar<uint32_t>(MaxStreakVariable), percentage);
|
||||||
self->SetNetworkVar<bool>(AudioFinalWaveDoneVariable, true);
|
self->SetNetworkVar<bool>(AudioFinalWaveDoneVariable, true);
|
||||||
|
|
||||||
@ -578,17 +581,6 @@ void SGCannon::StopGame(Entity* self, bool cancel) {
|
|||||||
self->SetNetworkVar<std::u16string>(u"UI_Rewards",
|
self->SetNetworkVar<std::u16string>(u"UI_Rewards",
|
||||||
GeneralUtils::to_u16string(self->GetVar<uint32_t>(TotalScoreVariable)) + u"_0_0_0_0_0_0"
|
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());
|
GameMessages::SendActivityStop(self->GetObjectID(), false, cancel, player->GetSystemAddress());
|
||||||
|
18
migrations/dlu/9_Update_Leaderboard_Storage.sql
Normal file
18
migrations/dlu/9_Update_Leaderboard_Storage.sql
Normal 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();
|
Loading…
Reference in New Issue
Block a user