From 37459e0019d70af3ec14a80436efed291339597a Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 18:55:56 -0500 Subject: [PATCH] fix(properties): top property slots show only their own world The news screen's slot tooltip names the slot's own property world whatever property is sent for it, so a property of another world was shown under the wrong name. Each slot now only shows a property of its own world: a location stored by the older per-slot setting is kept in the table but not used, full auto fills each slot with its own world's top property instead of the top four across every world, and the dashboard no longer offers a location or candidates from other worlds. Co-Authored-By: Claude Opus 5.5 --- dCommon/HotPropertySlots.h | 20 +++++---- .../routes/FeaturedProperties.cpp | 14 +++--- dDashboardServer/templates/properties.jinja2 | 45 ++++--------------- tests/dCommonTests/HotPropertySlotsTests.cpp | 21 ++++----- 4 files changed, 39 insertions(+), 61 deletions(-) diff --git a/dCommon/HotPropertySlots.h b/dCommon/HotPropertySlots.h index fcb62f2b7..e4735ef2f 100644 --- a/dCommon/HotPropertySlots.h +++ b/dCommon/HotPropertySlots.h @@ -135,9 +135,13 @@ namespace HotPropertySlots { 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& worlds) { - return stored != 0 && std::find(worlds.begin(), worlds.end(), stored) != worlds.end() ? stored : slot.mapId; + /** + * A slot's location: always the slot's own world. The news screen's tooltip names the slot's world whatever + * property is sent for it, so a property of another world would be shown under the wrong name. A location stored + * by an older version (featured_properties.zone_id) is kept in the table but not used. + */ + inline uint32_t Location(uint32_t /*stored*/, const Slot& slot, const std::vector& /*worlds*/) { + return slot.mapId; } constexpr int32_t PRIVACY_PUBLIC = 2; // ePropertyPrivacyOption::Public @@ -172,14 +176,13 @@ namespace HotPropertySlots { 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 + * The worlds whose best CANDIDATES_PER_WORLD candidates Resolve needs (full auto: every slot's world). 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 CandidateWorlds(const std::vector& choices, bool fullAuto) { - if (fullAuto) return { 0 }; std::vector 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); + if ((fullAuto || choice.mode != eMode::EMPTY) && std::find(worlds.begin(), worlds.end(), choice.mapId) == worlds.end()) worlds.push_back(choice.mapId); } return worlds; } @@ -187,7 +190,8 @@ namespace HotPropertySlots { /** * 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. + * Full auto ignores the modes and picks: each slot shows its location's candidate 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). @@ -210,7 +214,7 @@ namespace HotPropertySlots { }; if (fullAuto) { - for (size_t i = 0; i < choices.size(); i++) show(i, best(std::nullopt)); + for (size_t i = 0; i < choices.size(); i++) show(i, best(choices[i].mapId)); return showing; } diff --git a/dDashboardServer/routes/FeaturedProperties.cpp b/dDashboardServer/routes/FeaturedProperties.cpp index e2a4d4a6f..dcc7632c1 100644 --- a/dDashboardServer/routes/FeaturedProperties.cpp +++ b/dDashboardServer/routes/FeaturedProperties.cpp @@ -49,13 +49,13 @@ namespace { return slot == slots.end() ? nullptr : &*slot; } - // A location given by the dashboard (a property world's map id); missing: the slot's own world + // A location given by the dashboard: only the slot's own world (missing means that too), since the news screen + // names the slot's world whatever property is sent for it (HotPropertySlots::Location) std::optional ParseLocation(const nlohmann::json& value, const HotPropertySlots::Slot& slot) { if (value.is_null()) return slot.mapId; const auto mapId = value.is_string() ? GeneralUtils::TryParse(value.get()) : value.is_number_unsigned() ? std::optional(value.get()) : std::nullopt; - const auto& worlds = GetNewsWorlds().worlds; - if (!mapId || std::find(worlds.begin(), worlds.end(), *mapId) == worlds.end()) return std::nullopt; + if (mapId != slot.mapId) return std::nullopt; return mapId; } @@ -207,13 +207,13 @@ void FeaturedProperties::RegisterRoutes() { }); Route(eHTTPMethod::GET, "/api/featured_properties/:template/candidates", Perm("feature_properties"), - "Approved public properties a \"Today's Top Properties\" slot can show, most reputation first (at most 25). Query: ?location= (a property world's map id, default the slot's own), ?search= (name, description or owner)", + "Approved public properties a \"Today's Top Properties\" slot can show, most reputation first (at most 25). Only the slot's own world (the news screen names it). Query: ?search= (name, description or owner)", [](HTTPReply& reply, const HTTPContext& context) { const auto* slot = FindSlot(PathId(context.path, 2)); if (!slot) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "No such slot"); const auto locationValue = QueryValue(context.queryString, "location"); const auto location = ParseLocation(locationValue.empty() ? nlohmann::json(nullptr) : nlohmann::json(locationValue), *slot); - if (!location) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "location must be a property world"); + if (!location) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "A slot can only show a property of its own world (the news screen names that world)"); const auto candidates = Candidates(*location, MAX_MATCHES, QueryValue(context.queryString, "search").substr(0, MAX_SEARCH)); nlohmann::json list = nlohmann::json::array(); @@ -222,7 +222,7 @@ void FeaturedProperties::RegisterRoutes() { }); Route(eHTTPMethod::POST, "/api/featured_properties/:template", Perm("feature_properties"), - "Choose what a \"Today's Top Properties\" slot shows. Body: {location (a property world's map id, default the slot's own), mode: auto|picked|empty, property_id (for picked: an approved public property of the location that no other slot picked)}", + "Choose what a \"Today's Top Properties\" slot shows. Body: {mode: auto|picked|empty, property_id (for picked: an approved public property of the slot's own world that no other slot picked)}", [](HTTPReply& reply, const HTTPContext& context) { const auto templateId = PathId(context.path, 2); if (!templateId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid slot"); @@ -235,7 +235,7 @@ void FeaturedProperties::RegisterRoutes() { const auto mode = HotPropertySlots::ParseMode(body->value("mode", "")); if (!mode) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "mode must be auto, picked or empty"); const auto location = ParseLocation(body->contains("location") ? (*body)["location"] : nlohmann::json(nullptr), *slot); - if (!location) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "location must be a property world"); + if (!location) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "A slot can only show a property of its own world (the news screen names that world)"); IFeaturedProperties::FeaturedSlot row; row.templateId = slot->templateId; diff --git a/dDashboardServer/templates/properties.jinja2 b/dDashboardServer/templates/properties.jinja2 index ea2fe4c45..0cacab41a 100644 --- a/dDashboardServer/templates/properties.jinja2 +++ b/dDashboardServer/templates/properties.jinja2 @@ -19,7 +19,7 @@
-
How the slots are picked
The four slots of the game's news screen. Only approved public properties can be shown, and never one in two slots. Full auto shows the four with the most reputation across every property world; Per slot lets each slot show a property of any property world (its location): Auto the location's one with the most reputation not shown in another slot, a picked one, or nothing. Players see a change the next time they open the news screen (within 30 seconds).
+
How the slots are picked
The four slots of the game's news screen. Each slot belongs to one property world and can only show a property of that world, since the game names the slot's world whatever property is in it. Only approved public properties can be shown, and never one in two slots. Full auto shows each world's property with the most reputation; Per slot lets each slot show its world's Auto pick (the most reputation not shown in another slot), a picked one, or nothing. Players see a change the next time they open the news screen (within 30 seconds).
@@ -86,10 +86,6 @@ $(function () { var modeInputs = document.querySelectorAll('input[name="topPropertiesMode"]'); var propLabel = function (p) { return (p.name || '(unnamed)') + ' by ' + (p.owner_name || '?') + ' \u2014 ' + Number(p.reputation || 0).toLocaleString() + ' rep'; }; var countLabel = function (n) { return n + ' approved public ' + (n === 1 ? 'property' : 'properties'); }; - // The news screen's tooltip is the slot's own world, whatever world the property is on - var tooltipNote = function (s, location) { - return String(location) === String(s.map_id) ? '' : 'In game the slot\'s tooltip still names ' + s.zone_name + '.'; - }; var renderSlots = function (state) { var slots = state.slots || []; var fullAuto = !!state.full_auto; @@ -97,24 +93,15 @@ $(function () { document.getElementById('topPropertiesSetBy').textContent = state.settings_updated_at ? 'Switched to ' + (fullAuto ? 'full auto' : 'per slot') + ' by ' + (state.settings_updated_by || '?') + ' ' + new Date(state.settings_updated_at * 1000).toLocaleString() : ''; if (!slots.length) { slotsEl.innerHTML = '
No property worlds found in the client data.
'; return; } - var worldOptions = function (selected) { - return (state.worlds || []).map(function (w) { - return ''; - }).join(''); - }; slotsEl.innerHTML = slots.map(function (s) { var showing = s.showing - ? fmt.link('/properties/' + s.showing.id, s.showing.name || '(unnamed)') + ' by ' + esc(s.showing.owner_name) + - (String(s.showing.zone_id) !== String(s.map_id) ? ', on ' + esc(s.showing.zone_name) : '') + '' + ? fmt.link('/properties/' + s.showing.id, s.showing.name || '(unnamed)') + ' by ' + esc(s.showing.owner_name) + '' : 'Nothing (locked slot)'; - var showingNote = s.showing ? tooltipNote(s, s.showing.zone_id) : ''; - var warn = fullAuto ? '' : s.picked_unavailable ? '
The picked property is no longer approved and public in ' + esc(s.location_name) + ', so the slot falls back to Auto.
' + var warn = fullAuto ? '' : s.picked_unavailable ? '
The picked property isn\'t an approved public property of ' + esc(s.zone_name) + ' (any more), so the slot falls back to Auto.
' : s.picked_shown_elsewhere ? '
The picked property is already shown in an earlier slot, so this one falls back to Auto.
' : ''; var changed = s.updated_at ? '
Set by ' + esc(s.updated_by || '?') + ' ' + esc(new Date(s.updated_at * 1000).toLocaleString()) + '
' : ''; - var controls = fullAuto ? '
Full auto is on: this slot\'s own settings (' + esc(s.location_name) + ', ' + esc(s.mode) + ') are kept for when you switch back to per slot.
' : - '' + - '' + - '
' + countLabel(s.candidate_total) + '' + esc(tooltipNote(s, s.location)) + '
' + + var controls = fullAuto ? '
Full auto is on: this slot\'s own setting (' + esc(s.mode) + ') is kept for when you switch back to per slot.
' : + '
' + countLabel(s.candidate_total) + '
' + '