mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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>
122 lines
6.8 KiB
C++
122 lines
6.8 KiB
C++
#include "PropertyRentRoutes.h"
|
|
|
|
#include <ctime>
|
|
|
|
#include "CDClientDatabase.h"
|
|
#include "Database.h"
|
|
#include "DashboardRoutes.h"
|
|
#include "dConfig.h"
|
|
#include "EconomyPlaces.h"
|
|
#include "Game.h"
|
|
#include "GeneralUtils.h"
|
|
#include "HotPropertySlots.h"
|
|
#include "PropertyRentRules.h"
|
|
#include "RouteUtils.h"
|
|
#include "WSRoutes.h"
|
|
#include "eHTTPMethod.h"
|
|
|
|
using namespace RouteUtils;
|
|
|
|
namespace {
|
|
struct Templates {
|
|
std::vector<HotPropertySlots::TemplateRow> rows;
|
|
std::vector<HotPropertySlots::EntranceRow> entrances;
|
|
std::vector<uint32_t> worlds;
|
|
};
|
|
|
|
const Templates& GetTemplates() {
|
|
static const auto templates = [] {
|
|
Templates t;
|
|
auto rows = CDClientDatabase::ExecuteQuery("SELECT id, mapID, spawnName, minimumPrice, rentDuration, durationType, reputationPerMinute FROM PropertyTemplate;");
|
|
for (; !rows.eof(); rows.nextRow()) {
|
|
t.rows.push_back({ static_cast<uint32_t>(rows.getIntField("id")), static_cast<uint32_t>(rows.getIntField("mapID")), rows.getStringField("spawnName", ""),
|
|
rows.getIntField("minimumPrice", 0), rows.getIntField("rentDuration", 0), rows.getIntField("durationType", 0), rows.getIntField("reputationPerMinute", 0) });
|
|
}
|
|
auto entrances = CDClientDatabase::ExecuteQuery("SELECT mapID, propertyName FROM PropertyEntranceComponent;");
|
|
for (; !entrances.eof(); entrances.nextRow()) {
|
|
t.entrances.push_back({ static_cast<uint32_t>(entrances.getIntField("mapID")), entrances.getStringField("propertyName", "") });
|
|
}
|
|
t.worlds = HotPropertySlots::PropertyWorlds(t.rows, t.entrances);
|
|
return t;
|
|
}();
|
|
return templates;
|
|
}
|
|
|
|
nlohmann::json RateJson(const std::optional<PropertyRentRules::Rate>& rate) {
|
|
if (!rate) return nullptr;
|
|
return { {"price", rate->price}, {"periodDays", rate->periodSeconds / PropertyRentRules::DAY} };
|
|
}
|
|
}
|
|
|
|
namespace PropertyRentRoutes {
|
|
void RegisterRoutes() {
|
|
Route(eHTTPMethod::GET, "/property_rent", Perm("properties_view"), "What owners pay for each property world",
|
|
[](HTTPReply& reply, const HTTPContext& context) { RenderPage(reply, context, "property_rent.jinja2", "property_rent"); });
|
|
|
|
Route(eHTTPMethod::GET, "/api/property_rent", Perm("properties_view"),
|
|
"Each property world's rent: {enabled, graceDays, canManage, worlds: [{mapId, name, template: {price, periodDays}|null, "
|
|
"override: {price, periodDays, updated_by, updated_at}|null, rent: {price, periodDays}|null}]} (rent null: free)",
|
|
[](HTTPReply& reply, const HTTPContext& context) {
|
|
const auto& templates = GetTemplates();
|
|
const auto overrides = Database::Get()->GetPropertyRentRates();
|
|
nlohmann::json worlds = nlohmann::json::array();
|
|
for (const auto mapId : templates.worlds) {
|
|
const auto* row = HotPropertySlots::WorldTemplate(templates.rows, templates.entrances, mapId);
|
|
const auto fromTemplate = row ? PropertyRentRules::TemplateRate(row->minimumPrice, row->rentDuration, row->durationType) : std::nullopt;
|
|
nlohmann::json world{ {"mapId", mapId}, {"name", EconomyPlaces::ZoneName(mapId)}, {"template", RateJson(fromTemplate)}, {"override", nullptr},
|
|
{"rent", RateJson(fromTemplate)} };
|
|
for (const auto& rate : overrides) {
|
|
if (rate.mapId != mapId) continue;
|
|
world["override"] = { {"price", rate.price}, {"periodDays", rate.periodDays}, {"updated_by", rate.updatedBy}, {"updated_at", rate.updatedAt} };
|
|
world["rent"] = RateJson(PropertyRentRules::Resolve(fromTemplate, rate.price, rate.periodDays));
|
|
}
|
|
worlds.push_back(world);
|
|
}
|
|
JsonSuccess(reply, { {"enabled", Game::config->GetValue("property_rent_enabled") == "1"},
|
|
{"graceDays", GeneralUtils::TryParse<int64_t>(Game::config->GetValue("property_rent_grace_days")).value_or(3)},
|
|
{"canManage", Can(context, "property_rent_manage")}, {"worlds", worlds} });
|
|
});
|
|
|
|
Route(eHTTPMethod::POST, "/api/property_rent", Perm("property_rent_manage"),
|
|
"Set a property world's rent instead of its template's. Body: {mapId, price (coins, 0: free), periodDays (0: the template's)}",
|
|
[](HTTPReply& reply, const HTTPContext& context) {
|
|
const auto body = ParseBody(context);
|
|
if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON");
|
|
const auto mapId = body->value("mapId", 0u);
|
|
const auto& worlds = GetTemplates().worlds;
|
|
if (std::find(worlds.begin(), worlds.end(), mapId) == worlds.end()) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Not a property world");
|
|
const auto price = body->value("price", int64_t{ -1 });
|
|
const auto periodDays = body->value("periodDays", -1);
|
|
if (price < 0 || price > 1000000000) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "The price must be 0 to 1000000000 coins");
|
|
if (periodDays < 0 || periodDays > 3650) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "The period must be 0 to 3650 days");
|
|
Database::Get()->SetPropertyRentRate({ mapId, price, periodDays, context.authenticatedUser, static_cast<int64_t>(std::time(nullptr)) });
|
|
const auto name = EconomyPlaces::ZoneName(mapId);
|
|
Audit(context, "property_rent_set", name + " (" + std::to_string(mapId) + "): rent " + (price == 0 ? std::string("free") :
|
|
std::to_string(price) + " coins every " + (periodDays ? std::to_string(periodDays) + " days" : std::string("template period"))));
|
|
BroadcastTableChanged("property_rent");
|
|
JsonSuccess(reply, { {"message", "Rent for " + name + " saved"} });
|
|
});
|
|
|
|
Route(eHTTPMethod::GET, "/api/properties/:id/reputation", Perm("properties_view"),
|
|
"Reputation a property got from visitors in the last 30 days: {reputation, days: [{day, visitors, points, seconds}]} (days since the Unix epoch, newest first)",
|
|
[](HTTPReply& reply, const HTTPContext& context) {
|
|
const auto propertyId = PathId<LWOOBJID>(context.path, 2);
|
|
const auto info = propertyId ? Database::Get()->GetPropertyInfo(*propertyId) : std::nullopt;
|
|
if (!info) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "Property not found");
|
|
const auto today = static_cast<uint32_t>(std::time(nullptr) / PropertyRentRules::DAY);
|
|
JsonSuccess(reply, { {"reputation", info->reputation}, {"days", Database::Get()->GetPropertyReputationDays(*propertyId, today >= 30 ? today - 30 : 0)} });
|
|
});
|
|
|
|
Route(eHTTPMethod::POST, "/api/property_rent/delete", Perm("property_rent_manage"), "Go back to the template's rent for a property world. Body: {mapId}",
|
|
[](HTTPReply& reply, const HTTPContext& context) {
|
|
const auto body = ParseBody(context);
|
|
const auto mapId = body ? body->value("mapId", 0u) : 0u;
|
|
if (!Database::Get()->DeletePropertyRentRate(mapId)) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "That world uses its template's rent already");
|
|
const auto name = EconomyPlaces::ZoneName(mapId);
|
|
Audit(context, "property_rent_reset", name + " (" + std::to_string(mapId) + "): rent back to its template's");
|
|
BroadcastTableChanged("property_rent");
|
|
JsonSuccess(reply, { {"message", name + " uses its template's rent again"} });
|
|
});
|
|
}
|
|
}
|