Compare commits

..

10 Commits

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

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

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

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

3
.gitignore vendored
View File

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

View File

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

View File

@@ -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 $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR})

View File

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

View File

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

View File

@@ -25,6 +25,8 @@
#include "IAccountsRewardCodes.h"
#include "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.

View File

@@ -33,6 +33,9 @@ public:
// Get the character ids for the given account.
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.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;

View File

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

View File

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

View File

@@ -139,6 +139,15 @@ public:
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) 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;
bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

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

View File

@@ -52,6 +52,18 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
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) {
ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",

View File

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

View File

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

View File

@@ -123,6 +123,15 @@ public:
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) 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;
bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

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

View File

@@ -55,6 +55,18 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
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) {
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`))",

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
#ifndef TESTSQLDATABASE_H
#define TESTSQLDATABASE_H
#include <map>
#include <unordered_map>
#include "GameDatabase.h"
class TestSQLDatabase : public GameDatabase {
public:
void Connect() 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 DeleteUgcBuild(const LWOOBJID bigId) override {};
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; };
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; }
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) 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

View File

@@ -8,6 +8,7 @@
#include "Logger.h"
#include "BinaryPathFinder.h"
#include "ModelNormalizeMigration.h"
#include "CharacterReputationMigration.h"
#include <fstream>
@@ -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() {

View File

@@ -1,11 +1,4 @@
add_subdirectory(MovingPlatformComponent)
foreach(file ${DGAME_DCOMPONENTS_MOVINGPLATFORMCOMPONENT})
list(APPEND DGAME_DCOMPONENTS_SUBCOMPONENT_SOURCES "MovingPlatformComponent/${file}")
endforeach()
set(DGAME_DCOMPONENTS_SOURCES
${DGAME_DCOMPONENTS_SUBCOMPONENT_SOURCES}
"AchievementVendorComponent.cpp"
"ActivityComponent.cpp"
"BaseCombatAIComponent.cpp"
@@ -58,7 +51,7 @@ set(DGAME_DCOMPONENTS_SOURCES
)
add_library(dComponents OBJECT ${DGAME_DCOMPONENTS_SOURCES})
target_include_directories(dComponents PUBLIC "." "MovingPlatformComponent"
target_include_directories(dComponents PUBLIC "."
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors" # via ModelComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors/ControlBehaviorMessages"
"${PROJECT_SOURCE_DIR}/dGame/dMission" # via MissionComponent.h

View File

@@ -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->GetID());
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->GetID(), 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>

View File

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

View File

@@ -152,17 +152,7 @@ void ControllablePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo
outBitStream.Write(m_AngularVelocity.z);
}
bool hasLocalSpaceInfo = m_PlatformEntityID != LWOOBJID_EMPTY;
outBitStream.Write(hasLocalSpaceInfo);
if (hasLocalSpaceInfo) {
outBitStream.Write(m_PlatformEntityID);
outBitStream.Write(m_LocalSpacePosition.x);
outBitStream.Write(m_LocalSpacePosition.y);
outBitStream.Write(m_LocalSpacePosition.z);
outBitStream.Write(m_LocalSpaceLinearVelocity.x);
outBitStream.Write(m_LocalSpaceLinearVelocity.y);
outBitStream.Write(m_LocalSpaceLinearVelocity.z);
}
outBitStream.Write0(); // local_space_info, always zero for now.
if (!bIsInitialUpdate) {
m_DirtyPosition = false;

View File

@@ -261,19 +261,6 @@ public:
/**
* Push or Pop a layer of stun immunity to this entity
*/
/**
* Sets the platform the entity is standing on for local space serialization
*/
void SetPlatformEntity(LWOOBJID platformID) { m_PlatformEntityID = platformID; m_DirtyPosition = true; }
/**
* Returns the platform the entity is standing on
*/
LWOOBJID GetPlatformEntity() const { return m_PlatformEntityID; }
void SetLocalSpacePosition(const NiPoint3& pos) { m_LocalSpacePosition = pos; }
void SetLocalSpaceLinearVelocity(const NiPoint3& vel) { m_LocalSpaceLinearVelocity = vel; }
void SetStunImmunity(
const eStateChangeType state,
const LWOOBJID originator = LWOOBJID_EMPTY,
@@ -416,21 +403,6 @@ private:
/**
* stun immunity counters
*/
/**
* The platform entity the player is standing on (for local space serialization)
*/
LWOOBJID m_PlatformEntityID = LWOOBJID_EMPTY;
/**
* The player's position in the platform's local space
*/
NiPoint3 m_LocalSpacePosition{};
/**
* The player's linear velocity in the platform's local space
*/
NiPoint3 m_LocalSpaceLinearVelocity{};
int32_t m_ImmuneToStunAttackCount;
int32_t m_ImmuneToStunEquipCount;
int32_t m_ImmuneToStunInteractCount;

View File

@@ -4,119 +4,80 @@
*/
#include "MovingPlatformComponent.h"
#include "PlatformSubComponent.h"
#include "MoverSubComponent.h"
#include "SimpleMoverSubComponent.h"
#include "RotatorSubComponent.h"
#include "BitStream.h"
#include "GeneralUtils.h"
#include "dZoneManager.h"
#include "EntityManager.h"
#include "Logger.h"
#include "GameMessages.h"
#include "CppScripts.h"
#include "SimplePhysicsComponent.h"
#include "Zone.h"
#include "eMovementPlatformState.h"
MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName)
: Component(parent, componentID) {
MoverSubComponent::MoverSubComponent(const NiPoint3& startPos) {
mPosition = {};
mState = eMovementPlatformState::Stopped;
mDesiredWaypointIndex = 0; // -1;
mInReverse = false;
mShouldStopAtDesiredWaypoint = false;
mPercentBetweenPoints = 0.0f;
mCurrentWaypointIndex = 0;
mNextWaypointIndex = 0; //mCurrentWaypointIndex + 1;
mIdleTimeElapsed = 0.0f;
}
MoverSubComponent::~MoverSubComponent() = default;
void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
outBitStream.Write<bool>(true);
outBitStream.Write(mState);
outBitStream.Write<int32_t>(mDesiredWaypointIndex);
outBitStream.Write(mShouldStopAtDesiredWaypoint);
outBitStream.Write(mInReverse);
outBitStream.Write<float_t>(mPercentBetweenPoints);
outBitStream.Write<float_t>(mPosition.x);
outBitStream.Write<float_t>(mPosition.y);
outBitStream.Write<float_t>(mPosition.z);
outBitStream.Write<uint32_t>(mCurrentWaypointIndex);
outBitStream.Write<uint32_t>(mNextWaypointIndex);
outBitStream.Write<float_t>(mIdleTimeElapsed);
outBitStream.Write<float_t>(0.0f); // Move time elapsed
}
//------------- MovingPlatformComponent below --------------
MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName) : Component(parent, componentID) {
m_MoverSubComponentType = eMoverSubComponentType::mover;
m_MoverSubComponent = new MoverSubComponent(m_Parent->GetDefaultPosition());
m_PathName = GeneralUtils::ASCIIToUTF16(pathName);
m_Path = Game::zoneManager->GetZone()->GetPath(pathName);
m_NoAutoStart = false;
if (m_Path == nullptr && !pathName.empty()) {
if (m_Path == nullptr) {
LOG("Path not found: %s", pathName.c_str());
}
SetupPlatformSubComponents();
}
MovingPlatformComponent::~MovingPlatformComponent() = default;
void MovingPlatformComponent::SetupPlatformSubComponents() {
// Read component properties matching client SetupPlatform
bool isMover = m_Parent->GetVar<bool>(u"platformIsMover");
bool isSimpleMover = m_Parent->GetVar<bool>(u"platformIsSimpleMover");
bool isRotater = m_Parent->GetVar<bool>(u"platformIsRotater");
// Read sound GUIDs
m_PlatformSoundStart = m_Parent->GetVarAsString(u"platformSoundStart");
m_PlatformSoundTravel = m_Parent->GetVarAsString(u"platformSoundTravel");
m_PlatformSoundStop = m_Parent->GetVarAsString(u"platformSoundStop");
// If no flags set but we have a path, default to mover (backwards compatibility)
if (!isMover && !isSimpleMover && !isRotater) {
if (m_Path && m_Path->pathType == PathType::MovingPlatform) {
isMover = true;
}
}
// Create mover subcomponent
if (isMover && m_Path) {
m_MoverSubComponentType = eMoverSubComponentType::mover;
auto mover = std::make_unique<MoverSubComponent>(m_Parent, m_Path);
if (!m_PathName.empty()) {
bool reverse = m_Parent->GetVar<bool>(u"reverse");
int32_t startPoint = m_Parent->GetVarAs<int32_t>(u"startPoint");
mover->SetInReverse(reverse);
if (startPoint >= 0 && startPoint < static_cast<int32_t>(m_Path->pathWaypoints.size())) {
mover->SetupWaypointSegment(static_cast<uint32_t>(startPoint));
}
mover->SetActive(true);
}
m_MoverSubComponent = std::move(mover);
}
// Create simple mover subcomponent
if (isSimpleMover) {
m_MoverSubComponentType = eMoverSubComponentType::simpleMover;
NiPoint3 platformMove{};
platformMove.x = m_Parent->GetVar<float>(u"platformMoveX");
platformMove.y = m_Parent->GetVar<float>(u"platformMoveY");
platformMove.z = m_Parent->GetVar<float>(u"platformMoveZ");
float platformMoveTime = m_Parent->GetVar<float>(u"platformMoveTime");
NiPoint3 startPos = m_Parent->GetDefaultPosition();
NiQuaternion startRot = m_Parent->GetDefaultRotation();
m_MoverSubComponent = std::make_unique<SimpleMoverSubComponent>(
m_Parent, startPos, startRot, platformMove, platformMoveTime);
}
// Create rotator subcomponent (can coexist with mover)
if (isRotater && m_Path) {
auto rotator = std::make_unique<RotatorSubComponent>(m_Parent, m_Path);
if (!m_PathName.empty()) {
bool reverse = m_Parent->GetVar<bool>(u"reverse");
int32_t startPoint = m_Parent->GetVarAs<int32_t>(u"startPoint");
rotator->SetInReverse(reverse);
if (startPoint >= 0 && startPoint < static_cast<int32_t>(m_Path->pathWaypoints.size())) {
rotator->SetupWaypointSegment(static_cast<uint32_t>(startPoint));
}
rotator->SetActive(true);
}
m_RotatorSubComponent = std::move(rotator);
}
// Fallback: if nothing was created, create a default mover
if (!m_MoverSubComponent && !m_RotatorSubComponent) {
m_MoverSubComponentType = eMoverSubComponentType::mover;
m_MoverSubComponent = std::make_unique<MoverSubComponent>(m_Parent, m_Path);
}
MovingPlatformComponent::~MovingPlatformComponent() {
delete static_cast<MoverSubComponent*>(m_MoverSubComponent);
}
void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
// Here we don't serialize the moving platform to let the client simulate the movement
if (!m_Serialize) {
outBitStream.Write<bool>(false);
outBitStream.Write<bool>(false);
return;
}
@@ -126,6 +87,7 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI
outBitStream.Write(hasPath);
if (hasPath) {
// Is on rail
outBitStream.Write1();
outBitStream.Write<uint16_t>(m_PathName.size());
@@ -133,50 +95,25 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI
outBitStream.Write<uint16_t>(c);
}
outBitStream.Write<uint32_t>(m_MoverSubComponent ? m_MoverSubComponent->GetCurrentWaypointIndex() : 0);
outBitStream.Write<bool>(m_MoverSubComponent ? m_MoverSubComponent->GetInReverse() : false);
// Starting point
outBitStream.Write<uint32_t>(0);
// Reverse
outBitStream.Write<bool>(false);
}
const auto hasPlatform = m_MoverSubComponent != nullptr;
outBitStream.Write<bool>(hasPlatform);
if (hasPlatform) {
auto* mover = static_cast<MoverSubComponent*>(m_MoverSubComponent);
outBitStream.Write(m_MoverSubComponentType);
m_MoverSubComponent->Serialize(outBitStream, bIsInitialUpdate);
}
}
void MovingPlatformComponent::Update(float deltaTime) {
if (!m_Serialize) return;
// Track whether we were travelling before update for sound management
bool wasTravelling = m_MoverSubComponent &&
(m_MoverSubComponent->GetState() & PlatformState::Travelling);
bool dirty = false;
if (m_MoverSubComponent) {
m_MoverSubComponent->Update(deltaTime, dirty);
}
if (m_RotatorSubComponent) {
m_RotatorSubComponent->Update(deltaTime, dirty);
}
// Handle travel sound looping (matching client PlayTravelSound/StopTravelSound)
if (m_MoverSubComponent && !m_PlatformSoundTravel.empty()) {
bool isTravelling = m_MoverSubComponent->GetState() & PlatformState::Travelling;
if (isTravelling && !wasTravelling) {
// Started travelling — play looping travel sound
GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundTravel);
if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) {
// TODO
} else {
mover->Serialize(outBitStream, bIsInitialUpdate);
}
// Note: the client stops the travel sound on arrival/stop via StopTravelSound.
// SendPlayNDAudioEmitter doesn't support stopping, so the sound will naturally end
// or be replaced by the arrive/stop sound.
}
if (dirty) {
Game::entityManager->SerializeEntity(m_Parent);
}
}
@@ -185,66 +122,201 @@ void MovingPlatformComponent::OnQuickBuildInitilized() {
}
void MovingPlatformComponent::OnCompleteQuickBuild() {
if (m_NoAutoStart) return;
if (m_NoAutoStart)
return;
StartPathing();
}
void MovingPlatformComponent::SetMovementState(uint32_t state) {
if (m_MoverSubComponent) m_MoverSubComponent->SetState(state);
if (m_RotatorSubComponent) m_RotatorSubComponent->SetState(state);
void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) {
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
subComponent->mState = value;
Game::entityManager->SerializeEntity(m_Parent);
}
void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) {
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
subComponent->mDesiredWaypointIndex = index;
subComponent->mNextWaypointIndex = index;
subComponent->mShouldStopAtDesiredWaypoint = stopAtWaypoint;
StartPathing();
}
void MovingPlatformComponent::StartPathing() {
//GameMessages::SendStartPathing(m_Parent);
m_PathingStopped = false;
if (m_MoverSubComponent) m_MoverSubComponent->GotoWaypoint(index, stopAtWaypoint);
if (m_RotatorSubComponent) m_RotatorSubComponent->GotoWaypoint(index, stopAtWaypoint);
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
subComponent->mShouldStopAtDesiredWaypoint = true;
subComponent->mState = eMovementPlatformState::Stationary;
NiPoint3 targetPosition;
if (m_Path != nullptr) {
const auto& currentWaypoint = m_Path->pathWaypoints[subComponent->mCurrentWaypointIndex];
const auto& nextWaypoint = m_Path->pathWaypoints[subComponent->mNextWaypointIndex];
subComponent->mPosition = currentWaypoint.position;
subComponent->mSpeed = currentWaypoint.speed;
subComponent->mWaitTime = currentWaypoint.movingPlatform.wait;
targetPosition = nextWaypoint.position;
} else {
subComponent->mPosition = m_Parent->GetPosition();
subComponent->mSpeed = 1.0f;
subComponent->mWaitTime = 2.0f;
targetPosition = m_Parent->GetPosition() + NiPoint3(0.0f, 10.0f, 0.0f);
}
m_Parent->AddCallbackTimer(subComponent->mWaitTime, [this] {
SetMovementState(eMovementPlatformState::Moving);
});
const auto travelTime = Vector3::Distance(targetPosition, subComponent->mPosition) / subComponent->mSpeed + 1.5f;
const auto travelNext = subComponent->mWaitTime + travelTime;
m_Parent->AddCallbackTimer(travelTime, [subComponent, this] {
this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex);
});
m_Parent->AddCallbackTimer(travelNext, [this] {
ContinuePathing();
});
//GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS);
Game::entityManager->SerializeEntity(m_Parent);
}
void MovingPlatformComponent::StartPathing() {
m_PathingStopped = false;
void MovingPlatformComponent::ContinuePathing() {
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
if (m_MoverSubComponent) m_MoverSubComponent->StartPathing();
if (m_RotatorSubComponent) m_RotatorSubComponent->StartPathing();
subComponent->mState = eMovementPlatformState::Stationary;
if (m_MoverSubComponent) {
GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS,
m_MoverSubComponent->GetShouldStopAtDesiredWaypoint(),
m_MoverSubComponent->GetCurrentWaypointIndex(),
m_MoverSubComponent->GetDesiredWaypointIndex(),
m_MoverSubComponent->GetNextWaypointIndex(),
static_cast<eMovementPlatformState>(m_MoverSubComponent->GetSerializedState()),
m_MoverSubComponent->GetInReverse(),
m_MoverSubComponent->GetIdleTimeElapsed(),
m_MoverSubComponent->GetMoveTimeElapsed(),
m_MoverSubComponent->GetPercentBetweenPoints(),
m_MoverSubComponent->GetPosition());
subComponent->mCurrentWaypointIndex = subComponent->mNextWaypointIndex;
NiPoint3 targetPosition;
uint32_t pathSize;
PathBehavior behavior;
if (m_Path != nullptr) {
const auto& currentWaypoint = m_Path->pathWaypoints[subComponent->mCurrentWaypointIndex];
const auto& nextWaypoint = m_Path->pathWaypoints[subComponent->mNextWaypointIndex];
subComponent->mPosition = currentWaypoint.position;
subComponent->mSpeed = currentWaypoint.speed;
subComponent->mWaitTime = currentWaypoint.movingPlatform.wait; // + 2;
pathSize = m_Path->pathWaypoints.size() - 1;
behavior = static_cast<PathBehavior>(m_Path->pathBehavior);
targetPosition = nextWaypoint.position;
} else {
subComponent->mPosition = m_Parent->GetPosition();
subComponent->mSpeed = 1.0f;
subComponent->mWaitTime = 2.0f;
targetPosition = m_Parent->GetPosition() + NiPoint3(0.0f, 10.0f, 0.0f);
pathSize = 1;
behavior = PathBehavior::Loop;
}
if (!m_PlatformSoundStart.empty()) {
GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundStart);
if (m_Parent->GetLOT() == 9483) {
behavior = PathBehavior::Bounce;
} else {
return;
}
if (subComponent->mCurrentWaypointIndex >= pathSize) {
subComponent->mCurrentWaypointIndex = pathSize;
switch (behavior) {
case PathBehavior::Once:
Game::entityManager->SerializeEntity(m_Parent);
return;
case PathBehavior::Bounce:
subComponent->mInReverse = true;
break;
case PathBehavior::Loop:
subComponent->mNextWaypointIndex = 0;
break;
default:
break;
}
} else if (subComponent->mCurrentWaypointIndex == 0) {
subComponent->mInReverse = false;
}
if (subComponent->mInReverse) {
subComponent->mNextWaypointIndex = subComponent->mCurrentWaypointIndex - 1;
} else {
subComponent->mNextWaypointIndex = subComponent->mCurrentWaypointIndex + 1;
}
/*
subComponent->mNextWaypointIndex = 0;
subComponent->mCurrentWaypointIndex = 1;
*/
//GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS);
if (subComponent->mCurrentWaypointIndex == subComponent->mDesiredWaypointIndex) {
// TODO: Send event?
StopPathing();
return;
}
m_Parent->CancelCallbackTimers();
m_Parent->AddCallbackTimer(subComponent->mWaitTime, [this] {
SetMovementState(eMovementPlatformState::Moving);
});
auto travelTime = Vector3::Distance(targetPosition, subComponent->mPosition) / subComponent->mSpeed + 1.5;
if (m_Parent->GetLOT() == 9483) {
travelTime += 20;
}
const auto travelNext = subComponent->mWaitTime + travelTime;
m_Parent->AddCallbackTimer(travelTime, [subComponent, this] {
this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex);
});
m_Parent->AddCallbackTimer(travelNext, [this] {
ContinuePathing();
});
Game::entityManager->SerializeEntity(m_Parent);
}
void MovingPlatformComponent::StopPathing() {
//m_Parent->CancelCallbackTimers();
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
m_PathingStopped = true;
if (m_MoverSubComponent) m_MoverSubComponent->StopPathing();
if (m_RotatorSubComponent) m_RotatorSubComponent->StopPathing();
GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS,
false, 0, -1, 0, eMovementPlatformState::Stopped);
if (!m_PlatformSoundStop.empty()) {
GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundStop);
}
subComponent->mState = eMovementPlatformState::Stopped;
subComponent->mDesiredWaypointIndex = -1;
subComponent->mShouldStopAtDesiredWaypoint = false;
Game::entityManager->SerializeEntity(m_Parent);
//GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS);
}
void MovingPlatformComponent::SetSerialized(bool value) {
@@ -260,18 +332,18 @@ void MovingPlatformComponent::SetNoAutoStart(const bool value) {
}
void MovingPlatformComponent::WarpToWaypoint(size_t index) {
if (m_MoverSubComponent) m_MoverSubComponent->WarpToWaypoint(index);
if (m_RotatorSubComponent) m_RotatorSubComponent->WarpToWaypoint(index);
const auto& waypoint = m_Path->pathWaypoints[index];
m_Parent->SetPosition(waypoint.position);
m_Parent->SetRotation(waypoint.rotation);
Game::entityManager->SerializeEntity(m_Parent);
}
size_t MovingPlatformComponent::GetLastWaypointIndex() const {
if (m_MoverSubComponent) return m_MoverSubComponent->GetLastWaypointIndex();
if (m_RotatorSubComponent) return m_RotatorSubComponent->GetLastWaypointIndex();
return 0;
return m_Path->pathWaypoints.size() - 1;
}
PlatformSubComponent* MovingPlatformComponent::GetMoverSubComponent() const {
if (m_MoverSubComponent) return m_MoverSubComponent.get();
return nullptr;
MoverSubComponent* MovingPlatformComponent::GetMoverSubComponent() const {
return static_cast<MoverSubComponent*>(m_MoverSubComponent);
}

View File

@@ -6,34 +6,103 @@
#ifndef MOVINGPLATFORMCOMPONENT_H
#define MOVINGPLATFORMCOMPONENT_H
#include "Component.h"
#include "eReplicaComponentType.h"
#include "RakNetTypes.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include <string>
#include <memory>
#include "dCommonVars.h"
#include "EntityManager.h"
#include "Component.h"
#include "eMovementPlatformState.h"
#include "eReplicaComponentType.h"
class Path;
class Entity;
class PlatformSubComponent;
class RotatorSubComponent;
/**
* Different types of available platform subcomponents
*/
/**
* Different types of available platforms
*/
enum class eMoverSubComponentType : uint32_t {
mover = 4,
simpleMover = 5,
rotator = 6,
/**
* Used in NJ
*/
simpleMover = 5,
};
/**
* Sub component for moving platforms that determine the actual current movement state
*/
class MoverSubComponent {
public:
MoverSubComponent(const NiPoint3& startPos);
~MoverSubComponent();
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate);
/**
* The state the platform is currently in
*/
eMovementPlatformState mState = eMovementPlatformState::Stationary;
/**
* The waypoint this platform currently wants to traverse to
*/
int32_t mDesiredWaypointIndex = 0;
/**
* Whether the platform is currently reversing away from the desired waypoint
*/
bool mInReverse = false;
/**
* Whether the platform should stop moving when reaching the desired waypoint
*/
bool mShouldStopAtDesiredWaypoint = false;
/**
* The percentage of the way between the last point and the desired point
*/
float mPercentBetweenPoints = 0;
/**
* The current position of the platofrm
*/
NiPoint3 mPosition{};
/**
* The waypoint the platform is (was) at
*/
uint32_t mCurrentWaypointIndex;
/**
* The waypoint the platform is attempting to go to
*/
uint32_t mNextWaypointIndex;
/**
* The timer that handles the time before stopping idling and continue platform movement
*/
float mIdleTimeElapsed = 0;
/**
* The speed the platform is currently moving at
*/
float mSpeed = 0;
/**
* The time to wait before continuing movement
*/
float mWaitTime = 0;
};
/**
* Represents entities that may be moving platforms, indicating how they should move through the world.
* The server simulates platform movement each tick to maintain authoritative state for all players.
*
* An entity can have multiple subcomponents (mover + rotator), matching the client's architecture
* where SetupPlatform creates subcomponents based on platformIsMover/platformIsSimpleMover/platformIsRotater.
* NOTE: the logic in this component hardly does anything, apparently the client can figure most of this stuff out
* if you just serialize it correctly, resulting in smoother results anyway. Don't be surprised if the exposed APIs
* don't at all do what you expect them to as we don't instruct the client of changes made here.
* ^^^ Trivia: This made the red blocks platform and property platforms a pain to implement.
*/
class MovingPlatformComponent final : public Component {
public:
@@ -43,42 +112,117 @@ public:
~MovingPlatformComponent() override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
void Update(float deltaTime) override;
/**
* Stops all pathing, called when an entity starts a quick build associated with this platform
*/
void OnQuickBuildInitilized();
/**
* Starts the pathing, called when an entity completed a quick build associated with this platform
*/
void OnCompleteQuickBuild();
/**
* Updates the movement state for the moving platform
* @param value the movement state to set
*/
void SetMovementState(eMovementPlatformState value);
/**
* Instructs the moving platform to go to some waypoint
* @param index the index of the waypoint
* @param stopAtWaypoint determines if the platform should stop at the waypoint
*/
void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true);
/**
* Starts the pathing of this platform, setting appropriate waypoints and speeds
*/
void StartPathing();
/**
* Continues the path of the platform, after it's been stopped
*/
void ContinuePathing();
/**
* Stops the platform from moving, waiting for it to be activated again.
*/
void StopPathing();
/**
* Determines if the entity should be serialized on the next update
* @param value whether to serialize the entity or not
*/
void SetSerialized(bool value);
/**
* Returns if this platform will start automatically after spawn
* @return if this platform will start automatically after spawn
*/
bool GetNoAutoStart() const;
/**
* Sets the auto start value for this platform
* @param value the auto start value to set
*/
void SetNoAutoStart(bool value);
/**
* Warps the platform to a waypoint index, skipping its current path
* @param index the index to go to
*/
void WarpToWaypoint(size_t index);
/**
* Returns the waypoint this platform was previously at
* @return the waypoint this platform was previously at
*/
size_t GetLastWaypointIndex() const;
PlatformSubComponent* GetMoverSubComponent() const;
void SetMovementState(uint32_t state);
/**
* Returns the sub component that actually defines how the platform moves around (speeds, etc).
* @return the sub component that actually defines how the platform moves around
*/
MoverSubComponent* GetMoverSubComponent() const;
private:
void SetupPlatformSubComponents();
/**
* The path this platform is currently on
*/
const Path* m_Path = nullptr;
/**
* The name of the path this platform is currently on
*/
std::u16string m_PathName;
/**
* Whether the platform has stopped pathing
*/
bool m_PathingStopped = false;
/**
* The type of the subcomponent
*/
eMoverSubComponentType m_MoverSubComponentType;
std::unique_ptr<PlatformSubComponent> m_MoverSubComponent;
std::unique_ptr<RotatorSubComponent> m_RotatorSubComponent;
/**
* The mover sub component that belongs to this platform
*/
void* m_MoverSubComponent;
bool m_NoAutoStart = false;
/**
* Whether the platform shouldn't auto start
*/
bool m_NoAutoStart;
/**
* Whether to serialize the entity on the next update
*/
bool m_Serialize = false;
std::string m_PlatformSoundStart;
std::string m_PlatformSoundTravel;
std::string m_PlatformSoundStop;
};
#endif // MOVINGPLATFORMCOMPONENT_H

View File

@@ -1,7 +0,0 @@
set(DGAME_DCOMPONENTS_MOVINGPLATFORMCOMPONENT
"PlatformSubComponent.cpp"
"MoverSubComponent.cpp"
"SimpleMoverSubComponent.cpp"
"RotatorSubComponent.cpp"
PARENT_SCOPE
)

View File

@@ -1,5 +0,0 @@
#include "MoverSubComponent.h"
MoverSubComponent::MoverSubComponent(Entity* parentEntity, const Path* path)
: PlatformSubComponent(parentEntity, path) {
}

View File

@@ -1,19 +0,0 @@
#ifndef MOVERSUBCOMPONENT_H
#define MOVERSUBCOMPONENT_H
#include "PlatformSubComponent.h"
/**
* Standard mover - follows a pre-defined path from zone data.
* Corresponds to client LWOPlatformMover (type 4).
*/
class MoverSubComponent final : public PlatformSubComponent {
public:
MoverSubComponent(Entity* parentEntity, const Path* path);
private:
bool m_AllowPosSnap = true;
float m_MaxLerpDistance = 6.0f;
};
#endif // MOVERSUBCOMPONENT_H

View File

@@ -1,484 +0,0 @@
#include "PlatformSubComponent.h"
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "Entity.h"
#include "Game.h"
#include "dServer.h"
#include "GameMessages.h"
#include "CppScripts.h"
#include "SimplePhysicsComponent.h"
#include "Zone.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include <algorithm>
#include <cmath>
#include <glm/gtc/quaternion.hpp>
PlatformSubComponent::PlatformSubComponent(Entity* parentEntity, const Path* path)
: m_ParentEntity(parentEntity)
, m_Path(path) {
if (m_Path) {
m_TimeBasedMovement = m_Path->movingPlatform.timeBasedMovement != 0;
}
m_Position = parentEntity ? parentEntity->GetPosition() : NiPoint3{};
}
void PlatformSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
outBitStream.Write<bool>(true);
outBitStream.Write<uint32_t>(GetSerializedState());
outBitStream.Write<int32_t>(m_DesiredWaypointIndex);
outBitStream.Write<bool>(m_ShouldStopAtDesiredWaypoint);
outBitStream.Write<bool>(m_InReverse);
outBitStream.Write<float>(m_PercentBetweenPoints);
outBitStream.Write<float>(m_Position.x);
outBitStream.Write<float>(m_Position.y);
outBitStream.Write<float>(m_Position.z);
outBitStream.Write<uint32_t>(m_CurrentWaypointIndex);
outBitStream.Write<uint32_t>(m_NextWaypointIndex);
outBitStream.Write<float>(m_IdleTimeElapsed);
outBitStream.Write<float>(m_MoveTimeElapsed);
}
uint32_t PlatformSubComponent::GetSerializedState() const {
if (m_State & PlatformState::Stopped) return PlatformState::StoppedSerialized;
if (m_State & PlatformState::Travelling) return PlatformState::MovingSerialized;
return PlatformState::StationarySerialized;
}
void PlatformSubComponent::Update(float deltaTime, bool& dirtyOut) {
if (!m_Active) return;
if (m_State == 0) return;
if (!m_Path || m_Path->pathWaypoints.empty()) return;
if (IncrementWaitingTime(deltaTime)) {
StartTravelling();
dirtyOut = true;
}
if (m_State & PlatformState::Travelling) {
UpdatePositionAlongPath(deltaTime);
bool arrived = false;
if (m_TimeBasedMovement) {
arrived = m_TravelTime > 0.0f && std::abs(m_TravelTime - m_MoveTimeElapsed) < 0.001f;
} else {
arrived = CloseToNextWaypoint();
}
if (arrived) {
ArrivedAtWaypoint(dirtyOut);
}
}
}
// --- Movement control ---
void PlatformSubComponent::StartPathing() {
if (!m_Path || m_Path->pathWaypoints.empty()) return;
m_Active = true;
SetupWaypointSegment(m_CurrentWaypointIndex);
m_State = PlatformState::Waiting | PlatformState::Stopped;
m_IdleTimeElapsed = 0.0f;
m_MoveTimeElapsed = 0.0f;
m_PercentBetweenPoints = 0.0f;
m_HasStartedTravelling = false;
}
void PlatformSubComponent::StopPathing() {
m_State = PlatformState::Stopped;
m_DesiredWaypointIndex = -1;
m_ShouldStopAtDesiredWaypoint = false;
m_MoveTimeElapsed = 0.0f;
m_HasStartedTravelling = false;
ZeroPhysicsVelocity();
}
void PlatformSubComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) {
m_DesiredWaypointIndex = static_cast<int32_t>(index);
m_NextWaypointIndex = index;
m_ShouldStopAtDesiredWaypoint = stopAtWaypoint;
StartPathing();
}
void PlatformSubComponent::WarpToWaypoint(size_t index) {
if (!m_Path || index >= m_Path->pathWaypoints.size()) return;
const auto& waypoint = m_Path->pathWaypoints[index];
m_Position = waypoint.position;
m_CurrentWaypointIndex = static_cast<uint32_t>(index);
m_PercentBetweenPoints = 0.0f;
m_MoveTimeElapsed = 0.0f;
if (m_ParentEntity) {
m_ParentEntity->SetPosition(waypoint.position);
m_ParentEntity->SetRotation(waypoint.rotation);
}
}
size_t PlatformSubComponent::GetLastWaypointIndex() const {
if (!m_Path || m_Path->pathWaypoints.empty()) return 0;
return m_Path->pathWaypoints.size() - 1;
}
// --- Waypoint segment setup (mirrors client ProcessStateChange) ---
void PlatformSubComponent::SetupWaypointSegment(uint32_t waypointIndex) {
if (!m_Path || m_Path->pathWaypoints.empty()) return;
m_CurrentWaypointIndex = waypointIndex;
const auto& currentWP = m_Path->pathWaypoints[m_CurrentWaypointIndex];
m_CurrentWaypointPosition = currentWP.position;
m_CurrentWaypointRotation = currentWP.rotation;
m_WaitTime = currentWP.movingPlatform.wait;
m_Position = currentWP.position;
bool changedDirection = false;
if (!m_InReverse) {
m_NextWaypointIndex = GetNextWaypoint(m_CurrentWaypointIndex, changedDirection);
if (changedDirection) m_InReverse = true;
} else {
m_NextWaypointIndex = GetNextReversedWaypoint(m_CurrentWaypointIndex, changedDirection);
if (changedDirection) m_InReverse = false;
}
const auto& nextWP = m_Path->pathWaypoints[m_NextWaypointIndex];
m_NextWaypointPosition = nextWP.position;
m_NextWaypointRotation = nextWP.rotation;
m_DirectionVector = m_NextWaypointPosition - m_CurrentWaypointPosition;
m_TotalDistance = m_DirectionVector.Length();
if (m_TotalDistance > 0.0f) {
m_DirectionVector = m_DirectionVector / m_TotalDistance;
}
CalculateWaypointSpeeds();
m_MoveTimeElapsed = 0.0f;
m_IdleTimeElapsed = 0.0f;
m_HasStartedTravelling = false;
if (m_TimeBasedMovement) {
m_PercentBetweenPoints = 0.0f;
} else if (m_TotalDistance > 0.0f) {
m_PercentBetweenPoints = (m_Position - m_CurrentWaypointPosition).Length() / m_TotalDistance;
} else {
m_PercentBetweenPoints = 0.0f;
}
if (m_ParentEntity) {
m_ParentEntity->SetPosition(m_CurrentWaypointPosition);
m_ParentEntity->SetRotation(m_CurrentWaypointRotation);
}
}
// --- Waypoint navigation (exact match of client decompilation) ---
uint32_t PlatformSubComponent::GetNextWaypoint(uint32_t current, bool& changedDirection) const {
changedDirection = false;
uint32_t next = current + 1;
const auto numWaypoints = static_cast<uint32_t>(m_Path->pathWaypoints.size());
if (next >= numWaypoints) {
switch (m_Path->pathBehavior) {
case PathBehavior::Once:
next = numWaypoints - 1;
break;
case PathBehavior::Bounce:
next = numWaypoints >= 2 ? numWaypoints - 2 : 0;
changedDirection = true;
break;
case PathBehavior::Loop:
default:
next = 0;
break;
}
}
return next;
}
uint32_t PlatformSubComponent::GetNextReversedWaypoint(uint32_t current, bool& changedDirection) const {
changedDirection = false;
if (current == 0) {
switch (m_Path->pathBehavior) {
case PathBehavior::Once:
return 0;
case PathBehavior::Bounce:
changedDirection = true;
return 1;
case PathBehavior::Loop:
default:
return static_cast<uint32_t>(m_Path->pathWaypoints.size()) - 1;
}
}
return current - 1;
}
// --- Arrival detection ---
bool PlatformSubComponent::CloseToNextWaypoint() const {
if (m_TimeBasedMovement) return false;
const NiPoint3 toNext = m_NextWaypointPosition - m_Position;
const float distSq = toNext.SquaredLength();
if (distSq <= 0.001f) return true;
const float dot = toNext.DotProduct(m_DirectionVector);
return dot <= 0.0f;
}
// --- Travel time calculation ---
float PlatformSubComponent::CalculateAcceleration(float vi, float vf, float d) {
if (d < 0.0001f) return 0.0f;
return (vf * vf - vi * vi) / (2.0f * d);
}
float PlatformSubComponent::CalculateTime(float vi, float a, float d) {
if (d < 0.0001f) return 0.0f;
if (std::abs(a) < 0.0001f) {
return vi > 0.0f ? d / vi : 0.0f;
}
const float discriminant = 2.0f * a * d + vi * vi;
if (discriminant < 0.0f) return 0.0f;
return (std::sqrt(discriminant) - vi) / a;
}
void PlatformSubComponent::CalculateWaypointSpeeds() {
if (m_CurrentWaypointIndex == m_NextWaypointIndex) {
m_TravelTime = 0.0f;
return;
}
if (m_TimeBasedMovement) {
uint32_t minIdx = std::min(m_CurrentWaypointIndex, m_NextWaypointIndex);
m_CurrentSpeed = m_Path->pathWaypoints[minIdx].speed;
m_NextSpeed = 0.0f;
m_TravelTime = m_CurrentSpeed;
} else {
m_CurrentSpeed = m_Path->pathWaypoints[m_CurrentWaypointIndex].speed;
m_NextSpeed = m_Path->pathWaypoints[m_NextWaypointIndex].speed;
float a = CalculateAcceleration(m_CurrentSpeed, m_NextSpeed, m_TotalDistance);
m_TravelTime = CalculateTime(m_CurrentSpeed, a, m_TotalDistance);
}
}
float PlatformSubComponent::CalculateCurrentSpeed() const {
if (m_TimeBasedMovement) {
if (m_CurrentSpeed > 0.0f) {
return m_TotalDistance / m_CurrentSpeed;
}
return 0.0f;
}
return (m_NextSpeed - m_CurrentSpeed) * m_PercentBetweenPoints + m_CurrentSpeed;
}
// --- State machine helpers ---
bool PlatformSubComponent::IncrementWaitingTime(float deltaTime) {
if (!(m_State & PlatformState::Waiting)) return false;
if (m_State & PlatformState::Travelling) return false;
m_IdleTimeElapsed += deltaTime;
if (m_IdleTimeElapsed >= m_WaitTime) {
m_IdleTimeElapsed = 0.0f;
return true;
}
return false;
}
void PlatformSubComponent::StartTravelling() {
m_State = (m_State & ~(PlatformState::Stopped | PlatformState::Waiting)) | PlatformState::Travelling;
m_MoveTimeElapsed = 0.0f;
m_HasStartedTravelling = false;
}
void PlatformSubComponent::ArrivedAtWaypoint(bool& dirtyOut) {
dirtyOut = true;
m_Position = m_NextWaypointPosition;
m_PercentBetweenPoints = 1.0f;
if (m_ParentEntity) {
m_ParentEntity->SetPosition(m_NextWaypointPosition);
m_ParentEntity->SetRotation(m_NextWaypointRotation);
}
PlayArriveSound();
if (m_ParentEntity) {
m_ParentEntity->GetScript()->OnWaypointReached(m_ParentEntity, m_NextWaypointIndex);
}
bool isAtDesiredWaypoint = false;
bool stopAtDesired = false;
if (m_DesiredWaypointIndex >= 0 &&
static_cast<uint32_t>(m_DesiredWaypointIndex) == m_NextWaypointIndex) {
isAtDesiredWaypoint = true;
stopAtDesired = m_ShouldStopAtDesiredWaypoint;
m_ShouldStopAtDesiredWaypoint = false;
m_DesiredWaypointIndex = -1;
}
if (isAtDesiredWaypoint && m_ParentEntity) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(m_ParentEntity->GetObjectID());
bitStream.Write(MessageType::Game::ARRIVED_AT_DESIRED_WAYPOINT);
SEND_PACKET_BROADCAST;
}
bool atEnd = false;
const auto numWaypoints = static_cast<uint32_t>(m_Path->pathWaypoints.size());
if (m_NextWaypointIndex == 0 || m_NextWaypointIndex == numWaypoints - 1) {
atEnd = true;
}
if (atEnd && m_ParentEntity) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(m_ParentEntity->GetObjectID());
bitStream.Write(MessageType::Game::PLATFORM_AT_LAST_WAYPOINT);
SEND_PACKET_BROADCAST;
}
bool stopOnce = false;
if (atEnd && m_Path->pathBehavior == PathBehavior::Once) {
stopOnce = true;
m_InReverse = !m_InReverse;
}
if (stopAtDesired || stopOnce) {
m_State = PlatformState::Stopped;
m_MoveTimeElapsed = 0.0f;
m_HasStartedTravelling = false;
ZeroPhysicsVelocity();
if (m_ParentEntity) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(m_ParentEntity->GetObjectID());
bitStream.Write(MessageType::Game::ARRIVED);
SEND_PACKET_BROADCAST;
}
} else {
SetupWaypointSegment(m_NextWaypointIndex);
m_State = PlatformState::Waiting;
}
}
void PlatformSubComponent::UpdatePositionAlongPath(float deltaTime) {
if (m_TotalDistance <= 0.0f && !m_TimeBasedMovement) return;
m_MoveTimeElapsed += deltaTime;
// Calculate percent between waypoints matching client CalculatePercentTravelledToWaypoint:
// Distance-based: percent = dist(position, currentWP) / dist(nextWP, currentWP)
// Time-based: percent = moveTimeElapsed / travelTime
if (m_TimeBasedMovement) {
if (m_TravelTime > 0.0f) {
m_MoveTimeElapsed = std::min(m_MoveTimeElapsed, m_TravelTime);
m_PercentBetweenPoints = m_MoveTimeElapsed / m_TravelTime;
}
} else if (m_TotalDistance > 0.0f) {
float distanceTravelled = (m_Position - m_CurrentWaypointPosition).Length();
m_PercentBetweenPoints = std::min(distanceTravelled / m_TotalDistance, 1.0f);
}
// Send Departed message on first travel frame (matching client RunPlatform)
if (!m_HasStartedTravelling) {
m_HasStartedTravelling = true;
PlayDepartSound();
if (m_ParentEntity) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(m_ParentEntity->GetObjectID());
bitStream.Write(MessageType::Game::DEPARTED);
SEND_PACKET_BROADCAST;
}
}
// Advance position using velocity and deltaTime (matching client physics model)
// The client sets velocity then lets the physics engine move the object.
// We do the same: calculate speed, derive velocity, advance position.
float speed = CalculateCurrentSpeed();
NiPoint3 velocity = m_DirectionVector * speed;
m_Position = m_Position + velocity * deltaTime;
// Clamp position to not overshoot the next waypoint
float distToNext = (m_NextWaypointPosition - m_Position).DotProduct(m_DirectionVector);
if (distToNext <= 0.0f) {
m_Position = m_NextWaypointPosition;
}
if (m_ParentEntity) {
m_ParentEntity->SetPosition(m_Position);
SetPhysicsVelocity(velocity);
// Slerp rotation between waypoints
auto interpRot = glm::slerp(m_CurrentWaypointRotation, m_NextWaypointRotation, m_PercentBetweenPoints);
m_ParentEntity->SetRotation(interpRot);
}
}
// --- Physics velocity helpers ---
void PlatformSubComponent::SetPhysicsVelocity(const NiPoint3& velocity) {
if (!m_ParentEntity) return;
auto* simplePhysics = m_ParentEntity->GetComponent<SimplePhysicsComponent>();
if (simplePhysics) {
simplePhysics->SetVelocity(velocity);
}
}
void PlatformSubComponent::ZeroPhysicsVelocity() {
if (!m_ParentEntity) return;
auto* simplePhysics = m_ParentEntity->GetComponent<SimplePhysicsComponent>();
if (simplePhysics) {
simplePhysics->SetVelocity(NiPoint3Constant::ZERO);
simplePhysics->SetAngularVelocity(NiPoint3Constant::ZERO);
}
}
// --- Sound helpers ---
void PlatformSubComponent::PlayDepartSound() {
if (!m_ParentEntity || !m_Path) return;
if (m_CurrentWaypointIndex >= m_Path->pathWaypoints.size()) return;
const auto& sound = m_Path->pathWaypoints[m_CurrentWaypointIndex].movingPlatform.departSound;
if (!sound.empty()) {
GameMessages::SendPlayNDAudioEmitter(m_ParentEntity, UNASSIGNED_SYSTEM_ADDRESS, sound);
}
}
void PlatformSubComponent::PlayArriveSound() {
if (!m_ParentEntity || !m_Path) return;
if (m_NextWaypointIndex >= m_Path->pathWaypoints.size()) return;
const auto& sound = m_Path->pathWaypoints[m_NextWaypointIndex].movingPlatform.arriveSound;
if (!sound.empty()) {
GameMessages::SendPlayNDAudioEmitter(m_ParentEntity, UNASSIGNED_SYSTEM_ADDRESS, sound);
}
}

View File

@@ -1,127 +0,0 @@
#ifndef PLATFORMSUBCOMPONENT_H
#define PLATFORMSUBCOMPONENT_H
#include "RakNetTypes.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include <cstdint>
class Entity;
class Path;
class SimplePhysicsComponent;
/**
* Platform state flags (bitmask matching client LWOPlatform state bits)
*/
namespace PlatformState {
constexpr uint32_t Waiting = 1 << 0; // 0x01 - Waiting at waypoint
constexpr uint32_t Travelling = 1 << 1; // 0x02 - Moving between waypoints
constexpr uint32_t Stopped = 1 << 2; // 0x04 - Movement halted
// These map to the old eMovementPlatformState values for serialization
constexpr uint32_t MovingSerialized = 0b00010; // Travelling
constexpr uint32_t StationarySerialized = 0b11001; // Waiting
constexpr uint32_t StoppedSerialized = 0b01100; // Stopped
};
/**
* Base class for platform subcomponents. Mirrors the client's LWOPlatform base.
* Handles the core state machine: waiting at waypoints, travelling between them,
* arrival detection, and waypoint navigation (loop/bounce/once).
*/
class PlatformSubComponent {
public:
PlatformSubComponent(Entity* parentEntity, const Path* path);
virtual ~PlatformSubComponent() = default;
virtual void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate);
virtual void Update(float deltaTime, bool& dirtyOut);
void StartPathing();
void StopPathing();
void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true);
void WarpToWaypoint(size_t index);
void SetupWaypointSegment(uint32_t waypointIndex);
// --- State accessors ---
uint32_t GetState() const { return m_State; }
void SetState(uint32_t state) { m_State = state; }
int32_t GetDesiredWaypointIndex() const { return m_DesiredWaypointIndex; }
bool GetInReverse() const { return m_InReverse; }
bool GetShouldStopAtDesiredWaypoint() const { return m_ShouldStopAtDesiredWaypoint; }
float GetPercentBetweenPoints() const { return m_PercentBetweenPoints; }
NiPoint3 GetPosition() const { return m_Position; }
uint32_t GetCurrentWaypointIndex() const { return m_CurrentWaypointIndex; }
uint32_t GetNextWaypointIndex() const { return m_NextWaypointIndex; }
float GetIdleTimeElapsed() const { return m_IdleTimeElapsed; }
float GetMoveTimeElapsed() const { return m_MoveTimeElapsed; }
float GetSpeed() const { return m_CurrentSpeed; }
float GetWaitTime() const { return m_WaitTime; }
size_t GetLastWaypointIndex() const;
bool IsActive() const { return m_Active; }
void SetDesiredWaypointIndex(int32_t index) { m_DesiredWaypointIndex = index; }
void SetShouldStopAtDesiredWaypoint(bool value) { m_ShouldStopAtDesiredWaypoint = value; }
void SetInReverse(bool value) { m_InReverse = value; }
void SetActive(bool value) { m_Active = value; }
uint32_t GetSerializedState() const;
protected:
uint32_t GetNextWaypoint(uint32_t current, bool& changedDirection) const;
uint32_t GetNextReversedWaypoint(uint32_t current, bool& changedDirection) const;
bool CloseToNextWaypoint() const;
static float CalculateAcceleration(float vi, float vf, float d);
static float CalculateTime(float vi, float a, float d);
void CalculateWaypointSpeeds();
float CalculateCurrentSpeed() const;
bool IncrementWaitingTime(float deltaTime);
void StartTravelling();
void ArrivedAtWaypoint(bool& dirtyOut);
virtual void UpdatePositionAlongPath(float deltaTime);
void SetPhysicsVelocity(const NiPoint3& velocity);
void ZeroPhysicsVelocity();
void PlayDepartSound();
void PlayArriveSound();
Entity* m_ParentEntity = nullptr;
const Path* m_Path = nullptr;
bool m_Active = false;
uint32_t m_State = PlatformState::Stopped;
int32_t m_DesiredWaypointIndex = -1;
bool m_InReverse = false;
bool m_ShouldStopAtDesiredWaypoint = false;
float m_PercentBetweenPoints = 0.0f;
NiPoint3 m_Position{};
uint32_t m_CurrentWaypointIndex = 0;
uint32_t m_NextWaypointIndex = 0;
float m_IdleTimeElapsed = 0.0f;
float m_MoveTimeElapsed = 0.0f;
float m_CurrentSpeed = 0.0f;
float m_NextSpeed = 0.0f;
float m_WaitTime = 0.0f;
NiPoint3 m_CurrentWaypointPosition{};
NiPoint3 m_NextWaypointPosition{};
NiQuaternion m_CurrentWaypointRotation = QuatUtils::IDENTITY;
NiQuaternion m_NextWaypointRotation = QuatUtils::IDENTITY;
NiPoint3 m_DirectionVector{};
float m_TotalDistance = 0.0f;
float m_TravelTime = 0.0f;
bool m_TimeBasedMovement = false;
bool m_HasStartedTravelling = false;
};
#endif // PLATFORMSUBCOMPONENT_H

View File

@@ -1,93 +0,0 @@
#include "RotatorSubComponent.h"
#include "Entity.h"
#include "SimplePhysicsComponent.h"
#include <cmath>
#include <algorithm>
#include <glm/gtc/quaternion.hpp>
RotatorSubComponent::RotatorSubComponent(Entity* parentEntity, const Path* path)
: PlatformSubComponent(parentEntity, path) {
}
void RotatorSubComponent::UpdatePositionAlongPath(float deltaTime) {
// Do the base linear movement (position, velocity, departed message)
PlatformSubComponent::UpdatePositionAlongPath(deltaTime);
if (!m_ParentEntity) return;
// Angular velocity calculation matching client LWOPlatform::SetAngularVelocity:
// 1. If current and next rotations are the same, just set current rotation and zero angular velocity
// 2. Otherwise, SLERP to the target rotation based on percent, then compute angular velocity
// to reach the next waypoint rotation in the remaining travel time
if (m_CurrentWaypointRotation == m_NextWaypointRotation) {
m_ParentEntity->SetRotation(m_CurrentWaypointRotation);
auto* simplePhysics = m_ParentEntity->GetComponent<SimplePhysicsComponent>();
if (simplePhysics) {
simplePhysics->SetAngularVelocity(NiPoint3Constant::ZERO);
}
return;
}
// SLERP to get the current target rotation (matching client CalculateSlerp)
NiQuaternion targetRot = glm::slerp(m_CurrentWaypointRotation, m_NextWaypointRotation, m_PercentBetweenPoints);
// Check if we're already close enough to snap (matching client's angle threshold check)
NiQuaternion currentRot = m_ParentEntity->GetRotation();
float dotProduct = glm::dot(targetRot, currentRot);
if (dotProduct < 0.0f) dotProduct = -dotProduct;
float angleDiff = std::acos(std::clamp(dotProduct, 0.0f, 1.0f));
bool snappedToTarget = false;
if (angleDiff < m_MaxLerpAngle) {
// Close enough — snap to current rotation, use the actual current rotation for angular vel calc
snappedToTarget = true;
targetRot = currentRot;
} else {
// Set the SLERP'd rotation on the entity
m_ParentEntity->SetRotation(targetRot);
}
// Calculate remaining travel time for angular velocity
float remainingTime = 0.0f;
if (!m_TimeBasedMovement && m_TotalDistance > 0.0f) {
// Distance-based: calculate remaining time from remaining distance and speeds
NiPoint3 toNext = m_NextWaypointPosition - m_Position;
float remainingDist = toNext.Length();
float currentSpeed = CalculateCurrentSpeed();
if (currentSpeed > 0.0f) {
remainingTime = remainingDist / currentSpeed;
}
} else if (m_TimeBasedMovement) {
remainingTime = m_TravelTime - m_MoveTimeElapsed;
}
if (remainingTime > 0.0f) {
// Compute angular velocity from quaternion difference (matching client LWOPhysicsCalcAngularVelocity)
// Angular velocity = axis * (angle / time)
NiQuaternion rotDiff = m_NextWaypointRotation * glm::inverse(targetRot);
// Normalize to ensure valid quaternion
rotDiff = glm::normalize(rotDiff);
float angle = 2.0f * std::acos(std::clamp(rotDiff.w, -1.0f, 1.0f));
if (std::abs(angle) > 0.001f) {
float sinHalf = std::sqrt(1.0f - rotDiff.w * rotDiff.w);
NiPoint3 axis;
if (sinHalf > 0.001f) {
axis = NiPoint3(rotDiff.x / sinHalf, rotDiff.y / sinHalf, rotDiff.z / sinHalf);
} else {
axis = NiPoint3(0.0f, 1.0f, 0.0f);
}
m_AngularVelocity = axis * (angle / remainingTime);
auto* simplePhysics = m_ParentEntity->GetComponent<SimplePhysicsComponent>();
if (simplePhysics) {
simplePhysics->SetAngularVelocity(m_AngularVelocity);
}
}
}
}

View File

@@ -1,24 +0,0 @@
#ifndef ROTATORSUBCOMPONENT_H
#define ROTATORSUBCOMPONENT_H
#include "PlatformSubComponent.h"
/**
* Rotator - follows a path like Mover but also applies angular velocity.
* Corresponds to client LWOPlatformRotator (type 6).
*/
class RotatorSubComponent final : public PlatformSubComponent {
public:
RotatorSubComponent(Entity* parentEntity, const Path* path);
void UpdatePositionAlongPath(float deltaTime) override;
private:
NiPoint3 m_RotationAxis{};
float m_Rate = 0.0f;
NiPoint3 m_AngularVelocity{};
bool m_AllowRotSnap = true;
float m_MaxLerpAngle = 0.785398f; // ~45 degrees
};
#endif // ROTATORSUBCOMPONENT_H

View File

@@ -1,55 +0,0 @@
#include "SimpleMoverSubComponent.h"
#include "Zone.h"
SimpleMoverSubComponent::SimpleMoverSubComponent(Entity* parentEntity, const NiPoint3& startPos,
const NiQuaternion& startRot, const NiPoint3& platformMove, float platformMoveTime)
: PlatformSubComponent(parentEntity, nullptr) {
GeneratePath(startPos, startRot, platformMove, platformMoveTime);
m_Path = m_GeneratedPath.get();
m_TimeBasedMovement = false;
// Auto-activate and set up initial segment (matching client GenerateSimpleMoverPath)
if (m_Path && !m_Path->pathWaypoints.empty()) {
m_Active = true;
SetupWaypointSegment(m_CurrentWaypointIndex);
}
}
void SimpleMoverSubComponent::GeneratePath(const NiPoint3& startPos, const NiQuaternion& startRot,
const NiPoint3& platformMove, float platformMoveTime) {
auto path = std::make_unique<Path>();
path->pathType = PathType::MovingPlatform;
path->pathBehavior = PathBehavior::Once;
path->pathName = "SimpleMoverPath";
path->movingPlatform.timeBasedMovement = 0;
// Waypoint 0: start position
PathWaypoint wp0;
wp0.position = startPos;
wp0.rotation = startRot;
wp0.movingPlatform.wait = 0.0f;
// Waypoint 1: start + rotated platformMove
NiPoint3 move = platformMove;
NiPoint3 rotatedMove = move.RotateByQuaternion(startRot);
PathWaypoint wp1;
wp1.position = startPos + rotatedMove;
wp1.rotation = startRot;
wp1.movingPlatform.wait = 0.0f;
// Calculate speed: length(platformMove) / platformMoveTime
float speed = 0.0f;
if (move.SquaredLength() > 0.0f && platformMoveTime > 0.0f) {
speed = platformMove.Length() / platformMoveTime;
}
wp0.speed = speed;
wp1.speed = speed;
path->pathWaypoints.push_back(wp0);
path->pathWaypoints.push_back(wp1);
path->waypointCount = 2;
m_GeneratedPath = std::move(path);
}

View File

@@ -1,28 +0,0 @@
#ifndef SIMPLEMOVERSUBCOMPONENT_H
#define SIMPLEMOVERSUBCOMPONENT_H
#include "PlatformSubComponent.h"
#include <memory>
class Path;
/**
* Simple mover - auto-generates a 2-waypoint path from component properties.
* Corresponds to client LWOPlatformSimpleMover (type 5).
*/
class SimpleMoverSubComponent final : public PlatformSubComponent {
public:
SimpleMoverSubComponent(Entity* parentEntity, const NiPoint3& startPos,
const NiQuaternion& startRot, const NiPoint3& platformMove, float platformMoveTime);
const Path* GetGeneratedPath() const { return m_GeneratedPath.get(); }
private:
void GeneratePath(const NiPoint3& startPos, const NiQuaternion& startRot,
const NiPoint3& platformMove, float platformMoveTime);
std::unique_ptr<Path> m_GeneratedPath;
};
#endif // SIMPLEMOVERSUBCOMPONENT_H

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,9 +78,48 @@ 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
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 {
return owner;
}
@@ -832,3 +874,126 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess
modelComponent->OnChatMessageReceived(sMessage);
}
}
void PropertyManagementComponent::Update(float deltaTime) {
// Check for day rollover
const auto currentDate = GeneralUtils::GetCurrentUTCDate();
if (currentDate != m_CurrentDate) {
if (m_ReputationDirty) {
LOG_DEBUG("Saving dirty reputation data before daily rollover for property %llu", static_cast<unsigned long long>(propertyId));
SaveReputation();
}
m_CurrentDate = currentDate;
m_PlayerActivity.clear();
}
// Periodic reputation save
m_ReputationSaveTimer += deltaTime;
if (m_ReputationSaveTimer >= m_RepSaveInterval && m_ReputationDirty) {
SaveReputation();
m_ReputationSaveTimer = 0.0f;
}
// Property reputation decay
if (m_RepDecayRate > 0.0f && owner != LWOOBJID_EMPTY) {
m_DecayTimer += deltaTime;
if (m_DecayTimer >= m_RepDecayInterval) {
m_DecayTimer = 0.0f;
if (reputation > m_RepDecayMinimum) {
const auto loss = static_cast<uint32_t>(m_RepDecayRate);
reputation = (reputation > m_RepDecayMinimum + loss) ? reputation - loss : m_RepDecayMinimum;
m_ReputationDirty = true;
}
}
}
}
void PropertyManagementComponent::OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update) {
if (owner == LWOOBJID_EMPTY) return;
if (propertyId == LWOOBJID_EMPTY) return;
if (m_RepInterval <= 0.0f) return;
// Check same-account exclusion (covers owner + owner's alts)
auto* character = player->GetCharacter();
if (!character) return;
auto* parentUser = character->GetParentUser();
if (!parentUser) return;
if (parentUser->GetAccountID() == m_OwnerAccountId) return;
// Check velocity threshold (player must be active/moving)
if (update.velocity.SquaredLength() < m_RepVelocityThreshold * m_RepVelocityThreshold) return;
const auto playerId = character->GetID();
auto& info = m_PlayerActivity[playerId];
// Check daily cap
if (info.dailyContribution >= m_RepDailyCap) return;
// Compute delta time since last position update for this player
const auto now = std::chrono::steady_clock::now();
if (info.hasLastUpdate) {
const auto dt = std::chrono::duration<float>(now - info.lastUpdate).count();
// Cap delta to avoid spikes from reconnects etc.
const auto clampedDt = std::min(dt, 1.0f);
info.activeTime += clampedDt;
}
info.lastUpdate = now;
info.hasLastUpdate = true;
// Check if we've accumulated enough active time for a reputation tick
if (info.activeTime >= m_RepInterval) {
info.activeTime -= m_RepInterval;
const auto repGain = static_cast<uint32_t>(m_RepPerTick * m_RepMultiplier);
if (repGain == 0) return;
// Clamp to daily cap
const auto actualGain = std::min(repGain, m_RepDailyCap - info.dailyContribution);
if (actualGain == 0) return;
// Grant property reputation
reputation += actualGain;
info.dailyContribution += actualGain;
m_ReputationDirty = true;
// Grant character reputation to property owner
auto* ownerEntity = Game::entityManager->GetEntity(owner);
if (ownerEntity) {
auto* charComp = ownerEntity->GetComponent<CharacterComponent>();
if (charComp) {
charComp->SetReputation(charComp->GetReputation() + actualGain);
}
} else {
// Owner is offline, update DB directly
auto ownerCharId = owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
const auto currentRep = Database::Get()->GetCharacterReputation(ownerCharId);
Database::Get()->SetCharacterReputation(ownerCharId, currentRep + actualGain);
}
}
}
void PropertyManagementComponent::SaveReputation() {
if (!m_ReputationDirty) return;
if (propertyId == LWOOBJID_EMPTY) return;
Database::Get()->UpdatePropertyReputation(propertyId, reputation);
for (const auto& [playerId, info] : m_PlayerActivity) {
if (info.dailyContribution > 0) {
Database::Get()->UpdatePropertyReputationContribution(propertyId, playerId, m_CurrentDate, info.dailyContribution);
}
}
m_ReputationDirty = false;
}
void PropertyManagementComponent::LoadDailyContributions() {
if (propertyId == LWOOBJID_EMPTY) return;
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, m_CurrentDate);
for (const auto& contrib : contributions) {
m_PlayerActivity[contrib.playerId].dailyContribution = contrib.reputationGained;
}
}

View File

@@ -1,10 +1,13 @@
#pragma once
#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
*/

View File

@@ -12,7 +12,6 @@
#include "CDPhysicsComponentTable.h"
#include "Entity.h"
#include "MovingPlatformComponent.h"
#include "StringifiedEnum.h"
#include "Amf3.h"
@@ -40,11 +39,6 @@ SimplePhysicsComponent::~SimplePhysicsComponent() {
void SimplePhysicsComponent::Update(const float deltaTime) {
if (m_Velocity == NiPoint3Constant::ZERO) return;
// If this entity has a MovingPlatformComponent, it owns position updates.
// Don't double-move by also applying velocity here.
if (m_Parent->GetComponent<MovingPlatformComponent>()) return;
m_Position += m_Velocity * deltaTime;
m_DirtyPosition = true;
Game::entityManager->SerializeEntity(m_Parent);

View File

@@ -365,18 +365,31 @@ void GameMessages::SendResetMissions(Entity* entity, const SystemAddress& sysAdd
void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint,
int iIndex, int iDesiredWaypointIndex, int nextIndex,
eMovementPlatformState movementState,
bool bReverse, float fIdleTimeElapsed, float fMoveTimeElapsed,
float fPercentBetweenPoints, NiPoint3 ptUnexpectedLocation,
NiQuaternion qUnexpectedRotation) {
eMovementPlatformState movementState) {
CBITSTREAM;
CMSGHEADER;
const auto lot = entity->GetLOT();
if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308) {
iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0;
iIndex = 0;
nextIndex = 0;
bStopAtDesiredWaypoint = true;
movementState = eMovementPlatformState::Stationary;
}
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::PLATFORM_RESYNC);
bool bReverse = false;
int eCommand = 0;
int eUnexpectedCommand = 0;
float fIdleTimeElapsed = 0.0f;
float fMoveTimeElapsed = 0.0f;
float fPercentBetweenPoints = 0.0f;
NiPoint3 ptUnexpectedLocation = NiPoint3Constant::ZERO;
NiQuaternion qUnexpectedRotation = QuatUtils::IDENTITY;
bitStream.Write(bReverse);
bitStream.Write(bStopAtDesiredWaypoint);

View File

@@ -104,10 +104,7 @@ namespace GameMessages {
void SendStartPathing(Entity* entity);
void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false,
int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1,
eMovementPlatformState movementState = eMovementPlatformState::Moving,
bool bReverse = false, float fIdleTimeElapsed = 0.0f, float fMoveTimeElapsed = 0.0f,
float fPercentBetweenPoints = 0.0f, NiPoint3 ptUnexpectedLocation = NiPoint3Constant::ZERO,
NiQuaternion qUnexpectedRotation = QuatUtils::IDENTITY);
eMovementPlatformState movementState = eMovementPlatformState::Moving);
void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1);
void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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