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 scripts
!cmake/* !cmake/*
!cmake/toolchains/* !cmake/toolchains/*
.mcp.json
.claude/

View File

@@ -357,4 +357,18 @@ namespace GeneralUtils {
return value - modulo; return value - modulo;
} }
} }
//! Returns the current UTC date as a string in "YYYY-MM-DD" format
inline std::string GetCurrentUTCDate() {
const auto now = std::time(nullptr);
std::tm utcTime{};
#ifdef _MSC_VER
gmtime_s(&utcTime, &now);
#else
gmtime_r(&now, &utcTime);
#endif
char buf[11];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &utcTime);
return std::string(buf);
}
} }

View File

@@ -1,7 +1,7 @@
add_subdirectory(CDClientDatabase) add_subdirectory(CDClientDatabase)
add_subdirectory(GameDatabase) add_subdirectory(GameDatabase)
add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp") add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp" "CharacterReputationMigration.cpp")
add_custom_target(conncpp_dylib add_custom_target(conncpp_dylib
${CMAKE_COMMAND} -E copy $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR}) ${CMAKE_COMMAND} -E copy $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR})

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 "IAccountsRewardCodes.h"
#include "IBehaviors.h" #include "IBehaviors.h"
#include "IUgcModularBuild.h" #include "IUgcModularBuild.h"
#include "ICharacterReputation.h"
#include "IPropertyReputationContribution.h"
#ifdef _DEBUG #ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0) # define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
@@ -38,7 +40,8 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild { public IBehaviors, public IUgcModularBuild,
public ICharacterReputation, public IPropertyReputationContribution {
public: public:
virtual ~GameDatabase() = default; virtual ~GameDatabase() = default;
// TODO: These should be made private. // TODO: These should be made private.

View File

@@ -33,6 +33,9 @@ public:
// Get the character ids for the given account. // Get the character ids for the given account.
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0; virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
// Get all character ids.
virtual std::vector<LWOOBJID> GetAllCharacterIds() = 0;
// Insert a new character into the database. // Insert a new character into the database.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0; virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;

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 IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override; void DeleteUgcBuild(const LWOOBJID bigId) override;
std::vector<LWOOBJID> GetAllCharacterIds() override;
int64_t GetCharacterReputation(const LWOOBJID charId) override;
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override;
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override;
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override;
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override;
uint32_t GetAccountCount() override; uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override; bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override; std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

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

View File

@@ -52,6 +52,18 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
return toReturn; return toReturn;
} }
std::vector<LWOOBJID> MySQLDatabase::GetAllCharacterIds() {
auto result = ExecuteSelect("SELECT id FROM charinfo;");
std::vector<LWOOBJID> toReturn;
toReturn.reserve(result->rowsCount());
while (result->next()) {
toReturn.push_back(result->getInt64("id"));
}
return toReturn;
}
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) { void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert( ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)", "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",

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 IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override; void DeleteUgcBuild(const LWOOBJID bigId) override;
std::vector<LWOOBJID> GetAllCharacterIds() override;
int64_t GetCharacterReputation(const LWOOBJID charId) override;
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override;
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override;
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override;
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override;
uint32_t GetAccountCount() override; uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override; bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override; std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;

View File

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

View File

@@ -55,6 +55,18 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
return toReturn; return toReturn;
} }
std::vector<LWOOBJID> SQLiteDatabase::GetAllCharacterIds() {
auto [_, result] = ExecuteSelect("SELECT id FROM charinfo;");
std::vector<LWOOBJID> toReturn;
while (!result.eof()) {
toReturn.push_back(result.getInt64Field("id"));
result.nextRow();
}
return toReturn;
}
void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) { void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert( ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))", "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))",

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 #ifndef TESTSQLDATABASE_H
#define TESTSQLDATABASE_H #define TESTSQLDATABASE_H
#include <map>
#include <unordered_map>
#include "GameDatabase.h" #include "GameDatabase.h"
class TestSQLDatabase : public GameDatabase { class TestSQLDatabase : public GameDatabase {
public:
void Connect() override; void Connect() override;
void Destroy(std::string source = "") override; void Destroy(std::string source = "") override;
@@ -103,11 +106,54 @@ class TestSQLDatabase : public GameDatabase {
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override {}; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override {};
void DeleteUgcBuild(const LWOOBJID bigId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {};
uint32_t GetAccountCount() override { return 0; }; uint32_t GetAccountCount() override { return 0; };
std::vector<LWOOBJID> GetAllCharacterIds() override { return {}; };
int64_t GetCharacterReputation(const LWOOBJID charId) override {
auto it = m_CharacterReputation.find(charId);
return it != m_CharacterReputation.end() ? it->second : 0;
};
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override {
m_CharacterReputation[charId] = reputation;
};
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override {
const auto key = std::make_pair(propertyId, date);
if (m_PropertyContributions.contains(key)) {
return m_PropertyContributions.at(key);
}
return {};
};
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override {
const auto key = std::make_pair(propertyId, date);
auto& entries = m_PropertyContributions[key];
for (auto& entry : entries) {
if (entry.playerId == playerId) {
entry.reputationGained = reputationGained;
return;
}
}
entries.push_back({ playerId, reputationGained });
};
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override {
m_PropertyReputation[propertyId] = reputation;
};
// Test helper: retrieve the property reputation stored via UpdatePropertyReputation.
uint32_t GetPropertyReputation(const LWOOBJID propertyId) const {
if (m_PropertyReputation.contains(propertyId)) {
return m_PropertyReputation.at(propertyId);
}
return 0;
}
bool IsNameInUse(const std::string_view name) override { return false; }; bool IsNameInUse(const std::string_view name) override { return false; };
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; } std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; }
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override { return {}; } std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override { return {}; }
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override { return {}; } std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override { return {}; }
private:
std::unordered_map<LWOOBJID, int64_t> m_CharacterReputation;
std::unordered_map<LWOOBJID, uint32_t> m_PropertyReputation;
std::map<std::pair<LWOOBJID, std::string>, std::vector<IPropertyReputationContribution::ContributionInfo>> m_PropertyContributions;
}; };
#endif //!TESTSQLDATABASE_H #endif //!TESTSQLDATABASE_H

View File

@@ -8,6 +8,7 @@
#include "Logger.h" #include "Logger.h"
#include "BinaryPathFinder.h" #include "BinaryPathFinder.h"
#include "ModelNormalizeMigration.h" #include "ModelNormalizeMigration.h"
#include "CharacterReputationMigration.h"
#include <fstream> #include <fstream>
@@ -49,6 +50,7 @@ void MigrationRunner::RunMigrations() {
bool runNormalizeMigrations = false; bool runNormalizeMigrations = false;
bool runNormalizeAfterFirstPartMigrations = false; bool runNormalizeAfterFirstPartMigrations = false;
bool runBrickBuildsNotOnGrid = false; bool runBrickBuildsNotOnGrid = false;
bool runCharacterReputationMigration = false;
for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) {
auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry);
@@ -67,6 +69,9 @@ void MigrationRunner::RunMigrations() {
runNormalizeAfterFirstPartMigrations = true; runNormalizeAfterFirstPartMigrations = true;
} else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) { } else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) {
runBrickBuildsNotOnGrid = true; runBrickBuildsNotOnGrid = true;
} else if (migration.name.ends_with("_character_reputation.sql")) {
runCharacterReputationMigration = true;
finalSQL.append(migration.data.c_str());
} else { } else {
finalSQL.append(migration.data.c_str()); finalSQL.append(migration.data.c_str());
} }
@@ -74,7 +79,7 @@ void MigrationRunner::RunMigrations() {
Database::Get()->InsertMigration(migration.name); Database::Get()->InsertMigration(migration.name);
} }
if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations && !runBrickBuildsNotOnGrid) { if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations && !runBrickBuildsNotOnGrid && !runCharacterReputationMigration) {
LOG("Server database is up to date."); LOG("Server database is up to date.");
return; return;
} }
@@ -110,6 +115,11 @@ void MigrationRunner::RunMigrations() {
if (runBrickBuildsNotOnGrid) { if (runBrickBuildsNotOnGrid) {
ModelNormalizeMigration::RunBrickBuildGrid(); ModelNormalizeMigration::RunBrickBuildGrid();
} }
if (runCharacterReputationMigration) {
uint32_t charactersMigrated = CharacterReputationMigration::Run();
LOG("%u characters had their reputation migrated from XML to the database.", charactersMigrated);
}
} }
void MigrationRunner::RunSQLiteMigrations() { void MigrationRunner::RunSQLiteMigrations() {

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 set(DGAME_DCOMPONENTS_SOURCES
${DGAME_DCOMPONENTS_SUBCOMPONENT_SOURCES}
"AchievementVendorComponent.cpp" "AchievementVendorComponent.cpp"
"ActivityComponent.cpp" "ActivityComponent.cpp"
"BaseCombatAIComponent.cpp" "BaseCombatAIComponent.cpp"
@@ -58,7 +51,7 @@ set(DGAME_DCOMPONENTS_SOURCES
) )
add_library(dComponents OBJECT ${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" # via ModelComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors/ControlBehaviorMessages" "${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors/ControlBehaviorMessages"
"${PROJECT_SOURCE_DIR}/dGame/dMission" # via MissionComponent.h "${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_EditorEnabled = false;
m_EditorLevel = m_GMLevel; m_EditorLevel = m_GMLevel;
m_Reputation = 0; m_Reputation = Database::Get()->GetCharacterReputation(m_Character->GetID());
m_CurrentActivity = eGameActivity::NONE; m_CurrentActivity = eGameActivity::NONE;
m_CountryCode = 0; m_CountryCode = 0;
@@ -249,6 +249,12 @@ void CharacterComponent::SetPvpEnabled(const bool value) {
m_PvpEnabled = value; m_PvpEnabled = value;
} }
void CharacterComponent::SetReputation(int64_t newValue) {
m_Reputation = newValue;
Database::Get()->SetCharacterReputation(m_Character->GetID(), m_Reputation);
GameMessages::SendUpdateReputation(m_Parent->GetObjectID(), m_Reputation, m_Parent->GetSystemAddress());
}
void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) { void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) {
m_DirtyGMInfo = true; m_DirtyGMInfo = true;
if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true; if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true;
@@ -263,10 +269,6 @@ void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
LOG("Failed to find char tag while loading XML!"); LOG("Failed to find char tag while loading XML!");
return; return;
} }
if (character->QueryAttribute("rpt", &m_Reputation) == tinyxml2::XML_NO_ATTRIBUTE) {
SetReputation(0);
}
auto* vl = character->FirstChildElement("vl"); auto* vl = character->FirstChildElement("vl");
if (vl) LoadVisitedLevelsXml(*vl); if (vl) LoadVisitedLevelsXml(*vl);
character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]); character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]);
@@ -408,8 +410,6 @@ void CharacterComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]); if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]);
character->SetAttribute("ls", m_Uscore); character->SetAttribute("ls", m_Uscore);
// Custom attribute to keep track of reputation.
character->SetAttribute("rpt", GetReputation());
character->SetAttribute("stt", StatisticsToString().c_str()); character->SetAttribute("stt", StatisticsToString().c_str());
// Set the zone statistics of the form <zs><s/> ... <s/></zs> // Set the zone statistics of the form <zs><s/> ... <s/></zs>

View File

@@ -156,7 +156,7 @@ public:
* Sets the lifetime reputation of the character to newValue * Sets the lifetime reputation of the character to newValue
* @param newValue the value to set reputation to * @param newValue the value to set reputation to
*/ */
void SetReputation(int64_t newValue) { m_Reputation = newValue; }; void SetReputation(int64_t newValue);
/** /**
* Sets the current value of PvP combat being enabled * Sets the current value of PvP combat being enabled

View File

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

View File

@@ -261,19 +261,6 @@ public:
/** /**
* Push or Pop a layer of stun immunity to this entity * 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( void SetStunImmunity(
const eStateChangeType state, const eStateChangeType state,
const LWOOBJID originator = LWOOBJID_EMPTY, const LWOOBJID originator = LWOOBJID_EMPTY,
@@ -416,21 +403,6 @@ private:
/** /**
* stun immunity counters * 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_ImmuneToStunAttackCount;
int32_t m_ImmuneToStunEquipCount; int32_t m_ImmuneToStunEquipCount;
int32_t m_ImmuneToStunInteractCount; int32_t m_ImmuneToStunInteractCount;

View File

@@ -4,119 +4,80 @@
*/ */
#include "MovingPlatformComponent.h" #include "MovingPlatformComponent.h"
#include "PlatformSubComponent.h"
#include "MoverSubComponent.h"
#include "SimpleMoverSubComponent.h"
#include "RotatorSubComponent.h"
#include "BitStream.h" #include "BitStream.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
#include "dZoneManager.h" #include "dZoneManager.h"
#include "EntityManager.h" #include "EntityManager.h"
#include "Logger.h" #include "Logger.h"
#include "GameMessages.h" #include "GameMessages.h"
#include "CppScripts.h"
#include "SimplePhysicsComponent.h" #include "SimplePhysicsComponent.h"
#include "Zone.h" #include "Zone.h"
#include "eMovementPlatformState.h"
MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName) MoverSubComponent::MoverSubComponent(const NiPoint3& startPos) {
: Component(parent, componentID) { 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_MoverSubComponentType = eMoverSubComponentType::mover;
m_MoverSubComponent = new MoverSubComponent(m_Parent->GetDefaultPosition());
m_PathName = GeneralUtils::ASCIIToUTF16(pathName); m_PathName = GeneralUtils::ASCIIToUTF16(pathName);
m_Path = Game::zoneManager->GetZone()->GetPath(pathName); m_Path = Game::zoneManager->GetZone()->GetPath(pathName);
m_NoAutoStart = false; m_NoAutoStart = false;
if (m_Path == nullptr && !pathName.empty()) { if (m_Path == nullptr) {
LOG("Path not found: %s", pathName.c_str()); LOG("Path not found: %s", pathName.c_str());
} }
SetupPlatformSubComponents();
} }
MovingPlatformComponent::~MovingPlatformComponent() = default; MovingPlatformComponent::~MovingPlatformComponent() {
delete static_cast<MoverSubComponent*>(m_MoverSubComponent);
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);
}
} }
void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { 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) { if (!m_Serialize) {
outBitStream.Write<bool>(false); outBitStream.Write<bool>(false);
outBitStream.Write<bool>(false); outBitStream.Write<bool>(false);
return; return;
} }
@@ -126,6 +87,7 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI
outBitStream.Write(hasPath); outBitStream.Write(hasPath);
if (hasPath) { if (hasPath) {
// Is on rail
outBitStream.Write1(); outBitStream.Write1();
outBitStream.Write<uint16_t>(m_PathName.size()); 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<uint16_t>(c);
} }
outBitStream.Write<uint32_t>(m_MoverSubComponent ? m_MoverSubComponent->GetCurrentWaypointIndex() : 0); // Starting point
outBitStream.Write<bool>(m_MoverSubComponent ? m_MoverSubComponent->GetInReverse() : false); outBitStream.Write<uint32_t>(0);
// Reverse
outBitStream.Write<bool>(false);
} }
const auto hasPlatform = m_MoverSubComponent != nullptr; const auto hasPlatform = m_MoverSubComponent != nullptr;
outBitStream.Write<bool>(hasPlatform); outBitStream.Write<bool>(hasPlatform);
if (hasPlatform) { if (hasPlatform) {
auto* mover = static_cast<MoverSubComponent*>(m_MoverSubComponent);
outBitStream.Write(m_MoverSubComponentType); outBitStream.Write(m_MoverSubComponentType);
m_MoverSubComponent->Serialize(outBitStream, bIsInitialUpdate);
}
}
void MovingPlatformComponent::Update(float deltaTime) { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) {
if (!m_Serialize) return; // TODO
} else {
// Track whether we were travelling before update for sound management mover->Serialize(outBitStream, bIsInitialUpdate);
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);
} }
// 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() { void MovingPlatformComponent::OnCompleteQuickBuild() {
if (m_NoAutoStart) return; if (m_NoAutoStart)
return;
StartPathing(); StartPathing();
} }
void MovingPlatformComponent::SetMovementState(uint32_t state) { void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) {
if (m_MoverSubComponent) m_MoverSubComponent->SetState(state); auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
if (m_RotatorSubComponent) m_RotatorSubComponent->SetState(state);
subComponent->mState = value;
Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(m_Parent);
} }
void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { 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; m_PathingStopped = false;
if (m_MoverSubComponent) m_MoverSubComponent->GotoWaypoint(index, stopAtWaypoint); auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
if (m_RotatorSubComponent) m_RotatorSubComponent->GotoWaypoint(index, stopAtWaypoint);
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); Game::entityManager->SerializeEntity(m_Parent);
} }
void MovingPlatformComponent::StartPathing() { void MovingPlatformComponent::ContinuePathing() {
m_PathingStopped = false; auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
if (m_MoverSubComponent) m_MoverSubComponent->StartPathing(); subComponent->mState = eMovementPlatformState::Stationary;
if (m_RotatorSubComponent) m_RotatorSubComponent->StartPathing();
if (m_MoverSubComponent) { subComponent->mCurrentWaypointIndex = subComponent->mNextWaypointIndex;
GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS,
m_MoverSubComponent->GetShouldStopAtDesiredWaypoint(), NiPoint3 targetPosition;
m_MoverSubComponent->GetCurrentWaypointIndex(), uint32_t pathSize;
m_MoverSubComponent->GetDesiredWaypointIndex(), PathBehavior behavior;
m_MoverSubComponent->GetNextWaypointIndex(),
static_cast<eMovementPlatformState>(m_MoverSubComponent->GetSerializedState()), if (m_Path != nullptr) {
m_MoverSubComponent->GetInReverse(), const auto& currentWaypoint = m_Path->pathWaypoints[subComponent->mCurrentWaypointIndex];
m_MoverSubComponent->GetIdleTimeElapsed(), const auto& nextWaypoint = m_Path->pathWaypoints[subComponent->mNextWaypointIndex];
m_MoverSubComponent->GetMoveTimeElapsed(),
m_MoverSubComponent->GetPercentBetweenPoints(), subComponent->mPosition = currentWaypoint.position;
m_MoverSubComponent->GetPosition()); 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()) { if (m_Parent->GetLOT() == 9483) {
GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundStart); 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); Game::entityManager->SerializeEntity(m_Parent);
} }
void MovingPlatformComponent::StopPathing() { void MovingPlatformComponent::StopPathing() {
//m_Parent->CancelCallbackTimers();
auto* subComponent = static_cast<MoverSubComponent*>(m_MoverSubComponent);
m_PathingStopped = true; m_PathingStopped = true;
if (m_MoverSubComponent) m_MoverSubComponent->StopPathing(); subComponent->mState = eMovementPlatformState::Stopped;
if (m_RotatorSubComponent) m_RotatorSubComponent->StopPathing(); subComponent->mDesiredWaypointIndex = -1;
subComponent->mShouldStopAtDesiredWaypoint = false;
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);
}
Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(m_Parent);
//GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS);
} }
void MovingPlatformComponent::SetSerialized(bool value) { void MovingPlatformComponent::SetSerialized(bool value) {
@@ -260,18 +332,18 @@ void MovingPlatformComponent::SetNoAutoStart(const bool value) {
} }
void MovingPlatformComponent::WarpToWaypoint(size_t index) { void MovingPlatformComponent::WarpToWaypoint(size_t index) {
if (m_MoverSubComponent) m_MoverSubComponent->WarpToWaypoint(index); const auto& waypoint = m_Path->pathWaypoints[index];
if (m_RotatorSubComponent) m_RotatorSubComponent->WarpToWaypoint(index);
m_Parent->SetPosition(waypoint.position);
m_Parent->SetRotation(waypoint.rotation);
Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(m_Parent);
} }
size_t MovingPlatformComponent::GetLastWaypointIndex() const { size_t MovingPlatformComponent::GetLastWaypointIndex() const {
if (m_MoverSubComponent) return m_MoverSubComponent->GetLastWaypointIndex(); return m_Path->pathWaypoints.size() - 1;
if (m_RotatorSubComponent) return m_RotatorSubComponent->GetLastWaypointIndex();
return 0;
} }
PlatformSubComponent* MovingPlatformComponent::GetMoverSubComponent() const { MoverSubComponent* MovingPlatformComponent::GetMoverSubComponent() const {
if (m_MoverSubComponent) return m_MoverSubComponent.get(); return static_cast<MoverSubComponent*>(m_MoverSubComponent);
return nullptr;
} }

View File

@@ -6,34 +6,103 @@
#ifndef MOVINGPLATFORMCOMPONENT_H #ifndef MOVINGPLATFORMCOMPONENT_H
#define MOVINGPLATFORMCOMPONENT_H #define MOVINGPLATFORMCOMPONENT_H
#include "Component.h" #include "RakNetTypes.h"
#include "eReplicaComponentType.h"
#include "NiPoint3.h" #include "NiPoint3.h"
#include "NiQuaternion.h"
#include <string> #include <string>
#include <memory>
#include "dCommonVars.h"
#include "EntityManager.h"
#include "Component.h"
#include "eMovementPlatformState.h"
#include "eReplicaComponentType.h"
class Path; class Path;
class Entity;
class PlatformSubComponent;
class RotatorSubComponent;
/** /**
* Different types of available platform subcomponents * Different types of available platforms
*/ */
enum class eMoverSubComponentType : uint32_t { enum class eMoverSubComponentType : uint32_t {
mover = 4, 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. * 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. * 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
* An entity can have multiple subcomponents (mover + rotator), matching the client's architecture * don't at all do what you expect them to as we don't instruct the client of changes made here.
* where SetupPlatform creates subcomponents based on platformIsMover/platformIsSimpleMover/platformIsRotater. * ^^^ Trivia: This made the red blocks platform and property platforms a pain to implement.
*/ */
class MovingPlatformComponent final : public Component { class MovingPlatformComponent final : public Component {
public: public:
@@ -43,42 +112,117 @@ public:
~MovingPlatformComponent() override; ~MovingPlatformComponent() override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) 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(); void OnQuickBuildInitilized();
/**
* Starts the pathing, called when an entity completed a quick build associated with this platform
*/
void OnCompleteQuickBuild(); 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); void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true);
/**
* Starts the pathing of this platform, setting appropriate waypoints and speeds
*/
void StartPathing(); 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(); 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); 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; bool GetNoAutoStart() const;
/**
* Sets the auto start value for this platform
* @param value the auto start value to set
*/
void SetNoAutoStart(bool value); 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); 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; 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: private:
void SetupPlatformSubComponents();
/**
* The path this platform is currently on
*/
const Path* m_Path = nullptr; const Path* m_Path = nullptr;
/**
* The name of the path this platform is currently on
*/
std::u16string m_PathName; std::u16string m_PathName;
/**
* Whether the platform has stopped pathing
*/
bool m_PathingStopped = false; bool m_PathingStopped = false;
/**
* The type of the subcomponent
*/
eMoverSubComponentType m_MoverSubComponentType; 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; bool m_Serialize = false;
std::string m_PlatformSoundStart;
std::string m_PlatformSoundTravel;
std::string m_PlatformSoundStop;
}; };
#endif // MOVINGPLATFORMCOMPONENT_H #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 "CppScripts.h"
#include <ranges> #include <ranges>
#include "dConfig.h" #include "dConfig.h"
#include "PositionUpdate.h"
#include "GeneralUtils.h"
#include "User.h"
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
@@ -75,9 +78,48 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const i
this->reputation = propertyInfo->reputation; this->reputation = propertyInfo->reputation;
Load(); Load();
// Cache owner's account ID for same-account reputation exclusion
if (this->owner != LWOOBJID_EMPTY) {
auto ownerCharId = this->owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
auto charInfo = Database::Get()->GetCharacterInfo(ownerCharId);
if (charInfo) {
m_OwnerAccountId = charInfo->accountId;
}
}
// Load reputation config
m_RepInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_interval")).value_or(60.0f);
m_RepDailyCap = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_daily_cap")).value_or(50);
m_RepPerTick = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_per_tick")).value_or(1);
m_RepMultiplier = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_multiplier")).value_or(1.0f);
m_RepVelocityThreshold = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_velocity_threshold")).value_or(0.5f);
m_RepSaveInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_save_interval")).value_or(300.0f);
m_RepDecayRate = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_rate")).value_or(0.0f);
m_RepDecayInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_interval")).value_or(86400.0f);
m_RepDecayMinimum = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_decay_minimum")).value_or(0);
// Load daily reputation contributions and subscribe to position updates
m_CurrentDate = GeneralUtils::GetCurrentUTCDate();
LoadDailyContributions();
Entity::OnPlayerPositionUpdate += [](Entity* player, const PositionUpdate& update) {
auto* propertyManagementComponent = PropertyManagementComponent::instance;
if (propertyManagementComponent == nullptr) {
return;
}
propertyManagementComponent->OnPlayerPositionUpdateHandler(player, update);
};
} }
} }
PropertyManagementComponent::~PropertyManagementComponent() {
if (instance == this) {
instance = nullptr;
}
SaveReputation();
}
LWOOBJID PropertyManagementComponent::GetOwnerId() const { LWOOBJID PropertyManagementComponent::GetOwnerId() const {
return owner; return owner;
} }
@@ -832,3 +874,126 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess
modelComponent->OnChatMessageReceived(sMessage); modelComponent->OnChatMessageReceived(sMessage);
} }
} }
void PropertyManagementComponent::Update(float deltaTime) {
// Check for day rollover
const auto currentDate = GeneralUtils::GetCurrentUTCDate();
if (currentDate != m_CurrentDate) {
if (m_ReputationDirty) {
LOG_DEBUG("Saving dirty reputation data before daily rollover for property %llu", static_cast<unsigned long long>(propertyId));
SaveReputation();
}
m_CurrentDate = currentDate;
m_PlayerActivity.clear();
}
// Periodic reputation save
m_ReputationSaveTimer += deltaTime;
if (m_ReputationSaveTimer >= m_RepSaveInterval && m_ReputationDirty) {
SaveReputation();
m_ReputationSaveTimer = 0.0f;
}
// Property reputation decay
if (m_RepDecayRate > 0.0f && owner != LWOOBJID_EMPTY) {
m_DecayTimer += deltaTime;
if (m_DecayTimer >= m_RepDecayInterval) {
m_DecayTimer = 0.0f;
if (reputation > m_RepDecayMinimum) {
const auto loss = static_cast<uint32_t>(m_RepDecayRate);
reputation = (reputation > m_RepDecayMinimum + loss) ? reputation - loss : m_RepDecayMinimum;
m_ReputationDirty = true;
}
}
}
}
void PropertyManagementComponent::OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update) {
if (owner == LWOOBJID_EMPTY) return;
if (propertyId == LWOOBJID_EMPTY) return;
if (m_RepInterval <= 0.0f) return;
// Check same-account exclusion (covers owner + owner's alts)
auto* character = player->GetCharacter();
if (!character) return;
auto* parentUser = character->GetParentUser();
if (!parentUser) return;
if (parentUser->GetAccountID() == m_OwnerAccountId) return;
// Check velocity threshold (player must be active/moving)
if (update.velocity.SquaredLength() < m_RepVelocityThreshold * m_RepVelocityThreshold) return;
const auto playerId = character->GetID();
auto& info = m_PlayerActivity[playerId];
// Check daily cap
if (info.dailyContribution >= m_RepDailyCap) return;
// Compute delta time since last position update for this player
const auto now = std::chrono::steady_clock::now();
if (info.hasLastUpdate) {
const auto dt = std::chrono::duration<float>(now - info.lastUpdate).count();
// Cap delta to avoid spikes from reconnects etc.
const auto clampedDt = std::min(dt, 1.0f);
info.activeTime += clampedDt;
}
info.lastUpdate = now;
info.hasLastUpdate = true;
// Check if we've accumulated enough active time for a reputation tick
if (info.activeTime >= m_RepInterval) {
info.activeTime -= m_RepInterval;
const auto repGain = static_cast<uint32_t>(m_RepPerTick * m_RepMultiplier);
if (repGain == 0) return;
// Clamp to daily cap
const auto actualGain = std::min(repGain, m_RepDailyCap - info.dailyContribution);
if (actualGain == 0) return;
// Grant property reputation
reputation += actualGain;
info.dailyContribution += actualGain;
m_ReputationDirty = true;
// Grant character reputation to property owner
auto* ownerEntity = Game::entityManager->GetEntity(owner);
if (ownerEntity) {
auto* charComp = ownerEntity->GetComponent<CharacterComponent>();
if (charComp) {
charComp->SetReputation(charComp->GetReputation() + actualGain);
}
} else {
// Owner is offline, update DB directly
auto ownerCharId = owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
const auto currentRep = Database::Get()->GetCharacterReputation(ownerCharId);
Database::Get()->SetCharacterReputation(ownerCharId, currentRep + actualGain);
}
}
}
void PropertyManagementComponent::SaveReputation() {
if (!m_ReputationDirty) return;
if (propertyId == LWOOBJID_EMPTY) return;
Database::Get()->UpdatePropertyReputation(propertyId, reputation);
for (const auto& [playerId, info] : m_PlayerActivity) {
if (info.dailyContribution > 0) {
Database::Get()->UpdatePropertyReputationContribution(propertyId, playerId, m_CurrentDate, info.dailyContribution);
}
}
m_ReputationDirty = false;
}
void PropertyManagementComponent::LoadDailyContributions() {
if (propertyId == LWOOBJID_EMPTY) return;
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, m_CurrentDate);
for (const auto& contrib : contributions) {
m_PlayerActivity[contrib.playerId].dailyContribution = contrib.reputationGained;
}
}

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <unordered_map>
#include "Entity.h" #include "Entity.h"
#include "Component.h" #include "Component.h"
#include "eReplicaComponentType.h" #include "eReplicaComponentType.h"
class PositionUpdate;
/** /**
* Information regarding which players may visit this property * Information regarding which players may visit this property
*/ */
@@ -164,9 +167,40 @@ public:
LWOOBJID GetId() const noexcept { return propertyId; } LWOOBJID GetId() const noexcept { return propertyId; }
void OnChatMessageReceived(const std::string& sMessage) const; void OnChatMessageReceived(const std::string& sMessage) const;
void Update(float deltaTime) override;
~PropertyManagementComponent() override;
private: private:
void OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update);
void SaveReputation();
void LoadDailyContributions();
struct PlayerActivityInfo {
float activeTime = 0.0f;
uint32_t dailyContribution = 0;
std::chrono::steady_clock::time_point lastUpdate{};
bool hasLastUpdate = false;
};
std::unordered_map<LWOOBJID, PlayerActivityInfo> m_PlayerActivity;
float m_ReputationSaveTimer = 0.0f;
float m_DecayTimer = 0.0f;
bool m_ReputationDirty = false;
std::string m_CurrentDate;
uint32_t m_OwnerAccountId = 0;
// Cached config values
float m_RepInterval = 60.0f;
uint32_t m_RepDailyCap = 50;
uint32_t m_RepPerTick = 1;
float m_RepMultiplier = 1.0f;
float m_RepVelocityThreshold = 0.5f;
float m_RepSaveInterval = 300.0f;
float m_RepDecayRate = 0.0f;
float m_RepDecayInterval = 86400.0f;
uint32_t m_RepDecayMinimum = 0;
/** /**
* This * This
*/ */

View File

@@ -12,7 +12,6 @@
#include "CDPhysicsComponentTable.h" #include "CDPhysicsComponentTable.h"
#include "Entity.h" #include "Entity.h"
#include "MovingPlatformComponent.h"
#include "StringifiedEnum.h" #include "StringifiedEnum.h"
#include "Amf3.h" #include "Amf3.h"
@@ -40,11 +39,6 @@ SimplePhysicsComponent::~SimplePhysicsComponent() {
void SimplePhysicsComponent::Update(const float deltaTime) { void SimplePhysicsComponent::Update(const float deltaTime) {
if (m_Velocity == NiPoint3Constant::ZERO) return; 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_Position += m_Velocity * deltaTime;
m_DirtyPosition = true; m_DirtyPosition = true;
Game::entityManager->SerializeEntity(m_Parent); 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, void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint,
int iIndex, int iDesiredWaypointIndex, int nextIndex, int iIndex, int iDesiredWaypointIndex, int nextIndex,
eMovementPlatformState movementState, eMovementPlatformState movementState) {
bool bReverse, float fIdleTimeElapsed, float fMoveTimeElapsed,
float fPercentBetweenPoints, NiPoint3 ptUnexpectedLocation,
NiQuaternion qUnexpectedRotation) {
CBITSTREAM; CBITSTREAM;
CMSGHEADER; 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(entity->GetObjectID());
bitStream.Write(MessageType::Game::PLATFORM_RESYNC); bitStream.Write(MessageType::Game::PLATFORM_RESYNC);
bool bReverse = false;
int eCommand = 0; int eCommand = 0;
int eUnexpectedCommand = 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(bReverse);
bitStream.Write(bStopAtDesiredWaypoint); bitStream.Write(bStopAtDesiredWaypoint);

View File

@@ -104,10 +104,7 @@ namespace GameMessages {
void SendStartPathing(Entity* entity); void SendStartPathing(Entity* entity);
void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false, void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false,
int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1, int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1,
eMovementPlatformState movementState = eMovementPlatformState::Moving, 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);
void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1); void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1);
void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr); void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr);

View File

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

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 set(DCOMPONENTS_TESTS
"DestroyableComponentTests.cpp" "DestroyableComponentTests.cpp"
"PetComponentTests.cpp" "PetComponentTests.cpp"
"PropertyReputationTests.cpp"
"SimplePhysicsComponentTests.cpp" "SimplePhysicsComponentTests.cpp"
"SavingTests.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());
}