chore(dashboard): remove the Maintenance page

The page held one-off repairs from Nexus Dashboard (approve known pet
names, find/delete orphaned pet names, remove every buff, fix property
clone IDs, list mission rewards without a commendation price). Removed
the page, its sidebar entry, its /api/maintenance/* routes, the
`maintenance` permission and the two database calls only it used
(GetAllPetNames, FixPropertyCloneIds). The scheduled tasks (lift expired
bans, fill in pet owners, approve known pet names) and property model
import/removal stay.

Check: the Admin sidebar group has no Maintenance link; /maintenance is
a 404; the Tasks page still lists "Approve known pet names" and "Fill in
pet owners"; property import still works; the Permissions page no
longer lists "Data maintenance".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 23:19:41 -05:00
parent 7440e5744d
commit b0dab03807
14 changed files with 5 additions and 297 deletions

View File

@@ -102,7 +102,6 @@ namespace {
{ "events_manage", "Server", "Scheduled events: features", "Scheduled events that switch game features (event_1..event_8) on and off", 8 },
{ "server_live_update", "Server", "Live updates", "Move every server and world instance onto a new build without a restart (players see a short loading screen), and cancel one", 9 },
{ "instances_manage", "Server", "Instance limits", "Change players per instance and spare instances per zone (the master server applies them)", 9 },
{ "maintenance", "Server", "Data maintenance", "Repair tools on the Maintenance page", 9 },
{ "backups", "Server", "Database backups", "Make, download and delete database backups (downloads also need your password)", 9 },
{ "webhooks", "Server", "Webhooks", "Add and change outgoing alert webhooks", 9 },
{ "email_settings", "Server", "Email settings", "Connect the mail account and send test emails", 9 },

View File

@@ -443,7 +443,6 @@ void RegisterDashboardRoutes() {
SimplePage("/pet_names", Perm("moderate_pet_names"), "pet_names.jinja2", "pet_names", "Pet names");
SimplePage("/send_mail", Perm("mail_send"), "send_mail.jinja2", "send_mail", "Send mail");
SimplePage("/api_docs", 0, "api_docs.jinja2", "api_docs", "API documentation");
SimplePage("/maintenance", Perm("maintenance"), "maintenance.jinja2", "maintenance", "Data maintenance tools");
SimplePage("/webhooks", Perm("webhooks"), "webhooks.jinja2", "webhooks", "Outgoing webhooks for alerts");
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&) {

View File

@@ -1,8 +1,6 @@
#include "MaintenanceRoutes.h"
#include <chrono>
#include <map>
#include <set>
#include <sstream>
#include "RouteUtils.h"
@@ -10,10 +8,7 @@
#include "WSRoutes.h"
#include "Background.h"
#include "Scheduler.h"
#include "PlayerActions.h"
#include "ClientAssets.h"
#include "Database.h"
#include "CDClientDatabase.h"
#include "Game.h"
#include "Logger.h"
#include "eHTTPMethod.h"
@@ -30,11 +25,6 @@ namespace {
// Requests are capped at 3 MB by the web server, and JSON escaping makes the body bigger than the LXFML
constexpr size_t MAX_IMPORT_BYTES = 2 * 1024 * 1024;
bool AnyoneOnline() {
const auto state = ServerState::GetServerStateJson();
return state["stats"].value("onlinePlayers", 0u) > 0;
}
// Every pet in every inventory: pet id -> the character holding it
std::map<LWOOBJID, LWOOBJID> PetHolders(GameDatabase& db, size_t& characters) {
std::map<LWOOBJID, LWOOBJID> holders;
@@ -103,132 +93,6 @@ void RegisterMaintenanceTasks() {
}
void RegisterMaintenanceRoutes() {
Route(eHTTPMethod::POST, "/api/maintenance/pet_names/auto_approve", Perm("maintenance"), "Approve pending pet names that were approved for another pet before",
[](HTTPReply& reply, const HTTPContext& context) {
const auto approved = Database::Get()->ApprovePreviouslyApprovedPetNames();
Audit(context, "auto_approve_pet_names", std::to_string(approved) + " pet name(s)");
if (approved) BroadcastTableChanged("pet_names");
JsonSuccess(reply, { {"message", "Approved " + std::to_string(approved) + " pet name(s)"} });
});
Route(eHTTPMethod::POST, "/api/maintenance/pet_names/orphans", Perm("maintenance"),
"Find (or with {delete: true}, remove) pet names whose pet no longer exists. Runs in the background: returns {requestId}",
[](HTTPReply& reply, const HTTPContext& context) {
const auto body = ParseBody(context);
const bool remove = body && body->value("delete", false);
const auto requestId = PlayerActions::Begin(context.accountId, std::chrono::minutes(30));
const auto actor = context;
const bool queued = Background::Run("pet_name_orphans", [remove](GameDatabase& db) -> nlohmann::json {
size_t characters = 0;
const auto holders = PetHolders(db, characters);
const auto names = db.GetAllPetNames();
nlohmann::json orphans = nlohmann::json::array();
for (const auto& [id, name] : names) {
if (!holders.contains(id)) orphans.push_back({ {"id", std::to_string(id)}, {"name", name} });
}
nlohmann::json result{
{"characters", characters}, {"petsFound", holders.size()}, {"petNames", names.size()},
{"orphaned", orphans.size()}, {"sample", nlohmann::json(orphans.begin(), orphans.begin() + std::min<size_t>(orphans.size(), 20))}
};
if (!remove) return result;
// A mismatch in how ids are stored would make every name look orphaned; never delete in that case
if (!names.empty() && orphans.size() * 2 > names.size()) {
result["refused"] = "Refusing to delete: more than half of all pet names look orphaned, which suggests an ID mismatch rather than real orphans";
return result;
}
for (const auto& orphan : orphans) db.RejectPetName(std::stoll(orphan["id"].get<std::string>()));
result["deleted"] = orphans.size();
return result;
}, [requestId, actor](nlohmann::json result, const std::string& error) {
if (!error.empty()) return PlayerActions::Finish(requestId, { false, "The scan failed: " + error });
if (result.contains("refused")) return PlayerActions::Finish(requestId, { false, result["refused"].get<std::string>(), result });
if (result.contains("deleted")) {
Audit(actor, "delete_orphaned_pet_names", std::to_string(result["deleted"].get<size_t>()) + " pet name(s)");
BroadcastTableChanged("pet_names");
}
PlayerActions::Finish(requestId, { true, std::to_string(result["orphaned"].get<size_t>()) + " orphaned pet name(s)" +
(result.contains("deleted") ? ", deleted" : ""), result });
});
if (!queued) {
PlayerActions::Finish(requestId, { false, "This scan is already running" });
return JsonError(reply, eHTTPStatusCode::CONFLICT, "This scan is already running");
}
JsonSuccess(reply, { {"requestId", requestId} });
});
Route(eHTTPMethod::POST, "/api/maintenance/remove_buffs", Perm("maintenance"),
"Remove every buff from every character (only while nobody is online). Body: {dry_run}. Runs in the background: returns {requestId}",
[](HTTPReply& reply, const HTTPContext& context) {
const auto body = ParseBody(context);
const bool dryRun = !body || body->value("dry_run", true);
if (!dryRun && AnyoneOnline()) return JsonError(reply, eHTTPStatusCode::CONFLICT, "Players are online; their worlds would overwrite this. Try again when nobody is online.");
const auto requestId = PlayerActions::Begin(context.accountId, std::chrono::minutes(30));
const auto actor = context;
const bool queued = Background::Run("remove_buffs", [dryRun](GameDatabase& db) -> nlohmann::json {
// First pass only records ids so no writes happen while the cursor is open
std::vector<LWOOBJID> withBuffs;
db.ForEachCharacterXml([&](LWOOBJID id, const std::string& xml) {
if (xml.find("<buff") != std::string::npos) withBuffs.push_back(id);
});
if (dryRun) return { {"characters", withBuffs.size()}, {"dryRun", true} };
uint32_t changed = 0;
for (const auto id : withBuffs) {
tinyxml2::XMLDocument doc;
const auto xml = db.GetCharacterXml(id);
if (doc.Parse(xml.c_str()) != tinyxml2::XML_SUCCESS) continue;
auto* dest = doc.FirstChildElement("obj") ? doc.FirstChildElement("obj")->FirstChildElement("dest") : nullptr;
auto* buff = dest ? dest->FirstChildElement("buff") : nullptr;
if (!buff) continue;
dest->DeleteChild(buff);
tinyxml2::XMLPrinter printer(nullptr, true);
doc.Print(&printer);
db.UpdateCharacterXml(id, printer.CStr());
changed++;
}
return { {"characters", changed}, {"dryRun", false} };
}, [requestId, actor](nlohmann::json result, const std::string& error) {
if (!error.empty()) return PlayerActions::Finish(requestId, { false, "Failed: " + error });
const auto count = result["characters"].get<size_t>();
if (result["dryRun"].get<bool>()) return PlayerActions::Finish(requestId, { true, std::to_string(count) + " character(s) have buffs", result });
Audit(actor, "remove_all_buffs", std::to_string(count) + " character(s)");
PlayerActions::Finish(requestId, { true, "Removed buffs from " + std::to_string(count) + " character(s)", result });
});
if (!queued) {
PlayerActions::Finish(requestId, { false, "This is already running" });
return JsonError(reply, eHTTPStatusCode::CONFLICT, "This is already running");
}
JsonSuccess(reply, { {"requestId", requestId} });
});
Route(eHTTPMethod::POST, "/api/maintenance/fix_clone_ids", Perm("maintenance"), "Repair properties whose clone ID doesn't match their owner's",
[](HTTPReply& reply, const HTTPContext& context) {
const auto fixed = Database::Get()->FixPropertyCloneIds();
Audit(context, "fix_property_clone_ids", std::to_string(fixed) + " propert(ies)");
if (fixed) BroadcastTableChanged("properties");
JsonSuccess(reply, { {"fixed", fixed}, {"message", "Fixed " + std::to_string(fixed) + " propert" + (fixed == 1 ? "y" : "ies")} });
});
Route(eHTTPMethod::GET, "/api/maintenance/missing_commendation_items", Perm("maintenance"), "Mission reward items with no commendation vendor price (game data check)",
[](HTTPReply& reply, const HTTPContext& context) {
auto rows = CDClientDatabase::ExecuteQuery(
"SELECT DISTINCT r.lot FROM ("
"SELECT reward_item1 AS lot FROM Missions UNION SELECT reward_item2 FROM Missions "
"UNION SELECT reward_item3 FROM Missions UNION SELECT reward_item4 FROM Missions) r "
"LEFT JOIN ComponentsRegistry cr ON cr.id = r.lot AND cr.component_type = 11 "
"LEFT JOIN ItemComponent ic ON ic.id = cr.component_id "
"WHERE r.lot > 0 AND (ic.commendationLOT IS NULL OR ic.commendationCost IS NULL) ORDER BY r.lot;");
nlohmann::json items = nlohmann::json::array();
while (!rows.eof()) {
const LOT lot = rows.getIntField("lot");
items.push_back({ {"lot", lot}, {"name", ClientAssets::ItemName(lot)} });
rows.nextRow();
}
JsonReply(reply, eHTTPStatusCode::OK, { {"items", items} });
});
Route(eHTTPMethod::POST, "/api/properties/:id/import", Perm("properties_import"), "Import LXFML models onto a property. Body: {lxfml, position: [x,y,z] (optional)}",
[](HTTPReply& reply, const HTTPContext& context) {
const auto propertyId = PathId<LWOOBJID>(context.path, 2);

View File

@@ -1,6 +1,6 @@
#pragma once
// Operator data repairs (maintenance page) and model import
// Property model import and removal
void RegisterMaintenanceRoutes();
// Scheduled tasks (approving pet names that were approved before, filling in pet owners); before Scheduler::Initialize

View File

@@ -118,8 +118,8 @@
</div>
{% endif %}
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "maintenance", "webhooks", "client_assets", "ugc"] %}
{% if can.settings or can.permissions_manage or can.tasks_view or can.play_keys_manage or can.backups or can.maintenance or can.webhooks or can.client_files or can.ugc_manage %}
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "webhooks", "client_assets", "ugc"] %}
{% if can.settings or can.permissions_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files or can.ugc_manage %}
<div class="nav-group">
<button class="nav-group-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#nav-admin" aria-expanded="{% if current_page in adminPages %}true{% else %}false{% endif %}" aria-controls="nav-admin">
Server Admin<span class="nav-group-dot" title="Something in here needs attention"></span>
@@ -131,7 +131,6 @@
{% if can.tasks_view %}<a href="/tasks" class="list-group-item list-group-item-action{% if current_page == "tasks" %} active{% endif %}">Scheduled Tasks</a>{% endif %}
{% if can.play_keys_manage %}<a href="/play_keys" class="list-group-item list-group-item-action{% if current_page == "play_keys" %} active{% endif %}">Play Keys</a>{% endif %}
{% if can.backups %}<a href="/backups" class="list-group-item list-group-item-action{% if current_page == "backups" %} active{% endif %}">Backups</a>{% endif %}
{% if can.maintenance %}<a href="/maintenance" class="list-group-item list-group-item-action{% if current_page == "maintenance" %} active{% endif %}">Maintenance</a>{% endif %}
{% if can.webhooks %}<a href="/webhooks" class="list-group-item list-group-item-action{% if current_page == "webhooks" %} active{% endif %}">Webhooks</a>{% endif %}
{% if can.ugc_manage %}<a href="/ugc" class="list-group-item list-group-item-action{% if current_page == "ugc" %} active{% endif %}">UGC Server</a>{% endif %}
{% if can.client_files %}<a href="/client_assets" class="list-group-item list-group-item-action{% if current_page == "client_assets" %} active{% endif %}">Client Files</a>{% endif %}

View File

@@ -1,108 +0,0 @@
{% extends "base.jinja2" %}
{% block title %}Maintenance - DarkflameServer{% endblock %}
{% block content %}
<h2 class="mb-1">Maintenance</h2>
<p class="text-body-secondary mb-4">Data repairs. Every action is recorded in the audit log. Scans read every character, so they can pause the dashboard for a moment on large servers.</p>
<div class="row g-4">
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Pet names</h5></div>
<div class="card-body">
<p class="small">Pending names that were already approved for another pet are approved automatically every hour. Run it now:</p>
<button class="btn btn-sm btn-outline-primary mb-3" data-action="petApprove">Approve previously approved names</button>
<p class="small">Find pet names whose pet no longer exists on any character (e.g. the pet was released).</p>
<button class="btn btn-sm btn-outline-primary" data-action="petScan">Scan</button>
<button class="btn btn-sm btn-outline-danger d-none" data-action="petDelete" id="petDeleteBtn">Delete orphaned names</button>
<pre class="small mt-2 mb-0 d-none" id="petResult"></pre>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Buffs</h5></div>
<div class="card-body">
<p class="small">Remove every buff from every character, e.g. after a buff got stuck. Only possible while nobody is online.</p>
<button class="btn btn-sm btn-outline-primary" data-action="buffScan">Count characters with buffs</button>
<button class="btn btn-sm btn-outline-danger" data-action="buffRemove">Remove all buffs</button>
<pre class="small mt-2 mb-0 d-none" id="buffResult"></pre>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Property clone IDs</h5></div>
<div class="card-body">
<p class="small">Repair properties whose clone ID doesn't match their owner's (left behind by an old bug), which stops them loading.</p>
<button class="btn btn-sm btn-outline-primary" data-action="fixClones">Fix clone IDs</button>
<pre class="small mt-2 mb-0 d-none" id="cloneResult"></pre>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0">Game data check</h5></div>
<div class="card-body">
<p class="small">Mission reward items that have no commendation vendor price in the game data.</p>
<button class="btn btn-sm btn-outline-primary" data-action="commendation">Find items</button>
<div class="small mt-2" id="commendationResult"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function show(id, text) {
var el = document.getElementById(id);
el.textContent = text;
el.classList.remove('d-none');
}
var actions = {
petApprove: function () { return api.action('/api/maintenance/pet_names/auto_approve', {}).then(function (d) { toast(d.message, 'success'); }); },
petScan: function () {
show('petResult', 'Scanning every character...');
return api.job('/api/maintenance/pet_names/orphans', {}).then(function (d) {
show('petResult', d.characters + ' characters scanned, ' + d.petsFound + ' pets found, ' + d.petNames + ' pet names stored.\n' +
d.orphaned + ' orphaned' + (d.sample.length ? ', e.g.: ' + d.sample.map(function (p) { return p.name; }).join(', ') : ''));
document.getElementById('petDeleteBtn').classList.toggle('d-none', !d.orphaned);
});
},
petDelete: function () {
if (!confirm('Delete the orphaned pet names?')) return Promise.resolve();
return api.job('/api/maintenance/pet_names/orphans', { delete: true }).then(function (d) {
show('petResult', 'Deleted ' + d.deleted + ' orphaned pet name(s).');
document.getElementById('petDeleteBtn').classList.add('d-none');
});
},
buffScan: function () {
show('buffResult', 'Scanning every character...');
return api.job('/api/maintenance/remove_buffs', { dry_run: true }).then(function (d) { show('buffResult', d.characters + ' character(s) have buffs'); });
},
buffRemove: function () {
if (!confirm('Remove all buffs from every character?')) return Promise.resolve();
return api.job('/api/maintenance/remove_buffs', { dry_run: false }).then(function (d) { show('buffResult', 'Removed buffs from ' + d.characters + ' character(s)'); });
},
fixClones: function () { return api.action('/api/maintenance/fix_clone_ids', {}).then(function (d) { show('cloneResult', d.message); }); },
commendation: function () {
return api.get('/api/maintenance/missing_commendation_items').then(function (d) {
var el = document.getElementById('commendationResult');
el.innerHTML = d.items.length
? '<ul class="mb-0">' + d.items.map(function (i) { return '<li>' + esc(i.name) + ' <span class="text-body-secondary">(' + esc(i.lot) + ')</span></li>'; }).join('') + '</ul>'
: 'Every mission reward item has a commendation price.';
});
}
};
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
btn.disabled = true;
actions[btn.getAttribute('data-action')]().catch(function () {}).finally(function () { btn.disabled = false; });
});
</script>
{% endblock %}

View File

@@ -9,23 +9,17 @@
#include "dCommonVars.h"
// Bulk data repairs run from the dashboard's maintenance page
// Bulk data reads and repairs for dashboard reports and scheduled tasks
class IDashboardMaintenance {
public:
// Approve pending pet names that an earlier pet with the same name was approved for. Returns rows changed.
virtual uint32_t ApprovePreviouslyApprovedPetNames() = 0;
// Every pet name as (id, name)
virtual std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() = 0;
// Call visit for every character's XML, one row at a time
virtual void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) = 0;
// Same, but only characters whose XML contains `needle`; the database does the filtering
virtual void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) = 0;
// Set each property's clone id to its owner's prop_clone_id where they differ. Returns rows changed.
virtual uint32_t FixPropertyCloneIds() = 0;
};
#endif //!__IDASHBOARDMAINTENANCE__H__

View File

@@ -333,10 +333,8 @@ public:
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override;
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override;
uint32_t ApprovePreviouslyApprovedPetNames() override;
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override;
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override;
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override;
uint32_t FixPropertyCloneIds() override;
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override;
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override;
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override;

View File

@@ -339,13 +339,6 @@ uint32_t MySQLDatabase::ApprovePreviouslyApprovedPetNames() {
"(SELECT name FROM (SELECT pet_name AS name FROM pet_names WHERE approved = 2) AS approved_names);"));
}
std::vector<std::pair<LWOOBJID, std::string>> MySQLDatabase::GetAllPetNames() {
std::vector<std::pair<LWOOBJID, std::string>> pets;
auto result = ExecuteSelect("SELECT id, pet_name FROM pet_names;");
while (result->next()) pets.emplace_back(result->getInt64("id"), std::string(result->getString("pet_name").c_str()));
return pets;
}
void MySQLDatabase::ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) {
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml;");
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
@@ -355,10 +348,3 @@ void MySQLDatabase::ForEachCharacterXmlContaining(const std::string& needle, con
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml WHERE LOCATE(?, xml_data) > 0;", needle);
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
}
uint32_t MySQLDatabase::FixPropertyCloneIds() {
return static_cast<uint32_t>(ExecuteUpdate(
"UPDATE properties SET clone_id = (SELECT c.prop_clone_id FROM charinfo c WHERE c.id = properties.owner_id) "
"WHERE EXISTS (SELECT 1 FROM charinfo c WHERE c.id = properties.owner_id AND c.prop_clone_id IS NOT NULL "
"AND (properties.clone_id IS NULL OR c.prop_clone_id != properties.clone_id));"));
}

View File

@@ -317,10 +317,8 @@ public:
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override;
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override;
uint32_t ApprovePreviouslyApprovedPetNames() override;
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override;
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override;
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override;
uint32_t FixPropertyCloneIds() override;
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override;
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override;
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override;

View File

@@ -344,16 +344,6 @@ uint32_t SQLiteDatabase::ApprovePreviouslyApprovedPetNames() {
"(SELECT name FROM (SELECT pet_name AS name FROM pet_names WHERE approved = 2) AS approved_names);"));
}
std::vector<std::pair<LWOOBJID, std::string>> SQLiteDatabase::GetAllPetNames() {
std::vector<std::pair<LWOOBJID, std::string>> pets;
auto [_, result] = ExecuteSelect("SELECT id, pet_name FROM pet_names;");
while (!result.eof()) {
pets.emplace_back(result.getInt64Field("id"), result.getStringField("pet_name"));
result.nextRow();
}
return pets;
}
void SQLiteDatabase::ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) {
auto [_, result] = ExecuteSelect("SELECT id, xml_data FROM charxml;");
while (!result.eof()) {
@@ -369,10 +359,3 @@ void SQLiteDatabase::ForEachCharacterXmlContaining(const std::string& needle, co
result.nextRow();
}
}
uint32_t SQLiteDatabase::FixPropertyCloneIds() {
return static_cast<uint32_t>(ExecuteUpdate(
"UPDATE properties SET clone_id = (SELECT c.prop_clone_id FROM charinfo c WHERE c.id = properties.owner_id) "
"WHERE EXISTS (SELECT 1 FROM charinfo c WHERE c.id = properties.owner_id AND c.prop_clone_id IS NOT NULL "
"AND (properties.clone_id IS NULL OR c.prop_clone_id != properties.clone_id));"));
}

View File

@@ -367,10 +367,8 @@ class TestSQLDatabase : public GameDatabase {
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override { return { {"owners", nlohmann::json::array()}, {"properties", nlohmann::json::array()} }; }
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override { return nlohmann::json::array(); }
uint32_t ApprovePreviouslyApprovedPetNames() override { return 0; };
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override { return {}; };
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override {};
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override {};
uint32_t FixPropertyCloneIds() override { return 0; };
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override { return std::nullopt; };
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override {};
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override { return std::nullopt; };

View File

@@ -62,7 +62,7 @@ What people can do depends on their account's GM level. Out of the box:
| 4+ | Ban and lock accounts; email password reset links |
| 5+ | Moderate pet names, properties and leaderboards; revoke strikes; change a character's missions; change the chat filter's words; send and schedule announcements |
| 8+ | Manage accounts (create, change email or password, reset two-factor login) and GM levels; edit characters and replace their XML; give items back; read whispers and team chat; send chat into the game; attach items to mail, to one player or everyone; import models; shut down worlds; schedule restarts and events; run economy checks; play keys, client files and the vanity files; see scheduled tasks, the audit log, server logs, crash dumps, server health and instance load; the developer tools |
| 9 | Delete accounts; change scheduled tasks, instance limits, settings and permissions (these two are always GM 9 only), webhooks and email settings; backups; data maintenance |
| 9 | Delete accounts; change scheduled tasks, instance limits, settings and permissions (these two are always GM 9 only), webhooks and email settings; backups |
Each of these is a named permission, and you can change the lowest GM level allowed for any of them without rebuilding
or restarting:

View File

@@ -626,7 +626,6 @@ TEST_F(ParitySeeded, PetNames) {
Both("GetPetNamesTable order " + std::to_string(column), [&](GameDatabase& db) { return db.GetPetNamesTable(0, 10, "", column, false); });
}
Both("GetPetsWithUnknownOwner", [](GameDatabase& db) { return db.GetPetsWithUnknownOwner(); });
Both("GetAllPetNames", [](GameDatabase& db) { auto names = db.GetAllPetNames(); std::sort(names.begin(), names.end()); return names; });
Both("ApprovePreviouslyApprovedPetNames", [](GameDatabase& db) { return db.ApprovePreviouslyApprovedPetNames(); });
Both("SetPetOwner", [](GameDatabase& db) { db.SetPetOwner(PET3, 0); return json{ db.GetPetsWithUnknownOwner(), db.GetPetNameInfo(PET3) }; });
Both("ApprovePetName", [](GameDatabase& db) { db.ApprovePetName(PET3); return db.GetPetNameInfo(PET3); });
@@ -708,7 +707,6 @@ TEST_F(ParitySeeded, Properties) {
Both("GetPropertiesOwnedBy", [](GameDatabase& db) { return db.GetPropertiesOwnedBy(CHAR_ALICE); });
Both("GetPropertyRecord", [](GameDatabase& db) { return json{ db.GetPropertyRecord(PROP1), db.GetPropertyRecord(1) }; });
Both("GetPropertyModelRecords", [](GameDatabase& db) { return db.GetPropertyModelRecords(PROP1); });
Both("FixPropertyCloneIds", [](GameDatabase& db) { return db.FixPropertyCloneIds(); });
Both("GetDashboardSnapshot", [](GameDatabase& db) { return db.GetDashboardSnapshot(); });
}