mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-04-10 01:36:57 +00:00
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.
This commit is contained in:
@@ -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 <zs><s/> ... <s/></zs>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
#include "CppScripts.h"
|
||||
#include <ranges>
|
||||
#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<uint32_t>(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<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 = 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<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
|
||||
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#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<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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user