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},