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:
Aaron Kimbrell
2026-04-05 02:56:51 -05:00
parent 247576e101
commit e45e860ec0
29 changed files with 510 additions and 13 deletions

View File

@@ -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;
}
}