mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Off unless property_rent_enabled is on. Each property world's rent comes from its PropertyTemplate row (minimumPrice every rentDuration x durationType; Block Yard is free), unless the new Property Rent dashboard page sets another price or period (property_rent_manage). Rent is taken from the owner's coins shortly after their character loads, with a mail receipt; unpaid rent is mailed and, after property_rent_grace_days, makes the property private until it is paid, like live. The property management component refuses public or best friends privacy while rent is overdue and a property world that loads overdue makes itself private. Property game messages are unchanged. Fixes #943 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
11 KiB
C++
251 lines
11 KiB
C++
#ifndef __HOTPROPERTYSLOTS__H__
|
|
#define __HOTPROPERTYSLOTS__H__
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
/**
|
|
* The "Today's Top Properties" panel of the client's news screen (res/ui/ingame/news.gfx).
|
|
*
|
|
* The screen sends GetHotPropertyData (GM 1511) when it opens; the server answers with
|
|
* NewsSendHotPropertiesInfoToClient (GM 1510), a list of properties. For each entry the client looks up the
|
|
* PropertyTemplate row by the entry's template id (LWOCharacterComponent::HotPropertyData, 0x00cf83f0 in 1.10.64)
|
|
* and the UI puts it in the slot for that row's spawnName. The UI only has four slots, one per small property
|
|
* world, and ignores every other spawn name. Live always sent four entries, one per slot, in this order.
|
|
*
|
|
* A slot only displays what it is sent: the name, owner, reputation and so on of the entry (the property and clone
|
|
* ids are not used). So a slot can show a property of any property world, sent with the slot's template id; only the
|
|
* tooltip still names the slot's own world (it is the slot's fixed zone name and its template's map description).
|
|
*/
|
|
namespace HotPropertySlots {
|
|
// The slots of news.gfx, in the order live sent them
|
|
constexpr std::array<std::string_view, 4> NEWS_SPAWN_NAMES{ "AGSmallProperty", "NSSmallProperty", "GFSmallProperty", "FVSmallProperty" };
|
|
|
|
// What a slot shows
|
|
enum class eMode : uint8_t {
|
|
AUTO = 0, // the approved public property of the slot's location with the most reputation (not shown in another slot)
|
|
PICKED = 1, // the property staff chose (AUTO again whenever it can't be shown any more)
|
|
EMPTY = 2, // nothing: the slot stays locked
|
|
};
|
|
|
|
inline std::string_view ModeName(eMode mode) {
|
|
switch (mode) {
|
|
case eMode::PICKED: return "picked";
|
|
case eMode::EMPTY: return "empty";
|
|
default: return "auto";
|
|
}
|
|
}
|
|
|
|
inline std::optional<eMode> ParseMode(std::string_view name) {
|
|
if (name == "auto") return eMode::AUTO;
|
|
if (name == "picked") return eMode::PICKED;
|
|
if (name == "empty") return eMode::EMPTY;
|
|
return std::nullopt;
|
|
}
|
|
|
|
// A mode as stored in the database; anything unknown is AUTO
|
|
inline eMode ModeFromInt(int64_t value) {
|
|
return value == 1 ? eMode::PICKED : value == 2 ? eMode::EMPTY : eMode::AUTO;
|
|
}
|
|
|
|
struct TemplateRow { // PropertyTemplate
|
|
uint32_t id{};
|
|
uint32_t mapId{};
|
|
std::string spawnName;
|
|
// Rent (PropertyRent.h) and reputation (PropertyReputation.h) of the template
|
|
int64_t minimumPrice{};
|
|
int32_t rentDuration{};
|
|
int32_t durationType{};
|
|
int32_t reputationPerMinute{};
|
|
};
|
|
|
|
struct EntranceRow { // PropertyEntranceComponent
|
|
uint32_t mapId{};
|
|
std::string propertyName;
|
|
};
|
|
|
|
struct Slot {
|
|
uint32_t templateId{}; // PropertyTemplate id, sent to the client as the entry's template id
|
|
uint32_t mapId{}; // the property world
|
|
std::string spawnName;
|
|
bool operator==(const Slot&) const = default;
|
|
};
|
|
|
|
/**
|
|
* The slots, in NEWS_SPAWN_NAMES order: for each spawn name the news screen knows, the PropertyTemplate row of
|
|
* that spawn name whose map a property entrance (the rocket pads players launch to properties from) leads to.
|
|
* When several rows qualify the lowest id is used. A spawn name without such a row has no slot.
|
|
*/
|
|
inline std::vector<Slot> ResolveSlots(const std::vector<TemplateRow>& templates, const std::vector<EntranceRow>& entrances) {
|
|
std::vector<Slot> slots;
|
|
for (const auto spawnName : NEWS_SPAWN_NAMES) {
|
|
const TemplateRow* best = nullptr;
|
|
for (const auto& row : templates) {
|
|
if (row.spawnName != spawnName) continue;
|
|
const bool entered = std::any_of(entrances.begin(), entrances.end(), [&](const EntranceRow& entrance) {
|
|
return entrance.mapId == row.mapId && entrance.propertyName == spawnName;
|
|
});
|
|
if (entered && (!best || row.id < best->id)) best = &row;
|
|
}
|
|
if (best) slots.push_back({ best->id, best->mapId, best->spawnName });
|
|
}
|
|
return slots;
|
|
}
|
|
|
|
/**
|
|
* The property worlds a slot can show a property of (its location): every PropertyTemplate map a property entrance
|
|
* leads to, small and medium, ordered by their lowest template id.
|
|
*/
|
|
inline std::vector<uint32_t> PropertyWorlds(const std::vector<TemplateRow>& templates, const std::vector<EntranceRow>& entrances) {
|
|
std::vector<const TemplateRow*> rows;
|
|
for (const auto& row : templates) {
|
|
const bool entered = std::any_of(entrances.begin(), entrances.end(), [&](const EntranceRow& entrance) {
|
|
return entrance.mapId == row.mapId && entrance.propertyName == row.spawnName;
|
|
});
|
|
if (entered) rows.push_back(&row);
|
|
}
|
|
std::sort(rows.begin(), rows.end(), [](const TemplateRow* a, const TemplateRow* b) { return a->id < b->id; });
|
|
std::vector<uint32_t> worlds;
|
|
for (const auto* row : rows) {
|
|
if (std::find(worlds.begin(), worlds.end(), row->mapId) == worlds.end()) worlds.push_back(row->mapId);
|
|
}
|
|
return worlds;
|
|
}
|
|
|
|
/**
|
|
* The PropertyTemplate row a property world uses: the lowest id among its rows a property entrance leads to (as
|
|
* PropertyWorlds), or its lowest id row when no entrance leads there. nullptr when the map has no row.
|
|
*/
|
|
inline const TemplateRow* WorldTemplate(const std::vector<TemplateRow>& templates, const std::vector<EntranceRow>& entrances, uint32_t mapId) {
|
|
const TemplateRow* entered = nullptr;
|
|
const TemplateRow* any = nullptr;
|
|
for (const auto& row : templates) {
|
|
if (row.mapId != mapId) continue;
|
|
if (!any || row.id < any->id) any = &row;
|
|
const bool isEntered = std::any_of(entrances.begin(), entrances.end(), [&](const EntranceRow& entrance) {
|
|
return entrance.mapId == row.mapId && entrance.propertyName == row.spawnName;
|
|
});
|
|
if (isEntered && (!entered || row.id < entered->id)) entered = &row;
|
|
}
|
|
return entered ? entered : any;
|
|
}
|
|
|
|
// A slot's location as stored (0: none) if it is still a property world, else the slot's own world
|
|
inline uint32_t Location(uint32_t stored, const Slot& slot, const std::vector<uint32_t>& worlds) {
|
|
return stored != 0 && std::find(worlds.begin(), worlds.end(), stored) != worlds.end() ? stored : slot.mapId;
|
|
}
|
|
|
|
constexpr int32_t PRIVACY_PUBLIC = 2; // ePropertyPrivacyOption::Public
|
|
|
|
// Whether a property may be featured in a slot: approved by a moderator, public, and in the slot's location
|
|
inline bool Featurable(uint32_t modApproved, int32_t privacyOption, uint32_t propertyZone, uint32_t slotMap) {
|
|
return modApproved == 1 && privacyOption == PRIVACY_PUBLIC && propertyZone == slotMap;
|
|
}
|
|
|
|
// What a slot was set to show (featured_properties), with its location resolved
|
|
struct Choice {
|
|
eMode mode{ eMode::AUTO };
|
|
uint32_t mapId{}; // the location: the world whose properties it shows
|
|
int64_t propertyId{}; // the picked property (PICKED)
|
|
};
|
|
|
|
// An approved public property that can be shown
|
|
struct Candidate {
|
|
int64_t propertyId{};
|
|
uint32_t mapId{};
|
|
uint64_t reputation{};
|
|
};
|
|
|
|
// What a slot shows
|
|
struct Showing {
|
|
std::optional<int64_t> propertyId; // none: the slot stays locked
|
|
bool pickFellBack{}; // PICKED, but the pick can't be shown (any more), so it shows its AUTO
|
|
bool operator==(const Showing&) const = default;
|
|
};
|
|
|
|
// How many of each location's best properties are enough to resolve the slots: every slot could draw from one world
|
|
constexpr uint32_t CANDIDATES_PER_WORLD = NEWS_SPAWN_NAMES.size();
|
|
|
|
/**
|
|
* The worlds whose best CANDIDATES_PER_WORLD candidates Resolve needs (0: every world, for full auto). The picked
|
|
* properties have to be added to the candidates as well, when they may be shown in their slot's location.
|
|
*/
|
|
inline std::vector<uint32_t> CandidateWorlds(const std::vector<Choice>& choices, bool fullAuto) {
|
|
if (fullAuto) return { 0 };
|
|
std::vector<uint32_t> worlds;
|
|
for (const auto& choice : choices) {
|
|
if (choice.mode != eMode::EMPTY && std::find(worlds.begin(), worlds.end(), choice.mapId) == worlds.end()) worlds.push_back(choice.mapId);
|
|
}
|
|
return worlds;
|
|
}
|
|
|
|
/**
|
|
* What each slot shows (one entry per choice, in slot order), so that no property is shown twice.
|
|
* candidates: the properties that may be shown (see CandidateWorlds); a property listed twice counts once.
|
|
* Full auto ignores the choices: the slots show the candidates with the most reputation, in slot order.
|
|
* Otherwise the picks come first, in slot order: a pick is shown when it is a candidate of the slot's location that
|
|
* an earlier slot doesn't show, else the slot falls back to AUTO. Then the AUTO slots, in slot order, each show the
|
|
* candidate of their location with the most reputation that isn't shown yet (two on one world: its #1 and #2).
|
|
* A slot with nothing left to show, and every EMPTY slot, shows nothing.
|
|
*/
|
|
inline std::vector<Showing> Resolve(const std::vector<Choice>& choices, bool fullAuto, std::vector<Candidate> candidates) {
|
|
std::stable_sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) { return a.reputation > b.reputation; });
|
|
std::vector<Showing> showing(choices.size());
|
|
std::vector<int64_t> used;
|
|
const auto isUsed = [&](int64_t id) { return std::find(used.begin(), used.end(), id) != used.end(); };
|
|
const auto best = [&](std::optional<uint32_t> mapId) -> std::optional<int64_t> {
|
|
for (const auto& candidate : candidates) {
|
|
if ((!mapId || candidate.mapId == *mapId) && !isUsed(candidate.propertyId)) return candidate.propertyId;
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
const auto show = [&](size_t slot, std::optional<int64_t> id) {
|
|
showing[slot].propertyId = id;
|
|
if (id) used.push_back(*id);
|
|
};
|
|
|
|
if (fullAuto) {
|
|
for (size_t i = 0; i < choices.size(); i++) show(i, best(std::nullopt));
|
|
return showing;
|
|
}
|
|
|
|
std::vector<size_t> autos;
|
|
for (size_t i = 0; i < choices.size(); i++) {
|
|
const auto& choice = choices[i];
|
|
if (choice.mode == eMode::EMPTY) continue;
|
|
if (choice.mode == eMode::PICKED) {
|
|
const bool shown = !isUsed(choice.propertyId) && std::any_of(candidates.begin(), candidates.end(), [&](const Candidate& candidate) {
|
|
return candidate.propertyId == choice.propertyId && candidate.mapId == choice.mapId;
|
|
});
|
|
if (shown) {
|
|
show(i, choice.propertyId);
|
|
continue;
|
|
}
|
|
showing[i].pickFellBack = true;
|
|
}
|
|
autos.push_back(i);
|
|
}
|
|
for (const auto i : autos) show(i, best(choices[i].mapId));
|
|
return showing;
|
|
}
|
|
|
|
/**
|
|
* Which entries to send, by index. news.gfx always walks four entries and, for an entry that is missing, reuses
|
|
* the slot of the entry before it, filling it with "undefined". So fewer than four entries are padded by
|
|
* repeating the last one, which only fills its own slot again. None: nothing to send, every slot stays locked.
|
|
*/
|
|
inline std::vector<size_t> NewsOrder(size_t count) {
|
|
std::vector<size_t> order;
|
|
for (size_t i = 0; i < count; i++) order.push_back(i);
|
|
while (count > 0 && order.size() < NEWS_SPAWN_NAMES.size()) order.push_back(count - 1);
|
|
return order;
|
|
}
|
|
};
|
|
|
|
#endif //!__HOTPROPERTYSLOTS__H__
|