mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-14 10:44:21 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a4288e45 | ||
|
|
ce7b771a7c | ||
|
|
6ad65fcfca | ||
|
|
453624494c | ||
|
|
6a5fe599ad | ||
|
|
bdb06b4706 | ||
|
|
e2b534501c | ||
|
|
b058526e76 | ||
|
|
7cb34ffca2 | ||
|
|
e45e860ec0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -126,3 +126,6 @@ docker-compose.override.yml
|
|||||||
# CMake scripts
|
# CMake scripts
|
||||||
!cmake/*
|
!cmake/*
|
||||||
!cmake/toolchains/*
|
!cmake/toolchains/*
|
||||||
|
|
||||||
|
.mcp.json
|
||||||
|
.claude/
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
41
dDatabase/CharacterReputationMigration.cpp
Normal file
41
dDatabase/CharacterReputationMigration.cpp
Normal 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;
|
||||||
|
}
|
||||||
7
dDatabase/CharacterReputationMigration.h
Normal file
7
dDatabase/CharacterReputationMigration.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace CharacterReputationMigration {
|
||||||
|
uint32_t Run();
|
||||||
|
};
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
14
dDatabase/GameDatabase/ITables/ICharacterReputation.h
Normal file
14
dDatabase/GameDatabase/ITables/ICharacterReputation.h
Normal 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__
|
||||||
@@ -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__
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 (?,?,?,?,?,?)",
|
||||||
|
|||||||
17
dDatabase/GameDatabase/MySQL/Tables/CharacterReputation.cpp
Normal file
17
dDatabase/GameDatabase/MySQL/Tables/CharacterReputation.cpp
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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`))",
|
||||||
|
|||||||
17
dDatabase/GameDatabase/SQLite/Tables/CharacterReputation.cpp
Normal file
17
dDatabase/GameDatabase/SQLite/Tables/CharacterReputation.cpp
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
migrations/dlu/mysql/27_character_reputation.sql
Normal file
4
migrations/dlu/mysql/27_character_reputation.sql
Normal 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
|
||||||
|
);
|
||||||
7
migrations/dlu/mysql/28_property_reputation.sql
Normal file
7
migrations/dlu/mysql/28_property_reputation.sql
Normal 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)
|
||||||
|
);
|
||||||
4
migrations/dlu/sqlite/10_character_reputation.sql
Normal file
4
migrations/dlu/sqlite/10_character_reputation.sql
Normal 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
|
||||||
|
);
|
||||||
7
migrations/dlu/sqlite/11_property_reputation.sql
Normal file
7
migrations/dlu/sqlite/11_property_reputation.sql
Normal 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)
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
186
tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp
Normal file
186
tests/dGameTests/dComponentsTests/PropertyReputationTests.cpp
Normal 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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user