Compare commits

...

10 Commits

Author SHA1 Message Date
Aaron Kimbrell
a7a4288e45 refactor: streamline reputation configuration loading in PropertyManagementComponent 2026-04-22 10:57:19 -05:00
copilot-swe-agent[bot]
ce7b771a7c test: add property and character reputation unit tests
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/603b2808-f042-447c-ba49-e4a8d9f87856

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-04-05 08:40:50 +00:00
Aaron Kimbrell
6ad65fcfca Update dGame/dComponents/PropertyManagementComponent.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 03:31:36 -05:00
copilot-swe-agent[bot]
453624494c fix: cast LWOOBJID to uint64_t for %llu format specifier in CharacterReputationMigration
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/63bf6fda-1e52-4ad7-a08c-3535f744ae5d

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-04-05 08:26:51 +00:00
Aaron Kimbrell
6a5fe599ad Update dGame/dComponents/PropertyManagementComponent.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 03:25:09 -05:00
Aaron Kimbrell
bdb06b4706 Update dGame/dComponents/PropertyManagementComponent.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 03:24:37 -05:00
Aaron Kimbrell
e2b534501c Update dGame/dComponents/PropertyManagementComponent.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 03:24:26 -05:00
copilot-swe-agent[bot]
b058526e76 fix: use Character::GetID() in SetReputation to match DB schema key
Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/4cb00fe2-a85e-45f6-95c7-1ac972e244bc

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
2026-04-05 08:23:13 +00:00
Aaron Kimbrell
7cb34ffca2 Update dGame/dComponents/CharacterComponent.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 03:21:56 -05:00
Aaron Kimbrell
e45e860ec0 feat: implement character and property reputation system
- Added ICharacterReputation and IPropertyReputationContribution interfaces for managing character and property reputations.
- Implemented MySQL and SQLite database methods for getting and setting character reputations.
- Created migration scripts for character and property reputation tables in both MySQL and SQLite.
- Updated CharacterComponent to retrieve and set character reputation.
- Enhanced PropertyManagementComponent to manage property reputation and contributions.
- Added methods for handling reputation contributions and decay.
- Introduced CharacterReputationMigration to migrate existing character reputations from XML to the database.
2026-04-05 02:56:51 -05:00
32 changed files with 732 additions and 13 deletions

3
.gitignore vendored
View File

@@ -126,3 +126,6 @@ docker-compose.override.yml
# CMake scripts # CMake scripts
!cmake/* !cmake/*
!cmake/toolchains/* !cmake/toolchains/*
.mcp.json
.claude/

View File

@@ -357,4 +357,18 @@ namespace GeneralUtils {
return value - modulo; return value - modulo;
} }
} }
//! Returns the current UTC date as a string in "YYYY-MM-DD" format
inline std::string GetCurrentUTCDate() {
const auto now = std::time(nullptr);
std::tm utcTime{};
#ifdef _MSC_VER
gmtime_s(&utcTime, &now);
#else
gmtime_r(&now, &utcTime);
#endif
char buf[11];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &utcTime);
return std::string(buf);
}
} }

View File

@@ -1,7 +1,7 @@
add_subdirectory(CDClientDatabase) add_subdirectory(CDClientDatabase)
add_subdirectory(GameDatabase) add_subdirectory(GameDatabase)
add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp") add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp" "CharacterReputationMigration.cpp")
add_custom_target(conncpp_dylib add_custom_target(conncpp_dylib
${CMAKE_COMMAND} -E copy $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR}) ${CMAKE_COMMAND} -E copy $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR})

View File

@@ -0,0 +1,41 @@
#include "CharacterReputationMigration.h"
#include "Database.h"
#include "Logger.h"
#include "tinyxml2.h"
uint32_t CharacterReputationMigration::Run() {
uint32_t charactersMigrated = 0;
const auto allCharIds = Database::Get()->GetAllCharacterIds();
const bool previousCommitValue = Database::Get()->GetAutoCommit();
Database::Get()->SetAutoCommit(false);
for (const auto charId : allCharIds) {
const auto xmlStr = Database::Get()->GetCharacterXml(charId);
if (xmlStr.empty()) continue;
tinyxml2::XMLDocument doc;
if (doc.Parse(xmlStr.c_str(), xmlStr.size()) != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse XML for character %llu during reputation migration", static_cast<uint64_t>(charId));
continue;
}
auto* obj = doc.FirstChildElement("obj");
if (!obj) continue;
auto* character = obj->FirstChildElement("char");
if (!character) continue;
int64_t reputation = 0;
if (character->QueryInt64Attribute("rpt", &reputation) == tinyxml2::XML_SUCCESS && reputation != 0) {
Database::Get()->SetCharacterReputation(charId, reputation);
charactersMigrated++;
}
}
Database::Get()->Commit();
Database::Get()->SetAutoCommit(previousCommitValue);
return charactersMigrated;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
namespace CharacterReputationMigration {
uint32_t Run();
};

View File

@@ -25,6 +25,8 @@
#include "IAccountsRewardCodes.h" #include "IAccountsRewardCodes.h"
#include "IBehaviors.h" #include "IBehaviors.h"
#include "IUgcModularBuild.h" #include "IUgcModularBuild.h"
#include "ICharacterReputation.h"
#include "IPropertyReputationContribution.h"
#ifdef _DEBUG #ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0) # define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
@@ -38,7 +40,8 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild { public IBehaviors, public IUgcModularBuild,
public ICharacterReputation, public IPropertyReputationContribution {
public: public:
virtual ~GameDatabase() = default; virtual ~GameDatabase() = default;
// TODO: These should be made private. // TODO: These should be made private.

View File

@@ -33,6 +33,9 @@ public:
// Get the character ids for the given account. // Get the character ids for the given account.
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0; virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
// Get all character ids.
virtual std::vector<LWOOBJID> GetAllCharacterIds() = 0;
// Insert a new character into the database. // Insert a new character into the database.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0; virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;

View File

@@ -0,0 +1,14 @@
#ifndef __ICHARACTERREPUTATION__H__
#define __ICHARACTERREPUTATION__H__
#include <cstdint>
#include "dCommonVars.h"
class ICharacterReputation {
public:
virtual int64_t GetCharacterReputation(const LWOOBJID charId) = 0;
virtual void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) = 0;
};
#endif //!__ICHARACTERREPUTATION__H__

View File

@@ -0,0 +1,30 @@
#ifndef __IPROPERTYREPUTATIONCONTRIBUTION__H__
#define __IPROPERTYREPUTATIONCONTRIBUTION__H__
#include <cstdint>
#include <string>
#include <vector>
#include "dCommonVars.h"
class IPropertyReputationContribution {
public:
struct ContributionInfo {
LWOOBJID playerId{};
uint32_t reputationGained{};
};
// Get today's reputation contributions for a property.
virtual std::vector<ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) = 0;
// Upsert a player's reputation contribution for a property on a given date.
virtual void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) = 0;
// Update the total reputation on a property.
virtual void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) = 0;
};
#endif //!__IPROPERTYREPUTATIONCONTRIBUTION__H__

View File

@@ -139,6 +139,15 @@ public:
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override; void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override; void DeleteUgcBuild(const LWOOBJID bigId) override;
std::vector<LWOOBJID> GetAllCharacterIds() override;
int64_t GetCharacterReputation(const LWOOBJID charId) override;
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override;
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override;
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override;
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override;
uint32_t GetAccountCount() override; uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override; bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override; std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

@@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"Servers.cpp" "Servers.cpp"
"Ugc.cpp" "Ugc.cpp"
"UgcModularBuild.cpp" "UgcModularBuild.cpp"
"CharacterReputation.cpp"
"PropertyReputationContribution.cpp"
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -52,6 +52,18 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
return toReturn; return toReturn;
} }
std::vector<LWOOBJID> MySQLDatabase::GetAllCharacterIds() {
auto result = ExecuteSelect("SELECT id FROM charinfo;");
std::vector<LWOOBJID> toReturn;
toReturn.reserve(result->rowsCount());
while (result->next()) {
toReturn.push_back(result->getInt64("id"));
}
return toReturn;
}
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) { void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert( ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)", "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",

View File

@@ -0,0 +1,17 @@
#include "MySQLDatabase.h"
int64_t MySQLDatabase::GetCharacterReputation(const LWOOBJID charId) {
auto result = ExecuteSelect("SELECT reputation FROM character_reputation WHERE character_id = ? LIMIT 1;", charId);
if (!result->next()) {
return 0;
}
return result->getInt64("reputation");
}
void MySQLDatabase::SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) {
ExecuteInsert(
"INSERT INTO character_reputation (character_id, reputation) VALUES (?, ?) ON DUPLICATE KEY UPDATE reputation = ?;",
charId, reputation, reputation);
}

View File

@@ -0,0 +1,30 @@
#include "MySQLDatabase.h"
std::vector<IPropertyReputationContribution::ContributionInfo> MySQLDatabase::GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) {
auto result = ExecuteSelect(
"SELECT player_id, reputation_gained FROM property_reputation_contribution WHERE property_id = ? AND contribution_date = ?;",
propertyId, date);
std::vector<IPropertyReputationContribution::ContributionInfo> contributions;
while (result->next()) {
IPropertyReputationContribution::ContributionInfo info;
info.playerId = result->getUInt64("player_id");
info.reputationGained = static_cast<uint32_t>(result->getUInt("reputation_gained"));
contributions.push_back(info);
}
return contributions;
}
void MySQLDatabase::UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) {
ExecuteInsert(
"INSERT INTO property_reputation_contribution (property_id, player_id, contribution_date, reputation_gained) "
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE reputation_gained = ?;",
propertyId, playerId, date, reputationGained, reputationGained);
}
void MySQLDatabase::UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) {
ExecuteUpdate("UPDATE properties SET reputation = ? WHERE id = ?;", reputation, propertyId);
}

View File

@@ -123,6 +123,15 @@ public:
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override; void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override; void DeleteUgcBuild(const LWOOBJID bigId) override;
std::vector<LWOOBJID> GetAllCharacterIds() override;
int64_t GetCharacterReputation(const LWOOBJID charId) override;
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override;
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override;
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override;
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override;
uint32_t GetAccountCount() override; uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override; bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override; std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

@@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"Servers.cpp" "Servers.cpp"
"Ugc.cpp" "Ugc.cpp"
"UgcModularBuild.cpp" "UgcModularBuild.cpp"
"CharacterReputation.cpp"
"PropertyReputationContribution.cpp"
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -55,6 +55,18 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
return toReturn; return toReturn;
} }
std::vector<LWOOBJID> SQLiteDatabase::GetAllCharacterIds() {
auto [_, result] = ExecuteSelect("SELECT id FROM charinfo;");
std::vector<LWOOBJID> toReturn;
while (!result.eof()) {
toReturn.push_back(result.getInt64Field("id"));
result.nextRow();
}
return toReturn;
}
void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) { void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert( ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))", "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))",

View File

@@ -0,0 +1,17 @@
#include "SQLiteDatabase.h"
int64_t SQLiteDatabase::GetCharacterReputation(const LWOOBJID charId) {
auto [_, result] = ExecuteSelect("SELECT reputation FROM character_reputation WHERE character_id = ? LIMIT 1;", charId);
if (result.eof()) {
return 0;
}
return result.getInt64Field("reputation");
}
void SQLiteDatabase::SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) {
ExecuteInsert(
"INSERT OR REPLACE INTO character_reputation (character_id, reputation) VALUES (?, ?);",
charId, reputation);
}

View File

@@ -0,0 +1,31 @@
#include "SQLiteDatabase.h"
std::vector<IPropertyReputationContribution::ContributionInfo> SQLiteDatabase::GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) {
auto [_, result] = ExecuteSelect(
"SELECT player_id, reputation_gained FROM property_reputation_contribution WHERE property_id = ? AND contribution_date = ?;",
propertyId, date);
std::vector<IPropertyReputationContribution::ContributionInfo> contributions;
while (!result.eof()) {
IPropertyReputationContribution::ContributionInfo info;
info.playerId = result.getInt64Field("player_id");
info.reputationGained = static_cast<uint32_t>(result.getIntField("reputation_gained"));
contributions.push_back(info);
result.nextRow();
}
return contributions;
}
void SQLiteDatabase::UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) {
ExecuteInsert(
"INSERT OR REPLACE INTO property_reputation_contribution (property_id, player_id, contribution_date, reputation_gained) "
"VALUES (?, ?, ?, ?);",
propertyId, playerId, date, reputationGained);
}
void SQLiteDatabase::UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) {
ExecuteUpdate("UPDATE properties SET reputation = ? WHERE id = ?;", reputation, propertyId);
}

View File

@@ -1,9 +1,12 @@
#ifndef TESTSQLDATABASE_H #ifndef TESTSQLDATABASE_H
#define TESTSQLDATABASE_H #define TESTSQLDATABASE_H
#include <map>
#include <unordered_map>
#include "GameDatabase.h" #include "GameDatabase.h"
class TestSQLDatabase : public GameDatabase { class TestSQLDatabase : public GameDatabase {
public:
void Connect() override; void Connect() override;
void Destroy(std::string source = "") override; void Destroy(std::string source = "") override;
@@ -103,11 +106,54 @@ class TestSQLDatabase : public GameDatabase {
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override {}; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override {};
void DeleteUgcBuild(const LWOOBJID bigId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {};
uint32_t GetAccountCount() override { return 0; }; uint32_t GetAccountCount() override { return 0; };
std::vector<LWOOBJID> GetAllCharacterIds() override { return {}; };
int64_t GetCharacterReputation(const LWOOBJID charId) override {
auto it = m_CharacterReputation.find(charId);
return it != m_CharacterReputation.end() ? it->second : 0;
};
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override {
m_CharacterReputation[charId] = reputation;
};
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override {
const auto key = std::make_pair(propertyId, date);
if (m_PropertyContributions.contains(key)) {
return m_PropertyContributions.at(key);
}
return {};
};
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override {
const auto key = std::make_pair(propertyId, date);
auto& entries = m_PropertyContributions[key];
for (auto& entry : entries) {
if (entry.playerId == playerId) {
entry.reputationGained = reputationGained;
return;
}
}
entries.push_back({ playerId, reputationGained });
};
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override {
m_PropertyReputation[propertyId] = reputation;
};
// Test helper: retrieve the property reputation stored via UpdatePropertyReputation.
uint32_t GetPropertyReputation(const LWOOBJID propertyId) const {
if (m_PropertyReputation.contains(propertyId)) {
return m_PropertyReputation.at(propertyId);
}
return 0;
}
bool IsNameInUse(const std::string_view name) override { return false; }; bool IsNameInUse(const std::string_view name) override { return false; };
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; } std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; }
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override { return {}; } std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override { return {}; }
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override { return {}; } std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override { return {}; }
private:
std::unordered_map<LWOOBJID, int64_t> m_CharacterReputation;
std::unordered_map<LWOOBJID, uint32_t> m_PropertyReputation;
std::map<std::pair<LWOOBJID, std::string>, std::vector<IPropertyReputationContribution::ContributionInfo>> m_PropertyContributions;
}; };
#endif //!TESTSQLDATABASE_H #endif //!TESTSQLDATABASE_H

View File

@@ -8,6 +8,7 @@
#include "Logger.h" #include "Logger.h"
#include "BinaryPathFinder.h" #include "BinaryPathFinder.h"
#include "ModelNormalizeMigration.h" #include "ModelNormalizeMigration.h"
#include "CharacterReputationMigration.h"
#include <fstream> #include <fstream>
@@ -49,6 +50,7 @@ void MigrationRunner::RunMigrations() {
bool runNormalizeMigrations = false; bool runNormalizeMigrations = false;
bool runNormalizeAfterFirstPartMigrations = false; bool runNormalizeAfterFirstPartMigrations = false;
bool runBrickBuildsNotOnGrid = false; bool runBrickBuildsNotOnGrid = false;
bool runCharacterReputationMigration = false;
for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) {
auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry);
@@ -67,6 +69,9 @@ void MigrationRunner::RunMigrations() {
runNormalizeAfterFirstPartMigrations = true; runNormalizeAfterFirstPartMigrations = true;
} else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) { } else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) {
runBrickBuildsNotOnGrid = true; runBrickBuildsNotOnGrid = true;
} else if (migration.name.ends_with("_character_reputation.sql")) {
runCharacterReputationMigration = true;
finalSQL.append(migration.data.c_str());
} else { } else {
finalSQL.append(migration.data.c_str()); finalSQL.append(migration.data.c_str());
} }
@@ -74,7 +79,7 @@ void MigrationRunner::RunMigrations() {
Database::Get()->InsertMigration(migration.name); Database::Get()->InsertMigration(migration.name);
} }
if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations && !runBrickBuildsNotOnGrid) { if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations && !runBrickBuildsNotOnGrid && !runCharacterReputationMigration) {
LOG("Server database is up to date."); LOG("Server database is up to date.");
return; return;
} }
@@ -110,6 +115,11 @@ void MigrationRunner::RunMigrations() {
if (runBrickBuildsNotOnGrid) { if (runBrickBuildsNotOnGrid) {
ModelNormalizeMigration::RunBrickBuildGrid(); ModelNormalizeMigration::RunBrickBuildGrid();
} }
if (runCharacterReputationMigration) {
uint32_t charactersMigrated = CharacterReputationMigration::Run();
LOG("%u characters had their reputation migrated from XML to the database.", charactersMigrated);
}
} }
void MigrationRunner::RunSQLiteMigrations() { void MigrationRunner::RunSQLiteMigrations() {

View File

@@ -42,7 +42,7 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID
m_EditorEnabled = false; m_EditorEnabled = false;
m_EditorLevel = m_GMLevel; m_EditorLevel = m_GMLevel;
m_Reputation = 0; m_Reputation = Database::Get()->GetCharacterReputation(m_Character->GetID());
m_CurrentActivity = eGameActivity::NONE; m_CurrentActivity = eGameActivity::NONE;
m_CountryCode = 0; m_CountryCode = 0;
@@ -249,6 +249,12 @@ void CharacterComponent::SetPvpEnabled(const bool value) {
m_PvpEnabled = value; m_PvpEnabled = value;
} }
void CharacterComponent::SetReputation(int64_t newValue) {
m_Reputation = newValue;
Database::Get()->SetCharacterReputation(m_Character->GetID(), m_Reputation);
GameMessages::SendUpdateReputation(m_Parent->GetObjectID(), m_Reputation, m_Parent->GetSystemAddress());
}
void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) { void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) {
m_DirtyGMInfo = true; m_DirtyGMInfo = true;
if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true; if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true;
@@ -263,10 +269,6 @@ void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
LOG("Failed to find char tag while loading XML!"); LOG("Failed to find char tag while loading XML!");
return; return;
} }
if (character->QueryAttribute("rpt", &m_Reputation) == tinyxml2::XML_NO_ATTRIBUTE) {
SetReputation(0);
}
auto* vl = character->FirstChildElement("vl"); auto* vl = character->FirstChildElement("vl");
if (vl) LoadVisitedLevelsXml(*vl); if (vl) LoadVisitedLevelsXml(*vl);
character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]); character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]);
@@ -408,8 +410,6 @@ void CharacterComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]); if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]);
character->SetAttribute("ls", m_Uscore); character->SetAttribute("ls", m_Uscore);
// Custom attribute to keep track of reputation.
character->SetAttribute("rpt", GetReputation());
character->SetAttribute("stt", StatisticsToString().c_str()); character->SetAttribute("stt", StatisticsToString().c_str());
// Set the zone statistics of the form <zs><s/> ... <s/></zs> // Set the zone statistics of the form <zs><s/> ... <s/></zs>

View File

@@ -156,7 +156,7 @@ public:
* Sets the lifetime reputation of the character to newValue * Sets the lifetime reputation of the character to newValue
* @param newValue the value to set reputation to * @param newValue the value to set reputation to
*/ */
void SetReputation(int64_t newValue) { m_Reputation = newValue; }; void SetReputation(int64_t newValue);
/** /**
* Sets the current value of PvP combat being enabled * Sets the current value of PvP combat being enabled

View File

@@ -27,6 +27,9 @@
#include "CppScripts.h" #include "CppScripts.h"
#include <ranges> #include <ranges>
#include "dConfig.h" #include "dConfig.h"
#include "PositionUpdate.h"
#include "GeneralUtils.h"
#include "User.h"
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
@@ -75,9 +78,48 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const i
this->reputation = propertyInfo->reputation; this->reputation = propertyInfo->reputation;
Load(); Load();
// Cache owner's account ID for same-account reputation exclusion
if (this->owner != LWOOBJID_EMPTY) {
auto ownerCharId = this->owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
auto charInfo = Database::Get()->GetCharacterInfo(ownerCharId);
if (charInfo) {
m_OwnerAccountId = charInfo->accountId;
}
}
// Load reputation config
m_RepInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_interval")).value_or(60.0f);
m_RepDailyCap = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_daily_cap")).value_or(50);
m_RepPerTick = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_per_tick")).value_or(1);
m_RepMultiplier = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_multiplier")).value_or(1.0f);
m_RepVelocityThreshold = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_velocity_threshold")).value_or(0.5f);
m_RepSaveInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_save_interval")).value_or(300.0f);
m_RepDecayRate = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_rate")).value_or(0.0f);
m_RepDecayInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_interval")).value_or(86400.0f);
m_RepDecayMinimum = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_decay_minimum")).value_or(0);
// Load daily reputation contributions and subscribe to position updates
m_CurrentDate = GeneralUtils::GetCurrentUTCDate();
LoadDailyContributions();
Entity::OnPlayerPositionUpdate += [](Entity* player, const PositionUpdate& update) {
auto* propertyManagementComponent = PropertyManagementComponent::instance;
if (propertyManagementComponent == nullptr) {
return;
}
propertyManagementComponent->OnPlayerPositionUpdateHandler(player, update);
};
} }
} }
PropertyManagementComponent::~PropertyManagementComponent() {
if (instance == this) {
instance = nullptr;
}
SaveReputation();
}
LWOOBJID PropertyManagementComponent::GetOwnerId() const { LWOOBJID PropertyManagementComponent::GetOwnerId() const {
return owner; return owner;
} }
@@ -832,3 +874,126 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess
modelComponent->OnChatMessageReceived(sMessage); modelComponent->OnChatMessageReceived(sMessage);
} }
} }
void PropertyManagementComponent::Update(float deltaTime) {
// Check for day rollover
const auto currentDate = GeneralUtils::GetCurrentUTCDate();
if (currentDate != m_CurrentDate) {
if (m_ReputationDirty) {
LOG_DEBUG("Saving dirty reputation data before daily rollover for property %llu", static_cast<unsigned long long>(propertyId));
SaveReputation();
}
m_CurrentDate = currentDate;
m_PlayerActivity.clear();
}
// Periodic reputation save
m_ReputationSaveTimer += deltaTime;
if (m_ReputationSaveTimer >= m_RepSaveInterval && m_ReputationDirty) {
SaveReputation();
m_ReputationSaveTimer = 0.0f;
}
// Property reputation decay
if (m_RepDecayRate > 0.0f && owner != LWOOBJID_EMPTY) {
m_DecayTimer += deltaTime;
if (m_DecayTimer >= m_RepDecayInterval) {
m_DecayTimer = 0.0f;
if (reputation > m_RepDecayMinimum) {
const auto loss = static_cast<uint32_t>(m_RepDecayRate);
reputation = (reputation > m_RepDecayMinimum + loss) ? reputation - loss : m_RepDecayMinimum;
m_ReputationDirty = true;
}
}
}
}
void PropertyManagementComponent::OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update) {
if (owner == LWOOBJID_EMPTY) return;
if (propertyId == LWOOBJID_EMPTY) return;
if (m_RepInterval <= 0.0f) return;
// Check same-account exclusion (covers owner + owner's alts)
auto* character = player->GetCharacter();
if (!character) return;
auto* parentUser = character->GetParentUser();
if (!parentUser) return;
if (parentUser->GetAccountID() == m_OwnerAccountId) return;
// Check velocity threshold (player must be active/moving)
if (update.velocity.SquaredLength() < m_RepVelocityThreshold * m_RepVelocityThreshold) return;
const auto playerId = character->GetID();
auto& info = m_PlayerActivity[playerId];
// Check daily cap
if (info.dailyContribution >= m_RepDailyCap) return;
// Compute delta time since last position update for this player
const auto now = std::chrono::steady_clock::now();
if (info.hasLastUpdate) {
const auto dt = std::chrono::duration<float>(now - info.lastUpdate).count();
// Cap delta to avoid spikes from reconnects etc.
const auto clampedDt = std::min(dt, 1.0f);
info.activeTime += clampedDt;
}
info.lastUpdate = now;
info.hasLastUpdate = true;
// Check if we've accumulated enough active time for a reputation tick
if (info.activeTime >= m_RepInterval) {
info.activeTime -= m_RepInterval;
const auto repGain = static_cast<uint32_t>(m_RepPerTick * m_RepMultiplier);
if (repGain == 0) return;
// Clamp to daily cap
const auto actualGain = std::min(repGain, m_RepDailyCap - info.dailyContribution);
if (actualGain == 0) return;
// Grant property reputation
reputation += actualGain;
info.dailyContribution += actualGain;
m_ReputationDirty = true;
// Grant character reputation to property owner
auto* ownerEntity = Game::entityManager->GetEntity(owner);
if (ownerEntity) {
auto* charComp = ownerEntity->GetComponent<CharacterComponent>();
if (charComp) {
charComp->SetReputation(charComp->GetReputation() + actualGain);
}
} else {
// Owner is offline, update DB directly
auto ownerCharId = owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
const auto currentRep = Database::Get()->GetCharacterReputation(ownerCharId);
Database::Get()->SetCharacterReputation(ownerCharId, currentRep + actualGain);
}
}
}
void PropertyManagementComponent::SaveReputation() {
if (!m_ReputationDirty) return;
if (propertyId == LWOOBJID_EMPTY) return;
Database::Get()->UpdatePropertyReputation(propertyId, reputation);
for (const auto& [playerId, info] : m_PlayerActivity) {
if (info.dailyContribution > 0) {
Database::Get()->UpdatePropertyReputationContribution(propertyId, playerId, m_CurrentDate, info.dailyContribution);
}
}
m_ReputationDirty = false;
}
void PropertyManagementComponent::LoadDailyContributions() {
if (propertyId == LWOOBJID_EMPTY) return;
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, m_CurrentDate);
for (const auto& contrib : contributions) {
m_PlayerActivity[contrib.playerId].dailyContribution = contrib.reputationGained;
}
}

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <unordered_map>
#include "Entity.h" #include "Entity.h"
#include "Component.h" #include "Component.h"
#include "eReplicaComponentType.h" #include "eReplicaComponentType.h"
class PositionUpdate;
/** /**
* Information regarding which players may visit this property * Information regarding which players may visit this property
*/ */
@@ -164,9 +167,40 @@ public:
LWOOBJID GetId() const noexcept { return propertyId; } LWOOBJID GetId() const noexcept { return propertyId; }
void OnChatMessageReceived(const std::string& sMessage) const; void OnChatMessageReceived(const std::string& sMessage) const;
void Update(float deltaTime) override;
~PropertyManagementComponent() override;
private: private:
void OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update);
void SaveReputation();
void LoadDailyContributions();
struct PlayerActivityInfo {
float activeTime = 0.0f;
uint32_t dailyContribution = 0;
std::chrono::steady_clock::time_point lastUpdate{};
bool hasLastUpdate = false;
};
std::unordered_map<LWOOBJID, PlayerActivityInfo> m_PlayerActivity;
float m_ReputationSaveTimer = 0.0f;
float m_DecayTimer = 0.0f;
bool m_ReputationDirty = false;
std::string m_CurrentDate;
uint32_t m_OwnerAccountId = 0;
// Cached config values
float m_RepInterval = 60.0f;
uint32_t m_RepDailyCap = 50;
uint32_t m_RepPerTick = 1;
float m_RepMultiplier = 1.0f;
float m_RepVelocityThreshold = 0.5f;
float m_RepSaveInterval = 300.0f;
float m_RepDecayRate = 0.0f;
float m_RepDecayInterval = 86400.0f;
uint32_t m_RepDecayMinimum = 0;
/** /**
* This * This
*/ */

View File

@@ -480,7 +480,6 @@ void Mission::YieldRewards() {
auto* const character = entity->GetComponent<CharacterComponent>(); auto* const character = entity->GetComponent<CharacterComponent>();
if (character) { if (character) {
character->SetReputation(character->GetReputation() + info.reward_reputation); character->SetReputation(character->GetReputation() + info.reward_reputation);
GameMessages::SendUpdateReputation(entity->GetObjectID(), character->GetReputation(), entity->GetSystemAddress());
} }
} }

View File

@@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS character_reputation (
character_id BIGINT NOT NULL PRIMARY KEY,
reputation BIGINT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS property_reputation_contribution (
property_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
contribution_date DATE NOT NULL,
reputation_gained INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (property_id, player_id, contribution_date)
);

View File

@@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS character_reputation (
character_id BIGINT NOT NULL PRIMARY KEY,
reputation BIGINT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS property_reputation_contribution (
property_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
contribution_date DATE NOT NULL,
reputation_gained INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (property_id, player_id, contribution_date)
);

View File

@@ -1,6 +1,7 @@
set(DCOMPONENTS_TESTS set(DCOMPONENTS_TESTS
"DestroyableComponentTests.cpp" "DestroyableComponentTests.cpp"
"PetComponentTests.cpp" "PetComponentTests.cpp"
"PropertyReputationTests.cpp"
"SimplePhysicsComponentTests.cpp" "SimplePhysicsComponentTests.cpp"
"SavingTests.cpp" "SavingTests.cpp"
) )

View File

@@ -0,0 +1,186 @@
#include "GameDependencies.h"
#include <gtest/gtest.h>
#include "Character.h"
#include "CharacterComponent.h"
#include "Database.h"
#include "Entity.h"
#include "GameDatabase/TestSQL/TestSQLDatabase.h"
// ---------------------------------------------------------------------------
// CharacterComponent reputation tests
// ---------------------------------------------------------------------------
class CharacterReputationTest : public GameDependenciesTest {
protected:
std::unique_ptr<Entity> entity;
std::unique_ptr<Character> character;
CharacterComponent* characterComponent = nullptr;
void SetUp() override {
SetUpDependencies();
entity = std::make_unique<Entity>(1, GameDependenciesTest::info);
character = std::make_unique<Character>(1, nullptr);
entity->SetCharacter(character.get());
character->SetEntity(entity.get());
characterComponent = entity->AddComponent<CharacterComponent>(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS);
}
void TearDown() override {
entity->SetCharacter(nullptr);
entity.reset();
character.reset();
TearDownDependencies();
}
};
// SetReputation must persist using the character's raw DB id (Character::GetID),
// not the runtime object id (Character::GetObjectID with the CHARACTER bit set).
TEST_F(CharacterReputationTest, SetReputationUsesCharacterDBId) {
constexpr int64_t repValue = 12345;
characterComponent->SetReputation(repValue);
// Reputation must be stored at the DB char id (GetID() == 1).
EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), repValue);
}
// SetReputation / GetReputation round-trip via in-memory TestSQLDatabase.
TEST_F(CharacterReputationTest, ReputationRoundTrip) {
characterComponent->SetReputation(500);
EXPECT_EQ(characterComponent->GetReputation(), 500);
EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), 500);
}
// The CharacterComponent constructor must load reputation from the DB using
// Character::GetID() as the lookup key.
TEST_F(CharacterReputationTest, LoadReputationFromDBOnConstruction) {
Database::Get()->SetCharacterReputation(character->GetID(), 9876);
// Re-construct the component; the constructor reads from DB.
auto* freshComp = entity->AddComponent<CharacterComponent>(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS);
EXPECT_EQ(freshComp->GetReputation(), 9876);
}
// In the test context, Character::GetObjectID() returns LWOOBJID_EMPTY (0) because
// UpdateInfoFromDatabase() is never called for test characters. This mirrors the
// production scenario where GetObjectID() carries the CHARACTER bit and therefore
// differs from GetID(). Verify the two keys are treated independently.
TEST_F(CharacterReputationTest, DBIdAndObjectIdAreDistinctKeys) {
// Precondition: the two IDs must differ so the test is meaningful.
ASSERT_NE(character->GetID(), character->GetObjectID());
characterComponent->SetReputation(42);
// Reputation stored at GetID().
EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetID()), 42);
// No reputation stored at GetObjectID() (wrong key).
EXPECT_EQ(Database::Get()->GetCharacterReputation(character->GetObjectID()), 0);
}
// ---------------------------------------------------------------------------
// TestSQLDatabase property reputation + contribution storage tests
// ---------------------------------------------------------------------------
class PropertyReputationDBTest : public GameDependenciesTest {
protected:
TestSQLDatabase* testDB = nullptr;
void SetUp() override {
SetUpDependencies();
testDB = dynamic_cast<TestSQLDatabase*>(Database::Get());
ASSERT_NE(testDB, nullptr);
}
void TearDown() override {
TearDownDependencies();
}
};
// UpdatePropertyReputation / GetPropertyReputation round-trip.
TEST_F(PropertyReputationDBTest, PropertyReputationRoundTrip) {
constexpr LWOOBJID propertyId = 42;
Database::Get()->UpdatePropertyReputation(propertyId, 100);
EXPECT_EQ(testDB->GetPropertyReputation(propertyId), 100u);
}
// Overwriting property reputation replaces the previous value.
TEST_F(PropertyReputationDBTest, PropertyReputationOverwrite) {
constexpr LWOOBJID propertyId = 42;
Database::Get()->UpdatePropertyReputation(propertyId, 100);
Database::Get()->UpdatePropertyReputation(propertyId, 250);
EXPECT_EQ(testDB->GetPropertyReputation(propertyId), 250u);
}
// UpdatePropertyReputationContribution stores using the given player ID (expected
// to be the character DB id, not the runtime object id with bits set).
TEST_F(PropertyReputationDBTest, ContributionStoredByCharDBId) {
constexpr LWOOBJID propertyId = 5;
constexpr LWOOBJID charDbId = 1; // raw DB id, no object bits
const std::string date = "2024-01-01";
Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 10);
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date);
ASSERT_EQ(contributions.size(), 1u);
EXPECT_EQ(contributions[0].playerId, charDbId);
EXPECT_EQ(contributions[0].reputationGained, 10u);
}
// A second UpdatePropertyReputationContribution for the same player must upsert
// (update the existing entry) rather than append a duplicate.
TEST_F(PropertyReputationDBTest, ContributionUpsertUpdatesExistingEntry) {
constexpr LWOOBJID propertyId = 5;
constexpr LWOOBJID charDbId = 1;
const std::string date = "2024-01-01";
Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 10);
Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, date, 30);
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date);
ASSERT_EQ(contributions.size(), 1u);
EXPECT_EQ(contributions[0].reputationGained, 30u);
}
// Contributions from different dates must not bleed into each other.
TEST_F(PropertyReputationDBTest, ContributionsAreIsolatedByDate) {
constexpr LWOOBJID propertyId = 5;
constexpr LWOOBJID charDbId = 1;
Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, "2024-01-01", 10);
Database::Get()->UpdatePropertyReputationContribution(propertyId, charDbId, "2024-01-02", 20);
const auto day1 = Database::Get()->GetPropertyReputationContributions(propertyId, "2024-01-01");
const auto day2 = Database::Get()->GetPropertyReputationContributions(propertyId, "2024-01-02");
ASSERT_EQ(day1.size(), 1u);
EXPECT_EQ(day1[0].reputationGained, 10u);
ASSERT_EQ(day2.size(), 1u);
EXPECT_EQ(day2[0].reputationGained, 20u);
}
// Multiple distinct players can each have their own contribution entry per property/date.
TEST_F(PropertyReputationDBTest, MultiplePlayerContributionsForSameProperty) {
constexpr LWOOBJID propertyId = 5;
const std::string date = "2024-01-01";
Database::Get()->UpdatePropertyReputationContribution(propertyId, 1, date, 10);
Database::Get()->UpdatePropertyReputationContribution(propertyId, 2, date, 20);
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, date);
ASSERT_EQ(contributions.size(), 2u);
const auto FindPlayerRep = [&](const LWOOBJID playerId) {
for (const auto& c : contributions) {
if (c.playerId == playerId) return c.reputationGained;
}
return 0u;
};
EXPECT_EQ(FindPlayerRep(1), 10u);
EXPECT_EQ(FindPlayerRep(2), 20u);
}
// Querying contributions for a date with no entries returns an empty vector.
TEST_F(PropertyReputationDBTest, NoContributionsReturnsEmpty) {
const auto contributions = Database::Get()->GetPropertyReputationContributions(99, "2024-01-01");
EXPECT_TRUE(contributions.empty());
}