Compare commits

...

16 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
David Markowitz
247576e101 fix: use copy ellision (#1963)
* use copy ellision

tested that the server still starts

* Update dDatabase/GameDatabase/MySQL/MySQLDatabase.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 15:35:28 -07:00
David Markowitz
8dfdca7fbd feat: add mission progression for behaviors (#1962)
* feat: add mission progression for behaviors

* Add const to ptr

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 13:02:23 -07:00
Aaron Kimbrell
8283d1fa95 fix: mariadb on newer gcc and newer cmake version (#1961)
* Fix newer gcc issues in mariadb

* fix error with newer cmake versions

* update mariadb to latest

* fix macos and docker

* fix: update Windows MSI package comments and align Connector/C++ version

* Update cmake/FindMariaDB.cmake

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: only pass CMAKE_POLICY_VERSION_MINIMUM to ExternalProject when CMake >= 4.0

Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/a247f729-a0b1-4fb6-825e-d23045b1ee55

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-03-29 13:59:09 -05:00
David Markowitz
434c9b6315 fix: imaginite on racing minigames and add null checks (#1958) 2026-02-23 01:16:36 -08:00
David Markowitz
3c64b26c39 fix: macos ci (#1955) 2026-02-11 19:49:51 -08:00
David Markowitz
347b1d17d4 fix: not checking pending names on rename (#1954) 2026-02-11 19:49:39 -08:00
48 changed files with 824 additions and 53 deletions

View File

@@ -13,7 +13,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
os: [ windows-2022, ubuntu-22.04, macos-13 ]
os: [ windows-2022, ubuntu-22.04, macos-15-intel ]
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2

3
.gitignore vendored
View File

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

View File

@@ -15,6 +15,11 @@ set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
@@ -67,7 +72,11 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disabled no-register
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
if(UNIX)
add_link_options("-Wl,-rpath,$ORIGIN/")
if(APPLE)
add_link_options("-Wl,-rpath,@loader_path/")
else()
add_link_options("-Wl,-rpath,$ORIGIN/")
endif()
add_compile_options("-fPIC")
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)

View File

@@ -616,7 +616,7 @@
]
},
{
"name": "ci-macos-13",
"name": "ci-macos-15-intel",
"displayName": "[Release] MacOS",
"description": "CI workflow preset for MacOS",
"steps": [

View File

@@ -16,7 +16,9 @@ RUN --mount=type=cache,target=/app/build,id=build-cache \
cd /app/build && \
cmake .. && \
make -j$(nproc --ignore 1) && \
cp -r /app/build/* /tmp/persisted-build/
cp -r /app/build/* /tmp/persisted-build/ && \
mkdir -p /tmp/persisted-build/mariadbcpp && \
cp /app/build/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build/libmariadbcpp.so /tmp/persisted-build/mariadbcpp/
FROM debian:12 as runtime

View File

@@ -10,14 +10,15 @@ if(WIN32 AND NOT MARIADB_BUILD_SOURCE)
file(MAKE_DIRECTORY "${MARIADB_MSI_DIR}")
file(MAKE_DIRECTORY "${MARIADB_CONNECTOR_DIR}")
# These values need to be updated whenever a new minor release replaces an old one
# Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts
set(MARIADB_CONNECTOR_C_VERSION "3.2.7")
set(MARIADB_CONNECTOR_C_BUCKET "2319651")
set(MARIADB_CONNECTOR_C_MD5 "f8636d733f1d093af9d4f22f3239f885")
set(MARIADB_CONNECTOR_CPP_VERSION "1.0.2")
set(MARIADB_CONNECTOR_CPP_BUCKET "2531525")
set(MARIADB_CONNECTOR_CPP_MD5 "3034bbd6ca00a0125345f9fd1a178401")
# These values track the published Windows MSI packages used by the prebuilt path.
# Keep the Connector/C++ package version aligned with the checked out submodule tag when possible.
# Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts.
set(MARIADB_CONNECTOR_C_VERSION "3.4.8")
set(MARIADB_CONNECTOR_C_BUCKET "4516894")
set(MARIADB_CONNECTOR_C_MD5 "50f6fc0c77b8d3bacbeac0126e179861")
set(MARIADB_CONNECTOR_CPP_VERSION "1.1.7")
set(MARIADB_CONNECTOR_CPP_BUCKET "4464908")
set(MARIADB_CONNECTOR_CPP_MD5 "08644a7ff084b5933325cadb904796e5")
set(MARIADB_CONNECTOR_C_MSI "mariadb-connector-c-${MARIADB_CONNECTOR_C_VERSION}-win64.msi")
set(MARIADB_CONNECTOR_CPP_MSI "mariadb-connector-cpp-${MARIADB_CONNECTOR_CPP_VERSION}-win64.msi")
@@ -79,23 +80,39 @@ else() # Build from source
-DWITH_EXTERNAL_ZLIB=ON
-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR}
-DCMAKE_C_FLAGS=-w # disable zlib warnings
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0)
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint)
else()
set(MARIADB_EXTRA_CMAKE_ARGS
-DCMAKE_C_FLAGS=-w # disable zlib warnings
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0)
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint)
endif()
set(MARIADBCPP_BUILD_DIR "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build")
set(MARIADBCPP_INSTALL_DIR ${PROJECT_BINARY_DIR}/prefix)
set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp)
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin)
set(MARIADBCPP_SOURCE_DIR ${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp)
set(MARIADB_INCLUDE_DIR "${MARIADBCPP_SOURCE_DIR}/include")
if(WIN32)
set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp)
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin)
set(MARIADB_INSTALL_COMMAND)
else()
set(MARIADBCPP_LIBRARY_DIR ${MARIADBCPP_BUILD_DIR})
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_BUILD_DIR}/libmariadb)
set(MARIADB_INSTALL_COMMAND INSTALL_COMMAND ${CMAKE_COMMAND} -E true)
endif()
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set(MARIADB_POLICY_VERSION_ARG -DCMAKE_POLICY_VERSION_MINIMUM=3.5)
endif()
ExternalProject_Add(mariadb_connector_cpp
PREFIX "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp"
SOURCE_DIR ${MARIADBCPP_SOURCE_DIR}
BINARY_DIR ${MARIADBCPP_BUILD_DIR}
INSTALL_DIR ${MARIADBCPP_INSTALL_DIR}
CMAKE_ARGS -Wno-dev
${MARIADB_POLICY_VERSION_ARG}
-DWITH_UNIT_TESTS=OFF
-DMARIADB_LINK_DYNAMIC=OFF
-DCMAKE_BUILD_RPATH_USE_ORIGIN=${CMAKE_BUILD_RPATH_USE_ORIGIN}
@@ -103,6 +120,7 @@ else() # Build from source
-DINSTALL_LIBDIR=${MARIADBCPP_LIBRARY_DIR}
-DINSTALL_PLUGINDIR=${MARIADBCPP_PLUGIN_DIR}
${MARIADB_EXTRA_CMAKE_ARGS}
${MARIADB_INSTALL_COMMAND}
BUILD_ALWAYS true
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,20 @@
typedef std::unique_ptr<sql::PreparedStatement>& UniquePreppedStmtRef;
typedef std::unique_ptr<sql::ResultSet> UniqueResultSet;
// This struct is used to keep the PreparedStatement alive alongside the ResultSet, since the ResultSet will be invalidated if the PreparedStatement is destroyed.
// Declaring the members in reverse order of usage to ensure the PreparedStatement is destroyed after the ResultSet. This is guaranteed by the C++ standard.
struct PreparedStmtResultSet {
std::unique_ptr<sql::PreparedStatement> m_stmt;
std::unique_ptr<sql::ResultSet> m_resultSet;
PreparedStmtResultSet(sql::PreparedStatement* stmt = nullptr, sql::ResultSet* resultSet = nullptr)
: m_stmt(stmt), m_resultSet(resultSet) {}
sql::ResultSet* operator->() const {
return m_resultSet.get();
}
};
// Purposefully no definition for this to provide linker errors in the case someone tries to
// bind a parameter to a type that isn't defined.
template<typename ParamType>
@@ -125,6 +139,15 @@ public:
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override;
std::vector<LWOOBJID> GetAllCharacterIds() override;
int64_t GetCharacterReputation(const LWOOBJID charId) override;
void SetCharacterReputation(const LWOOBJID charId, const int64_t reputation) override;
std::vector<IPropertyReputationContribution::ContributionInfo> GetPropertyReputationContributions(
const LWOOBJID propertyId, const std::string& date) override;
void UpdatePropertyReputationContribution(
const LWOOBJID propertyId, const LWOOBJID playerId,
const std::string& date, const uint32_t reputationGained) override;
void UpdatePropertyReputation(const LWOOBJID propertyId, const uint32_t reputation) override;
uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
@@ -136,12 +159,15 @@ private:
// Generic query functions that can be used for any query.
// Return type may be different depending on the query, so it is up to the caller to check the return type.
// The first argument is the query string, and the rest are the parameters to bind to the query.
// The return type is a unique_ptr to the result set, which is deleted automatically when it goes out of scope
// The return type is a PreparedStmtResultSet which keeps the PreparedStatement alive alongside the ResultSet.
template<typename... Args>
inline std::unique_ptr<sql::ResultSet> ExecuteSelect(const std::string& query, Args&&... args) {
std::unique_ptr<sql::PreparedStatement> preppedStmt(CreatePreppedStmt(query));
SetParams(preppedStmt, std::forward<Args>(args)...);
DLU_SQL_TRY_CATCH_RETHROW(return std::unique_ptr<sql::ResultSet>(preppedStmt->executeQuery()));
inline PreparedStmtResultSet ExecuteSelect(const std::string& query, Args&&... args) {
PreparedStmtResultSet toReturn;
toReturn.m_stmt.reset(CreatePreppedStmt(query));
SetParams(toReturn.m_stmt, std::forward<Args>(args)...);
DLU_SQL_TRY_CATCH_RETHROW(toReturn.m_resultSet.reset(toReturn.m_stmt->executeQuery()));
// Return the PreparedStmtResultSet, which now owns both the PreparedStatement and ResultSet via unique_ptr and will ensure they are properly cleaned up.
return toReturn;
}
template<typename... Args>

View File

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

View File

@@ -12,7 +12,7 @@ std::vector<std::string> MySQLDatabase::GetApprovedCharacterNames() {
return toReturn;
}
std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::ResultSet> stmt) {
std::optional<ICharInfo::Info> CharInfoFromQueryResult(PreparedStmtResultSet& stmt) {
if (!stmt->next()) {
return std::nullopt;
}
@@ -31,15 +31,13 @@ std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::Resu
}
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const LWOOBJID charId) {
return CharInfoFromQueryResult(
ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId)
);
auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId);
return CharInfoFromQueryResult(result);
}
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const std::string_view name) {
return CharInfoFromQueryResult(
ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name)
);
auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name);
return CharInfoFromQueryResult(result);
}
std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) {
@@ -54,6 +52,18 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
return toReturn;
}
std::vector<LWOOBJID> MySQLDatabase::GetAllCharacterIds() {
auto result = ExecuteSelect("SELECT id FROM charinfo;");
std::vector<LWOOBJID> toReturn;
toReturn.reserve(result->rowsCount());
while (result->next()) {
toReturn.push_back(result->getInt64("id"));
}
return toReturn;
}
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",

View File

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

View File

@@ -14,7 +14,7 @@ std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityI
return donation_total->getUInt("donation_total");
}
std::vector<ILeaderboard::Entry> ProcessQuery(UniqueResultSet& rows) {
std::vector<ILeaderboard::Entry> ProcessQuery(PreparedStmtResultSet& rows) {
std::vector<ILeaderboard::Entry> entries;
entries.reserve(rows->rowsCount());

View File

@@ -1,7 +1,7 @@
#include "MySQLDatabase.h"
#include "ePropertySortType.h"
IProperty::Info ReadPropertyInfo(UniqueResultSet& result) {
IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) {
IProperty::Info info;
info.id = result->getUInt64("id");
info.ownerId = result->getInt64("owner_id");
@@ -21,7 +21,7 @@ IProperty::Info ReadPropertyInfo(UniqueResultSet& result) {
std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
std::optional<IProperty::PropertyEntranceResult> result;
std::string query;
std::unique_ptr<sql::ResultSet> properties;
PreparedStmtResultSet properties;
if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) {
query = R"QUERY(

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

@@ -1,6 +1,6 @@
#include "MySQLDatabase.h"
IUgc::Model ReadModel(UniqueResultSet& result) {
IUgc::Model ReadModel(PreparedStmtResultSet& result) {
IUgc::Model model;
// blob is owned by the query, so we need to do a deep copy :/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -514,7 +514,7 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
return;
}
if (!Database::Get()->GetCharacterInfo(newName)) {
if (!Database::Get()->IsNameInUse(newName)) {
if (autoRejectNames) {
Database::Get()->SetCharacterName(objectID, newName);
LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str());

View File

@@ -325,9 +325,8 @@ bool ActivityComponent::CheckCost(Entity* player) const {
}
bool ActivityComponent::TakeCost(Entity* player) const {
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
return CheckCost(player) && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL);
return CheckCost(player) && inventoryComponent && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL);
}
void ActivityComponent::PlayerReady(Entity* player, bool bReady) {

View File

@@ -42,7 +42,7 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID
m_EditorEnabled = false;
m_EditorLevel = m_GMLevel;
m_Reputation = 0;
m_Reputation = Database::Get()->GetCharacterReputation(m_Character->GetID());
m_CurrentActivity = eGameActivity::NONE;
m_CountryCode = 0;
@@ -249,6 +249,12 @@ void CharacterComponent::SetPvpEnabled(const bool value) {
m_PvpEnabled = value;
}
void CharacterComponent::SetReputation(int64_t newValue) {
m_Reputation = newValue;
Database::Get()->SetCharacterReputation(m_Character->GetID(), m_Reputation);
GameMessages::SendUpdateReputation(m_Parent->GetObjectID(), m_Reputation, m_Parent->GetSystemAddress());
}
void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) {
m_DirtyGMInfo = true;
if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true;
@@ -263,10 +269,6 @@ void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
LOG("Failed to find char tag while loading XML!");
return;
}
if (character->QueryAttribute("rpt", &m_Reputation) == tinyxml2::XML_NO_ATTRIBUTE) {
SetReputation(0);
}
auto* vl = character->FirstChildElement("vl");
if (vl) LoadVisitedLevelsXml(*vl);
character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]);
@@ -408,8 +410,6 @@ void CharacterComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]);
character->SetAttribute("ls", m_Uscore);
// Custom attribute to keep track of reputation.
character->SetAttribute("rpt", GetReputation());
character->SetAttribute("stt", StatisticsToString().c_str());
// Set the zone statistics of the form <zs><s/> ... <s/></zs>

View File

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

View File

@@ -8,7 +8,9 @@
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "InventoryComponent.h"
#include "MissionComponent.h"
#include "SimplePhysicsComponent.h"
#include "eMissionTaskType.h"
#include "eObjectBits.h"
#include "Database.h"
@@ -187,6 +189,8 @@ void ModelComponent::AddBehavior(AddMessage& msg) {
// Check if this behavior is able to be found via lot (if so, its a loot behavior).
insertedBehavior.SetIsLoot(inventoryComponent->FindItemByLot(msg.GetBehaviorId(), eInventoryType::BEHAVIORS));
}
auto* const missionComponent = playerEntity->GetComponent<MissionComponent>();
if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0);
}
auto* const simplePhysComponent = m_Parent->GetComponent<SimplePhysicsComponent>();

View File

@@ -27,6 +27,9 @@
#include "CppScripts.h"
#include <ranges>
#include "dConfig.h"
#include "PositionUpdate.h"
#include "GeneralUtils.h"
#include "User.h"
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
@@ -75,9 +78,48 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent, const i
this->reputation = propertyInfo->reputation;
Load();
// Cache owner's account ID for same-account reputation exclusion
if (this->owner != LWOOBJID_EMPTY) {
auto ownerCharId = this->owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
auto charInfo = Database::Get()->GetCharacterInfo(ownerCharId);
if (charInfo) {
m_OwnerAccountId = charInfo->accountId;
}
}
// Load reputation config
m_RepInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_interval")).value_or(60.0f);
m_RepDailyCap = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_daily_cap")).value_or(50);
m_RepPerTick = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_per_tick")).value_or(1);
m_RepMultiplier = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_multiplier")).value_or(1.0f);
m_RepVelocityThreshold = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_velocity_threshold")).value_or(0.5f);
m_RepSaveInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_save_interval")).value_or(300.0f);
m_RepDecayRate = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_rate")).value_or(0.0f);
m_RepDecayInterval = GeneralUtils::TryParse<float>(Game::config->GetValue("property_rep_decay_interval")).value_or(86400.0f);
m_RepDecayMinimum = GeneralUtils::TryParse<std::uint32_t>(Game::config->GetValue("property_rep_decay_minimum")).value_or(0);
// Load daily reputation contributions and subscribe to position updates
m_CurrentDate = GeneralUtils::GetCurrentUTCDate();
LoadDailyContributions();
Entity::OnPlayerPositionUpdate += [](Entity* player, const PositionUpdate& update) {
auto* propertyManagementComponent = PropertyManagementComponent::instance;
if (propertyManagementComponent == nullptr) {
return;
}
propertyManagementComponent->OnPlayerPositionUpdateHandler(player, update);
};
}
}
PropertyManagementComponent::~PropertyManagementComponent() {
if (instance == this) {
instance = nullptr;
}
SaveReputation();
}
LWOOBJID PropertyManagementComponent::GetOwnerId() const {
return owner;
}
@@ -832,3 +874,126 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess
modelComponent->OnChatMessageReceived(sMessage);
}
}
void PropertyManagementComponent::Update(float deltaTime) {
// Check for day rollover
const auto currentDate = GeneralUtils::GetCurrentUTCDate();
if (currentDate != m_CurrentDate) {
if (m_ReputationDirty) {
LOG_DEBUG("Saving dirty reputation data before daily rollover for property %llu", static_cast<unsigned long long>(propertyId));
SaveReputation();
}
m_CurrentDate = currentDate;
m_PlayerActivity.clear();
}
// Periodic reputation save
m_ReputationSaveTimer += deltaTime;
if (m_ReputationSaveTimer >= m_RepSaveInterval && m_ReputationDirty) {
SaveReputation();
m_ReputationSaveTimer = 0.0f;
}
// Property reputation decay
if (m_RepDecayRate > 0.0f && owner != LWOOBJID_EMPTY) {
m_DecayTimer += deltaTime;
if (m_DecayTimer >= m_RepDecayInterval) {
m_DecayTimer = 0.0f;
if (reputation > m_RepDecayMinimum) {
const auto loss = static_cast<uint32_t>(m_RepDecayRate);
reputation = (reputation > m_RepDecayMinimum + loss) ? reputation - loss : m_RepDecayMinimum;
m_ReputationDirty = true;
}
}
}
}
void PropertyManagementComponent::OnPlayerPositionUpdateHandler(Entity* player, const PositionUpdate& update) {
if (owner == LWOOBJID_EMPTY) return;
if (propertyId == LWOOBJID_EMPTY) return;
if (m_RepInterval <= 0.0f) return;
// Check same-account exclusion (covers owner + owner's alts)
auto* character = player->GetCharacter();
if (!character) return;
auto* parentUser = character->GetParentUser();
if (!parentUser) return;
if (parentUser->GetAccountID() == m_OwnerAccountId) return;
// Check velocity threshold (player must be active/moving)
if (update.velocity.SquaredLength() < m_RepVelocityThreshold * m_RepVelocityThreshold) return;
const auto playerId = character->GetID();
auto& info = m_PlayerActivity[playerId];
// Check daily cap
if (info.dailyContribution >= m_RepDailyCap) return;
// Compute delta time since last position update for this player
const auto now = std::chrono::steady_clock::now();
if (info.hasLastUpdate) {
const auto dt = std::chrono::duration<float>(now - info.lastUpdate).count();
// Cap delta to avoid spikes from reconnects etc.
const auto clampedDt = std::min(dt, 1.0f);
info.activeTime += clampedDt;
}
info.lastUpdate = now;
info.hasLastUpdate = true;
// Check if we've accumulated enough active time for a reputation tick
if (info.activeTime >= m_RepInterval) {
info.activeTime -= m_RepInterval;
const auto repGain = static_cast<uint32_t>(m_RepPerTick * m_RepMultiplier);
if (repGain == 0) return;
// Clamp to daily cap
const auto actualGain = std::min(repGain, m_RepDailyCap - info.dailyContribution);
if (actualGain == 0) return;
// Grant property reputation
reputation += actualGain;
info.dailyContribution += actualGain;
m_ReputationDirty = true;
// Grant character reputation to property owner
auto* ownerEntity = Game::entityManager->GetEntity(owner);
if (ownerEntity) {
auto* charComp = ownerEntity->GetComponent<CharacterComponent>();
if (charComp) {
charComp->SetReputation(charComp->GetReputation() + actualGain);
}
} else {
// Owner is offline, update DB directly
auto ownerCharId = owner;
GeneralUtils::ClearBit(ownerCharId, eObjectBits::CHARACTER);
const auto currentRep = Database::Get()->GetCharacterReputation(ownerCharId);
Database::Get()->SetCharacterReputation(ownerCharId, currentRep + actualGain);
}
}
}
void PropertyManagementComponent::SaveReputation() {
if (!m_ReputationDirty) return;
if (propertyId == LWOOBJID_EMPTY) return;
Database::Get()->UpdatePropertyReputation(propertyId, reputation);
for (const auto& [playerId, info] : m_PlayerActivity) {
if (info.dailyContribution > 0) {
Database::Get()->UpdatePropertyReputationContribution(propertyId, playerId, m_CurrentDate, info.dailyContribution);
}
}
m_ReputationDirty = false;
}
void PropertyManagementComponent::LoadDailyContributions() {
if (propertyId == LWOOBJID_EMPTY) return;
const auto contributions = Database::Get()->GetPropertyReputationContributions(propertyId, m_CurrentDate);
for (const auto& contrib : contributions) {
m_PlayerActivity[contrib.playerId].dailyContribution = contrib.reputationGained;
}
}

View File

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

View File

@@ -33,7 +33,7 @@
#endif
RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t componentID)
: Component(parent, componentID) {
: ActivityComponent(parent, componentID) {
m_PathName = u"MainPath";
m_NumberOfLaps = 3;
m_RemainingLaps = m_NumberOfLaps;
@@ -70,7 +70,7 @@ void RacingControlComponent::OnPlayerLoaded(Entity* player) {
auto* vehicle = inventoryComponent->FindItemByLot(8092);
// If the race has already started, send the player back to the main world.
if (m_Loaded || !vehicle) {
if (m_Loaded || !vehicle || !TakeCost(player)) {
auto* characterComponent = player->GetComponent<CharacterComponent>();
if (characterComponent) characterComponent->SendToZone(m_MainWorld);
return;

View File

@@ -6,7 +6,7 @@
#include "BitStream.h"
#include "Entity.h"
#include "Component.h"
#include "ActivityComponent.h"
#include "eReplicaComponentType.h"
#include <chrono>
@@ -104,7 +104,7 @@ struct RacingPlayerInfo {
/**
* Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc.
*/
class RacingControlComponent final : public Component {
class RacingControlComponent final : public ActivityComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL;

View File

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

View File

@@ -446,6 +446,8 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string&
break;
}
case eMissionTaskType::PLACE_MODEL:
[[fallthrough]];
case eMissionTaskType::ADD_BEHAVIOR:
{
AddProgress(count);
break;

View File

@@ -7,6 +7,7 @@
#include "Logger.h"
#include "Loot.h"
#include "ShootingGalleryComponent.h"
#include "RacingControlComponent.h"
bool ActivityManager::IsPlayerInActivity(Entity* self, LWOOBJID playerID) {
const auto* sac = self->GetComponent<ScriptedActivityComponent>();
@@ -99,6 +100,8 @@ bool ActivityManager::TakeActivityCost(const Entity* self, const LWOOBJID player
activityComponent = self->GetComponent<ShootingGalleryComponent>();
}
if (!activityComponent) return false;
auto* player = Game::entityManager->GetEntity(playerID);
if (player == nullptr)
return false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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