From 1f8dd73e0bdf14944a630585958ee6de4fceceac Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 22:06:30 -0500 Subject: [PATCH] refactor(dashboard): routes name zones, objects and phrases through GameText The routes' own zone name helpers and the CDClient's English zone and leaderboard header copies give way to GameText, so every name comes from the locale in the viewer's language; the Network page's world labels, mail, missions, leaderboards, item info (cached per language) and the settings/vanity help strings too. Reward code 4 and the plaque text name their zone from the locale. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/APIRoutes.cpp | 19 +++----- dDashboardServer/routes/Announcements.cpp | 7 +-- dDashboardServer/routes/ChallengeRoutes.cpp | 7 +-- dDashboardServer/routes/CharacterProgress.cpp | 5 +-- dDashboardServer/routes/CharacterTools.cpp | 8 +--- dDashboardServer/routes/ChatRoutes.cpp | 3 +- dDashboardServer/routes/ClientAssets.cpp | 15 ++++--- dDashboardServer/routes/DashboardRoutes.cpp | 45 ++++--------------- dDashboardServer/routes/DashboardRoutes.h | 3 -- dDashboardServer/routes/EconomyPlaces.cpp | 6 +-- dDashboardServer/routes/EventsCalendar.cpp | 18 +++----- .../routes/FeaturedProperties.cpp | 20 ++++----- dDashboardServer/routes/Inspector.cpp | 5 +-- dDashboardServer/routes/InstanceLoad.cpp | 14 +++--- dDashboardServer/routes/LeaderboardRoutes.cpp | 38 ++++++++-------- dDashboardServer/routes/LiveEventRoutes.cpp | 7 +-- dDashboardServer/routes/LiveUpdateRoutes.cpp | 5 +-- dDashboardServer/routes/LiveWorld.cpp | 9 ++-- dDashboardServer/routes/MissionTools.cpp | 6 +-- dDashboardServer/routes/ModerationTools.cpp | 5 ++- dDashboardServer/routes/ModeratorHelper.cpp | 10 ++--- dDashboardServer/routes/PublicRoutes.cpp | 3 +- dDashboardServer/routes/ReportRoutes.cpp | 16 +++---- dDashboardServer/routes/ServerRoutes.cpp | 3 +- dDashboardServer/routes/SettingsRoutes.cpp | 14 +++--- dDashboardServer/routes/Showcase.cpp | 28 +++++------- dDashboardServer/routes/Traffic.cpp | 12 +++-- dDashboardServer/routes/VanityRoutes.cpp | 7 +-- dDashboardServer/routes/WorldView.cpp | 14 +++--- 29 files changed, 146 insertions(+), 206 deletions(-) diff --git a/dDashboardServer/routes/APIRoutes.cpp b/dDashboardServer/routes/APIRoutes.cpp index 95c80e3c7..346d5f172 100644 --- a/dDashboardServer/routes/APIRoutes.cpp +++ b/dDashboardServer/routes/APIRoutes.cpp @@ -1,4 +1,5 @@ #include "APIRoutes.h" +#include "GameText.h" #include "PropertyAssets.h" #include "OpenApi.h" #include "RouteUtils.h" @@ -40,8 +41,6 @@ #include "Sd0.h" #include #include -#include "Locale.h" -#include "LocaleText.h" #include #include #include @@ -84,11 +83,7 @@ namespace { std::string WithZoneNames(const std::string& table) { auto json = nlohmann::json::parse(table, nullptr, false); if (json.is_discarded()) return table; - const auto& zones = ZoneNames(); - for (auto& row : json["data"]) { - const auto zone = std::to_string(row.value("zone_id", 0)); - row["zone_name"] = zones.contains(zone) ? zones[zone].get() : "Zone " + zone; - } + for (auto& row : json["data"]) row["zone_name"] = GameText::ZoneName(row.value("zone_id", 0u)); return json.dump(); } @@ -130,7 +125,7 @@ namespace { } std::string Localized(const std::string& text) { - return LocaleText::Expand(text, [](const std::string& key) -> const std::string& { return Locale::GetPhrase(key); }); + return GameText::Expand(text); } // One mail row for the Mail page and mailboxes: the stored text, the text as the client shows it (locale keys @@ -338,9 +333,9 @@ namespace { row["path"] = points; } const auto prefix = "PropertyTemplate_" + std::to_string(templateId); - row["localized_name"] = Locale::GetPhrase(prefix + "_name"); - row["localized_description"] = Locale::GetPhrase(prefix + "_description"); - const auto& zones = ZoneNames(); + row["localized_name"] = GameText::Phrase(prefix + "_name"); + row["localized_description"] = GameText::Phrase(prefix + "_description"); + const auto& zones = GameText::ZoneNames(); for (const auto* key : { "mapID", "vendorMapID" }) { if (!row.contains(key) || !row[key].is_number_integer()) continue; const auto zone = std::to_string(row[key].get()); @@ -1244,7 +1239,7 @@ namespace { {"claimed", info->claimedTime}, {"performance_cost", info->performanceCost}, {"zone_id", info->zoneId}, - {"zone_name", ZoneNames().contains(std::to_string(info->zoneId)) ? ZoneNames()[std::to_string(info->zoneId)].get() : ""}, + {"zone_name", GameText::ZoneNames().contains(std::to_string(info->zoneId)) ? GameText::ZoneNames()[std::to_string(info->zoneId)].get() : ""}, {"models", models} }); }); diff --git a/dDashboardServer/routes/Announcements.cpp b/dDashboardServer/routes/Announcements.cpp index 9f0c61ed1..6617b00cd 100644 --- a/dDashboardServer/routes/Announcements.cpp +++ b/dDashboardServer/routes/Announcements.cpp @@ -1,4 +1,5 @@ #include "Announcements.h" +#include "GameText.h" #include "AnnouncementSchedule.h" #include "LiveWorld.h" #include "DashboardRoutes.h" @@ -61,7 +62,7 @@ namespace { std::string ZoneList(const std::vector& zones) { if (zones.empty()) return "every world"; std::string text; - const auto& names = ZoneNames(); + const auto& names = GameText::ZoneNames(); for (const auto zone : zones) { const auto key = std::to_string(zone); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get() : "zone " + key); @@ -80,7 +81,7 @@ namespace { // Every zone a message can be limited to, for the picker nlohmann::json ZoneOptions() { std::vector> zones; - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.emplace_back(*zone, name.get()); } std::sort(zones.begin(), zones.end()); @@ -101,7 +102,7 @@ namespace { row.enabled = body.value("enabled", true); row.zones.clear(); if (body.contains("zones") && body["zones"].is_array()) { - const auto& known = ZoneNames(); + const auto& known = GameText::ZoneNames(); for (const auto& zone : body["zones"]) { if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get()))) { error = "Pick zones from the list"; return false; } if (std::find(row.zones.begin(), row.zones.end(), zone.get()) == row.zones.end()) row.zones.push_back(zone.get()); diff --git a/dDashboardServer/routes/ChallengeRoutes.cpp b/dDashboardServer/routes/ChallengeRoutes.cpp index 7ae27aca4..a4e4915b2 100644 --- a/dDashboardServer/routes/ChallengeRoutes.cpp +++ b/dDashboardServer/routes/ChallengeRoutes.cpp @@ -1,4 +1,5 @@ #include "ChallengeRoutes.h" +#include "GameText.h" #include "LiveEventRoutes.h" #include "LiveWorld.h" #include "DashboardRoutes.h" @@ -46,7 +47,7 @@ namespace { std::string ZoneList(const std::vector& zones) { if (zones.empty()) return "every zone"; std::string text; - const auto& names = ZoneNames(); + const auto& names = GameText::ZoneNames(); for (const auto zone : zones) { const auto key = std::to_string(zone); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get() : "zone " + key); @@ -257,7 +258,7 @@ namespace { nlohmann::json ZoneOptions() { std::vector> zones; - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.emplace_back(*zone, name.get()); } std::sort(zones.begin(), zones.end()); @@ -284,7 +285,7 @@ namespace { if (challenge.lot < 0 || (challenge.lot > 0 && !ClientAssets::ObjectName(challenge.lot))) return fail("Unknown LOT"); challenge.zones.clear(); if (body.contains("zones") && body["zones"].is_array()) { - const auto& known = ZoneNames(); + const auto& known = GameText::ZoneNames(); for (const auto& zone : body["zones"]) { if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get()))) return fail("Pick zones from the list"); if (std::find(challenge.zones.begin(), challenge.zones.end(), zone.get()) == challenge.zones.end()) challenge.zones.push_back(zone.get()); diff --git a/dDashboardServer/routes/CharacterProgress.cpp b/dDashboardServer/routes/CharacterProgress.cpp index 52c73838b..48e9aaf9f 100644 --- a/dDashboardServer/routes/CharacterProgress.cpp +++ b/dDashboardServer/routes/CharacterProgress.cpp @@ -1,4 +1,5 @@ #include "CharacterProgress.h" +#include "GameText.h" #include #include @@ -186,10 +187,8 @@ namespace { for (const auto& [zoneId, s] : stats) zoneIds.insert(zoneId); for (const auto& [zoneId, zone] : catalog.zones) if (zone.released) zoneIds.insert(zoneId); zoneIds.erase(0); // character select - const auto& names = ZoneNames(); nlohmann::json zones = nlohmann::json::array(); for (const auto zoneId : zoneIds) { - const auto key = std::to_string(zoneId); const auto zone = catalog.zones.find(zoneId); nlohmann::json summary = nlohmann::json::array(); if (zone != catalog.zones.end()) { @@ -206,7 +205,7 @@ namespace { } } const auto category = zone != catalog.zones.end() ? zone->second.category : std::nullopt; - zones.push_back({ {"id", zoneId}, {"name", names.contains(key) ? names[key] : nlohmann::json("Zone " + key)}, {"visited", visited.contains(zoneId)}, + zones.push_back({ {"id", zoneId}, {"name", GameText::ZoneName(zoneId)}, {"visited", visited.contains(zoneId)}, {"category", category ? nlohmann::json(*category) : nlohmann::json(nullptr)}, {"stats", stats.contains(zoneId) ? stats[zoneId] : nlohmann::json(nullptr)}, {"summary", summary} }); } diff --git a/dDashboardServer/routes/CharacterTools.cpp b/dDashboardServer/routes/CharacterTools.cpp index 53225189c..7353f0adf 100644 --- a/dDashboardServer/routes/CharacterTools.cpp +++ b/dDashboardServer/routes/CharacterTools.cpp @@ -1,4 +1,5 @@ #include "CharacterTools.h" +#include "GameText.h" #include @@ -72,14 +73,9 @@ namespace { for (auto& inventory : summary["inventories"]) for (auto& item : inventory["items"]) item["name"] = ClientAssets::ItemName(item["lot"].get()); } - nlohmann::json ZoneName(uint32_t zone) { - const auto& zones = ZoneNames(); - const auto key = std::to_string(zone); - return zones.contains(key) ? zones[key] : nlohmann::json("Zone " + key); - } nlohmann::json WithZones(nlohmann::json rows) { - for (auto& row : rows) row["zone_name"] = ZoneName(row.value("zone_id", 0u)); + for (auto& row : rows) row["zone_name"] = GameText::ZoneName(row.value("zone_id", 0u)); return rows; } diff --git a/dDashboardServer/routes/ChatRoutes.cpp b/dDashboardServer/routes/ChatRoutes.cpp index c735dbb67..d3172069f 100644 --- a/dDashboardServer/routes/ChatRoutes.cpp +++ b/dDashboardServer/routes/ChatRoutes.cpp @@ -1,4 +1,5 @@ #include "ChatRoutes.h" +#include "GameText.h" #include #include @@ -29,7 +30,7 @@ namespace { } nlohmann::json RowJson(const IChatLog::ChatMessage& m, const ChatHistory::Access& access) { - const auto& zones = ZoneNames(); + const auto& zones = GameText::ZoneNames(); const auto zone = std::to_string(m.zoneId); auto json = ChatHistory::MessageJson(m, access); json["zone_name"] = zones.contains(zone) ? zones[zone] : nlohmann::json(""); diff --git a/dDashboardServer/routes/ClientAssets.cpp b/dDashboardServer/routes/ClientAssets.cpp index ac469d97d..afcab9463 100644 --- a/dDashboardServer/routes/ClientAssets.cpp +++ b/dDashboardServer/routes/ClientAssets.cpp @@ -1,4 +1,5 @@ #include "ClientAssets.h" +#include "GameText.h" #include #include @@ -15,7 +16,6 @@ #include "CDClientDatabase.h" #include "Game.h" #include "Logger.h" -#include "Locale.h" #include "dConfig.h" #include "eHTTPMethod.h" #include "GeneralUtils.h" @@ -27,7 +27,7 @@ namespace { constexpr uint32_t MAX_ITEMS_PER_REQUEST = 500; constexpr size_t MAX_LISTING_ENTRIES = 2000; - std::map g_ItemCache; + std::map, nlohmann::json> g_ItemCache; // by language (GameText) and LOT std::map> g_ItemSetsByLot; // lot -> set ids containing it bool g_ItemSetsLoaded = false; @@ -95,9 +95,9 @@ namespace { return data; } + // In the viewer's language (GameText.h) std::string Phrase(const std::string& key) { - const auto& phrase = Locale::GetPhrase(key); - return phrase; + return GameText::Phrase(key); } struct Stats { @@ -243,7 +243,7 @@ namespace ClientAssets { } std::string ItemName(LOT lot) { - const auto& localized = Locale::GetPhrase("Objects_" + std::to_string(lot) + "_name"); + const auto& localized = GameText::Phrase(GameText::Key("Objects", lot)); if (!localized.empty()) return localized; const auto& table = ObjectNameTable(); const auto it = table.find(lot); @@ -252,7 +252,8 @@ namespace ClientAssets { } nlohmann::json ItemInfo(LOT lot) { - if (const auto cached = g_ItemCache.find(lot); cached != g_ItemCache.end()) return cached->second; + const auto cacheKey = std::make_pair(GameText::Language(), lot); + if (const auto cached = g_ItemCache.find(cacheKey); cached != g_ItemCache.end()) return cached->second; nlohmann::json info{ {"lot", lot}, {"name", ItemName(lot)} }; @@ -284,7 +285,7 @@ namespace ClientAssets { info["stats"] = StatsJson(stats); info["set"] = ItemSet(lot); - g_ItemCache[lot] = info; + g_ItemCache[cacheKey] = info; return info; } diff --git a/dDashboardServer/routes/DashboardRoutes.cpp b/dDashboardServer/routes/DashboardRoutes.cpp index 7eac742d8..b27e7c973 100644 --- a/dDashboardServer/routes/DashboardRoutes.cpp +++ b/dDashboardServer/routes/DashboardRoutes.cpp @@ -1,4 +1,5 @@ #include "DashboardRoutes.h" +#include "GameText.h" #include "RouteUtils.h" #include "Permissions.h" #include "EmailService.h" @@ -14,7 +15,6 @@ #include "Game.h" #include "Database.h" #include "Logger.h" -#include "Locale.h" #include "GeneralUtils.h" #include "CDClientDatabase.h" #include "tinyxml2.h" @@ -32,35 +32,12 @@ using namespace RouteUtils; namespace { - std::string ZoneName(int zoneId) { - if (zoneId == 0) return "Character Select"; - const auto& name = Locale::GetPhrase("ZoneTable_" + std::to_string(zoneId) + "_DisplayDescription"); - return name.empty() ? "Zone " + std::to_string(zoneId) : name; - } - - // Built once (worker threads read it too, so never lazily twice) - const nlohmann::json& GetZoneNamesJson() { - static const nlohmann::json names = [] { - nlohmann::json names; - names["0"] = "Character Select"; - for (const auto& key : Locale::GetPhraseIdsWithPrefix("ZoneTable_")) { - if (key.find("_DisplayDescription") == std::string::npos) continue; - const auto start = std::string("ZoneTable_").length(); - const auto end = key.find("_DisplayDescription"); - const auto& name = Locale::GetPhrase(key); - if (!name.empty()) names[key.substr(start, end - start)] = name; - } - return names; - }(); - return names; - } - nlohmann::json ParseCharacterXml(const std::string& xml) { // Every value the template reads has a default, since inja fails on missing variables and // character XML may lack sections (e.g. a character that never finished loading in) nlohmann::json stats{ {"coins", 0}, {"universe_score", 0}, {"reputation", 0}, {"gm_level", 0}, {"level", 0}, - {"last_zone_id", 0}, {"last_zone", ZoneName(0)}, + {"last_zone_id", 0}, {"last_zone", GameText::ZoneName(0)}, {"health", 0}, {"max_health", 0}, {"armor", 0}, {"max_armor", 0}, {"imagination", 0}, {"max_imagination", 0}, {"missions_completed", 0}, {"missions_active", 0}, {"total_items", 0} }; @@ -79,7 +56,7 @@ namespace { auto lastZone = charEl->IntAttribute("lwid", 0); stats["last_zone_id"] = lastZone; - stats["last_zone"] = ZoneName(lastZone); + stats["last_zone"] = GameText::ZoneName(lastZone); } auto* lvlEl = obj->FirstChildElement("lvl"); @@ -117,7 +94,7 @@ namespace { std::map> clones; for (auto* l = vl->FirstChildElement("l"); l; l = l->NextSiblingElement("l")) clones[l->IntAttribute("id", 0)].insert(l->IntAttribute("cid", 0)); nlohmann::json visited = nlohmann::json::array(); - for (const auto& [zid, cids] : clones) visited.push_back({ {"id", zid}, {"name", ZoneName(zid)}, {"clones", cids.size()} }); + for (const auto& [zid, cids] : clones) visited.push_back({ {"id", zid}, {"name", GameText::ZoneName(zid)}, {"clones", cids.size()} }); stats["visited_zones"] = visited; } @@ -127,7 +104,7 @@ namespace { for (auto* s = zs->FirstChildElement("s"); s; s = s->NextSiblingElement("s")) { auto zid = s->IntAttribute("map", 0); zoneStats.push_back({ - {"map_id", zid}, {"name", ZoneName(zid)}, + {"map_id", zid}, {"name", GameText::ZoneName(zid)}, {"achievements", s->IntAttribute("ac", 0)}, {"coins_collected", s->IntAttribute("cc", 0)}, {"enemies_smashed", s->IntAttribute("es", 0)} @@ -207,10 +184,6 @@ namespace { } } -const nlohmann::json& ZoneNames() { - return GetZoneNamesJson(); -} - namespace { constexpr size_t LOG_TAIL_BYTES = 256 * 1024; constexpr size_t LOG_TAIL_LINES = 500; @@ -430,7 +403,7 @@ void RegisterDashboardRoutes() { const std::string selection = report.value("selection", std::string{}); report["selection_text"] = selection; if (selection.starts_with("%[") && selection.ends_with("]")) { - const auto& phrase = Locale::GetPhrase(selection.substr(2, selection.size() - 3)); + const auto& phrase = GameText::Phrase(selection.substr(2, selection.size() - 3)); if (!phrase.empty()) report["selection_text"] = phrase; } RenderPage(reply, context, "bug_report-view.jinja2", "bug_reports", { {"report", report} }); @@ -453,7 +426,7 @@ void RegisterDashboardRoutes() { SimplePage("/tasks", Perm("tasks_view"), "tasks.jinja2", "tasks", "Scheduled tasks: schedules, runs and logs"); Route(eHTTPMethod::GET, "/api/zones", Perm("characters_rescue"), "Every zone with its name, for pickers: {zones: [{id, name}]}", [](HTTPReply& reply, const HTTPContext&) { nlohmann::json zones = nlohmann::json::array(); - for (const auto& [id, name] : GetZoneNamesJson().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.push_back({ {"id", *zone}, {"name", name} }); } std::sort(zones.begin(), zones.end(), [](const auto& a, const auto& b) { return a["id"].template get() < b["id"].template get(); }); @@ -492,7 +465,7 @@ void RegisterDashboardRoutes() { // Logs Route(eHTTPMethod::GET, "/activity_log", Perm("logs_activity"), "Activity log", [](HTTPReply& reply, const HTTPContext& context) { - RenderPage(reply, context, "activity_log.jinja2", "activity_log", { {"zone_names", GetZoneNamesJson()} }); + RenderPage(reply, context, "activity_log.jinja2", "activity_log", { {"zone_names", GameText::ZoneNames()} }); }); SimplePage("/command_log", Perm("logs_command"), "command_log.jinja2", "command_log", "Command log"); SimplePage("/audit_log", Perm("logs_audit"), "audit_log.jinja2", "audit_log", "Audit log"); @@ -516,7 +489,7 @@ void RegisterDashboardRoutes() { if (const auto zone = GeneralUtils::TryParse(server.substr(12, server.find('_', 12) - 12))) zoneIds.insert(*zone); } nlohmann::json zones = nlohmann::json::array(); - for (const auto zone : zoneIds) zones.push_back({ {"id", zone}, {"name", GetZoneNamesJson().value(std::to_string(zone), "")} }); + for (const auto zone : zoneIds) zones.push_back({ {"id", zone}, {"name", GameText::ZoneNames().value(std::to_string(zone), "")} }); nlohmann::json data{ {"servers", servers}, {"server_types", types}, {"selected_server", selected}, {"files", nlohmann::json::array()}, {"bundle_zones", zones} }; if (!selected.empty()) { const auto& files = logs.at(selected); diff --git a/dDashboardServer/routes/DashboardRoutes.h b/dDashboardServer/routes/DashboardRoutes.h index 3d23e5d76..8eebbac29 100644 --- a/dDashboardServer/routes/DashboardRoutes.h +++ b/dDashboardServer/routes/DashboardRoutes.h @@ -5,6 +5,3 @@ class HTTPContext; void RegisterDashboardRoutes(); - -// Zone ID (as a string) -> display name, from the client's locale -const nlohmann::json& ZoneNames(); diff --git a/dDashboardServer/routes/EconomyPlaces.cpp b/dDashboardServer/routes/EconomyPlaces.cpp index 0160fa214..8988aa11b 100644 --- a/dDashboardServer/routes/EconomyPlaces.cpp +++ b/dDashboardServer/routes/EconomyPlaces.cpp @@ -1,4 +1,5 @@ #include "EconomyPlaces.h" +#include "GameText.h" #include #include @@ -47,10 +48,7 @@ namespace EconomyPlaces { } std::string ZoneName(uint32_t zone) { - const auto& names = ZoneNames(); - const auto key = std::to_string(zone); - if (names.contains(key) && names[key].is_string()) return names[key].get(); - return "Zone " + key; + return GameText::ZoneName(zone); } std::optional Parse(std::string_view place) { diff --git a/dDashboardServer/routes/EventsCalendar.cpp b/dDashboardServer/routes/EventsCalendar.cpp index c9b2356c1..ef4550a52 100644 --- a/dDashboardServer/routes/EventsCalendar.cpp +++ b/dDashboardServer/routes/EventsCalendar.cpp @@ -1,4 +1,5 @@ #include "EventsCalendar.h" +#include "GameText.h" #include "EventSchedule.h" #include "AnnouncementSchedule.h" #include "LevelGating.h" @@ -156,22 +157,17 @@ namespace { GeneralUtils::TryParse(Game::config->GetValue("version_minor")).value_or(ClientVersion::minor) }; } - std::string ZoneName(uint32_t zone) { - const auto& names = ZoneNames(); - const auto key = std::to_string(zone); - return names.contains(key) ? names[key].get() : "Zone " + key; - } std::string ZoneList(const std::vector& zones) { if (zones.empty()) return "every world"; std::string text; - for (const auto zone : zones) text += (text.empty() ? "" : ", ") + ZoneName(zone); + for (const auto zone : zones) text += (text.empty() ? "" : ", ") + GameText::ZoneName(zone); return text; } nlohmann::json ZoneOptions() { std::vector> zones; - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.emplace_back(*zone, name.get()); } std::sort(zones.begin(), zones.end()); @@ -186,14 +182,14 @@ namespace { std::lock_guard lock(ServerState::g_StatusMutex); for (const auto& world : ServerState::g_WorldInstances) { if (!zones.contains(world.mapID)) continue; - worlds.push_back({ {"zone", world.mapID}, {"zoneName", ZoneName(world.mapID)}, {"instance", world.instanceID}, {"clone", world.cloneID}, {"players", world.players} }); + worlds.push_back({ {"zone", world.mapID}, {"zoneName", GameText::ZoneName(world.mapID)}, {"instance", world.instanceID}, {"clone", world.cloneID}, {"players", world.players} }); } return worlds; } nlohmann::json FeatureJson(const std::string& name, const Feature& feature) { nlohmann::json zones = nlohmann::json::array(); - for (const auto& [zone, objects] : feature.zones) zones.push_back({ {"id", zone}, {"name", ZoneName(zone)}, {"objects", objects} }); + for (const auto& [zone, objects] : feature.zones) zones.push_back({ {"id", zone}, {"name", GameText::ZoneName(zone)}, {"objects", objects} }); nlohmann::json json{ {"name", name}, {"description", feature.description}, {"zones", zones}, {"version", nullptr}, {"unlocked", false} }; if (feature.version) { const auto& [major, current, minor] = *feature.version; @@ -214,7 +210,7 @@ namespace { const auto zones = ZonesOf(feature); if (zones.empty()) return "No zone has objects gated on it; players see client-side changes the next time they log in."; std::string names; - for (const auto& [zone, objects] : zones) names += (names.empty() ? "" : ", ") + ZoneName(zone); + for (const auto& [zone, objects] : zones) names += (names.empty() ? "" : ", ") + GameText::ZoneName(zone); return "Worlds of " + names + " that are already running keep what they loaded until they are restarted; new logins get the change straight away."; } @@ -608,7 +604,7 @@ namespace { } nlohmann::json zones = nlohmann::json::array(); if (config.contains("zones") && config["zones"].is_array()) { - const auto& known = ZoneNames(); + const auto& known = GameText::ZoneNames(); for (const auto& zone : config["zones"]) { if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get()))) { error = "Pick zones from the list"; return std::nullopt; } if (std::find(zones.begin(), zones.end(), zone) == zones.end()) zones.push_back(zone); diff --git a/dDashboardServer/routes/FeaturedProperties.cpp b/dDashboardServer/routes/FeaturedProperties.cpp index dcc7632c1..ba377a2f8 100644 --- a/dDashboardServer/routes/FeaturedProperties.cpp +++ b/dDashboardServer/routes/FeaturedProperties.cpp @@ -1,4 +1,5 @@ #include "FeaturedProperties.h" +#include "GameText.h" #include #include @@ -59,11 +60,6 @@ namespace { return mapId; } - std::string ZoneName(uint32_t zoneId) { - const auto& zones = ZoneNames(); - const auto key = std::to_string(zoneId); - return zones.contains(key) && zones[key].is_string() ? zones[key].get() : "Zone " + key; - } nlohmann::json PropertyJson(const IProperty::Info& info, const std::string& ownerName) { return { @@ -75,7 +71,7 @@ namespace { {"performance_cost", info.performanceCost}, {"last_updated", info.lastUpdatedTime}, {"zone_id", info.zoneId}, - {"zone_name", ZoneName(info.zoneId)}, + {"zone_name", GameText::ZoneName(info.zoneId)}, }; } @@ -144,10 +140,10 @@ namespace { nlohmann::json entry{ {"template_id", slot.templateId}, {"map_id", slot.mapId}, - {"zone_name", ZoneName(slot.mapId)}, + {"zone_name", GameText::ZoneName(slot.mapId)}, {"spawn_name", slot.spawnName}, {"location", choice.mapId}, - {"location_name", ZoneName(choice.mapId)}, + {"location_name", GameText::ZoneName(choice.mapId)}, {"mode", HotPropertySlots::ModeName(choice.mode)}, {"property_id", nullptr}, {"picked", nullptr}, @@ -169,7 +165,7 @@ namespace { } nlohmann::json worlds = nlohmann::json::array(); - for (const auto world : news.worlds) worlds.push_back({ {"map_id", world}, {"zone_name", ZoneName(world)} }); + for (const auto world : news.worlds) worlds.push_back({ {"map_id", world}, {"zone_name", GameText::ZoneName(world)} }); return { {"full_auto", settings.fullAuto}, {"settings_updated_at", settings.updatedAt}, @@ -252,12 +248,12 @@ void FeaturedProperties::RegisterRoutes() { : idValue.is_number_integer() ? std::optional(idValue.get()) : std::nullopt; if (!propertyId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "property_id is required to pick a property"); const auto picked = Featurable(*propertyId, *location); - if (!picked) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Only an approved public property in " + ZoneName(*location) + " can be picked there"); + if (!picked) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Only an approved public property in " + GameText::ZoneName(*location) + " can be picked there"); for (const auto& other : Database::Get()->GetFeaturedPropertySlots()) { if (other.templateId == slot->templateId || HotPropertySlots::ModeFromInt(other.mode) != eMode::PICKED || other.propertyId != *propertyId) continue; const auto* otherSlot = FindSlot(other.templateId); if (!otherSlot) continue; - return JsonError(reply, eHTTPStatusCode::CONFLICT, "That property is already picked for the " + ZoneName(otherSlot->mapId) + " slot"); + return JsonError(reply, eHTTPStatusCode::CONFLICT, "That property is already picked for the " + GameText::ZoneName(otherSlot->mapId) + " slot"); } const auto& [info, ownerName] = *picked; row.propertyId = *propertyId; @@ -266,7 +262,7 @@ void FeaturedProperties::RegisterRoutes() { } Database::Get()->SetFeaturedPropertySlot(row); - Audit(context, "feature_property", "Top properties slot " + ZoneName(slot->mapId) + " (template " + std::to_string(slot->templateId) + "): " + ZoneName(*location) + ", " + what, target); + Audit(context, "feature_property", "Top properties slot " + GameText::ZoneName(slot->mapId) + " (template " + std::to_string(slot->templateId) + "): " + GameText::ZoneName(*location) + ", " + what, target); BroadcastTableChanged("featured_properties", std::to_string(slot->templateId)); JsonSuccess(reply, StateJson()); }); diff --git a/dDashboardServer/routes/Inspector.cpp b/dDashboardServer/routes/Inspector.cpp index 624982989..a28fdfdba 100644 --- a/dDashboardServer/routes/Inspector.cpp +++ b/dDashboardServer/routes/Inspector.cpp @@ -1,4 +1,5 @@ #include "Inspector.h" +#include "GameText.h" #include "MasterPackets.h" #include @@ -114,9 +115,7 @@ namespace { } std::string ZoneName(uint32_t zoneId) { - const auto& zones = ZoneNames(); - const auto zone = std::to_string(zoneId); - return zoneId == 0 ? "" : zones.contains(zone) ? zones[zone].get() : "Zone " + zone; + return zoneId == 0 ? "" : GameText::ZoneName(zoneId); } // A session for the page; `state` is the live state, or "ended" for one only in the database diff --git a/dDashboardServer/routes/InstanceLoad.cpp b/dDashboardServer/routes/InstanceLoad.cpp index a3491e1e4..12656cb48 100644 --- a/dDashboardServer/routes/InstanceLoad.cpp +++ b/dDashboardServer/routes/InstanceLoad.cpp @@ -1,4 +1,5 @@ #include "InstanceLoad.h" +#include "GameText.h" #include "MasterPackets.h" #include "InstanceLimits.h" #include "DashboardRoutes.h" @@ -50,11 +51,6 @@ namespace { return it == ClientCaps().end() ? InstanceLimits::Caps{ 8, 12 } : it->second; } - std::string ZoneName(uint32_t zone) { - const auto& names = ZoneNames(); - const auto key = std::to_string(zone); - return names.contains(key) ? names[key].get() : "Zone " + key; - } nlohmann::json OptionalJson(const std::optional& value) { return value ? nlohmann::json(*value) : nlohmann::json(nullptr); @@ -114,7 +110,7 @@ namespace InstanceLoad { const auto caps = ClientCapsOf(zone); nlohmann::json points = nlohmann::json::array(); for (const auto& s : samples) points.push_back({ {"time", s.time}, {"instance", s.instanceId}, {"clone", s.cloneId}, {"players", s.players} }); - json["zone"] = { {"id", zone}, {"name", ZoneName(zone)} }; + json["zone"] = { {"id", zone}, {"name", GameText::ZoneName(zone)} }; json["samples"] = points; json["clientCaps"] = { {"soft", caps.soft}, {"hard", caps.hard} }; } else { @@ -135,7 +131,7 @@ namespace InstanceLoad { } nlohmann::json list = nlohmann::json::array(); for (const auto& [id, summary] : zones) { - list.push_back({ {"id", id}, {"name", ZoneName(id)}, {"busiest", summary.busiest}, {"instances", summary.instances}, {"players", summary.players} }); + list.push_back({ {"id", id}, {"name", GameText::ZoneName(id)}, {"busiest", summary.busiest}, {"instances", summary.instances}, {"players", summary.players} }); } json["zones"] = list; } @@ -153,7 +149,7 @@ namespace InstanceLoad { const auto it = limits.find(id); const auto* limit = it == limits.end() ? nullptr : &it->second; const auto effective = InstanceLimits::Effective(limit ? limit->softCap : std::nullopt, limit ? limit->hardCap : std::nullopt, caps); - zones.push_back({ {"id", id}, {"name", ZoneName(id)}, {"clientSoft", caps.soft}, {"clientHard", caps.hard}, + zones.push_back({ {"id", id}, {"name", GameText::ZoneName(id)}, {"clientSoft", caps.soft}, {"clientHard", caps.hard}, {"softCap", limit ? OptionalJson(limit->softCap) : nullptr}, {"hardCap", limit ? OptionalJson(limit->hardCap) : nullptr}, {"spare", limit ? limit->spareInstances : 0}, {"soft", effective.soft}, {"hard", effective.hard}, {"updatedAt", limit ? limit->updatedAt : 0}, {"updatedBy", limit ? limit->updatedBy : ""} }); @@ -193,7 +189,7 @@ namespace InstanceLoad { else Database::Get()->SetZoneLimit(limit); const auto effective = InstanceLimits::Effective(limit.softCap, limit.hardCap, client); const auto text = [](const std::optional& value) { return value ? std::to_string(*value) : std::string("client's"); }; - Audit(context, "set_zone_limits", ZoneName(zone) + " (" + std::to_string(zone) + "): " + (reset ? std::string("back to the client's caps, no spare instances") : + Audit(context, "set_zone_limits", GameText::ZoneName(zone) + " (" + std::to_string(zone) + "): " + (reset ? std::string("back to the client's caps, no spare instances") : "soft cap " + text(limit.softCap) + ", hard cap " + text(limit.hardCap) + ", " + std::to_string(limit.spareInstances) + " spare instance(s)")); BroadcastTableChanged("zone_limits", std::to_string(zone)); const bool told = TellMaster(); diff --git a/dDashboardServer/routes/LeaderboardRoutes.cpp b/dDashboardServer/routes/LeaderboardRoutes.cpp index 70518931a..854274887 100644 --- a/dDashboardServer/routes/LeaderboardRoutes.cpp +++ b/dDashboardServer/routes/LeaderboardRoutes.cpp @@ -1,4 +1,5 @@ #include "LeaderboardRoutes.h" +#include "GameText.h" #include #include @@ -8,7 +9,7 @@ #include "WSRoutes.h" #include "CDClientDatabase.h" #include "Database.h" -#include "Locale.h" +#include "GameLabels.h" #include "Game.h" #include "dConfig.h" #include "GeneralUtils.h" @@ -27,7 +28,7 @@ namespace { }; std::string ActivityName(uint32_t id) { - std::string name = Locale::GetPhrase("Activities_" + std::to_string(id) + "_ActivityName"); + std::string name = GameText::Phrase(GameText::Key("Activities", id, "ActivityName")); if (name.empty()) return "Activity " + std::to_string(id); // The client's locale has Windows-1252 dashes stored as U+0096 ("Foot Race \u0096 Nimbus Station"); make them real dashes for (size_t at = name.find("\xC2\x96"); at != std::string::npos; at = name.find("\xC2\x96", at)) name.replace(at, 2, "\xE2\x80\x93"); @@ -57,10 +58,9 @@ namespace { const char* hint = ""; }; - // The client's column header for a locale phrase ("RACE_BESTTIME" -> "Best Time"); fallback when the locale lacks it - std::string Label(const char* phrase, const char* fallback) { - const auto& text = Locale::GetPhrase(phrase); - return text.empty() ? fallback : text; + // The client's column header for a locale phrase ("RACE_BESTTIME" -> "Best Time"); the key as words when the locale lacks it + std::string Label(const char* phrase) { + return GameText::Text(phrase, GameLabels::Words(phrase)); } /** @@ -71,26 +71,26 @@ namespace { */ nlohmann::json Columns(eLeaderboardType type) { using enum eLeaderboardType; - std::vector> columns; // column, English fallback + std::vector columns; switch (type) { - case ShootingGallery: columns = { { {"primary", "SCORE", "number"}, "Score" }, { {"secondary", "UI_SG_STREAK", "number"}, "Streak" }, { {"tertiary", "UI_SG_ACCURACY", "percent"}, "Accuracy" } }; break; - case Racing: columns = { { {"primary", "RACE_BESTTIME", "laptime", "Less is better"}, "Best Time" }, { {"secondary", "RACE_BESTLAP", "laptime"}, "Best Lap" }, { {"wins", "RACE_NUMWINS", "number"}, "Wins" } }; break; - case MonumentRace: columns = { { {"primary", "TIME", "time", "Time taken: less is better"}, "Time" } }; break; + case ShootingGallery: columns = { {"primary", "SCORE", "number"}, {"secondary", "UI_SG_STREAK", "number"}, {"tertiary", "UI_SG_ACCURACY", "percent"} }; break; + case Racing: columns = { {"primary", "RACE_BESTTIME", "laptime", "Less is better"}, {"secondary", "RACE_BESTLAP", "laptime"}, {"wins", "RACE_NUMWINS", "number"} }; break; + case MonumentRace: columns = { {"primary", "TIME", "time", "Time taken: less is better"} }; break; // The foot race scripts save the time left on the race's countdown when the player finishes - case FootRace: columns = { { {"primary", "TIME", "time", "Time left on the clock at the finish: more is better"}, "Time" } }; break; - case UnusedLeaderboard4: columns = { { {"primary", "POINTS", "number"}, "Points" } }; break; - case Survival: columns = { { {"secondary", "TIME", "time"}, "Time" }, { {"primary", "POINTS", "number"}, "Points" } }; break; - case SurvivalNS: columns = { { {"primary", "WAVE", "number"}, "Wave" }, { {"secondary", "TIME", "time", "Less is better for the same wave"}, "Time" } }; break; - case Donations: columns = { { {"primary", "DONATIONS", "number"}, "Donations" } }; break; - default: columns = { { {"primary", "SCORE", "number"}, "Score" } }; break; + case FootRace: columns = { {"primary", "TIME", "time", "Time left on the clock at the finish: more is better"} }; break; + case UnusedLeaderboard4: columns = { {"primary", "POINTS", "number"} }; break; + case Survival: columns = { {"secondary", "TIME", "time"}, {"primary", "POINTS", "number"} }; break; + case SurvivalNS: columns = { {"primary", "WAVE", "number"}, {"secondary", "TIME", "time", "Less is better for the same wave"} }; break; + case Donations: columns = { {"primary", "DONATIONS", "number"} }; break; + default: columns = { {"primary", "SCORE", "number"} }; break; } - if (type != Donations) columns.push_back({ {"played", "TIMES_PLAYED", "number"}, "Times Played" }); + if (type != Donations) columns.push_back({ "played", "TIMES_PLAYED", "number" }); // The score the order goes by first: survival ranks by time with classic_survival_scoring (as GetAgsLeaderboard does) const bool byTime = type == Survival && Game::config && Game::config->GetValue("classic_survival_scoring") == "1"; const std::string ranksBy = byTime ? "secondary" : "primary"; nlohmann::json out = nlohmann::json::array(); - for (const auto& [column, fallback] : columns) { - out.push_back({ {"key", column.key}, {"label", Label(column.phrase, fallback)}, {"format", column.format}, {"hint", column.hint}, {"ranks", column.key == ranksBy} }); + for (const auto& column : columns) { + out.push_back({ {"key", column.key}, {"label", Label(column.phrase)}, {"format", column.format}, {"hint", column.hint}, {"ranks", column.key == ranksBy} }); } return out; } diff --git a/dDashboardServer/routes/LiveEventRoutes.cpp b/dDashboardServer/routes/LiveEventRoutes.cpp index cca40ba29..e37cdff2c 100644 --- a/dDashboardServer/routes/LiveEventRoutes.cpp +++ b/dDashboardServer/routes/LiveEventRoutes.cpp @@ -1,4 +1,5 @@ #include "LiveEventRoutes.h" +#include "GameText.h" #include "LiveWorld.h" #include "DashboardRoutes.h" #include "ClientAssets.h" @@ -48,7 +49,7 @@ namespace { std::string ZoneList(const std::vector& zones) { if (zones.empty()) return "every world"; std::string text; - const auto& names = ZoneNames(); + const auto& names = GameText::ZoneNames(); for (const auto zone : zones) { const auto key = std::to_string(zone); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get() : "zone " + key); @@ -58,7 +59,7 @@ namespace { nlohmann::json ZoneOptions() { std::vector> zones; - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.emplace_back(*zone, name.get()); } std::sort(zones.begin(), zones.end()); @@ -277,7 +278,7 @@ namespace { if (event.message.size() > MAX_MESSAGE) { error = "The message is up to 300 characters"; return false; } event.zones.clear(); if (body.contains("zones") && body["zones"].is_array()) { - const auto& known = ZoneNames(); + const auto& known = GameText::ZoneNames(); for (const auto& zone : body["zones"]) { if (!zone.is_number_unsigned() || zone.get() == 0 || !known.contains(std::to_string(zone.get()))) { error = "Pick zones from the list"; return false; } if (std::find(event.zones.begin(), event.zones.end(), zone.get()) == event.zones.end()) event.zones.push_back(zone.get()); diff --git a/dDashboardServer/routes/LiveUpdateRoutes.cpp b/dDashboardServer/routes/LiveUpdateRoutes.cpp index a2c049a0b..c039a28b7 100644 --- a/dDashboardServer/routes/LiveUpdateRoutes.cpp +++ b/dDashboardServer/routes/LiveUpdateRoutes.cpp @@ -1,4 +1,5 @@ #include "LiveUpdateRoutes.h" +#include "GameText.h" #include #include @@ -29,7 +30,6 @@ namespace { nlohmann::json ToJson(const LiveUpdateStatus& status) { using namespace LiveUpdate; - const auto& zones = ZoneNames(); nlohmann::json units = nlohmann::json::array(); size_t done = 0, failed = 0; for (const auto& unit : status.units) { @@ -37,9 +37,8 @@ namespace { if (unit.state == eUnitState::FAILED) failed++; nlohmann::json row{ {"kind", KindName(unit.kind)}, {"state", StateName(unit.state)}, {"message", unit.message} }; if (unit.kind == eUnitKind::WORLD) { - const auto zone = std::to_string(unit.zoneId); row["zone"] = unit.zoneId; - row["zoneName"] = unit.zoneId == 0 ? "Character Select" : zones.contains(zone) ? zones[zone].get() : "Zone " + zone; + row["zoneName"] = GameText::ZoneName(unit.zoneId); row["instance"] = unit.instanceId; row["clone"] = unit.cloneId; row["replacement"] = unit.replacement; diff --git a/dDashboardServer/routes/LiveWorld.cpp b/dDashboardServer/routes/LiveWorld.cpp index 33ea6811b..984ad7baa 100644 --- a/dDashboardServer/routes/LiveWorld.cpp +++ b/dDashboardServer/routes/LiveWorld.cpp @@ -1,4 +1,5 @@ #include "LiveWorld.h" +#include "GameText.h" #include "MasterPackets.h" #include "WorldView.h" #include "Permissions.h" @@ -250,13 +251,13 @@ namespace LiveWorld { nlohmann::json worlds = nlohmann::json::array(); std::lock_guard lock(ServerState::g_StatusMutex); for (const auto& w : ServerState::g_WorldInstances) { - worlds.push_back({ {"mapID", w.mapID}, {"zoneName", w.zoneName}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", w.players}, + worlds.push_back({ {"mapID", w.mapID}, {"zoneName", GameText::ZoneName(w.mapID)}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", w.players}, {"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName}, {"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} }); } // Launched but not connected yet, or shutting down for (const auto& w : ServerState::g_PendingWorlds) { - worlds.push_back({ {"mapID", w.mapID}, {"zoneName", w.zoneName}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", 0}, + worlds.push_back({ {"mapID", w.mapID}, {"zoneName", GameText::ZoneName(w.mapID)}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", 0}, {"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName}, {"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} }); } @@ -266,10 +267,8 @@ namespace LiveWorld { Route(eHTTPMethod::GET, "/api/players/online", Perm("players_view"), "Everyone online: character, account, GM level, world and position", [](HTTPReply& reply, const HTTPContext&) { auto players = PlayersJson(); - const auto& zones = ZoneNames(); for (auto& player : players) { - const auto zone = std::to_string(player.value("zone", 0)); - player["zone_name"] = zones.contains(zone) ? zones[zone].get() : "Zone " + zone; + player["zone_name"] = GameText::ZoneName(player.value("zone", 0u)); const auto id = GeneralUtils::TryParse(player.value("id", std::string{})).value_or(0); if (const auto info = Database::Get()->GetCharacterInfo(id)) { player["account_id"] = info->accountId; diff --git a/dDashboardServer/routes/MissionTools.cpp b/dDashboardServer/routes/MissionTools.cpp index a5cc21c88..ad40d58f9 100644 --- a/dDashboardServer/routes/MissionTools.cpp +++ b/dDashboardServer/routes/MissionTools.cpp @@ -1,4 +1,5 @@ #include "MissionTools.h" +#include "GameText.h" #include #include @@ -12,7 +13,6 @@ #include "WSRoutes.h" #include "CDClientDatabase.h" #include "Database.h" -#include "Locale.h" #include "GeneralUtils.h" #include "eHTTPMethod.h" @@ -72,12 +72,12 @@ namespace MissionCatalog { } std::string Name(uint32_t id) { - const auto& name = Locale::GetPhrase("Missions_" + std::to_string(id) + "_name"); + const auto& name = GameText::Phrase(GameText::Key("Missions", id)); return name.empty() ? "Mission " + std::to_string(id) : name; } std::string TaskText(uint32_t uid) { - return Locale::GetPhrase("MissionTasks_" + std::to_string(uid) + "_description"); + return GameText::Phrase(GameText::Key("MissionTasks", uid, "description")); } } diff --git a/dDashboardServer/routes/ModerationTools.cpp b/dDashboardServer/routes/ModerationTools.cpp index 359f4e802..517288fad 100644 --- a/dDashboardServer/routes/ModerationTools.cpp +++ b/dDashboardServer/routes/ModerationTools.cpp @@ -1,4 +1,5 @@ #include "ModerationTools.h" +#include "GameText.h" #include "ChatFilterWords.h" #include @@ -73,7 +74,7 @@ namespace { nlohmann::json ReportJson(const IModeration::PlayerReport& r, Names& names) { const auto kind = magic_enum::enum_cast(r.kind); const auto status = static_cast(r.status); - const auto& zones = ZoneNames(); + const auto& zones = GameText::ZoneNames(); const auto zone = std::to_string(r.zoneId); std::string propertyName; if (r.propertyId) { @@ -387,7 +388,7 @@ namespace { query.blockedOnly = allowed; query.newestFirst = true; query.limit = CHECK_MESSAGES; - const auto& zones = ZoneNames(); + const auto& zones = GameText::ZoneNames(); nlohmann::json messages = nlohmann::json::array(); uint32_t searched = 0; for (const auto& m : Database::Get()->GetChatMessages(query)) { diff --git a/dDashboardServer/routes/ModeratorHelper.cpp b/dDashboardServer/routes/ModeratorHelper.cpp index 32e99e983..5e27ac369 100644 --- a/dDashboardServer/routes/ModeratorHelper.cpp +++ b/dDashboardServer/routes/ModeratorHelper.cpp @@ -1,4 +1,5 @@ #include "ModeratorHelper.h" +#include "GameText.h" #include #include @@ -197,11 +198,6 @@ namespace { return info ? info->name : ""; } - std::string ZoneName(uint32_t zoneId) { - const auto& zones = ZoneNames(); - const auto key = std::to_string(zoneId); - return zones.contains(key) && zones[key].is_string() ? zones[key].get() : "Zone " + key; - } ModeratorPrompt::ChatLine Line(const IChatLog::ChatMessage& m, uint64_t subjectId, LWOOBJID subjectSender) { return { m.time, m.channel, m.senderName, m.recipientName, m.message, m.blocked, subjectId ? m.id == subjectId : m.senderId == subjectSender }; @@ -291,7 +287,7 @@ namespace { item["sent"] = ModeratorPrompt::FormatTime(report->createdAt); item["reporter"] = CharacterName(report->reporterId); item["reported_player"] = report->targetCharacterId ? CharacterName(report->targetCharacterId) : "unknown"; - if (report->zoneId) item["where"] = ZoneName(report->zoneId); + if (report->zoneId) item["where"] = GameText::ZoneName(report->zoneId); if (report->objectLot && (!kind || *kind != ePlayerReportKind::PLAYER)) item["model"] = ClientAssets::ItemName(report->objectLot); if (report->propertyId) { if (const auto property = Database::Get()->GetPropertyInfo(report->propertyId)) item["property"] = property->name; @@ -339,7 +335,7 @@ namespace { item["sender"] = message->senderName; item["channel"] = message->channel; if (!message->recipientName.empty()) item["to"] = message->recipientName; - if (message->zoneId) item["where"] = ZoneName(message->zoneId); + if (message->zoneId) item["where"] = GameText::ZoneName(message->zoneId); item["when"] = ModeratorPrompt::FormatTime(message->time); item["message"] = message->message; item["stopped_by_filter"] = message->blocked; diff --git a/dDashboardServer/routes/PublicRoutes.cpp b/dDashboardServer/routes/PublicRoutes.cpp index ace449e7b..836f0b8a3 100644 --- a/dDashboardServer/routes/PublicRoutes.cpp +++ b/dDashboardServer/routes/PublicRoutes.cpp @@ -1,4 +1,5 @@ #include "PublicRoutes.h" +#include "GameText.h" #include #include @@ -79,7 +80,7 @@ namespace { std::lock_guard lock(ServerState::g_StatusMutex); loginUp = ServerState::g_AuthStatus.online; chatUp = ServerState::g_ChatStatus.online; - for (const auto& world : ServerState::g_WorldInstances) worlds.push_back({ world.mapID, world.zoneName, world.players }); + for (const auto& world : ServerState::g_WorldInstances) worlds.push_back({ world.mapID, GameText::ZoneName(world.mapID), world.players }); } nlohmann::json status{ {"name", ServerName()}, {"online", loginUp}, {"generated_at", now}, {"refresh_seconds", CacheSeconds()} }; diff --git a/dDashboardServer/routes/ReportRoutes.cpp b/dDashboardServer/routes/ReportRoutes.cpp index 58a6f9ccb..5759ad893 100644 --- a/dDashboardServer/routes/ReportRoutes.cpp +++ b/dDashboardServer/routes/ReportRoutes.cpp @@ -1,4 +1,5 @@ #include "ReportRoutes.h" +#include "GameText.h" #include "OnceCache.h" #include #include @@ -385,8 +386,8 @@ namespace { return stats; } + // Where a zone's files are (its name comes from the locale: GameText::ZoneName) struct ZoneInfo { - std::string name; std::string luzPath; // relative to res/maps }; @@ -414,11 +415,10 @@ namespace { std::map ReadZoneTable() { std::map zones; try { - auto result = CDClientDatabase::ExecuteQuery("SELECT zoneID, zoneName, DisplayDescription FROM ZoneTable;"); + auto result = CDClientDatabase::ExecuteQuery("SELECT zoneID, zoneName FROM ZoneTable;"); for (; !result.eof(); result.nextRow()) { - const std::string description = result.getStringField("DisplayDescription", ""); const auto zoneId = static_cast(result.getIntField("zoneID", 0)); - zones.try_emplace(zoneId, ZoneInfo{ description.empty() ? "Zone " + std::to_string(zoneId) : description, result.getStringField("zoneName", "") }); + zones.try_emplace(zoneId, ZoneInfo{ result.getStringField("zoneName", "") }); } } catch (const std::exception& ex) { LOG("Could not read the zone table: %s", ex.what()); @@ -959,7 +959,7 @@ void RegisterReportRoutes() { WithItemNames(history); for (auto& hop : history) { const auto zone = GetZoneInfo(hop.value("zone", 0u)); - hop["zone_name"] = zone ? zone->name : ""; + hop["zone_name"] = zone ? GameText::ZoneName(hop.value("zone", 0u)) : ""; } const auto names = CharacterNames(); std::set latest; @@ -1066,7 +1066,7 @@ void RegisterReportRoutes() { if (zone.is_null()) { const auto info = GetZoneInfo(zoneId); const bool property = EconomyPlaces::IsPropertyZone(zoneId); - zone = { {"zone", zoneId}, {"name", info ? info->name : "Zone " + std::to_string(zoneId)}, {"events", 0}, {"quantity", 0}, {"property", property} }; + zone = { {"zone", zoneId}, {"name", GameText::ZoneName(zoneId)}, {"events", 0}, {"quantity", 0}, {"property", property} }; if (property) zone["properties"] = nlohmann::json::array(); } zone["events"] = zone["events"].get() + events; @@ -1107,7 +1107,7 @@ void RegisterReportRoutes() { } const auto info = GetZoneInfo(*zone); nlohmann::json response{ - {"zone", *zone}, {"name", info ? info->name : ""}, {"from", from}, {"to", to}, {"lot", lot}, {"clone", clone ? nlohmann::json(*clone) : nlohmann::json()}, + {"zone", *zone}, {"name", info ? GameText::ZoneName(*zone) : ""}, {"from", from}, {"to", to}, {"lot", lot}, {"clone", clone ? nlohmann::json(*clone) : nlohmann::json()}, {"cellSize", static_cast(IEconomyLedger::MAP_CELL_SIZE)}, {"lots", lots}, {"cells", Database::Get()->GetMapCells(*zone, clone, *kind, from, to, lot)} }; if (EconomyPlaces::IsPropertyZone(*zone)) { @@ -1159,7 +1159,7 @@ void RegisterReportRoutes() { const auto describe = [&owners](nlohmann::json& row) { const auto zoneId = row.value("zone", 0u); const auto zone = GetZoneInfo(zoneId); - row["name"] = zone ? zone->name : "Zone " + std::to_string(zoneId); + row["name"] = GameText::ZoneName(zoneId); if (!EconomyPlaces::IsPropertyZone(zoneId)) return; const auto info = owners.Info(zoneId, row.value("clone", 0u)); row["property"] = true; diff --git a/dDashboardServer/routes/ServerRoutes.cpp b/dDashboardServer/routes/ServerRoutes.cpp index 998ef85d7..1addfd543 100644 --- a/dDashboardServer/routes/ServerRoutes.cpp +++ b/dDashboardServer/routes/ServerRoutes.cpp @@ -1,4 +1,5 @@ #include "ServerRoutes.h" +#include "GameText.h" #include #include @@ -365,7 +366,7 @@ void RegisterServerRoutes() { const auto filter = BundleFilter(reply, context); if (!filter) return; const auto files = LogBundle::Select(LogFolder(), DumpFolder(), *filter); - const auto& zoneNames = ZoneNames(); + const auto& zoneNames = GameText::ZoneNames(); nlohmann::json list = nlohmann::json::array(); uint64_t total = 0; for (const auto& file : files) { diff --git a/dDashboardServer/routes/SettingsRoutes.cpp b/dDashboardServer/routes/SettingsRoutes.cpp index 38b0f3728..3449d8473 100644 --- a/dDashboardServer/routes/SettingsRoutes.cpp +++ b/dDashboardServer/routes/SettingsRoutes.cpp @@ -1,4 +1,5 @@ #include "SettingsRoutes.h" +#include "GameText.h" #include "MasterPackets.h" #include "Permissions.h" #include "SettingsCatalog.h" @@ -63,7 +64,8 @@ namespace { {"title", info ? info->title : name}, {"section", info ? info->section : "Custom settings"}, {"category", section ? section->category : "other"}, // Known settings use the catalog's description (the .ini comments are for people editing the file) - {"description", info ? info->description : row ? row->description : ""}, + // Game names in descriptions are locale keys (%[ZoneTable_1000_DisplayDescription]), shown in the viewer's language + {"description", GameText::Expand(info ? info->description : row ? row->description : "")}, {"type", info ? SettingsCatalog::TypeName(info->type) : "text"}, {"default", info ? info->defaultValue : ""}, {"unit", info ? info->unit : ""}, {"format", info ? SettingsCatalog::FormatName(info->format) : "plain"}, {"listOf", info ? SettingsCatalog::ListOfName(info->listOf) : "number"}, {"condition", ConditionJson(info ? info->condition : std::nullopt)}, @@ -105,7 +107,7 @@ namespace { // Names for the numbers in a list setting: zones, items or reward codes nlohmann::json ListNames(const std::string& of, const std::vector& ids) { nlohmann::json names = nlohmann::json::object(); - const auto& zones = ZoneNames(); + const auto& zones = GameText::ZoneNames(); for (const auto id : ids) { const auto key = std::to_string(id); if (of == "zone") { @@ -128,14 +130,14 @@ namespace { nlohmann::json options = nlohmann::json::array(); if (of == "zone") { std::vector> zones; - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zoneId = GeneralUtils::TryParse(id); zoneId && *zoneId > 0) zones.emplace_back(*zoneId, name.get()); } std::sort(zones.begin(), zones.end()); for (const auto& [id, name] : zones) options.push_back({ {"id", id}, {"label", name}, {"detail", ""} }); } else if (of == "reward_code") { // What the client does with the two codes that don't just give an item - static const std::map effects{ { 4, "Opens LEGO Club" }, { 30, "Bricks aren't used up in build mode" } }; + const std::map effects{ { 4, "Opens " + GameText::ZoneName(1700) }, { 30, "Bricks aren't used up in build mode" } }; auto result = CDClientDatabase::ExecuteQuery("SELECT id, code, attachmentLOT FROM RewardCodes ORDER BY id;"); for (; !result.eof(); result.nextRow()) { const int64_t id = result.getIntField(0); @@ -375,11 +377,11 @@ void RegisterSettingsRoutes() { nlohmann::json files = nlohmann::json::array(); for (const auto& [file, name] : SettingsCatalog::Files()) files.push_back({ {"file", file}, {"name", name} }); nlohmann::json categories = nlohmann::json::array(); - for (const auto& category : SettingsCatalog::Categories()) categories.push_back({ {"id", category.id}, {"name", category.name}, {"description", category.description} }); + for (const auto& category : SettingsCatalog::Categories()) categories.push_back({ {"id", category.id}, {"name", category.name}, {"description", GameText::Expand(category.description)} }); categories.push_back({ {"id", "other"}, {"name", "Custom"}, {"description", "Settings this page doesn't know, e.g. from a modified server."} }); nlohmann::json sections = nlohmann::json::array(); for (const auto& section : SettingsCatalog::Sections()) { - sections.push_back({ {"category", section.category}, {"name", section.name}, {"description", section.description}, + sections.push_back({ {"category", section.category}, {"name", section.name}, {"description", GameText::Expand(section.description)}, {"layout", SettingsCatalog::LayoutName(section.layout)}, {"condition", ConditionJson(section.condition)} }); } sections.push_back({ {"category", "other"}, {"name", "Custom settings"}, {"description", ""}, {"layout", "rows"}, {"condition", nullptr} }); diff --git a/dDashboardServer/routes/Showcase.cpp b/dDashboardServer/routes/Showcase.cpp index 93bb457f9..a0d7f053a 100644 --- a/dDashboardServer/routes/Showcase.cpp +++ b/dDashboardServer/routes/Showcase.cpp @@ -1,4 +1,5 @@ #include "Showcase.h" +#include "GameText.h" #include "Workers.h" #include @@ -88,24 +89,19 @@ namespace { return info; } - std::string ZoneName(uint32_t zoneId) { - const auto& zones = ZoneNames(); - const auto key = std::to_string(zoneId); - return zones.contains(key) && zones[key].is_string() ? zones[key].get() : "Zone " + key; - } // The zones properties can be claimed in (PropertyTemplate), for the zone filter - const nlohmann::json& PropertyZones() { - static const nlohmann::json zones = [] { - nlohmann::json list = nlohmann::json::array(); + nlohmann::json PropertyZones() { + static const std::vector ids = [] { + std::vector ids; auto result = CDClientDatabase::ExecuteQuery("SELECT DISTINCT mapID FROM PropertyTemplate ORDER BY mapID;"); - for (; !result.eof(); result.nextRow()) { - const auto id = static_cast(result.getIntField(0)); - list.push_back({ {"id", id}, {"name", ZoneName(id)} }); - } - return list; + for (; !result.eof(); result.nextRow()) ids.push_back(static_cast(result.getIntField(0))); + return ids; }(); - return zones; + // Named in the viewer's language each time + nlohmann::json list = nlohmann::json::array(); + for (const auto id : ids) list.push_back({ {"id", id}, {"name", GameText::ZoneName(id)} }); + return list; } std::shared_ptr> Models(LWOOBJID propertyId) { @@ -139,7 +135,7 @@ namespace { {"description", info.description}, {"owner_name", owner ? owner->name : ""}, {"zone_id", info.zoneId}, - {"zone_name", ZoneName(info.zoneId)}, + {"zone_name", GameText::ZoneName(info.zoneId)}, {"reputation", info.reputation}, {"last_updated", info.lastUpdatedTime}, {"models", models} @@ -233,7 +229,7 @@ void RegisterShowcaseRoutes() { {"description", entry.info.description}, {"owner_name", entry.ownerName}, {"zone_id", entry.info.zoneId}, - {"zone_name", ZoneName(entry.info.zoneId)}, + {"zone_name", GameText::ZoneName(entry.info.zoneId)}, {"models", entry.modelCount}, {"reputation", entry.info.reputation}, {"last_updated", entry.info.lastUpdatedTime} diff --git a/dDashboardServer/routes/Traffic.cpp b/dDashboardServer/routes/Traffic.cpp index 069e86e23..7d3c5d608 100644 --- a/dDashboardServer/routes/Traffic.cpp +++ b/dDashboardServer/routes/Traffic.cpp @@ -16,6 +16,7 @@ #include "NetworkView.h" #include "Permissions.h" #include "RouteUtils.h" +#include "GameText.h" #include "ServerState.h" #include "ServiceType.h" #include "TrafficHistory.h" @@ -107,13 +108,10 @@ namespace { case ServiceType::DASHBOARD: return "Dashboard"; case ServiceType::UGC: return "UGC"; case ServiceType::WORLD: { - std::string zoneName; - { - std::lock_guard lock(ServerState::g_StatusMutex); - for (const auto& world : ServerState::g_WorldInstances) { - if (world.mapID == server.zoneId && world.instanceID == server.instanceId) zoneName = world.zoneName; - } - } + // The zone's name from the client's locale, in the viewer's language (none for a zone it doesn't name) + const auto& names = GameText::ZoneNames(); + const auto known = names.find(std::to_string(server.zoneId)); + const std::string zoneName = known != names.end() && known->is_string() ? known->get() : ""; return "World " + std::to_string(server.zoneId) + (zoneName.empty() ? "" : " " + zoneName) + " #" + std::to_string(server.instanceId); } default: return server.key; diff --git a/dDashboardServer/routes/VanityRoutes.cpp b/dDashboardServer/routes/VanityRoutes.cpp index facb3d3c0..cdbb0a865 100644 --- a/dDashboardServer/routes/VanityRoutes.cpp +++ b/dDashboardServer/routes/VanityRoutes.cpp @@ -1,4 +1,5 @@ #include "VanityRoutes.h" +#include "GameText.h" #include #include @@ -31,7 +32,7 @@ namespace { // The markdown files the game reads, and where players see them const std::vector> TEXTS{ - { "TESTAMENT.md", "The plaque by the Nimbus Station launch pad (zone 1200)" }, + { "TESTAMENT.md", "The plaque by the %[ZoneTable_1200_DisplayDescription] launch pad (zone 1200)" }, { "CREDITS.md", "Shown by the /credits command" }, { "INFO.md", "Shown by the /info command; __VERSION__, __SOURCE__, __LICENSE__ and __TIMESTAMP__ are filled in" }, }; @@ -180,9 +181,9 @@ void RegisterVanityRoutes() { files.push_back(entry); } nlohmann::json texts = nlohmann::json::array(); - for (const auto& [name, where] : TEXTS) texts.push_back({ {"name", name}, {"where", where} }); + for (const auto& [name, where] : TEXTS) texts.push_back({ {"name", name}, {"where", GameText::Expand(where)} }); nlohmann::json zones = nlohmann::json::array(); - for (const auto& [id, name] : ZoneNames().items()) { + for (const auto& [id, name] : GameText::ZoneNames().items()) { if (const auto zone = GeneralUtils::TryParse(id); zone && *zone > 0) zones.push_back({ {"id", *zone}, {"name", name} }); } std::sort(zones.begin(), zones.end(), [](const auto& a, const auto& b) { return a["id"].template get() < b["id"].template get(); }); diff --git a/dDashboardServer/routes/WorldView.cpp b/dDashboardServer/routes/WorldView.cpp index fbb189e09..630868a31 100644 --- a/dDashboardServer/routes/WorldView.cpp +++ b/dDashboardServer/routes/WorldView.cpp @@ -1,4 +1,5 @@ #include "WorldView.h" +#include "GameText.h" #include "PositionHistory.h" #include "WorldScene.h" #include "Scenery.h" @@ -78,11 +79,6 @@ namespace { // ---- Zone data ---- - std::string ZoneName(uint32_t zone) { - const auto& names = ZoneNames(); - const auto key = std::to_string(zone); - return names.contains(key) ? names[key].get() : "Zone " + key; - } std::optional LuzPath(uint32_t zone) { return ZoneLuzPath(zone); @@ -158,7 +154,7 @@ namespace { } nlohmann::json json{ - {"zone", zone}, {"name", ZoneName(zone)}, + {"zone", zone}, {"name", GameText::ZoneName(zone)}, {"spawn", { Round(zoneFile->spawnpoint.x), Round(zoneFile->spawnpoint.y), Round(zoneFile->spawnpoint.z) }}, {"scenes", scenes}, {"kinds", kinds}, {"objects", { {"lot", lots}, {"kind", kindOf}, {"flags", flags}, {"scene", sceneOf}, {"pos", positions}, {"names", names} }}, {"lotNames", lotNames}, @@ -289,7 +285,7 @@ namespace WorldView { nlohmann::json mapKinds = nlohmann::json::array(); for (const auto kind : magic_enum::enum_values()) mapKinds.push_back({ {"value", static_cast(kind)}, {"name", GameLabels::Name(kind)} }); JsonReply(reply, eHTTPStatusCode::OK, { - {"zones", ZoneNames()}, {"worlds", worlds}, {"mapKinds", mapKinds}, {"cellSize", IEconomyLedger::MAP_CELL_SIZE}, + {"zones", GameText::ZoneNames()}, {"worlds", worlds}, {"mapKinds", mapKinds}, {"cellSize", IEconomyLedger::MAP_CELL_SIZE}, {"propertyZones", EconomyPlaces::PropertyZones()}, {"today", Now() / DAY_SECONDS}, {"now", Now()}, {"history", { {"enabled", Setting("position_history", 1) != 0}, {"days", Setting("position_history_days", 3)}, @@ -381,7 +377,7 @@ namespace WorldView { const auto from = QueryInt(context, "from", 0); nlohmann::json instances = nlohmann::json::array(); for (const auto& i : Database::Get()->GetPositionInstances(zone, from, to)) { - instances.push_back({ {"zone", i.zoneId}, {"zoneName", ZoneName(i.zoneId)}, {"instance", i.instanceId}, {"clone", i.cloneId}, + instances.push_back({ {"zone", i.zoneId}, {"zoneName", GameText::ZoneName(i.zoneId)}, {"instance", i.instanceId}, {"clone", i.cloneId}, {"first", i.first}, {"last", i.last}, {"players", i.players} }); } JsonReply(reply, eHTTPStatusCode::OK, { {"instances", instances} }); @@ -416,7 +412,7 @@ namespace WorldView { } players.push_back({ {"id", std::to_string(first.characterId)}, {"name", CharacterName(first.characterId)}, {"instances", instances}, {"samples", points} }); } - Audit(context, "view_position_history", ZoneName(zone) + (instance ? " instance " + std::to_string(instance) : "") + ", " + + Audit(context, "view_position_history", GameText::ZoneName(zone) + (instance ? " instance " + std::to_string(instance) : "") + ", " + std::to_string((to - from + 59) / 60) + " minute(s) ending " + std::to_string(to)); JsonReply(reply, eHTTPStatusCode::OK, { {"zone", zone}, {"instance", instance}, {"from", from}, {"to", to}, {"bucket", bucket}, {"interval", interval}, {"idleSeconds", IDLE_INTERVAL},