diff --git a/dCommon/GeneralUtils.h b/dCommon/GeneralUtils.h index 828fcee3..9c1d071d 100644 --- a/dCommon/GeneralUtils.h +++ b/dCommon/GeneralUtils.h @@ -357,4 +357,18 @@ namespace GeneralUtils { 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); + } } diff --git a/dDatabase/CMakeLists.txt b/dDatabase/CMakeLists.txt index 63bd5fb6..00e6cc8f 100644 --- a/dDatabase/CMakeLists.txt +++ b/dDatabase/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(CDClientDatabase) 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 ${CMAKE_COMMAND} -E copy $ ${PROJECT_BINARY_DIR}) diff --git a/dDatabase/CharacterReputationMigration.cpp b/dDatabase/CharacterReputationMigration.cpp new file mode 100644 index 00000000..d2fe1dea --- /dev/null +++ b/dDatabase/CharacterReputationMigration.cpp @@ -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", 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; +} diff --git a/dDatabase/CharacterReputationMigration.h b/dDatabase/CharacterReputationMigration.h new file mode 100644 index 00000000..16f1fbbb --- /dev/null +++ b/dDatabase/CharacterReputationMigration.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace CharacterReputationMigration { + uint32_t Run(); +}; diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index 2126b9be..d7eb1d4a 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -25,6 +25,8 @@ #include "IAccountsRewardCodes.h" #include "IBehaviors.h" #include "IUgcModularBuild.h" +#include "ICharacterReputation.h" +#include "IPropertyReputationContribution.h" #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) @@ -38,7 +40,8 @@ class GameDatabase : public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, - public IBehaviors, public IUgcModularBuild { + public IBehaviors, public IUgcModularBuild, + public ICharacterReputation, public IPropertyReputationContribution { public: virtual ~GameDatabase() = default; // TODO: These should be made private. diff --git a/dDatabase/GameDatabase/ITables/ICharInfo.h b/dDatabase/GameDatabase/ITables/ICharInfo.h index d28017f3..56aa2781 100644 --- a/dDatabase/GameDatabase/ITables/ICharInfo.h +++ b/dDatabase/GameDatabase/ITables/ICharInfo.h @@ -33,6 +33,9 @@ public: // Get the character ids for the given account. virtual std::vector GetAccountCharacterIds(const LWOOBJID accountId) = 0; + // Get all character ids. + virtual std::vector GetAllCharacterIds() = 0; + // Insert a new character into the database. virtual void InsertNewCharacter(const ICharInfo::Info info) = 0; diff --git a/dDatabase/GameDatabase/ITables/ICharacterReputation.h b/dDatabase/GameDatabase/ITables/ICharacterReputation.h new file mode 100644 index 00000000..fa74f89d --- /dev/null +++ b/dDatabase/GameDatabase/ITables/ICharacterReputation.h @@ -0,0 +1,14 @@ +#ifndef __ICHARACTERREPUTATION__H__ +#define __ICHARACTERREPUTATION__H__ + +#include + +#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__ diff --git a/dDatabase/GameDatabase/ITables/IPropertyReputationContribution.h b/dDatabase/GameDatabase/ITables/IPropertyReputationContribution.h new file mode 100644 index 00000000..8f643579 --- /dev/null +++ b/dDatabase/GameDatabase/ITables/IPropertyReputationContribution.h @@ -0,0 +1,30 @@ +#ifndef __IPROPERTYREPUTATIONCONTRIBUTION__H__ +#define __IPROPERTYREPUTATIONCONTRIBUTION__H__ + +#include +#include +#include + +#include "dCommonVars.h" + +class IPropertyReputationContribution { +public: + struct ContributionInfo { + LWOOBJID playerId{}; + uint32_t reputationGained{}; + }; + + // Get today's reputation contributions for a property. + virtual std::vector 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__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index b481877c..0df02202 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -139,6 +139,15 @@ public: void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; void DeleteUgcBuild(const LWOOBJID bigId) override; + std::vector GetAllCharacterIds() override; + int64_t GetCharacterReputation(const LWOOBJID charId) override; + void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override; + std::vector 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; bool IsNameInUse(const std::string_view name) override; std::optional GetModel(const LWOOBJID modelID) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt index 2f1fa6de..befcfb9e 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt @@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES "Servers.cpp" "Ugc.cpp" "UgcModularBuild.cpp" + "CharacterReputation.cpp" + "PropertyReputationContribution.cpp" PARENT_SCOPE ) diff --git a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp index 719a8372..f18130c7 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp @@ -52,6 +52,18 @@ std::vector MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou return toReturn; } +std::vector MySQLDatabase::GetAllCharacterIds() { + auto result = ExecuteSelect("SELECT id FROM charinfo;"); + + std::vector toReturn; + toReturn.reserve(result->rowsCount()); + while (result->next()) { + toReturn.push_back(result->getInt64("id")); + } + + return toReturn; +} + void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) { ExecuteInsert( "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)", diff --git a/dDatabase/GameDatabase/MySQL/Tables/CharacterReputation.cpp b/dDatabase/GameDatabase/MySQL/Tables/CharacterReputation.cpp new file mode 100644 index 00000000..fcbab1de --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/CharacterReputation.cpp @@ -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); +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/PropertyReputationContribution.cpp b/dDatabase/GameDatabase/MySQL/Tables/PropertyReputationContribution.cpp new file mode 100644 index 00000000..916731a2 --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/PropertyReputationContribution.cpp @@ -0,0 +1,30 @@ +#include "MySQLDatabase.h" + +std::vector 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 contributions; + while (result->next()) { + IPropertyReputationContribution::ContributionInfo info; + info.playerId = result->getUInt64("player_id"); + info.reputationGained = static_cast(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); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 3b6dc643..b40f7f0c 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -123,6 +123,15 @@ public: void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; void DeleteUgcBuild(const LWOOBJID bigId) override; + std::vector GetAllCharacterIds() override; + int64_t GetCharacterReputation(const LWOOBJID charId) override; + void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override; + std::vector 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; bool IsNameInUse(const std::string_view name) override; std::optional GetModel(const LWOOBJID modelID) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt index 91d5b5e2..3af751b0 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES "Servers.cpp" "Ugc.cpp" "UgcModularBuild.cpp" + "CharacterReputation.cpp" + "PropertyReputationContribution.cpp" PARENT_SCOPE ) diff --git a/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp b/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp index 6b3dab3b..340fc827 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp @@ -55,6 +55,18 @@ std::vector SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco return toReturn; } +std::vector SQLiteDatabase::GetAllCharacterIds() { + auto [_, result] = ExecuteSelect("SELECT id FROM charinfo;"); + + std::vector toReturn; + while (!result.eof()) { + toReturn.push_back(result.getInt64Field("id")); + result.nextRow(); + } + + return toReturn; +} + void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) { 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`))", diff --git a/dDatabase/GameDatabase/SQLite/Tables/CharacterReputation.cpp b/dDatabase/GameDatabase/SQLite/Tables/CharacterReputation.cpp new file mode 100644 index 00000000..fbed410a --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/CharacterReputation.cpp @@ -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); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyReputationContribution.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyReputationContribution.cpp new file mode 100644 index 00000000..9db80946 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyReputationContribution.cpp @@ -0,0 +1,31 @@ +#include "SQLiteDatabase.h" + +std::vector 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 contributions; + while (!result.eof()) { + IPropertyReputationContribution::ContributionInfo info; + info.playerId = result.getInt64Field("player_id"); + info.reputationGained = static_cast(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); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 2c7890dd..6381dd89 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -1,6 +1,7 @@ #ifndef TESTSQLDATABASE_H #define TESTSQLDATABASE_H +#include #include "GameDatabase.h" class TestSQLDatabase : public GameDatabase { @@ -103,11 +104,27 @@ class TestSQLDatabase : public GameDatabase { void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {}; uint32_t GetAccountCount() override { return 0; }; + std::vector 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 GetPropertyReputationContributions( + const LWOOBJID propertyId, const std::string& date) override { return {}; }; + 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 {}; bool IsNameInUse(const std::string_view name) override { return false; }; std::optional GetModel(const LWOOBJID modelID) override { return {}; } std::optional GetPropertyInfo(const LWOOBJID id) override { return {}; } std::optional GetUgcModel(const LWOOBJID ugcId) override { return {}; } +private: + std::unordered_map m_CharacterReputation; }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index b3310a4f..24f2d3cf 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -8,6 +8,7 @@ #include "Logger.h" #include "BinaryPathFinder.h" #include "ModelNormalizeMigration.h" +#include "CharacterReputationMigration.h" #include @@ -49,6 +50,7 @@ void MigrationRunner::RunMigrations() { bool runNormalizeMigrations = false; bool runNormalizeAfterFirstPartMigrations = false; bool runBrickBuildsNotOnGrid = false; + bool runCharacterReputationMigration = false; for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); @@ -67,6 +69,9 @@ void MigrationRunner::RunMigrations() { runNormalizeAfterFirstPartMigrations = true; } else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) { runBrickBuildsNotOnGrid = true; + } else if (migration.name.ends_with("_character_reputation.sql")) { + runCharacterReputationMigration = true; + finalSQL.append(migration.data.c_str()); } else { finalSQL.append(migration.data.c_str()); } @@ -74,7 +79,7 @@ void MigrationRunner::RunMigrations() { 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."); return; } @@ -110,6 +115,11 @@ void MigrationRunner::RunMigrations() { if (runBrickBuildsNotOnGrid) { 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() { diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index 54708226..5e7e09da 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -42,7 +42,7 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID m_EditorEnabled = false; m_EditorLevel = m_GMLevel; - m_Reputation = 0; + m_Reputation = Database::Get()->GetCharacterReputation(m_Character->GetObjectID()); m_CurrentActivity = eGameActivity::NONE; m_CountryCode = 0; @@ -249,6 +249,12 @@ void CharacterComponent::SetPvpEnabled(const bool value) { m_PvpEnabled = value; } +void CharacterComponent::SetReputation(int64_t newValue) { + m_Reputation = newValue; + Database::Get()->SetCharacterReputation(m_Character->GetObjectID(), m_Reputation); + GameMessages::SendUpdateReputation(m_Parent->GetObjectID(), m_Reputation, m_Parent->GetSystemAddress()); +} + void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) { m_DirtyGMInfo = 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!"); return; } - if (character->QueryAttribute("rpt", &m_Reputation) == tinyxml2::XML_NO_ATTRIBUTE) { - SetReputation(0); - } - auto* vl = character->FirstChildElement("vl"); if (vl) LoadVisitedLevelsXml(*vl); 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]); character->SetAttribute("ls", m_Uscore); - // Custom attribute to keep track of reputation. - character->SetAttribute("rpt", GetReputation()); character->SetAttribute("stt", StatisticsToString().c_str()); // Set the zone statistics of the form ... diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index c1f107b5..4ffc2201 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -156,7 +156,7 @@ public: * Sets the lifetime reputation of the character to newValue * @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 diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 59b918e6..609ef6aa 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -27,6 +27,9 @@ #include "CppScripts.h" #include #include "dConfig.h" +#include "PositionUpdate.h" +#include "GeneralUtils.h" +#include "User.h" PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; @@ -75,6 +78,42 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const i this->reputation = propertyInfo->reputation; 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 + auto configFloat = [](const std::string& key, float def) { + const auto& val = Game::config->GetValue(key); + return val.empty() ? def : std::stof(val); + }; + auto configUint = [](const std::string& key, uint32_t def) { + const auto& val = Game::config->GetValue(key); + return val.empty() ? def : static_cast(std::stoul(val)); + }; + m_RepInterval = configFloat("property_rep_interval", 60.0f); + m_RepDailyCap = configUint("property_rep_daily_cap", 50); + m_RepPerTick = configUint("property_rep_per_tick", 1); + m_RepMultiplier = configFloat("property_rep_multiplier", 1.0f); + m_RepVelocityThreshold = configFloat("property_rep_velocity_threshold", 0.5f); + m_RepSaveInterval = configFloat("property_rep_save_interval", 300.0f); + m_RepDecayRate = configFloat("property_rep_decay_rate", 0.0f); + m_RepDecayInterval = configFloat("property_rep_decay_interval", 86400.0f); + m_RepDecayMinimum = configUint("property_rep_decay_minimum", 0); + + // Load daily reputation contributions and subscribe to position updates + m_CurrentDate = GeneralUtils::GetCurrentUTCDate(); + LoadDailyContributions(); + Entity::OnPlayerPositionUpdate += [this](Entity* player, const PositionUpdate& update) { + OnPlayerPositionUpdateHandler(player, update); + }; } } @@ -832,3 +871,126 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess modelComponent->OnChatMessageReceived(sMessage); } } + +PropertyManagementComponent::~PropertyManagementComponent() { + SaveReputation(); +} + +void PropertyManagementComponent::Update(float deltaTime) { + // Check for day rollover + const auto currentDate = GeneralUtils::GetCurrentUTCDate(); + if (currentDate != m_CurrentDate) { + 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(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 = player->GetObjectID(); + 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(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(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(); + 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; + } +} + diff --git a/dGame/dComponents/PropertyManagementComponent.h b/dGame/dComponents/PropertyManagementComponent.h index c13fe991..c7bdd959 100644 --- a/dGame/dComponents/PropertyManagementComponent.h +++ b/dGame/dComponents/PropertyManagementComponent.h @@ -1,10 +1,13 @@ #pragma once #include +#include #include "Entity.h" #include "Component.h" #include "eReplicaComponentType.h" +class PositionUpdate; + /** * Information regarding which players may visit this property */ @@ -164,9 +167,40 @@ public: LWOOBJID GetId() const noexcept { return propertyId; } - void OnChatMessageReceived(const std::string& sMessage) const; + + void Update(float deltaTime) override; + + ~PropertyManagementComponent() override; + 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 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 */ diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 3d46cde8..bc1e1200 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -480,7 +480,6 @@ void Mission::YieldRewards() { auto* const character = entity->GetComponent(); if (character) { character->SetReputation(character->GetReputation() + info.reward_reputation); - GameMessages::SendUpdateReputation(entity->GetObjectID(), character->GetReputation(), entity->GetSystemAddress()); } } diff --git a/migrations/dlu/mysql/27_character_reputation.sql b/migrations/dlu/mysql/27_character_reputation.sql new file mode 100644 index 00000000..1e65bfcd --- /dev/null +++ b/migrations/dlu/mysql/27_character_reputation.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS character_reputation ( + character_id BIGINT NOT NULL PRIMARY KEY, + reputation BIGINT NOT NULL DEFAULT 0 +); diff --git a/migrations/dlu/mysql/28_property_reputation.sql b/migrations/dlu/mysql/28_property_reputation.sql new file mode 100644 index 00000000..bf224318 --- /dev/null +++ b/migrations/dlu/mysql/28_property_reputation.sql @@ -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) +); diff --git a/migrations/dlu/sqlite/10_character_reputation.sql b/migrations/dlu/sqlite/10_character_reputation.sql new file mode 100644 index 00000000..1e65bfcd --- /dev/null +++ b/migrations/dlu/sqlite/10_character_reputation.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS character_reputation ( + character_id BIGINT NOT NULL PRIMARY KEY, + reputation BIGINT NOT NULL DEFAULT 0 +); diff --git a/migrations/dlu/sqlite/11_property_reputation.sql b/migrations/dlu/sqlite/11_property_reputation.sql new file mode 100644 index 00000000..432c5191 --- /dev/null +++ b/migrations/dlu/sqlite/11_property_reputation.sql @@ -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) +);