mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): UGC settings beside what they change on the UGC page, their own Settings category
The settings catalog gives the UGC server a category of its own (serving, processing, models, storage, icons). The UGC page shows those sections next to the server status, the purge tools and the icon presets, read from the catalog and saved through the Settings page's own path; each links to its entry on the Settings page, which now opens a #file/name link at that setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,6 @@ namespace {
|
||||
c.AddSection("Startup");
|
||||
c.Add(Bool(MASTER, "prestart_servers", "Start auth, chat and char servers", "Master starts the other servers itself.", true, true));
|
||||
c.Add(Bool(MASTER, "enable_dashboard", "Start the web dashboard", "", false, true));
|
||||
c.Add(Bool(MASTER, "enable_ugc_server", "Start the UGC server", "Makes and serves the meshes and icons of what players build (docs/UgcServer.md).", false, true));
|
||||
c.Add(Bool(SHARED, "skip_account_creation", "Skip the first-account prompt", "For non-interactive setups: master doesn't ask for an account when there are none.", false, true));
|
||||
|
||||
c.AddSection("Game client");
|
||||
@@ -298,8 +297,6 @@ namespace {
|
||||
c.Add(Port(DASHBOARD, "port", "Web port", "Where the dashboard listens for browsers.", "2006"));
|
||||
c.Add(Format(Text(DASHBOARD, "listen_ip", "Listen address", "127.0.0.1 for this machine only (put a reverse proxy in front); 0.0.0.0 for everyone.", "127.0.0.1", true), eFormat::HOST));
|
||||
c.Add(Port(DASHBOARD, "net_port", "Server link port", "UDP port for the connection to master (the next one is used too). Keep clear of other servers' ports.", "2010"));
|
||||
c.Add(Format(Text(DASHBOARD, "ugc_internal_url", "UGC server address (internal)", "Where the dashboard itself reaches the UGC server for its status and files. Empty: http://127.0.0.1:2008.", ""), eFormat::URL));
|
||||
c.Add(Format(Text(DASHBOARD, "ugc_public_url", "UGC server address (public)", "Only for the \"open on the UGC server\" links, e.g. https://ugc.example.com. The dashboard's own pages don't need the browser to reach it.", ""), eFormat::URL));
|
||||
c.Add(Format(Text(DASHBOARD, "dashboard_url", "Public address", "For links in emails and alerts, e.g. https://dashboard.example.com.", ""), eFormat::URL));
|
||||
c.Add(Bool(DASHBOARD, "secure_cookies", "HTTPS only cookies", "Turn on when the dashboard is served over HTTPS.", false));
|
||||
c.Add(Bool(DASHBOARD, "behind_proxy", "Behind a reverse proxy", "Use the proxy's X-Forwarded-For for rate limits. Only when the dashboard can't be reached directly.", false));
|
||||
@@ -412,14 +409,17 @@ namespace {
|
||||
c.AddSection("Character history");
|
||||
c.Add(Days("snapshot_days", "Snapshots", "Older snapshots are deleted, but each character keeps its newest few (below).", "90", 1));
|
||||
c.Add(Unit(Int(DASHBOARD, "snapshot_keep", "Always keep per character", "", "10", 1, 1000), "snapshots"));
|
||||
c.AddSection("UGC server", "Makes the meshes and icons of what players build and serves them to the game client. See docs/UgcServer.md.",
|
||||
eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
|
||||
// The UGC server's settings, also shown beside what they change on the UGC page (/ugc reads these sections)
|
||||
c.AddCategory("ugc", "UGC server", "Makes the meshes and icons of what players build and serves them to the game client. See docs/UgcServer.md; the UGC page shows these beside what they change.");
|
||||
c.AddSection("UGC serving", "Where the game client downloads from and where the dashboard reaches the UGC server.");
|
||||
c.Add(Bool(MASTER, "enable_ugc_server", "Start the UGC server", "Makes and serves the meshes and icons of what players build (docs/UgcServer.md).", false, true));
|
||||
c.Add(Port(UGC, "port", "Download port", "The game client's UGCSERVERPORT.", "2008"));
|
||||
c.Add(Format(Text(UGC, "listen_ip", "Listen address", "The game client has to reach it.", "0.0.0.0", true), eFormat::HOST));
|
||||
c.Add(Port(UGC, "net_port", "Master connection port", "UDP; the next port is used too.", "2012"));
|
||||
c.Add(Text(UGC, "client_path", "Download path", "The game client's UGCSERVERDIR.", "/ugc", true));
|
||||
c.Add(Format(Text(UGC, "ugc_output_dir", "Files folder", "Relative to the server binaries.", "ugc", true), eFormat::PATH));
|
||||
c.Add(Unit(Int(UGC, "ugc_max_storage_mb", "Most space for files", "The files used longest ago are deleted past this and made again when asked for. 0: no limit.", "2048", 0, std::nullopt, true), "MB"));
|
||||
c.Add(Format(Text(DASHBOARD, "ugc_internal_url", "UGC server address (internal)", "Where the dashboard itself reaches the UGC server for its status and files. Empty: http://127.0.0.1:2008.", ""), eFormat::URL));
|
||||
c.Add(Format(Text(DASHBOARD, "ugc_public_url", "UGC server address (public)", "Only for the \"open on the UGC server\" links, e.g. https://ugc.example.com. The dashboard's own pages don't need the browser to reach it.", ""), eFormat::URL));
|
||||
c.AddSection("UGC processing", "When models are made and how much of the machine the workers may use.", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
|
||||
c.Add(Unit(Int(SHARED, "ugc_debounce_seconds", "Wait after a save", "A saved model is made this long after the owner's last save (their other waiting models wait too), or as soon as a game client asks for it or the owner leaves. 0: right away.", "120", 0, 86400), "seconds"));
|
||||
c.Add(Int(UGC, "worker_threads", "Worker threads", "0: half the CPU cores.", "0", 0, 64, true));
|
||||
c.Add(Unit(Float(UGC, "max_cpu_percent", "Most CPU", "The workers together average at most this share of all CPU cores; they pause between bits of work to stay under it. 0: no limit.", "0", 0, 100), "%"));
|
||||
@@ -430,6 +430,7 @@ namespace {
|
||||
c.Add(Unit(Int(UGC, "poll_interval_ms", "Look for new models every", "", "2000", 100, 600000, true), "ms"));
|
||||
c.Add(Int(UGC, "poll_batch", "Models taken at once", "", "32", 1, 1000, true));
|
||||
c.Add(Int(UGC, "max_attempts", "Attempts before giving up", "", "3", 1, 100, true));
|
||||
c.AddSection("UGC models", "How a model's mesh is made from its bricks (LU Toolbox's steps).", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
|
||||
c.Add(Labels(Choice(UGC, "color_palette", "Colors", "LU Toolbox's LU palette (unknown colors black), or the client's Materials.xml.", "lu_toolbox", { "lu_toolbox", "brickdb" }), { "LU Toolbox", "Brick database" }));
|
||||
c.Add(Unit(Float(UGC, "color_variation", "Color variation", "Each brick's brightness is shifted by up to this much (the same every time the model is made). 0: none.", "5", 0, 100), "%"));
|
||||
c.Add(Unit(Float(UGC, "transparent_opacity", "Transparent opacity", "", "58.82", 0, 100), "%"));
|
||||
@@ -449,6 +450,11 @@ namespace {
|
||||
c.Add(Float(UGC, "ao_distance", "Occlusion distance", "", "5", 0, 1000));
|
||||
c.Add(Float(UGC, "ao_strength", "Darkening strength", "0 to 1.", "1", 0, 1));
|
||||
c.Add(Float(UGC, "glow_strength", "Glow strength", "What glowing colors add to the baked light.", "6", 0, 100));
|
||||
c.AddSection("UGC storage", "The files made, kept on disk.", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
|
||||
c.Add(Format(Text(UGC, "ugc_output_dir", "Files folder", "Relative to the server binaries.", "ugc", true), eFormat::PATH));
|
||||
c.Add(Unit(Int(UGC, "ugc_max_storage_mb", "Most space for files", "The files used longest ago are deleted past this and made again when asked for. 0: no limit.", "2048", 0, std::nullopt, true), "MB"));
|
||||
c.AddSection("UGC icons", "The icons' size and the defaults of their framing and light. Presets per type and values for single items are set in the UGC page's icon editor.",
|
||||
eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });
|
||||
c.Add(Unit(Int(UGC, "icon_size", "Icon size", "", "128", 16, 1024), "pixels"));
|
||||
// The icon's framing and light, from the one list of them (UgcIconParams); presets and overrides are set on the
|
||||
// UGC page's icon editor
|
||||
|
||||
@@ -535,10 +535,12 @@
|
||||
d.sections.forEach(function (sec) { if (sec.condition) state.controllers[sec.condition.file + '/' + sec.condition.key] = true; });
|
||||
// Drop edits to settings that no longer exist
|
||||
Object.keys(state.pending).forEach(function (key) { if (!state.byKey[key]) delete state.pending[key]; });
|
||||
var fromHash = location.hash.slice(1);
|
||||
if (!state.category) state.category = state.categories.some(function (c) { return c.id === fromHash; }) ? fromHash : state.categories[0].id;
|
||||
// #<category>, or #<file>/<name> for one setting (the UGC page's "Open in Settings" links)
|
||||
var fromHash = decodeURIComponent(location.hash.slice(1)), linked = !state.category && state.byKey[fromHash];
|
||||
if (!state.category) state.category = state.categories.some(function (c) { return c.id === fromHash; }) ? fromHash : linked ? linked.category : state.categories[0].id;
|
||||
document.getElementById('addFile').innerHTML = d.files.map(function (f) { return '<option value="' + esc(f.file) + '">' + esc(f.name) + ' (' + esc(f.file) + ')</option>'; }).join('');
|
||||
render();
|
||||
if (linked) goTo(fromHash);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
78
dDashboardServer/static/js/ugc-settings.js
Normal file
78
dDashboardServer/static/js/ugc-settings.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* The UGC server's settings on the UGC page, beside what they change: any element with data-ugc-settings="<section>,
|
||||
* <section>" shows those sections of the settings catalog (the same entries, values and save path as the Settings
|
||||
* page, /api/settings, so a change in either place is the other's too; the servers reload at once). Settings only read
|
||||
* at start say so. Each has a link to it on the Settings page. Only for those with the settings permission.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (!window.DASH || !DASH.can('settings')) return;
|
||||
var boxes = document.querySelectorAll('[data-ugc-settings]');
|
||||
if (!boxes.length) return;
|
||||
var settings = null;
|
||||
|
||||
function key(s) { return s.file + '/' + s.name; }
|
||||
function sourceText(s) {
|
||||
if (s.source === 'web') return 'set here';
|
||||
if (s.source === 'file') return 'from ' + s.file;
|
||||
if (s.source === 'env') return 'from the environment';
|
||||
return 'default';
|
||||
}
|
||||
function input(s) {
|
||||
var id = 'ugcset-' + key(s).replace(/[^a-z0-9_]/gi, '-'), value = s.value === undefined || s.value === null ? '' : String(s.value);
|
||||
var attrs = ' id="' + id + '" data-ugc-setting="' + esc(key(s)) + '"' + (s.fileOnly ? ' disabled' : '');
|
||||
if (s.type === 'bool') {
|
||||
return '<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox"' + attrs + (value === '1' || value === 'true' ? ' checked' : '') + '></div>';
|
||||
}
|
||||
if (s.type === 'choice') {
|
||||
return '<select class="form-select form-select-sm"' + attrs + '>' + (s.choices || []).map(function (c, i) {
|
||||
return '<option value="' + esc(c) + '"' + (c === value ? ' selected' : '') + '>' + esc((s.choiceLabels || [])[i] || c) + '</option>';
|
||||
}).join('') + '</select>';
|
||||
}
|
||||
var number = s.type === 'int' || s.type === 'float';
|
||||
return '<input class="form-control form-control-sm" type="' + (number ? 'number' : 'text') + '"' + attrs + ' value="' + esc(value) + '"' +
|
||||
(number && s.min !== undefined ? ' min="' + s.min + '"' : '') + (number && s.max !== undefined ? ' max="' + s.max + '"' : '') +
|
||||
(s.type === 'float' ? ' step="any"' : '') + '>';
|
||||
}
|
||||
function row(s) {
|
||||
return '<div class="col-md-6 col-xl-4"><div class="border rounded p-2 h-100"><div class="d-flex justify-content-between gap-2"><label class="fw-semibold" for="ugcset-' +
|
||||
esc(key(s).replace(/[^a-z0-9_]/gi, '-')) + '">' + esc(s.title) + '</label><a class="text-nowrap" href="/settings#' + encodeURIComponent(key(s)) + '" title="The same setting on the Settings page">Open in Settings</a></div>' +
|
||||
'<div class="d-flex align-items-center gap-2 mt-1">' + input(s) + (s.unit ? '<span class="text-body-secondary">' + esc(s.unit) + '</span>' : '') + '</div>' +
|
||||
'<div class="text-body-secondary mt-1">' + (s.description ? esc(s.description) + ' ' : '') + 'Default ' + esc(s.default === '' ? '(empty)' : s.default) + '; ' +
|
||||
'<span data-ugc-source="' + esc(key(s)) + '">' + esc(sourceText(s)) + '</span>.' + (s.restart ? ' ' + fmt.badge('restart', 'warning') : '') + '</div></div></div>';
|
||||
}
|
||||
function render(box) {
|
||||
var sections = box.dataset.ugcSettings.split(',').map(function (x) { return x.trim(); });
|
||||
var shown = settings.filter(function (s) { return sections.indexOf(s.section) !== -1 && !s.secret; });
|
||||
var open = box.querySelector('details[open]') ? ' open' : '';
|
||||
box.innerHTML = '<details class="mt-2"' + open + '><summary class="small fw-semibold">' + esc(box.dataset.title || 'Settings') + ' (' + shown.length + ')</summary>' +
|
||||
'<div class="small mt-2"><div class="text-body-secondary mb-2">The same settings as on the <a href="/settings#ugc">Settings page</a>; a change saves at once and the servers reload. ' +
|
||||
'Those marked restart are only read when the UGC server starts.</div><div class="row g-2">' + shown.map(row).join('') + '</div></div></details>';
|
||||
}
|
||||
function load() {
|
||||
return api.get('/api/settings').then(function (d) {
|
||||
if (!d.success) return;
|
||||
settings = d.settings;
|
||||
boxes.forEach(render);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
var el = e.target.closest && e.target.closest('[data-ugc-setting]');
|
||||
if (!el || !settings) return;
|
||||
var s = settings.find(function (x) { return key(x) === el.dataset.ugcSetting; });
|
||||
if (!s) return;
|
||||
var value = s.type === 'bool' ? el.checked : el.value;
|
||||
// A value the .ini file (or environment) sets would win over one set here unless this one is told to win
|
||||
api.post('/api/settings', { file: s.file, name: s.name, value: value, webWins: s.source === 'file' || s.source === 'env' || s.webWins }).then(function (d) {
|
||||
toast(d.success ? d.message : (d.error || 'Not saved'), d.success ? 'success' : 'danger');
|
||||
if (!d.success) return;
|
||||
s.value = d.value;
|
||||
s.source = 'web';
|
||||
document.querySelectorAll('[data-ugc-source="' + CSS.escape(key(s)) + '"]').forEach(function (x) { x.textContent = sourceText(s); });
|
||||
});
|
||||
});
|
||||
if (window.Live) Live.on('settings', load);
|
||||
load();
|
||||
})();
|
||||
@@ -14,7 +14,10 @@
|
||||
<div class="row g-2 mb-3" id="counts"></div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2 small" id="serverStatus">Asking the UGC server…</div>
|
||||
<div class="card-body py-2 small"><div id="serverStatus">Asking the UGC server…</div>
|
||||
<div data-ugc-settings="UGC processing" data-title="Processing settings"></div>
|
||||
<div data-ugc-settings="UGC models" data-title="How models are made"></div>
|
||||
<div data-ugc-settings="UGC serving" data-title="Serving settings"></div></div>
|
||||
</div>
|
||||
|
||||
<details class="card mb-3" id="presetsCard">
|
||||
@@ -23,6 +26,7 @@
|
||||
<p class="text-body-secondary mb-2">Each type of icon (player models, and each car or rocket build type in the client's data) has a preset over the
|
||||
default settings. Open one to set its camera, the model's turn, the light and the border on an example.</p>
|
||||
<div class="d-flex flex-wrap gap-2" id="presetsList">Loading…</div>
|
||||
<div data-ugc-settings="UGC icons" data-title="Icon defaults (under every preset)"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -41,6 +45,7 @@
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" id="purgeAllButton">Purge all…</button>
|
||||
</div>
|
||||
<div class="mt-2" id="purgeResult"></div>
|
||||
<div data-ugc-settings="UGC storage" data-title="Storage settings"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -205,6 +210,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/ldd-materials.js"></script>
|
||||
<script src="/js/ugc-settings.js"></script>
|
||||
<script src="/js/ugc-icon-editor.js"></script>
|
||||
<script src="/js/ugc.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "UgcAssemblies.h"
|
||||
#include "UgcLookup.h"
|
||||
#include "UgcLookupSql.h"
|
||||
|
||||
@@ -103,14 +104,17 @@ namespace {
|
||||
Exec("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT);"
|
||||
"CREATE TABLE charinfo (id BIGINT PRIMARY KEY, account_id INTEGER, name TEXT);"
|
||||
"CREATE TABLE properties (id BIGINT PRIMARY KEY, owner_id BIGINT, name TEXT, zone_id INTEGER);"
|
||||
"CREATE TABLE ugc (id INTEGER PRIMARY KEY, account_id INTEGER, character_id BIGINT, is_optimized INTEGER, filename TEXT, process_error TEXT DEFAULT '');"
|
||||
"CREATE TABLE ugc_modular_build (ugc_id BIGINT PRIMARY KEY, character_id BIGINT, ldf_config TEXT, is_optimized INTEGER DEFAULT 0, process_error TEXT DEFAULT '');"
|
||||
"CREATE TABLE ugc (id INTEGER PRIMARY KEY, account_id INTEGER, character_id BIGINT, is_optimized INTEGER, filename TEXT, process_error TEXT DEFAULT '', "
|
||||
"process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_after BIGINT DEFAULT 0, bake_ao INTEGER DEFAULT 0, brick_count INTEGER DEFAULT 0, triangle_count INTEGER DEFAULT 0);"
|
||||
"CREATE TABLE ugc_modular_build (ugc_id BIGINT PRIMARY KEY, character_id BIGINT, ldf_config TEXT, is_optimized INTEGER DEFAULT 0, process_error TEXT DEFAULT '', "
|
||||
"process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0);"
|
||||
"CREATE TABLE properties_contents (id BIGINT PRIMARY KEY, property_id BIGINT, ugc_id BIGINT, lot INTEGER, model_name TEXT DEFAULT '', model_description TEXT DEFAULT '');"
|
||||
"INSERT INTO accounts VALUES (1, 'builder'), (2, 'racer');"
|
||||
"INSERT INTO charinfo VALUES (10, 1, 'Bricky'), (20, 2, 'Speedy');"
|
||||
"INSERT INTO properties VALUES (500, 10, 'Castle Hill', 1150);"
|
||||
"INSERT INTO ugc VALUES (1000, 1, 10, 1, 'tower.lxfml', ''), (1001, 1, 10, 0, 'boat.lxfml', '');"
|
||||
"INSERT INTO ugc_modular_build VALUES (2000, 20, '1:8129+1:8130+1:9330', 1, '');"
|
||||
"INSERT INTO ugc (id, account_id, character_id, is_optimized, filename, process_error, brick_count, triangle_count) VALUES "
|
||||
"(1000, 1, 10, 1, 'tower.lxfml', '', 40, 900), (1001, 1, 10, 0, 'boat.lxfml', '', 0, 0);"
|
||||
"INSERT INTO ugc_modular_build (ugc_id, character_id, ldf_config, is_optimized, process_error) VALUES (2000, 20, '1:8129+1:8130+1:9330', 1, '');"
|
||||
"INSERT INTO properties_contents VALUES (7000, 500, 1000, 14, 'Big tower', 'the tallest');");
|
||||
}
|
||||
|
||||
@@ -132,9 +136,125 @@ namespace {
|
||||
sqlite3_finalize(stmt);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// A list page's ids in order, and the count, as ListUgc runs them (all, or a search)
|
||||
std::pair<std::vector<int64_t>, int64_t> List(const IUgcLookup::UgcListQuery& query, bool modular) {
|
||||
const bool searching = UgcLookupSql::Searching(query.search);
|
||||
const auto where = (searching ? UgcLookupSql::Where(query.search, modular) : std::string("WHERE 1=1 ")) + UgcLookupSql::ListFilter(query, modular);
|
||||
const auto pattern = "%" + query.search.text + "%";
|
||||
const auto run = [&](const std::string& sql, bool page) {
|
||||
std::vector<int64_t> out;
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
EXPECT_EQ(sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr), SQLITE_OK) << sqlite3_errmsg(db) << "\n" << sql;
|
||||
int i = 1;
|
||||
if (searching) for (; i <= UgcLookupSql::TEXT_BINDS; i++) sqlite3_bind_text(stmt, i, pattern.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (page) {
|
||||
sqlite3_bind_int(stmt, i++, static_cast<int>(query.limit));
|
||||
sqlite3_bind_int(stmt, i++, static_cast<int>(query.offset));
|
||||
}
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) out.push_back(sqlite3_column_int64(stmt, 0));
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
};
|
||||
const auto ids = run(UgcLookupSql::Select(modular) + where + UgcLookupSql::ListOrder(query, modular) + "LIMIT ? OFFSET ?;", true);
|
||||
const auto count = run("SELECT COUNT(*) " + UgcLookupSql::From(modular) + where + ";", false);
|
||||
return { ids, count.empty() ? -1 : count.front() };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST_F(UgcLookupSqlTests, ListsPagesWithCounts) {
|
||||
IUgcLookup::UgcListQuery query;
|
||||
auto [ids, total] = List(query, false);
|
||||
EXPECT_EQ(ids, (std::vector<int64_t>{ 1001, 1000 })); // newest first
|
||||
EXPECT_EQ(total, 2);
|
||||
query.sort = IUgcLookup::eSort::BRICKS;
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
||||
query.sort = IUgcLookup::eSort::NAME; // boat before tower
|
||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||
query.limit = 1;
|
||||
query.offset = 1;
|
||||
std::tie(ids, total) = List(query, false);
|
||||
EXPECT_EQ(ids, (std::vector<int64_t>{ 1000 }));
|
||||
EXPECT_EQ(total, 2); // the count is of every match, not the page
|
||||
query = {};
|
||||
query.state = IUgc::eProcessState::DONE;
|
||||
EXPECT_EQ(List(query, false), (std::pair<std::vector<int64_t>, int64_t>{ { 1000 }, 1 }));
|
||||
query.search = ParseQuery("boat"); // searched and in a state: none
|
||||
EXPECT_EQ(List(query, false).second, 0);
|
||||
query.state.reset();
|
||||
EXPECT_EQ(List(query, false), (std::pair<std::vector<int64_t>, int64_t>{ { 1001 }, 1 }));
|
||||
EXPECT_EQ(List({}, true), (std::pair<std::vector<int64_t>, int64_t>{ { 2000 }, 1 }));
|
||||
}
|
||||
|
||||
TEST(UgcAssemblies, GroupsBuildsByTheirModules) {
|
||||
using State = IUgc::eProcessState;
|
||||
const auto build = [](LWOOBJID id, LWOOBJID owner, const std::string& ldf, State state) {
|
||||
IUgcLookup::UgcEntry e;
|
||||
e.kind = IUgcLookup::eUgcKind::MODULAR;
|
||||
e.id = id;
|
||||
e.characterId = owner;
|
||||
e.detail = ldf;
|
||||
e.state = state;
|
||||
if (state == State::FAILED) e.error = "broken";
|
||||
return e;
|
||||
};
|
||||
const std::map<uint32_t, UgcAssemblies::ModuleInfo> modules = {
|
||||
{ 4713, { 3, "Classic Rocket Nose Cone" } }, { 4714, { 3, "Classic Rocket Cockpit" } }, { 4715, { 3, "Classic Rocket Engine" } },
|
||||
{ 8129, { 6, "Racing Car Chassis" } }, { 8130, { 6, "Racing Car Engine" } },
|
||||
};
|
||||
const std::vector<IUgcLookup::UgcEntry> builds = {
|
||||
build(10, 1, "1:4713+1:4714+1:4715", State::DONE),
|
||||
build(11, 2, "1:4715+1:4714+1:4713", State::PENDING), // the same modules written differently
|
||||
build(12, 2, "1:8129+1:8130", State::FAILED),
|
||||
build(13, 3, "", State::PENDING), // no modules: left out
|
||||
build(14, 1, "1:4713+1:4714+1:4715", State::FAILED),
|
||||
};
|
||||
auto list = UgcAssemblies::Group(builds, modules);
|
||||
ASSERT_EQ(list.size(), 2u);
|
||||
const auto rocket = std::find_if(list.begin(), list.end(), [](const auto& a) { return a.key == "4713-4714-4715"; });
|
||||
ASSERT_NE(rocket, list.end());
|
||||
EXPECT_EQ(rocket->builds, (std::vector<LWOOBJID>{ 14, 11, 10 }));
|
||||
EXPECT_EQ(rocket->owners.size(), 2u);
|
||||
EXPECT_EQ(rocket->buildType, 3);
|
||||
EXPECT_EQ(rocket->state, State::DONE); // one build's icon is made: the combination's is
|
||||
EXPECT_EQ(rocket->iconBuild, 10);
|
||||
EXPECT_TRUE(rocket->error.empty());
|
||||
const auto car = std::find_if(list.begin(), list.end(), [](const auto& a) { return a.key == "8129-8130"; });
|
||||
EXPECT_EQ(car->state, State::FAILED);
|
||||
EXPECT_EQ(car->error, "broken");
|
||||
|
||||
UgcAssemblies::Filter filter;
|
||||
filter.buildType = 6;
|
||||
EXPECT_FALSE(UgcAssemblies::Matches(*rocket, filter, modules));
|
||||
EXPECT_TRUE(UgcAssemblies::Matches(*car, filter, modules));
|
||||
filter = {};
|
||||
filter.moduleText = "cockpit";
|
||||
EXPECT_TRUE(UgcAssemblies::Matches(*rocket, filter, modules));
|
||||
EXPECT_FALSE(UgcAssemblies::Matches(*car, filter, modules));
|
||||
filter = {};
|
||||
filter.moduleLot = 8130;
|
||||
EXPECT_TRUE(UgcAssemblies::Matches(*car, filter, modules));
|
||||
filter = {};
|
||||
filter.builds = std::set<LWOOBJID>{ 11 }; // an owner's build
|
||||
EXPECT_TRUE(UgcAssemblies::Matches(*rocket, filter, modules));
|
||||
EXPECT_FALSE(UgcAssemblies::Matches(*car, filter, modules));
|
||||
filter = {};
|
||||
filter.state = State::FAILED;
|
||||
EXPECT_FALSE(UgcAssemblies::Matches(*rocket, filter, modules));
|
||||
|
||||
UgcAssemblies::Sort(list, UgcAssemblies::eSort::REFERENCES, modules);
|
||||
EXPECT_EQ(list.front().key, "4713-4714-4715");
|
||||
UgcAssemblies::Sort(list, UgcAssemblies::eSort::NEWEST, modules);
|
||||
EXPECT_EQ(list.front().key, "4713-4714-4715"); // build 14
|
||||
UgcAssemblies::Sort(list, UgcAssemblies::eSort::NAME, modules);
|
||||
EXPECT_EQ(list.front().key, "4713-4714-4715"); // Classic before Racing
|
||||
UgcAssemblies::Sort(list, UgcAssemblies::eSort::OLDEST, modules);
|
||||
EXPECT_EQ(list.front().key, "4713-4714-4715"); // build 10
|
||||
EXPECT_EQ(UgcAssemblies::ParseSort("uses"), UgcAssemblies::eSort::REFERENCES);
|
||||
EXPECT_FALSE(UgcAssemblies::ParseSort("nonsense"));
|
||||
}
|
||||
|
||||
TEST_F(UgcLookupSqlTests, FindsByIds) {
|
||||
EXPECT_EQ(Find("1000", false), (std::set<int64_t>{ 1000 })); // the blueprint id
|
||||
EXPECT_EQ(Find("7000", false), (std::set<int64_t>{ 1000 })); // the placed model's object id
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user