feat: property reputation from visitors, resistant to farming

Property worlds now give their property reputation for the time other people spend on it, which
fills the property lists and the news screen's Today's Top Properties. Visitors are counted per
account; the owner's account, accounts linked to it and staff don't count. A visit earns nothing
for the first property_reputation_min_visit seconds (WorldConfig's propertyReputationDelay), then
each active minute (the visitor moved) earns reputationPerMinute times a multiplier, for a capped
number of minutes per visit. Repeat visitors earn less the more recent days they already gave
reputation, and each visitor and each property have a daily cap. Every parameter is a setting;
what each account gave each property per day is kept in property_reputation_visits. The rules are
pure functions with unit tests.

Fixes #636
Fixes #637

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:36:57 -05:00
parent 68dccab52a
commit cecf784a9e
23 changed files with 684 additions and 1 deletions

View File

@@ -49,6 +49,7 @@
#include "IMessageCaptures.h"
#include "IContraband.h"
#include "IPropertyRent.h"
#include "IPropertyReputation.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)
@@ -62,7 +63,7 @@ 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 IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent {
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.

View File

@@ -0,0 +1,32 @@
#ifndef __IPROPERTYREPUTATION__H__
#define __IPROPERTYREPUTATION__H__
#include <cstdint>
#include "dCommonVars.h"
#include "json.hpp"
/**
* Property reputation (see dCommon/PropertyReputationRules.h): what visitors gave each property per day.
*/
class IPropertyReputation {
public:
struct VisitorHistory {
int64_t today{}; // points the account gave the property today
uint32_t previousDays{}; // other days in the window it gave the property any
};
// days: how many days before `day` count as recent
virtual VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) = 0;
// Points the property got from everyone on a day
virtual int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) = 0;
// Add a visitor's points and time for a day, and the points to properties.reputation
virtual void AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) = 0;
// For the property page: [{day, visitors, points, seconds}] of the last `days` days, newest first
virtual nlohmann::json GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) = 0;
};
#endif //!__IPROPERTYREPUTATION__H__

View File

@@ -215,6 +215,12 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IPropertyReputation
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override;
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override;
void AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) override;
nlohmann::json GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) override;
// IPropertyRent
std::vector<RentRate> GetPropertyRentRates() override;
void SetPropertyRentRate(const RentRate& rate) override;

View File

@@ -20,6 +20,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"FeaturedProperties.cpp"
"Contraband.cpp"
"PropertyRent.cpp"
"PropertyReputation.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,39 @@
#include "MySQLDatabase.h"
IPropertyReputation::VisitorHistory MySQLDatabase::GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) {
VisitorHistory history;
auto result = ExecuteSelect(
"SELECT COALESCE(SUM(CASE WHEN day = ? THEN points ELSE 0 END), 0) AS today, "
"COALESCE(SUM(CASE WHEN day < ? AND points > 0 THEN 1 ELSE 0 END), 0) AS previous_days "
"FROM property_reputation_visits WHERE property_id = ? AND account_id = ? AND day >= ? AND day <= ?;",
day, day, propertyId, accountId, day >= days ? day - days : 0, day);
if (result->next()) {
history.today = result->getInt64("today");
history.previousDays = static_cast<uint32_t>(result->getInt64("previous_days"));
}
return history;
}
int64_t MySQLDatabase::GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) {
auto result = ExecuteSelect("SELECT COALESCE(SUM(points), 0) AS points FROM property_reputation_visits WHERE property_id = ? AND day = ?;", propertyId, day);
return result->next() ? result->getInt64("points") : 0;
}
void MySQLDatabase::AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) {
ExecuteInsert(
"INSERT INTO property_reputation_visits (property_id, account_id, day, points, seconds) VALUES (?, ?, ?, ?, ?) "
"ON DUPLICATE KEY UPDATE points = points + VALUES(points), seconds = seconds + VALUES(seconds);",
propertyId, accountId, day, points, seconds);
if (points > 0) ExecuteUpdate("UPDATE properties SET reputation = reputation + ? WHERE id = ?;", points, propertyId);
}
nlohmann::json MySQLDatabase::GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) {
auto result = ExecuteSelect(
"SELECT day, COUNT(*) AS visitors, SUM(points) AS points, SUM(seconds) AS seconds FROM property_reputation_visits "
"WHERE property_id = ? AND day >= ? GROUP BY day ORDER BY day DESC;", propertyId, fromDay);
nlohmann::json days = nlohmann::json::array();
while (result->next()) {
days.push_back({ {"day", result->getInt("day")}, {"visitors", result->getInt64("visitors")}, {"points", result->getInt64("points")}, {"seconds", result->getInt64("seconds")} });
}
return days;
}

View File

@@ -199,6 +199,12 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IPropertyReputation
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override;
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override;
void AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) override;
nlohmann::json GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) override;
// IPropertyRent
std::vector<RentRate> GetPropertyRentRates() override;
void SetPropertyRentRate(const RentRate& rate) override;

View File

@@ -20,6 +20,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"FeaturedProperties.cpp"
"Contraband.cpp"
"PropertyRent.cpp"
"PropertyReputation.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,39 @@
#include "SQLiteDatabase.h"
IPropertyReputation::VisitorHistory SQLiteDatabase::GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) {
VisitorHistory history;
auto [_, result] = ExecuteSelect(
"SELECT COALESCE(SUM(CASE WHEN day = ? THEN points ELSE 0 END), 0) AS today, "
"COALESCE(SUM(CASE WHEN day < ? AND points > 0 THEN 1 ELSE 0 END), 0) AS previous_days "
"FROM property_reputation_visits WHERE property_id = ? AND account_id = ? AND day >= ? AND day <= ?;",
day, day, propertyId, accountId, day >= days ? day - days : 0, day);
if (!result.eof()) {
history.today = result.getInt64Field("today");
history.previousDays = static_cast<uint32_t>(result.getInt64Field("previous_days"));
}
return history;
}
int64_t SQLiteDatabase::GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) {
auto [_, result] = ExecuteSelect("SELECT COALESCE(SUM(points), 0) AS points FROM property_reputation_visits WHERE property_id = ? AND day = ?;", propertyId, day);
return result.eof() ? 0 : result.getInt64Field("points");
}
void SQLiteDatabase::AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) {
ExecuteInsert(
"INSERT INTO property_reputation_visits (property_id, account_id, day, points, seconds) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(property_id, account_id, day) DO UPDATE SET points = points + excluded.points, seconds = seconds + excluded.seconds;",
propertyId, accountId, day, points, seconds);
if (points > 0) ExecuteUpdate("UPDATE properties SET reputation = reputation + ? WHERE id = ?;", points, propertyId);
}
nlohmann::json SQLiteDatabase::GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) {
auto [_, result] = ExecuteSelect(
"SELECT day, COUNT(*) AS visitors, SUM(points) AS points, SUM(seconds) AS seconds FROM property_reputation_visits "
"WHERE property_id = ? AND day >= ? GROUP BY day ORDER BY day DESC;", propertyId, fromDay);
nlohmann::json days = nlohmann::json::array();
for (; !result.eof(); result.nextRow()) {
days.push_back({ {"day", result.getIntField("day")}, {"visitors", result.getInt64Field("visitors")}, {"points", result.getInt64Field("points")}, {"seconds", result.getInt64Field("seconds")} });
}
return days;
}

View File

@@ -245,6 +245,12 @@ class TestSQLDatabase : public GameDatabase {
std::vector<ZoneLimit> GetZoneLimits() override { return {}; }
void SetZoneLimit(const ZoneLimit& limit) override {}
void DeleteZoneLimit(uint32_t zoneId) override {}
// IPropertyReputation
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override { return {}; }
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override { return 0; }
void AddPropertyReputation(LWOOBJID propertyId, uint32_t accountId, uint32_t day, int64_t points, int64_t seconds) override {}
nlohmann::json GetPropertyReputationDays(LWOOBJID propertyId, uint32_t fromDay) override { return nlohmann::json::array(); }
// IPropertyRent
std::vector<RentRate> GetPropertyRentRates() override { return {}; }
void SetPropertyRentRate(const RentRate& rate) override {}