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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 22:06:30 -05:00
parent 17b9479c0e
commit 1f8dd73e0b
29 changed files with 146 additions and 206 deletions

View File

@@ -1,4 +1,5 @@
#include "APIRoutes.h" #include "APIRoutes.h"
#include "GameText.h"
#include "PropertyAssets.h" #include "PropertyAssets.h"
#include "OpenApi.h" #include "OpenApi.h"
#include "RouteUtils.h" #include "RouteUtils.h"
@@ -40,8 +41,6 @@
#include "Sd0.h" #include "Sd0.h"
#include <sstream> #include <sstream>
#include <map> #include <map>
#include "Locale.h"
#include "LocaleText.h"
#include <bcrypt/BCrypt.hpp> #include <bcrypt/BCrypt.hpp>
#include <algorithm> #include <algorithm>
#include <ctime> #include <ctime>
@@ -84,11 +83,7 @@ namespace {
std::string WithZoneNames(const std::string& table) { std::string WithZoneNames(const std::string& table) {
auto json = nlohmann::json::parse(table, nullptr, false); auto json = nlohmann::json::parse(table, nullptr, false);
if (json.is_discarded()) return table; if (json.is_discarded()) return table;
const auto& zones = ZoneNames(); for (auto& row : json["data"]) row["zone_name"] = GameText::ZoneName(row.value("zone_id", 0u));
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<std::string>() : "Zone " + zone;
}
return json.dump(); return json.dump();
} }
@@ -130,7 +125,7 @@ namespace {
} }
std::string Localized(const std::string& text) { 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 // 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; row["path"] = points;
} }
const auto prefix = "PropertyTemplate_" + std::to_string(templateId); const auto prefix = "PropertyTemplate_" + std::to_string(templateId);
row["localized_name"] = Locale::GetPhrase(prefix + "_name"); row["localized_name"] = GameText::Phrase(prefix + "_name");
row["localized_description"] = Locale::GetPhrase(prefix + "_description"); row["localized_description"] = GameText::Phrase(prefix + "_description");
const auto& zones = ZoneNames(); const auto& zones = GameText::ZoneNames();
for (const auto* key : { "mapID", "vendorMapID" }) { for (const auto* key : { "mapID", "vendorMapID" }) {
if (!row.contains(key) || !row[key].is_number_integer()) continue; if (!row.contains(key) || !row[key].is_number_integer()) continue;
const auto zone = std::to_string(row[key].get<int64_t>()); const auto zone = std::to_string(row[key].get<int64_t>());
@@ -1244,7 +1239,7 @@ namespace {
{"claimed", info->claimedTime}, {"claimed", info->claimedTime},
{"performance_cost", info->performanceCost}, {"performance_cost", info->performanceCost},
{"zone_id", info->zoneId}, {"zone_id", info->zoneId},
{"zone_name", ZoneNames().contains(std::to_string(info->zoneId)) ? ZoneNames()[std::to_string(info->zoneId)].get<std::string>() : ""}, {"zone_name", GameText::ZoneNames().contains(std::to_string(info->zoneId)) ? GameText::ZoneNames()[std::to_string(info->zoneId)].get<std::string>() : ""},
{"models", models} {"models", models}
}); });
}); });

View File

@@ -1,4 +1,5 @@
#include "Announcements.h" #include "Announcements.h"
#include "GameText.h"
#include "AnnouncementSchedule.h" #include "AnnouncementSchedule.h"
#include "LiveWorld.h" #include "LiveWorld.h"
#include "DashboardRoutes.h" #include "DashboardRoutes.h"
@@ -61,7 +62,7 @@ namespace {
std::string ZoneList(const std::vector<uint32_t>& zones) { std::string ZoneList(const std::vector<uint32_t>& zones) {
if (zones.empty()) return "every world"; if (zones.empty()) return "every world";
std::string text; std::string text;
const auto& names = ZoneNames(); const auto& names = GameText::ZoneNames();
for (const auto zone : zones) { for (const auto zone : zones) {
const auto key = std::to_string(zone); const auto key = std::to_string(zone);
text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key);
@@ -80,7 +81,7 @@ namespace {
// Every zone a message can be limited to, for the picker // Every zone a message can be limited to, for the picker
nlohmann::json ZoneOptions() { nlohmann::json ZoneOptions() {
std::vector<std::pair<uint32_t, std::string>> zones; std::vector<std::pair<uint32_t, std::string>> zones;
for (const auto& [id, name] : ZoneNames().items()) { for (const auto& [id, name] : GameText::ZoneNames().items()) {
if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>()); if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>());
} }
std::sort(zones.begin(), zones.end()); std::sort(zones.begin(), zones.end());
@@ -101,7 +102,7 @@ namespace {
row.enabled = body.value("enabled", true); row.enabled = body.value("enabled", true);
row.zones.clear(); row.zones.clear();
if (body.contains("zones") && body["zones"].is_array()) { if (body.contains("zones") && body["zones"].is_array()) {
const auto& known = ZoneNames(); const auto& known = GameText::ZoneNames();
for (const auto& zone : body["zones"]) { for (const auto& zone : body["zones"]) {
if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return false; } if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return false; }
if (std::find(row.zones.begin(), row.zones.end(), zone.get<uint32_t>()) == row.zones.end()) row.zones.push_back(zone.get<uint32_t>()); if (std::find(row.zones.begin(), row.zones.end(), zone.get<uint32_t>()) == row.zones.end()) row.zones.push_back(zone.get<uint32_t>());

View File

@@ -1,4 +1,5 @@
#include "ChallengeRoutes.h" #include "ChallengeRoutes.h"
#include "GameText.h"
#include "LiveEventRoutes.h" #include "LiveEventRoutes.h"
#include "LiveWorld.h" #include "LiveWorld.h"
#include "DashboardRoutes.h" #include "DashboardRoutes.h"
@@ -46,7 +47,7 @@ namespace {
std::string ZoneList(const std::vector<uint32_t>& zones) { std::string ZoneList(const std::vector<uint32_t>& zones) {
if (zones.empty()) return "every zone"; if (zones.empty()) return "every zone";
std::string text; std::string text;
const auto& names = ZoneNames(); const auto& names = GameText::ZoneNames();
for (const auto zone : zones) { for (const auto zone : zones) {
const auto key = std::to_string(zone); const auto key = std::to_string(zone);
text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key);
@@ -257,7 +258,7 @@ namespace {
nlohmann::json ZoneOptions() { nlohmann::json ZoneOptions() {
std::vector<std::pair<uint32_t, std::string>> zones; std::vector<std::pair<uint32_t, std::string>> zones;
for (const auto& [id, name] : ZoneNames().items()) { for (const auto& [id, name] : GameText::ZoneNames().items()) {
if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>()); if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>());
} }
std::sort(zones.begin(), zones.end()); 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"); if (challenge.lot < 0 || (challenge.lot > 0 && !ClientAssets::ObjectName(challenge.lot))) return fail("Unknown LOT");
challenge.zones.clear(); challenge.zones.clear();
if (body.contains("zones") && body["zones"].is_array()) { if (body.contains("zones") && body["zones"].is_array()) {
const auto& known = ZoneNames(); const auto& known = GameText::ZoneNames();
for (const auto& zone : body["zones"]) { for (const auto& zone : body["zones"]) {
if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) return fail("Pick zones from the list"); if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) return fail("Pick zones from the list");
if (std::find(challenge.zones.begin(), challenge.zones.end(), zone.get<uint32_t>()) == challenge.zones.end()) challenge.zones.push_back(zone.get<uint32_t>()); if (std::find(challenge.zones.begin(), challenge.zones.end(), zone.get<uint32_t>()) == challenge.zones.end()) challenge.zones.push_back(zone.get<uint32_t>());

View File

@@ -1,4 +1,5 @@
#include "CharacterProgress.h" #include "CharacterProgress.h"
#include "GameText.h"
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
@@ -186,10 +187,8 @@ namespace {
for (const auto& [zoneId, s] : stats) zoneIds.insert(zoneId); for (const auto& [zoneId, s] : stats) zoneIds.insert(zoneId);
for (const auto& [zoneId, zone] : catalog.zones) if (zone.released) zoneIds.insert(zoneId); for (const auto& [zoneId, zone] : catalog.zones) if (zone.released) zoneIds.insert(zoneId);
zoneIds.erase(0); // character select zoneIds.erase(0); // character select
const auto& names = ZoneNames();
nlohmann::json zones = nlohmann::json::array(); nlohmann::json zones = nlohmann::json::array();
for (const auto zoneId : zoneIds) { for (const auto zoneId : zoneIds) {
const auto key = std::to_string(zoneId);
const auto zone = catalog.zones.find(zoneId); const auto zone = catalog.zones.find(zoneId);
nlohmann::json summary = nlohmann::json::array(); nlohmann::json summary = nlohmann::json::array();
if (zone != catalog.zones.end()) { if (zone != catalog.zones.end()) {
@@ -206,7 +205,7 @@ namespace {
} }
} }
const auto category = zone != catalog.zones.end() ? zone->second.category : std::nullopt; 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)}, {"category", category ? nlohmann::json(*category) : nlohmann::json(nullptr)}, {"stats", stats.contains(zoneId) ? stats[zoneId] : nlohmann::json(nullptr)},
{"summary", summary} }); {"summary", summary} });
} }

View File

@@ -1,4 +1,5 @@
#include "CharacterTools.h" #include "CharacterTools.h"
#include "GameText.h"
#include <ctime> #include <ctime>
@@ -72,14 +73,9 @@ namespace {
for (auto& inventory : summary["inventories"]) for (auto& item : inventory["items"]) item["name"] = ClientAssets::ItemName(item["lot"].get<LOT>()); for (auto& inventory : summary["inventories"]) for (auto& item : inventory["items"]) item["name"] = ClientAssets::ItemName(item["lot"].get<LOT>());
} }
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) { 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; return rows;
} }

View File

@@ -1,4 +1,5 @@
#include "ChatRoutes.h" #include "ChatRoutes.h"
#include "GameText.h"
#include <ctime> #include <ctime>
#include <map> #include <map>
@@ -29,7 +30,7 @@ namespace {
} }
nlohmann::json RowJson(const IChatLog::ChatMessage& m, const ChatHistory::Access& access) { 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); const auto zone = std::to_string(m.zoneId);
auto json = ChatHistory::MessageJson(m, access); auto json = ChatHistory::MessageJson(m, access);
json["zone_name"] = zones.contains(zone) ? zones[zone] : nlohmann::json(""); json["zone_name"] = zones.contains(zone) ? zones[zone] : nlohmann::json("");

View File

@@ -1,4 +1,5 @@
#include "ClientAssets.h" #include "ClientAssets.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
@@ -15,7 +16,6 @@
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "Game.h" #include "Game.h"
#include "Logger.h" #include "Logger.h"
#include "Locale.h"
#include "dConfig.h" #include "dConfig.h"
#include "eHTTPMethod.h" #include "eHTTPMethod.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
@@ -27,7 +27,7 @@ namespace {
constexpr uint32_t MAX_ITEMS_PER_REQUEST = 500; constexpr uint32_t MAX_ITEMS_PER_REQUEST = 500;
constexpr size_t MAX_LISTING_ENTRIES = 2000; constexpr size_t MAX_LISTING_ENTRIES = 2000;
std::map<LOT, nlohmann::json> g_ItemCache; std::map<std::pair<std::string, LOT>, nlohmann::json> g_ItemCache; // by language (GameText) and LOT
std::map<LOT, std::vector<int>> g_ItemSetsByLot; // lot -> set ids containing it std::map<LOT, std::vector<int>> g_ItemSetsByLot; // lot -> set ids containing it
bool g_ItemSetsLoaded = false; bool g_ItemSetsLoaded = false;
@@ -95,9 +95,9 @@ namespace {
return data; return data;
} }
// In the viewer's language (GameText.h)
std::string Phrase(const std::string& key) { std::string Phrase(const std::string& key) {
const auto& phrase = Locale::GetPhrase(key); return GameText::Phrase(key);
return phrase;
} }
struct Stats { struct Stats {
@@ -243,7 +243,7 @@ namespace ClientAssets {
} }
std::string ItemName(LOT lot) { 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; if (!localized.empty()) return localized;
const auto& table = ObjectNameTable(); const auto& table = ObjectNameTable();
const auto it = table.find(lot); const auto it = table.find(lot);
@@ -252,7 +252,8 @@ namespace ClientAssets {
} }
nlohmann::json ItemInfo(LOT lot) { 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)} }; nlohmann::json info{ {"lot", lot}, {"name", ItemName(lot)} };
@@ -284,7 +285,7 @@ namespace ClientAssets {
info["stats"] = StatsJson(stats); info["stats"] = StatsJson(stats);
info["set"] = ItemSet(lot); info["set"] = ItemSet(lot);
g_ItemCache[lot] = info; g_ItemCache[cacheKey] = info;
return info; return info;
} }

View File

@@ -1,4 +1,5 @@
#include "DashboardRoutes.h" #include "DashboardRoutes.h"
#include "GameText.h"
#include "RouteUtils.h" #include "RouteUtils.h"
#include "Permissions.h" #include "Permissions.h"
#include "EmailService.h" #include "EmailService.h"
@@ -14,7 +15,6 @@
#include "Game.h" #include "Game.h"
#include "Database.h" #include "Database.h"
#include "Logger.h" #include "Logger.h"
#include "Locale.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "tinyxml2.h" #include "tinyxml2.h"
@@ -32,35 +32,12 @@
using namespace RouteUtils; using namespace RouteUtils;
namespace { 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) { nlohmann::json ParseCharacterXml(const std::string& xml) {
// Every value the template reads has a default, since inja fails on missing variables and // 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) // character XML may lack sections (e.g. a character that never finished loading in)
nlohmann::json stats{ nlohmann::json stats{
{"coins", 0}, {"universe_score", 0}, {"reputation", 0}, {"gm_level", 0}, {"level", 0}, {"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}, {"health", 0}, {"max_health", 0}, {"armor", 0}, {"max_armor", 0}, {"imagination", 0}, {"max_imagination", 0},
{"missions_completed", 0}, {"missions_active", 0}, {"total_items", 0} {"missions_completed", 0}, {"missions_active", 0}, {"total_items", 0}
}; };
@@ -79,7 +56,7 @@ namespace {
auto lastZone = charEl->IntAttribute("lwid", 0); auto lastZone = charEl->IntAttribute("lwid", 0);
stats["last_zone_id"] = lastZone; stats["last_zone_id"] = lastZone;
stats["last_zone"] = ZoneName(lastZone); stats["last_zone"] = GameText::ZoneName(lastZone);
} }
auto* lvlEl = obj->FirstChildElement("lvl"); auto* lvlEl = obj->FirstChildElement("lvl");
@@ -117,7 +94,7 @@ namespace {
std::map<int, std::set<int>> clones; std::map<int, std::set<int>> clones;
for (auto* l = vl->FirstChildElement("l"); l; l = l->NextSiblingElement("l")) clones[l->IntAttribute("id", 0)].insert(l->IntAttribute("cid", 0)); 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(); 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; stats["visited_zones"] = visited;
} }
@@ -127,7 +104,7 @@ namespace {
for (auto* s = zs->FirstChildElement("s"); s; s = s->NextSiblingElement("s")) { for (auto* s = zs->FirstChildElement("s"); s; s = s->NextSiblingElement("s")) {
auto zid = s->IntAttribute("map", 0); auto zid = s->IntAttribute("map", 0);
zoneStats.push_back({ zoneStats.push_back({
{"map_id", zid}, {"name", ZoneName(zid)}, {"map_id", zid}, {"name", GameText::ZoneName(zid)},
{"achievements", s->IntAttribute("ac", 0)}, {"achievements", s->IntAttribute("ac", 0)},
{"coins_collected", s->IntAttribute("cc", 0)}, {"coins_collected", s->IntAttribute("cc", 0)},
{"enemies_smashed", s->IntAttribute("es", 0)} {"enemies_smashed", s->IntAttribute("es", 0)}
@@ -207,10 +184,6 @@ namespace {
} }
} }
const nlohmann::json& ZoneNames() {
return GetZoneNamesJson();
}
namespace { namespace {
constexpr size_t LOG_TAIL_BYTES = 256 * 1024; constexpr size_t LOG_TAIL_BYTES = 256 * 1024;
constexpr size_t LOG_TAIL_LINES = 500; constexpr size_t LOG_TAIL_LINES = 500;
@@ -430,7 +403,7 @@ void RegisterDashboardRoutes() {
const std::string selection = report.value("selection", std::string{}); const std::string selection = report.value("selection", std::string{});
report["selection_text"] = selection; report["selection_text"] = selection;
if (selection.starts_with("%[") && selection.ends_with("]")) { 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; if (!phrase.empty()) report["selection_text"] = phrase;
} }
RenderPage(reply, context, "bug_report-view.jinja2", "bug_reports", { {"report", report} }); 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"); 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&) { 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(); 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<uint32_t>(id); zone && *zone > 0) zones.push_back({ {"id", *zone}, {"name", name} }); if (const auto zone = GeneralUtils::TryParse<uint32_t>(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<uint32_t>() < b["id"].template get<uint32_t>(); }); std::sort(zones.begin(), zones.end(), [](const auto& a, const auto& b) { return a["id"].template get<uint32_t>() < b["id"].template get<uint32_t>(); });
@@ -492,7 +465,7 @@ void RegisterDashboardRoutes() {
// Logs // Logs
Route(eHTTPMethod::GET, "/activity_log", Perm("logs_activity"), "Activity log", [](HTTPReply& reply, const HTTPContext& context) { 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("/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"); 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<uint32_t>(server.substr(12, server.find('_', 12) - 12))) zoneIds.insert(*zone); if (const auto zone = GeneralUtils::TryParse<uint32_t>(server.substr(12, server.find('_', 12) - 12))) zoneIds.insert(*zone);
} }
nlohmann::json zones = nlohmann::json::array(); 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} }; nlohmann::json data{ {"servers", servers}, {"server_types", types}, {"selected_server", selected}, {"files", nlohmann::json::array()}, {"bundle_zones", zones} };
if (!selected.empty()) { if (!selected.empty()) {
const auto& files = logs.at(selected); const auto& files = logs.at(selected);

View File

@@ -5,6 +5,3 @@
class HTTPContext; class HTTPContext;
void RegisterDashboardRoutes(); void RegisterDashboardRoutes();
// Zone ID (as a string) -> display name, from the client's locale
const nlohmann::json& ZoneNames();

View File

@@ -1,4 +1,5 @@
#include "EconomyPlaces.h" #include "EconomyPlaces.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -47,10 +48,7 @@ namespace EconomyPlaces {
} }
std::string ZoneName(uint32_t zone) { std::string ZoneName(uint32_t zone) {
const auto& names = ZoneNames(); return GameText::ZoneName(zone);
const auto key = std::to_string(zone);
if (names.contains(key) && names[key].is_string()) return names[key].get<std::string>();
return "Zone " + key;
} }
std::optional<IEconomyLedger::PlaceFilter> Parse(std::string_view place) { std::optional<IEconomyLedger::PlaceFilter> Parse(std::string_view place) {

View File

@@ -1,4 +1,5 @@
#include "EventsCalendar.h" #include "EventsCalendar.h"
#include "GameText.h"
#include "EventSchedule.h" #include "EventSchedule.h"
#include "AnnouncementSchedule.h" #include "AnnouncementSchedule.h"
#include "LevelGating.h" #include "LevelGating.h"
@@ -156,22 +157,17 @@ namespace {
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_minor")).value_or(ClientVersion::minor) }; GeneralUtils::TryParse<int32_t>(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<std::string>() : "Zone " + key;
}
std::string ZoneList(const std::vector<uint32_t>& zones) { std::string ZoneList(const std::vector<uint32_t>& zones) {
if (zones.empty()) return "every world"; if (zones.empty()) return "every world";
std::string text; 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; return text;
} }
nlohmann::json ZoneOptions() { nlohmann::json ZoneOptions() {
std::vector<std::pair<uint32_t, std::string>> zones; std::vector<std::pair<uint32_t, std::string>> zones;
for (const auto& [id, name] : ZoneNames().items()) { for (const auto& [id, name] : GameText::ZoneNames().items()) {
if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>()); if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>());
} }
std::sort(zones.begin(), zones.end()); std::sort(zones.begin(), zones.end());
@@ -186,14 +182,14 @@ namespace {
std::lock_guard lock(ServerState::g_StatusMutex); std::lock_guard lock(ServerState::g_StatusMutex);
for (const auto& world : ServerState::g_WorldInstances) { for (const auto& world : ServerState::g_WorldInstances) {
if (!zones.contains(world.mapID)) continue; 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; return worlds;
} }
nlohmann::json FeatureJson(const std::string& name, const Feature& feature) { nlohmann::json FeatureJson(const std::string& name, const Feature& feature) {
nlohmann::json zones = nlohmann::json::array(); 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} }; nlohmann::json json{ {"name", name}, {"description", feature.description}, {"zones", zones}, {"version", nullptr}, {"unlocked", false} };
if (feature.version) { if (feature.version) {
const auto& [major, current, minor] = *feature.version; const auto& [major, current, minor] = *feature.version;
@@ -214,7 +210,7 @@ namespace {
const auto zones = ZonesOf(feature); 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."; 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; 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."; 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(); nlohmann::json zones = nlohmann::json::array();
if (config.contains("zones") && config["zones"].is_array()) { if (config.contains("zones") && config["zones"].is_array()) {
const auto& known = ZoneNames(); const auto& known = GameText::ZoneNames();
for (const auto& zone : config["zones"]) { for (const auto& zone : config["zones"]) {
if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return std::nullopt; } if (!zone.is_number_unsigned() || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return std::nullopt; }
if (std::find(zones.begin(), zones.end(), zone) == zones.end()) zones.push_back(zone); if (std::find(zones.begin(), zones.end(), zone) == zones.end()) zones.push_back(zone);

View File

@@ -1,4 +1,5 @@
#include "FeaturedProperties.h" #include "FeaturedProperties.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <ctime> #include <ctime>
@@ -59,11 +60,6 @@ namespace {
return mapId; 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<std::string>() : "Zone " + key;
}
nlohmann::json PropertyJson(const IProperty::Info& info, const std::string& ownerName) { nlohmann::json PropertyJson(const IProperty::Info& info, const std::string& ownerName) {
return { return {
@@ -75,7 +71,7 @@ namespace {
{"performance_cost", info.performanceCost}, {"performance_cost", info.performanceCost},
{"last_updated", info.lastUpdatedTime}, {"last_updated", info.lastUpdatedTime},
{"zone_id", info.zoneId}, {"zone_id", info.zoneId},
{"zone_name", ZoneName(info.zoneId)}, {"zone_name", GameText::ZoneName(info.zoneId)},
}; };
} }
@@ -144,10 +140,10 @@ namespace {
nlohmann::json entry{ nlohmann::json entry{
{"template_id", slot.templateId}, {"template_id", slot.templateId},
{"map_id", slot.mapId}, {"map_id", slot.mapId},
{"zone_name", ZoneName(slot.mapId)}, {"zone_name", GameText::ZoneName(slot.mapId)},
{"spawn_name", slot.spawnName}, {"spawn_name", slot.spawnName},
{"location", choice.mapId}, {"location", choice.mapId},
{"location_name", ZoneName(choice.mapId)}, {"location_name", GameText::ZoneName(choice.mapId)},
{"mode", HotPropertySlots::ModeName(choice.mode)}, {"mode", HotPropertySlots::ModeName(choice.mode)},
{"property_id", nullptr}, {"property_id", nullptr},
{"picked", nullptr}, {"picked", nullptr},
@@ -169,7 +165,7 @@ namespace {
} }
nlohmann::json worlds = nlohmann::json::array(); 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 { return {
{"full_auto", settings.fullAuto}, {"full_auto", settings.fullAuto},
{"settings_updated_at", settings.updatedAt}, {"settings_updated_at", settings.updatedAt},
@@ -252,12 +248,12 @@ void FeaturedProperties::RegisterRoutes() {
: idValue.is_number_integer() ? std::optional<LWOOBJID>(idValue.get<LWOOBJID>()) : std::nullopt; : idValue.is_number_integer() ? std::optional<LWOOBJID>(idValue.get<LWOOBJID>()) : std::nullopt;
if (!propertyId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "property_id is required to pick a property"); if (!propertyId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "property_id is required to pick a property");
const auto picked = Featurable(*propertyId, *location); 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()) { for (const auto& other : Database::Get()->GetFeaturedPropertySlots()) {
if (other.templateId == slot->templateId || HotPropertySlots::ModeFromInt(other.mode) != eMode::PICKED || other.propertyId != *propertyId) continue; if (other.templateId == slot->templateId || HotPropertySlots::ModeFromInt(other.mode) != eMode::PICKED || other.propertyId != *propertyId) continue;
const auto* otherSlot = FindSlot(other.templateId); const auto* otherSlot = FindSlot(other.templateId);
if (!otherSlot) continue; 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; const auto& [info, ownerName] = *picked;
row.propertyId = *propertyId; row.propertyId = *propertyId;
@@ -266,7 +262,7 @@ void FeaturedProperties::RegisterRoutes() {
} }
Database::Get()->SetFeaturedPropertySlot(row); 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)); BroadcastTableChanged("featured_properties", std::to_string(slot->templateId));
JsonSuccess(reply, StateJson()); JsonSuccess(reply, StateJson());
}); });

View File

@@ -1,4 +1,5 @@
#include "Inspector.h" #include "Inspector.h"
#include "GameText.h"
#include "MasterPackets.h" #include "MasterPackets.h"
#include <chrono> #include <chrono>
@@ -114,9 +115,7 @@ namespace {
} }
std::string ZoneName(uint32_t zoneId) { std::string ZoneName(uint32_t zoneId) {
const auto& zones = ZoneNames(); return zoneId == 0 ? "" : GameText::ZoneName(zoneId);
const auto zone = std::to_string(zoneId);
return zoneId == 0 ? "" : zones.contains(zone) ? zones[zone].get<std::string>() : "Zone " + zone;
} }
// A session for the page; `state` is the live state, or "ended" for one only in the database // A session for the page; `state` is the live state, or "ended" for one only in the database

View File

@@ -1,4 +1,5 @@
#include "InstanceLoad.h" #include "InstanceLoad.h"
#include "GameText.h"
#include "MasterPackets.h" #include "MasterPackets.h"
#include "InstanceLimits.h" #include "InstanceLimits.h"
#include "DashboardRoutes.h" #include "DashboardRoutes.h"
@@ -50,11 +51,6 @@ namespace {
return it == ClientCaps().end() ? InstanceLimits::Caps{ 8, 12 } : it->second; 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<std::string>() : "Zone " + key;
}
nlohmann::json OptionalJson(const std::optional<uint32_t>& value) { nlohmann::json OptionalJson(const std::optional<uint32_t>& value) {
return value ? nlohmann::json(*value) : nlohmann::json(nullptr); return value ? nlohmann::json(*value) : nlohmann::json(nullptr);
@@ -114,7 +110,7 @@ namespace InstanceLoad {
const auto caps = ClientCapsOf(zone); const auto caps = ClientCapsOf(zone);
nlohmann::json points = nlohmann::json::array(); 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} }); 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["samples"] = points;
json["clientCaps"] = { {"soft", caps.soft}, {"hard", caps.hard} }; json["clientCaps"] = { {"soft", caps.soft}, {"hard", caps.hard} };
} else { } else {
@@ -135,7 +131,7 @@ namespace InstanceLoad {
} }
nlohmann::json list = nlohmann::json::array(); nlohmann::json list = nlohmann::json::array();
for (const auto& [id, summary] : zones) { 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; json["zones"] = list;
} }
@@ -153,7 +149,7 @@ namespace InstanceLoad {
const auto it = limits.find(id); const auto it = limits.find(id);
const auto* limit = it == limits.end() ? nullptr : &it->second; 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); 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}, {"softCap", limit ? OptionalJson(limit->softCap) : nullptr}, {"hardCap", limit ? OptionalJson(limit->hardCap) : nullptr},
{"spare", limit ? limit->spareInstances : 0}, {"soft", effective.soft}, {"hard", effective.hard}, {"spare", limit ? limit->spareInstances : 0}, {"soft", effective.soft}, {"hard", effective.hard},
{"updatedAt", limit ? limit->updatedAt : 0}, {"updatedBy", limit ? limit->updatedBy : ""} }); {"updatedAt", limit ? limit->updatedAt : 0}, {"updatedBy", limit ? limit->updatedBy : ""} });
@@ -193,7 +189,7 @@ namespace InstanceLoad {
else Database::Get()->SetZoneLimit(limit); else Database::Get()->SetZoneLimit(limit);
const auto effective = InstanceLimits::Effective(limit.softCap, limit.hardCap, client); const auto effective = InstanceLimits::Effective(limit.softCap, limit.hardCap, client);
const auto text = [](const std::optional<uint32_t>& value) { return value ? std::to_string(*value) : std::string("client's"); }; const auto text = [](const std::optional<uint32_t>& 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)")); "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)); BroadcastTableChanged("zone_limits", std::to_string(zone));
const bool told = TellMaster(); const bool told = TellMaster();

View File

@@ -1,4 +1,5 @@
#include "LeaderboardRoutes.h" #include "LeaderboardRoutes.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <set> #include <set>
@@ -8,7 +9,7 @@
#include "WSRoutes.h" #include "WSRoutes.h"
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "Database.h" #include "Database.h"
#include "Locale.h" #include "GameLabels.h"
#include "Game.h" #include "Game.h"
#include "dConfig.h" #include "dConfig.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
@@ -27,7 +28,7 @@ namespace {
}; };
std::string ActivityName(uint32_t id) { 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); 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 // 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"); 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 = ""; const char* hint = "";
}; };
// The client's column header for a locale phrase ("RACE_BESTTIME" -> "Best Time"); fallback when the locale lacks it // 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, const char* fallback) { std::string Label(const char* phrase) {
const auto& text = Locale::GetPhrase(phrase); return GameText::Text(phrase, GameLabels::Words(phrase));
return text.empty() ? fallback : text;
} }
/** /**
@@ -71,26 +71,26 @@ namespace {
*/ */
nlohmann::json Columns(eLeaderboardType type) { nlohmann::json Columns(eLeaderboardType type) {
using enum eLeaderboardType; using enum eLeaderboardType;
std::vector<std::pair<Column, const char*>> columns; // column, English fallback std::vector<Column> columns;
switch (type) { switch (type) {
case ShootingGallery: columns = { { {"primary", "SCORE", "number"}, "Score" }, { {"secondary", "UI_SG_STREAK", "number"}, "Streak" }, { {"tertiary", "UI_SG_ACCURACY", "percent"}, "Accuracy" } }; 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"}, "Best Time" }, { {"secondary", "RACE_BESTLAP", "laptime"}, "Best Lap" }, { {"wins", "RACE_NUMWINS", "number"}, "Wins" } }; 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"}, "Time" } }; 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 // 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 FootRace: columns = { {"primary", "TIME", "time", "Time left on the clock at the finish: more is better"} }; break;
case UnusedLeaderboard4: columns = { { {"primary", "POINTS", "number"}, "Points" } }; break; case UnusedLeaderboard4: columns = { {"primary", "POINTS", "number"} }; break;
case Survival: columns = { { {"secondary", "TIME", "time"}, "Time" }, { {"primary", "POINTS", "number"}, "Points" } }; break; case Survival: columns = { {"secondary", "TIME", "time"}, {"primary", "POINTS", "number"} }; break;
case SurvivalNS: columns = { { {"primary", "WAVE", "number"}, "Wave" }, { {"secondary", "TIME", "time", "Less is better for the same wave"}, "Time" } }; break; case SurvivalNS: columns = { {"primary", "WAVE", "number"}, {"secondary", "TIME", "time", "Less is better for the same wave"} }; break;
case Donations: columns = { { {"primary", "DONATIONS", "number"}, "Donations" } }; break; case Donations: columns = { {"primary", "DONATIONS", "number"} }; break;
default: columns = { { {"primary", "SCORE", "number"}, "Score" } }; 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) // 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 bool byTime = type == Survival && Game::config && Game::config->GetValue("classic_survival_scoring") == "1";
const std::string ranksBy = byTime ? "secondary" : "primary"; const std::string ranksBy = byTime ? "secondary" : "primary";
nlohmann::json out = nlohmann::json::array(); nlohmann::json out = nlohmann::json::array();
for (const auto& [column, fallback] : columns) { for (const auto& column : columns) {
out.push_back({ {"key", column.key}, {"label", Label(column.phrase, fallback)}, {"format", column.format}, {"hint", column.hint}, {"ranks", column.key == ranksBy} }); out.push_back({ {"key", column.key}, {"label", Label(column.phrase)}, {"format", column.format}, {"hint", column.hint}, {"ranks", column.key == ranksBy} });
} }
return out; return out;
} }

View File

@@ -1,4 +1,5 @@
#include "LiveEventRoutes.h" #include "LiveEventRoutes.h"
#include "GameText.h"
#include "LiveWorld.h" #include "LiveWorld.h"
#include "DashboardRoutes.h" #include "DashboardRoutes.h"
#include "ClientAssets.h" #include "ClientAssets.h"
@@ -48,7 +49,7 @@ namespace {
std::string ZoneList(const std::vector<uint32_t>& zones) { std::string ZoneList(const std::vector<uint32_t>& zones) {
if (zones.empty()) return "every world"; if (zones.empty()) return "every world";
std::string text; std::string text;
const auto& names = ZoneNames(); const auto& names = GameText::ZoneNames();
for (const auto zone : zones) { for (const auto zone : zones) {
const auto key = std::to_string(zone); const auto key = std::to_string(zone);
text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key); text += (text.empty() ? "" : ", ") + (names.contains(key) ? names[key].get<std::string>() : "zone " + key);
@@ -58,7 +59,7 @@ namespace {
nlohmann::json ZoneOptions() { nlohmann::json ZoneOptions() {
std::vector<std::pair<uint32_t, std::string>> zones; std::vector<std::pair<uint32_t, std::string>> zones;
for (const auto& [id, name] : ZoneNames().items()) { for (const auto& [id, name] : GameText::ZoneNames().items()) {
if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>()); if (const auto zone = GeneralUtils::TryParse<uint32_t>(id); zone && *zone > 0) zones.emplace_back(*zone, name.get<std::string>());
} }
std::sort(zones.begin(), zones.end()); 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; } if (event.message.size() > MAX_MESSAGE) { error = "The message is up to 300 characters"; return false; }
event.zones.clear(); event.zones.clear();
if (body.contains("zones") && body["zones"].is_array()) { if (body.contains("zones") && body["zones"].is_array()) {
const auto& known = ZoneNames(); const auto& known = GameText::ZoneNames();
for (const auto& zone : body["zones"]) { for (const auto& zone : body["zones"]) {
if (!zone.is_number_unsigned() || zone.get<uint32_t>() == 0 || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return false; } if (!zone.is_number_unsigned() || zone.get<uint32_t>() == 0 || !known.contains(std::to_string(zone.get<uint32_t>()))) { error = "Pick zones from the list"; return false; }
if (std::find(event.zones.begin(), event.zones.end(), zone.get<uint32_t>()) == event.zones.end()) event.zones.push_back(zone.get<uint32_t>()); if (std::find(event.zones.begin(), event.zones.end(), zone.get<uint32_t>()) == event.zones.end()) event.zones.push_back(zone.get<uint32_t>());

View File

@@ -1,4 +1,5 @@
#include "LiveUpdateRoutes.h" #include "LiveUpdateRoutes.h"
#include "GameText.h"
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
@@ -29,7 +30,6 @@ namespace {
nlohmann::json ToJson(const LiveUpdateStatus& status) { nlohmann::json ToJson(const LiveUpdateStatus& status) {
using namespace LiveUpdate; using namespace LiveUpdate;
const auto& zones = ZoneNames();
nlohmann::json units = nlohmann::json::array(); nlohmann::json units = nlohmann::json::array();
size_t done = 0, failed = 0; size_t done = 0, failed = 0;
for (const auto& unit : status.units) { for (const auto& unit : status.units) {
@@ -37,9 +37,8 @@ namespace {
if (unit.state == eUnitState::FAILED) failed++; if (unit.state == eUnitState::FAILED) failed++;
nlohmann::json row{ {"kind", KindName(unit.kind)}, {"state", StateName(unit.state)}, {"message", unit.message} }; nlohmann::json row{ {"kind", KindName(unit.kind)}, {"state", StateName(unit.state)}, {"message", unit.message} };
if (unit.kind == eUnitKind::WORLD) { if (unit.kind == eUnitKind::WORLD) {
const auto zone = std::to_string(unit.zoneId);
row["zone"] = unit.zoneId; row["zone"] = unit.zoneId;
row["zoneName"] = unit.zoneId == 0 ? "Character Select" : zones.contains(zone) ? zones[zone].get<std::string>() : "Zone " + zone; row["zoneName"] = GameText::ZoneName(unit.zoneId);
row["instance"] = unit.instanceId; row["instance"] = unit.instanceId;
row["clone"] = unit.cloneId; row["clone"] = unit.cloneId;
row["replacement"] = unit.replacement; row["replacement"] = unit.replacement;

View File

@@ -1,4 +1,5 @@
#include "LiveWorld.h" #include "LiveWorld.h"
#include "GameText.h"
#include "MasterPackets.h" #include "MasterPackets.h"
#include "WorldView.h" #include "WorldView.h"
#include "Permissions.h" #include "Permissions.h"
@@ -250,13 +251,13 @@ namespace LiveWorld {
nlohmann::json worlds = nlohmann::json::array(); nlohmann::json worlds = nlohmann::json::array();
std::lock_guard lock(ServerState::g_StatusMutex); std::lock_guard lock(ServerState::g_StatusMutex);
for (const auto& w : ServerState::g_WorldInstances) { 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}, {"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} }); {"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} });
} }
// Launched but not connected yet, or shutting down // Launched but not connected yet, or shutting down
for (const auto& w : ServerState::g_PendingWorlds) { 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}, {"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} }); {"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", Route(eHTTPMethod::GET, "/api/players/online", Perm("players_view"), "Everyone online: character, account, GM level, world and position",
[](HTTPReply& reply, const HTTPContext&) { [](HTTPReply& reply, const HTTPContext&) {
auto players = PlayersJson(); auto players = PlayersJson();
const auto& zones = ZoneNames();
for (auto& player : players) { for (auto& player : players) {
const auto zone = std::to_string(player.value("zone", 0)); player["zone_name"] = GameText::ZoneName(player.value("zone", 0u));
player["zone_name"] = zones.contains(zone) ? zones[zone].get<std::string>() : "Zone " + zone;
const auto id = GeneralUtils::TryParse<LWOOBJID>(player.value("id", std::string{})).value_or(0); const auto id = GeneralUtils::TryParse<LWOOBJID>(player.value("id", std::string{})).value_or(0);
if (const auto info = Database::Get()->GetCharacterInfo(id)) { if (const auto info = Database::Get()->GetCharacterInfo(id)) {
player["account_id"] = info->accountId; player["account_id"] = info->accountId;

View File

@@ -1,4 +1,5 @@
#include "MissionTools.h" #include "MissionTools.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <ctime> #include <ctime>
@@ -12,7 +13,6 @@
#include "WSRoutes.h" #include "WSRoutes.h"
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "Database.h" #include "Database.h"
#include "Locale.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
#include "eHTTPMethod.h" #include "eHTTPMethod.h"
@@ -72,12 +72,12 @@ namespace MissionCatalog {
} }
std::string Name(uint32_t id) { 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; return name.empty() ? "Mission " + std::to_string(id) : name;
} }
std::string TaskText(uint32_t uid) { std::string TaskText(uint32_t uid) {
return Locale::GetPhrase("MissionTasks_" + std::to_string(uid) + "_description"); return GameText::Phrase(GameText::Key("MissionTasks", uid, "description"));
} }
} }

View File

@@ -1,4 +1,5 @@
#include "ModerationTools.h" #include "ModerationTools.h"
#include "GameText.h"
#include "ChatFilterWords.h" #include "ChatFilterWords.h"
#include <ctime> #include <ctime>
@@ -73,7 +74,7 @@ namespace {
nlohmann::json ReportJson(const IModeration::PlayerReport& r, Names& names) { nlohmann::json ReportJson(const IModeration::PlayerReport& r, Names& names) {
const auto kind = magic_enum::enum_cast<ePlayerReportKind>(r.kind); const auto kind = magic_enum::enum_cast<ePlayerReportKind>(r.kind);
const auto status = static_cast<ePlayerReportStatus>(r.status); const auto status = static_cast<ePlayerReportStatus>(r.status);
const auto& zones = ZoneNames(); const auto& zones = GameText::ZoneNames();
const auto zone = std::to_string(r.zoneId); const auto zone = std::to_string(r.zoneId);
std::string propertyName; std::string propertyName;
if (r.propertyId) { if (r.propertyId) {
@@ -387,7 +388,7 @@ namespace {
query.blockedOnly = allowed; query.blockedOnly = allowed;
query.newestFirst = true; query.newestFirst = true;
query.limit = CHECK_MESSAGES; query.limit = CHECK_MESSAGES;
const auto& zones = ZoneNames(); const auto& zones = GameText::ZoneNames();
nlohmann::json messages = nlohmann::json::array(); nlohmann::json messages = nlohmann::json::array();
uint32_t searched = 0; uint32_t searched = 0;
for (const auto& m : Database::Get()->GetChatMessages(query)) { for (const auto& m : Database::Get()->GetChatMessages(query)) {

View File

@@ -1,4 +1,5 @@
#include "ModeratorHelper.h" #include "ModeratorHelper.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <condition_variable> #include <condition_variable>
@@ -197,11 +198,6 @@ namespace {
return info ? info->name : ""; 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<std::string>() : "Zone " + key;
}
ModeratorPrompt::ChatLine Line(const IChatLog::ChatMessage& m, uint64_t subjectId, LWOOBJID subjectSender) { 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 }; 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["sent"] = ModeratorPrompt::FormatTime(report->createdAt);
item["reporter"] = CharacterName(report->reporterId); item["reporter"] = CharacterName(report->reporterId);
item["reported_player"] = report->targetCharacterId ? CharacterName(report->targetCharacterId) : "unknown"; 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->objectLot && (!kind || *kind != ePlayerReportKind::PLAYER)) item["model"] = ClientAssets::ItemName(report->objectLot);
if (report->propertyId) { if (report->propertyId) {
if (const auto property = Database::Get()->GetPropertyInfo(report->propertyId)) item["property"] = property->name; if (const auto property = Database::Get()->GetPropertyInfo(report->propertyId)) item["property"] = property->name;
@@ -339,7 +335,7 @@ namespace {
item["sender"] = message->senderName; item["sender"] = message->senderName;
item["channel"] = message->channel; item["channel"] = message->channel;
if (!message->recipientName.empty()) item["to"] = message->recipientName; 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["when"] = ModeratorPrompt::FormatTime(message->time);
item["message"] = message->message; item["message"] = message->message;
item["stopped_by_filter"] = message->blocked; item["stopped_by_filter"] = message->blocked;

View File

@@ -1,4 +1,5 @@
#include "PublicRoutes.h" #include "PublicRoutes.h"
#include "GameText.h"
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -79,7 +80,7 @@ namespace {
std::lock_guard lock(ServerState::g_StatusMutex); std::lock_guard lock(ServerState::g_StatusMutex);
loginUp = ServerState::g_AuthStatus.online; loginUp = ServerState::g_AuthStatus.online;
chatUp = ServerState::g_ChatStatus.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()} }; nlohmann::json status{ {"name", ServerName()}, {"online", loginUp}, {"generated_at", now}, {"refresh_seconds", CacheSeconds()} };

View File

@@ -1,4 +1,5 @@
#include "ReportRoutes.h" #include "ReportRoutes.h"
#include "GameText.h"
#include "OnceCache.h" #include "OnceCache.h"
#include <deque> #include <deque>
#include <future> #include <future>
@@ -385,8 +386,8 @@ namespace {
return stats; return stats;
} }
// Where a zone's files are (its name comes from the locale: GameText::ZoneName)
struct ZoneInfo { struct ZoneInfo {
std::string name;
std::string luzPath; // relative to res/maps std::string luzPath; // relative to res/maps
}; };
@@ -414,11 +415,10 @@ namespace {
std::map<uint32_t, ZoneInfo> ReadZoneTable() { std::map<uint32_t, ZoneInfo> ReadZoneTable() {
std::map<uint32_t, ZoneInfo> zones; std::map<uint32_t, ZoneInfo> zones;
try { 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()) { for (; !result.eof(); result.nextRow()) {
const std::string description = result.getStringField("DisplayDescription", "");
const auto zoneId = static_cast<uint32_t>(result.getIntField("zoneID", 0)); const auto zoneId = static_cast<uint32_t>(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) { } catch (const std::exception& ex) {
LOG("Could not read the zone table: %s", ex.what()); LOG("Could not read the zone table: %s", ex.what());
@@ -959,7 +959,7 @@ void RegisterReportRoutes() {
WithItemNames(history); WithItemNames(history);
for (auto& hop : history) { for (auto& hop : history) {
const auto zone = GetZoneInfo(hop.value("zone", 0u)); 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(); const auto names = CharacterNames();
std::set<std::string> latest; std::set<std::string> latest;
@@ -1066,7 +1066,7 @@ void RegisterReportRoutes() {
if (zone.is_null()) { if (zone.is_null()) {
const auto info = GetZoneInfo(zoneId); const auto info = GetZoneInfo(zoneId);
const bool property = EconomyPlaces::IsPropertyZone(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(); if (property) zone["properties"] = nlohmann::json::array();
} }
zone["events"] = zone["events"].get<int64_t>() + events; zone["events"] = zone["events"].get<int64_t>() + events;
@@ -1107,7 +1107,7 @@ void RegisterReportRoutes() {
} }
const auto info = GetZoneInfo(*zone); const auto info = GetZoneInfo(*zone);
nlohmann::json response{ 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<int>(IEconomyLedger::MAP_CELL_SIZE)}, {"lots", lots}, {"cells", Database::Get()->GetMapCells(*zone, clone, *kind, from, to, lot)} {"cellSize", static_cast<int>(IEconomyLedger::MAP_CELL_SIZE)}, {"lots", lots}, {"cells", Database::Get()->GetMapCells(*zone, clone, *kind, from, to, lot)}
}; };
if (EconomyPlaces::IsPropertyZone(*zone)) { if (EconomyPlaces::IsPropertyZone(*zone)) {
@@ -1159,7 +1159,7 @@ void RegisterReportRoutes() {
const auto describe = [&owners](nlohmann::json& row) { const auto describe = [&owners](nlohmann::json& row) {
const auto zoneId = row.value("zone", 0u); const auto zoneId = row.value("zone", 0u);
const auto zone = GetZoneInfo(zoneId); 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; if (!EconomyPlaces::IsPropertyZone(zoneId)) return;
const auto info = owners.Info(zoneId, row.value("clone", 0u)); const auto info = owners.Info(zoneId, row.value("clone", 0u));
row["property"] = true; row["property"] = true;

View File

@@ -1,4 +1,5 @@
#include "ServerRoutes.h" #include "ServerRoutes.h"
#include "GameText.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
@@ -365,7 +366,7 @@ void RegisterServerRoutes() {
const auto filter = BundleFilter(reply, context); const auto filter = BundleFilter(reply, context);
if (!filter) return; if (!filter) return;
const auto files = LogBundle::Select(LogFolder(), DumpFolder(), *filter); const auto files = LogBundle::Select(LogFolder(), DumpFolder(), *filter);
const auto& zoneNames = ZoneNames(); const auto& zoneNames = GameText::ZoneNames();
nlohmann::json list = nlohmann::json::array(); nlohmann::json list = nlohmann::json::array();
uint64_t total = 0; uint64_t total = 0;
for (const auto& file : files) { for (const auto& file : files) {

View File

@@ -1,4 +1,5 @@
#include "SettingsRoutes.h" #include "SettingsRoutes.h"
#include "GameText.h"
#include "MasterPackets.h" #include "MasterPackets.h"
#include "Permissions.h" #include "Permissions.h"
#include "SettingsCatalog.h" #include "SettingsCatalog.h"
@@ -63,7 +64,8 @@ namespace {
{"title", info ? info->title : name}, {"section", info ? info->section : "Custom settings"}, {"title", info ? info->title : name}, {"section", info ? info->section : "Custom settings"},
{"category", section ? section->category : "other"}, {"category", section ? section->category : "other"},
// Known settings use the catalog's description (the .ini comments are for people editing the file) // 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 : ""}, {"type", info ? SettingsCatalog::TypeName(info->type) : "text"}, {"default", info ? info->defaultValue : ""},
{"unit", info ? info->unit : ""}, {"format", info ? SettingsCatalog::FormatName(info->format) : "plain"}, {"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)}, {"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 // Names for the numbers in a list setting: zones, items or reward codes
nlohmann::json ListNames(const std::string& of, const std::vector<int64_t>& ids) { nlohmann::json ListNames(const std::string& of, const std::vector<int64_t>& ids) {
nlohmann::json names = nlohmann::json::object(); nlohmann::json names = nlohmann::json::object();
const auto& zones = ZoneNames(); const auto& zones = GameText::ZoneNames();
for (const auto id : ids) { for (const auto id : ids) {
const auto key = std::to_string(id); const auto key = std::to_string(id);
if (of == "zone") { if (of == "zone") {
@@ -128,14 +130,14 @@ namespace {
nlohmann::json options = nlohmann::json::array(); nlohmann::json options = nlohmann::json::array();
if (of == "zone") { if (of == "zone") {
std::vector<std::pair<int64_t, std::string>> zones; std::vector<std::pair<int64_t, std::string>> zones;
for (const auto& [id, name] : ZoneNames().items()) { for (const auto& [id, name] : GameText::ZoneNames().items()) {
if (const auto zoneId = GeneralUtils::TryParse<int64_t>(id); zoneId && *zoneId > 0) zones.emplace_back(*zoneId, name.get<std::string>()); if (const auto zoneId = GeneralUtils::TryParse<int64_t>(id); zoneId && *zoneId > 0) zones.emplace_back(*zoneId, name.get<std::string>());
} }
std::sort(zones.begin(), zones.end()); std::sort(zones.begin(), zones.end());
for (const auto& [id, name] : zones) options.push_back({ {"id", id}, {"label", name}, {"detail", ""} }); for (const auto& [id, name] : zones) options.push_back({ {"id", id}, {"label", name}, {"detail", ""} });
} else if (of == "reward_code") { } else if (of == "reward_code") {
// What the client does with the two codes that don't just give an item // What the client does with the two codes that don't just give an item
static const std::map<int64_t, std::string> effects{ { 4, "Opens LEGO Club" }, { 30, "Bricks aren't used up in build mode" } }; const std::map<int64_t, std::string> 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;"); auto result = CDClientDatabase::ExecuteQuery("SELECT id, code, attachmentLOT FROM RewardCodes ORDER BY id;");
for (; !result.eof(); result.nextRow()) { for (; !result.eof(); result.nextRow()) {
const int64_t id = result.getIntField(0); const int64_t id = result.getIntField(0);
@@ -375,11 +377,11 @@ void RegisterSettingsRoutes() {
nlohmann::json files = nlohmann::json::array(); nlohmann::json files = nlohmann::json::array();
for (const auto& [file, name] : SettingsCatalog::Files()) files.push_back({ {"file", file}, {"name", name} }); for (const auto& [file, name] : SettingsCatalog::Files()) files.push_back({ {"file", file}, {"name", name} });
nlohmann::json categories = nlohmann::json::array(); 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."} }); 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(); nlohmann::json sections = nlohmann::json::array();
for (const auto& section : SettingsCatalog::Sections()) { 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)} }); {"layout", SettingsCatalog::LayoutName(section.layout)}, {"condition", ConditionJson(section.condition)} });
} }
sections.push_back({ {"category", "other"}, {"name", "Custom settings"}, {"description", ""}, {"layout", "rows"}, {"condition", nullptr} }); sections.push_back({ {"category", "other"}, {"name", "Custom settings"}, {"description", ""}, {"layout", "rows"}, {"condition", nullptr} });

View File

@@ -1,4 +1,5 @@
#include "Showcase.h" #include "Showcase.h"
#include "GameText.h"
#include "Workers.h" #include "Workers.h"
#include <chrono> #include <chrono>
@@ -88,24 +89,19 @@ namespace {
return info; 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<std::string>() : "Zone " + key;
}
// The zones properties can be claimed in (PropertyTemplate), for the zone filter // The zones properties can be claimed in (PropertyTemplate), for the zone filter
const nlohmann::json& PropertyZones() { nlohmann::json PropertyZones() {
static const nlohmann::json zones = [] { static const std::vector<uint32_t> ids = [] {
nlohmann::json list = nlohmann::json::array(); std::vector<uint32_t> ids;
auto result = CDClientDatabase::ExecuteQuery("SELECT DISTINCT mapID FROM PropertyTemplate ORDER BY mapID;"); auto result = CDClientDatabase::ExecuteQuery("SELECT DISTINCT mapID FROM PropertyTemplate ORDER BY mapID;");
for (; !result.eof(); result.nextRow()) { for (; !result.eof(); result.nextRow()) ids.push_back(static_cast<uint32_t>(result.getIntField(0)));
const auto id = static_cast<uint32_t>(result.getIntField(0)); return ids;
list.push_back({ {"id", id}, {"name", ZoneName(id)} });
}
return list;
}(); }();
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<const std::map<LWOOBJID, IPropertyContents::Model>> Models(LWOOBJID propertyId) { std::shared_ptr<const std::map<LWOOBJID, IPropertyContents::Model>> Models(LWOOBJID propertyId) {
@@ -139,7 +135,7 @@ namespace {
{"description", info.description}, {"description", info.description},
{"owner_name", owner ? owner->name : ""}, {"owner_name", owner ? owner->name : ""},
{"zone_id", info.zoneId}, {"zone_id", info.zoneId},
{"zone_name", ZoneName(info.zoneId)}, {"zone_name", GameText::ZoneName(info.zoneId)},
{"reputation", info.reputation}, {"reputation", info.reputation},
{"last_updated", info.lastUpdatedTime}, {"last_updated", info.lastUpdatedTime},
{"models", models} {"models", models}
@@ -233,7 +229,7 @@ void RegisterShowcaseRoutes() {
{"description", entry.info.description}, {"description", entry.info.description},
{"owner_name", entry.ownerName}, {"owner_name", entry.ownerName},
{"zone_id", entry.info.zoneId}, {"zone_id", entry.info.zoneId},
{"zone_name", ZoneName(entry.info.zoneId)}, {"zone_name", GameText::ZoneName(entry.info.zoneId)},
{"models", entry.modelCount}, {"models", entry.modelCount},
{"reputation", entry.info.reputation}, {"reputation", entry.info.reputation},
{"last_updated", entry.info.lastUpdatedTime} {"last_updated", entry.info.lastUpdatedTime}

View File

@@ -16,6 +16,7 @@
#include "NetworkView.h" #include "NetworkView.h"
#include "Permissions.h" #include "Permissions.h"
#include "RouteUtils.h" #include "RouteUtils.h"
#include "GameText.h"
#include "ServerState.h" #include "ServerState.h"
#include "ServiceType.h" #include "ServiceType.h"
#include "TrafficHistory.h" #include "TrafficHistory.h"
@@ -107,13 +108,10 @@ namespace {
case ServiceType::DASHBOARD: return "Dashboard"; case ServiceType::DASHBOARD: return "Dashboard";
case ServiceType::UGC: return "UGC"; case ServiceType::UGC: return "UGC";
case ServiceType::WORLD: { case ServiceType::WORLD: {
std::string 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();
std::lock_guard lock(ServerState::g_StatusMutex); const auto known = names.find(std::to_string(server.zoneId));
for (const auto& world : ServerState::g_WorldInstances) { const std::string zoneName = known != names.end() && known->is_string() ? known->get<std::string>() : "";
if (world.mapID == server.zoneId && world.instanceID == server.instanceId) zoneName = world.zoneName;
}
}
return "World " + std::to_string(server.zoneId) + (zoneName.empty() ? "" : " " + zoneName) + " #" + std::to_string(server.instanceId); return "World " + std::to_string(server.zoneId) + (zoneName.empty() ? "" : " " + zoneName) + " #" + std::to_string(server.instanceId);
} }
default: return server.key; default: return server.key;

View File

@@ -1,4 +1,5 @@
#include "VanityRoutes.h" #include "VanityRoutes.h"
#include "GameText.h"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -31,7 +32,7 @@ namespace {
// The markdown files the game reads, and where players see them // The markdown files the game reads, and where players see them
const std::vector<std::pair<std::string, std::string>> TEXTS{ const std::vector<std::pair<std::string, std::string>> 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" }, { "CREDITS.md", "Shown by the /credits command" },
{ "INFO.md", "Shown by the /info command; __VERSION__, __SOURCE__, __LICENSE__ and __TIMESTAMP__ are filled in" }, { "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); files.push_back(entry);
} }
nlohmann::json texts = nlohmann::json::array(); 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(); 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<uint32_t>(id); zone && *zone > 0) zones.push_back({ {"id", *zone}, {"name", name} }); if (const auto zone = GeneralUtils::TryParse<uint32_t>(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<uint32_t>() < b["id"].template get<uint32_t>(); }); std::sort(zones.begin(), zones.end(), [](const auto& a, const auto& b) { return a["id"].template get<uint32_t>() < b["id"].template get<uint32_t>(); });

View File

@@ -1,4 +1,5 @@
#include "WorldView.h" #include "WorldView.h"
#include "GameText.h"
#include "PositionHistory.h" #include "PositionHistory.h"
#include "WorldScene.h" #include "WorldScene.h"
#include "Scenery.h" #include "Scenery.h"
@@ -78,11 +79,6 @@ namespace {
// ---- Zone data ---- // ---- 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<std::string>() : "Zone " + key;
}
std::optional<std::string> LuzPath(uint32_t zone) { std::optional<std::string> LuzPath(uint32_t zone) {
return ZoneLuzPath(zone); return ZoneLuzPath(zone);
@@ -158,7 +154,7 @@ namespace {
} }
nlohmann::json json{ 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) }}, {"spawn", { Round(zoneFile->spawnpoint.x), Round(zoneFile->spawnpoint.y), Round(zoneFile->spawnpoint.z) }},
{"scenes", scenes}, {"kinds", kinds}, {"scenes", scenes}, {"kinds", kinds},
{"objects", { {"lot", lots}, {"kind", kindOf}, {"flags", flags}, {"scene", sceneOf}, {"pos", positions}, {"names", names} }}, {"lotNames", lotNames}, {"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(); nlohmann::json mapKinds = nlohmann::json::array();
for (const auto kind : magic_enum::enum_values<IEconomyLedger::eMapEvent>()) mapKinds.push_back({ {"value", static_cast<int>(kind)}, {"name", GameLabels::Name(kind)} }); for (const auto kind : magic_enum::enum_values<IEconomyLedger::eMapEvent>()) mapKinds.push_back({ {"value", static_cast<int>(kind)}, {"name", GameLabels::Name(kind)} });
JsonReply(reply, eHTTPStatusCode::OK, { 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()}, {"propertyZones", EconomyPlaces::PropertyZones()},
{"today", Now() / DAY_SECONDS}, {"now", Now()}, {"today", Now() / DAY_SECONDS}, {"now", Now()},
{"history", { {"enabled", Setting("position_history", 1) != 0}, {"days", Setting("position_history_days", 3)}, {"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); const auto from = QueryInt(context, "from", 0);
nlohmann::json instances = nlohmann::json::array(); nlohmann::json instances = nlohmann::json::array();
for (const auto& i : Database::Get()->GetPositionInstances(zone, from, to)) { 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} }); {"first", i.first}, {"last", i.last}, {"players", i.players} });
} }
JsonReply(reply, eHTTPStatusCode::OK, { {"instances", instances} }); 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} }); 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)); std::to_string((to - from + 59) / 60) + " minute(s) ending " + std::to_string(to));
JsonReply(reply, eHTTPStatusCode::OK, { JsonReply(reply, eHTTPStatusCode::OK, {
{"zone", zone}, {"instance", instance}, {"from", from}, {"to", to}, {"bucket", bucket}, {"interval", interval}, {"idleSeconds", IDLE_INTERVAL}, {"zone", zone}, {"instance", instance}, {"from", from}, {"to", to}, {"bucket", bucket}, {"interval", interval}, {"idleSeconds", IDLE_INTERVAL},