mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): the UGC page replaces UGC Search
The UGC Search page and the UGC page searched the same things (owner, account, property, the name and description a player gave a model, LOT, UGC / blueprint id) with the same SQL. The UGC page's lists are now the one search: - models' List view has a Where column: placed on a property (with the name given there and a link to the model in the property's 3D view), in a mail or in the creator's inventories, or not found (/api/ugc?where=1, UgcLinks::Whereabouts as before) - owners link to their character and, with accounts_view, account - while searching, the kind buttons say how many models and how many cars and rockets match (/api/ugc "matches") - /ugc_search?q= redirects to /ugc?view=list&q=; the menu's UGC Search entry is now UGC, which opens the UGC page (it was only under Server Admin, for ugc_manage) - References lose their Find button (Where it is has the same) and name the inventory a build is in The ugc-search page and script are removed; /api/ugc_links/search stays for API users. Check: the menu's UGC entry; an old /ugc_search?q= link opens the UGC list with the search filled in; owner:, account:, property:, name:, lot: and id: searches, and the counts on the kind buttons; the Where column's links (property, 3D, character, account). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "UgcLinks.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
@@ -25,6 +26,17 @@ namespace {
|
||||
constexpr size_t MAX_INVENTORIES = 25;
|
||||
|
||||
|
||||
// A query value for an address: letters, digits and -._~ as they are, everything else %XX
|
||||
std::string UrlEncode(std::string_view value) {
|
||||
static constexpr char HEX[] = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
for (const unsigned char c : value) {
|
||||
if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') out += static_cast<char>(c);
|
||||
else out += { '%', HEX[c >> 4], HEX[c & 15] };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Who may see a creation ----
|
||||
|
||||
// The property page's rule (as /api/properties/:id): properties_view, or the owner with own_properties
|
||||
@@ -128,9 +140,14 @@ namespace UgcLinks {
|
||||
}
|
||||
|
||||
void RegisterRoutes() {
|
||||
Route(eHTTPMethod::GET, "/ugc_search", Perm("properties_view"), "Find players' models, cars and rockets and where they are",
|
||||
// The UGC page's lists replaced the search page: its links (?q=) open them with the search filled in
|
||||
Route(eHTTPMethod::GET, "/ugc_search", Perm("properties_view"), "Moved: the UGC page's lists (/ugc?view=list&q=)",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
RenderPage(reply, context, "ugc-search.jinja2", "ugc_search", { { "query", QueryValue(context.queryString, "q").substr(0, 100) } });
|
||||
const auto query = QueryValue(context.queryString, "q").substr(0, 100);
|
||||
reply.status = eHTTPStatusCode::FOUND;
|
||||
reply.location = "/ugc?view=list" + (query.empty() ? std::string() : "&q=" + UrlEncode(query));
|
||||
reply.message = "";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/ugc_links/search", Perm("properties_view"),
|
||||
|
||||
@@ -284,8 +284,8 @@ namespace UgcRoutes {
|
||||
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
||||
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
||||
"bricks|triangles|slowest|made|cpu|memory|savings (models) or newest|oldest|owner|name|modules|made|slowest|cpu|memory|references (assemblies), "
|
||||
"reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, totals, kinds, ugcPublicUrl, "
|
||||
"canManage}",
|
||||
"reverse=1 (the sort's other direction), page= (from 0), size= (1-200), where=1 (models: where each is, as an assembly's builds). {items, "
|
||||
"total, page, size, counts, totals, matches: {model, modular} (while searching: how many of each kind match), kinds, ugcPublicUrl, canManage}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
||||
auto parsed = ParseListSearch(QueryValue(context.queryString, "q").substr(0, 100));
|
||||
@@ -295,6 +295,7 @@ namespace UgcRoutes {
|
||||
const auto size = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "size")).value_or(PAGE_SIZE), 1u, 200u);
|
||||
const auto sortText = QueryValue(context.queryString, "sort");
|
||||
const bool reverse = QueryValue(context.queryString, "reverse") == "1";
|
||||
const bool where = QueryValue(context.queryString, "where") == "1";
|
||||
const auto kinds = IconKinds();
|
||||
|
||||
// A page of models, and how many match
|
||||
@@ -345,10 +346,21 @@ namespace UgcRoutes {
|
||||
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
uint64_t total = 0;
|
||||
nlohmann::json matches = nullptr;
|
||||
const bool searching = !parsed.search.text.empty() || parsed.search.number.has_value();
|
||||
if (!modular) {
|
||||
const auto [entries, count] = models(page * size, size);
|
||||
total = count;
|
||||
for (const auto& entry : entries) items.push_back(EntryJson(entry));
|
||||
const auto whereabouts = where ? UgcLinks::Whereabouts(entries) : std::map<LWOOBJID, nlohmann::json>{};
|
||||
for (const auto& entry : entries) {
|
||||
auto item = EntryJson(entry);
|
||||
if (where) {
|
||||
const auto found = whereabouts.find(entry.id);
|
||||
item["where"] = found == whereabouts.end() ? nlohmann::json::array() : found->second;
|
||||
}
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
if (searching) matches = { { "model", total }, { "modular", assemblies().size() } };
|
||||
} else {
|
||||
const auto list = assemblies();
|
||||
total = list.size();
|
||||
@@ -370,11 +382,12 @@ namespace UgcRoutes {
|
||||
{ "characterName", a.characterName }, { "accountId", a.accountId }, { "accountName", a.accountName }, { "processedAt", a.processedAt },
|
||||
{ "processMs", a.processMs }, { "processCpuMs", a.processCpuMs }, { "processMemoryKb", a.processMemoryKb } });
|
||||
}
|
||||
if (searching) matches = { { "model", models(0, 1).second }, { "modular", total } };
|
||||
}
|
||||
JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
|
||||
{ "totals", { { "model", Totals(Database::Get()->GetUgcProcessTotals(false)) }, { "modular", Totals(Database::Get()->GetUgcProcessTotals(true)) } } },
|
||||
{ "items", items }, { "total", total }, { "page", page }, { "size", size }, { "more", static_cast<uint64_t>(page + 1) * size < total }, { "kinds", kinds },
|
||||
{ "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } });
|
||||
{ "matches", matches }, { "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/ugc/assembly/builds", Perm("properties_view"),
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* The UGC search (/ugc_search): players' creations by name, id or LOT and where each one is (/api/ugc_links/search).
|
||||
* ?q= fills the search in; the address keeps the last search so it can be shared.
|
||||
*/
|
||||
(function () {
|
||||
var input = document.getElementById('ugcSearchInput');
|
||||
var rows = document.getElementById('ugcSearchRows');
|
||||
var status = document.getElementById('ugcSearchStatus');
|
||||
var latest = 0, timer = null;
|
||||
var INVENTORIES = { 5: 'Models', 12: 'Vault models', 14: 'Brick-building models' };
|
||||
|
||||
function inventoryName(type) { return (window.Labels && Labels.name('inventories', type)) || INVENTORIES[type] || 'Inventory ' + type; }
|
||||
|
||||
function where(item) {
|
||||
if (!item.where.length) {
|
||||
return '<span class="text-body-secondary small" title="Not placed on a property, not in the mail and not in its creator\'s inventories: traded, sold or deleted">Not found</span>';
|
||||
}
|
||||
return item.where.map(function (w) {
|
||||
if (w.type === 'property') {
|
||||
// A model the player didn't name has the client's placeholder (Objects_<lot>_name)
|
||||
var given = w.modelName && !/^Objects_\d+_name$/.test(w.modelName);
|
||||
var name = given ? ' as <span class="text-break">' + esc(w.modelName) + '</span>' : '';
|
||||
return '<div>On ' + fmt.property(w.propertyId, w.propertyName || 'Property ' + w.propertyId) + name +
|
||||
' <span class="small text-body-secondary">(' + fmt.character(w.ownerId, w.ownerName) + ', ' + esc(w.zoneId) + ')</span>' +
|
||||
' <a class="small" href="/properties/' + esc(w.propertyId) + '/3d#model=' + esc(w.modelId) + '">3D</a></div>';
|
||||
}
|
||||
if (w.type === 'mail') return '<div>In the mail to ' + fmt.character(w.characterId, w.characterName) + '</div>';
|
||||
return '<div>In ' + fmt.character(w.characterId, item.characterName) + '\'s ' + esc(inventoryName(w.inventory)) + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
rows.innerHTML = items.length ? items.map(function (item) {
|
||||
var lot = item.kind === 'modular' ? 6416 : 6662;
|
||||
var label = item.kind === 'modular' ? 'Car or rocket' : 'Model';
|
||||
var title = item.link ? '<a href="' + esc(item.link) + '">' + label + ' ' + esc(item.ugcId) + '</a>' : label + ' ' + esc(item.ugcId);
|
||||
var detail = item.detail ? '<div class="small text-body-secondary text-break">' + (item.kind === 'modular' ? 'Modules <code>' + esc(item.detail) + '</code>' : esc(item.detail)) + '</div>' : '';
|
||||
var account = item.accountName ? '<div class="small text-body-secondary">' + (document.getElementById('ugcSearchPage').dataset.canAccounts ? fmt.link('/accounts/' + item.accountId, item.accountName) : esc(item.accountName)) + '</div>' : '';
|
||||
return '<tr><td data-label="">' + UgcLinks.icon(item, lot, 48) + '</td><td data-label="Creation">' + title + detail + '</td>' +
|
||||
'<td data-label="Made by">' + fmt.character(item.characterId, item.characterName) + account + '</td>' +
|
||||
'<td data-label="Where">' + where(item) + '</td><td data-label="UGC server">' + UgcLinks.state(item) + '</td></tr>';
|
||||
}).join('') : '<tr><td colspan="5" class="text-body-secondary">Nothing matches.</td></tr>';
|
||||
}
|
||||
|
||||
function search() {
|
||||
var q = input.value.trim();
|
||||
var request = ++latest;
|
||||
var url = new URL(window.location.href);
|
||||
if (q) url.searchParams.set('q', q); else url.searchParams.delete('q');
|
||||
history.replaceState(null, '', url);
|
||||
if (!q) { rows.innerHTML = '<tr><td colspan="5" class="text-body-secondary">Type something to search.</td></tr>'; status.textContent = ''; return; }
|
||||
status.textContent = 'Searching…';
|
||||
api.get('/api/ugc_links/search?q=' + encodeURIComponent(q)).then(function (d) {
|
||||
if (request !== latest) return;
|
||||
var items = (d && d.items) || [];
|
||||
status.textContent = items.length + ' found' + (items.length >= 50 ? ' (the newest 50 of each kind; narrow the search for more)' : '');
|
||||
render(items);
|
||||
}).catch(function () { if (request === latest) status.textContent = 'The search failed.'; });
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(search, 300); });
|
||||
document.getElementById('ugcSearchForm').addEventListener('submit', function (e) { e.preventDefault(); clearTimeout(timer); search(); });
|
||||
input.value = document.getElementById('ugcSearchPage').dataset.query || '';
|
||||
if (input.value) search();
|
||||
input.focus();
|
||||
})();
|
||||
@@ -95,8 +95,11 @@
|
||||
'<table class="table table-sm mb-0 small"><tbody>' + rows.map(function (r) { return '<tr><th class="fw-normal text-body-secondary">' + r[0] + '</th><td>' + r[1] + '</td></tr>'; }).join('') +
|
||||
'</tbody></table></div></div></div>';
|
||||
}
|
||||
// The creator's character, and account for those who may see accounts
|
||||
function owner(i) {
|
||||
return i.characterName ? '<a href="/characters/' + esc(i.characterId) + '">' + esc(i.characterName) + '</a>' : '<span class="text-body-secondary">' + esc(i.characterId) + '</span>';
|
||||
var character = i.characterName ? '<a href="/characters/' + esc(i.characterId) + '">' + esc(i.characterName) + '</a>' : '<span class="text-body-secondary">' + esc(i.characterId) + '</span>';
|
||||
var account = i.accountId && i.accountName ? '<div class="small text-body-secondary">' + (DASH.can('accounts_view') ? fmt.link('/accounts/' + i.accountId, i.accountName) : esc(i.accountName)) + '</div>' : '';
|
||||
return character + account;
|
||||
}
|
||||
function badge(state) { var s = STATES[state] || [state, 'secondary']; return fmt.badge(s[0], s[1]); }
|
||||
// The id whose files show an item's icon: a model's own, an assembly's made build's (the UGC server finds its combination)
|
||||
@@ -183,6 +186,7 @@
|
||||
}, 'savings', true),
|
||||
column('Size', function (i) { return '<span class="small">' + (i.bricks ? esc(i.bricks) + ' bricks<br>' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '</span>'; }, 'bricks', true),
|
||||
column('File', function (i) { return '<span class="small" title="' + esc(i.detail || '') + '">' + esc(fileName(i)) + '</span>' + errorText(i); }, 'name'),
|
||||
column('Where', function (i) { return '<span class="small">' + whereText(i.where, i) + '</span>'; }),
|
||||
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.id) + '</div>'; })
|
||||
]),
|
||||
// The models' columns but Saved: an assembly's ID and owner are its newest build's, its size its modules, its
|
||||
@@ -246,6 +250,11 @@
|
||||
.map(function (k) { return '<option value="' + esc(k.kind) + '">' + esc(k.label) + '</option>'; }).join('');
|
||||
$('typeFilter').value = list.type;
|
||||
}
|
||||
// While searching, how many of each kind match (the search looks in both)
|
||||
$('kindButtons').querySelectorAll('[data-kind]').forEach(function (b) {
|
||||
var count = d.matches ? d.matches[b.dataset.kind] : null;
|
||||
b.textContent = (b.dataset.kind === 'modular' ? 'Cars and rockets' : 'Models') + (count === null || count === undefined ? '' : ' (' + Number(count).toLocaleString() + ')');
|
||||
});
|
||||
$('manageButtons').classList.toggle('d-none', !canManage);
|
||||
$('cacheCard').classList.toggle('d-none', !canManage);
|
||||
$('counts').innerHTML = countCard('Models', d.counts.model) + countCard('Cars and rockets (builds)', d.counts.modular) +
|
||||
@@ -269,7 +278,7 @@
|
||||
language: { emptyTable: 'Nothing here.', zeroRecords: 'Nothing matches.' },
|
||||
ajax: function (data, callback) {
|
||||
var size = data.length > 0 ? data.length : 200;
|
||||
api.get('/api/ugc?' + filterQuery() + '&' + orderQuery(columns, data.order) + '&page=' + Math.floor(data.start / size) + '&size=' + size).then(function (d) {
|
||||
api.get('/api/ugc?' + filterQuery() + '&' + orderQuery(columns, data.order) + '&page=' + Math.floor(data.start / size) + '&size=' + size + (kind === 'model' ? '&where=1' : '')).then(function (d) {
|
||||
if (!d.success) { callback({ draw: data.draw, recordsTotal: 0, recordsFiltered: 0, data: [], error: d.error || 'Failed' }); return; }
|
||||
applyMeta(d);
|
||||
if (list.kind === kind) items = d.items;
|
||||
@@ -507,12 +516,22 @@
|
||||
// ---- an assembly's references: the builds that use it, and where they are ----
|
||||
|
||||
var refs = null, refTimer = null;
|
||||
function whereText(w) {
|
||||
var INVENTORIES = { 5: 'Models', 12: 'Vault models', 14: 'Brick-building models' };
|
||||
function inventoryName(type) { return (window.Labels && Labels.name('inventories', type)) || INVENTORIES[type] || 'Inventory ' + type; }
|
||||
// Where a creation is: placed on a property (with the name the player gave it there, and its 3D view), in a mail or
|
||||
// in its creator's inventories (`item`: the creation, for its creator's name)
|
||||
function whereText(w, item) {
|
||||
return (w || []).map(function (x) {
|
||||
if (x.type === 'property') return 'On ' + fmt.property(x.propertyId, x.propertyName || 'property ' + x.propertyId) + (x.ownerName ? ' of ' + esc(x.ownerName) : '');
|
||||
if (x.type === 'mail') return 'In a mail to <a href="/characters/' + esc(x.characterId) + '">' + esc(x.characterName || x.characterId) + '</a>';
|
||||
return 'In the creator\'s ' + esc(x.inventory || 'inventory');
|
||||
}).join('<br>') || '<span class="text-body-secondary">Not found placed, mailed or with its creator</span>';
|
||||
if (x.type === 'property') {
|
||||
// A model the player didn't name has the client's placeholder (Objects_<lot>_name)
|
||||
var named = x.modelName && !/^Objects_\d+_name$/.test(x.modelName) ? ' as <span class="text-break">' + esc(x.modelName) + '</span>' : '';
|
||||
return '<div>On ' + fmt.link('/properties/' + x.propertyId, x.propertyName || 'property ' + x.propertyId) + named +
|
||||
(x.ownerName ? ' of ' + fmt.character(x.ownerId, x.ownerName) : '') +
|
||||
(x.modelId ? ' <a href="/properties/' + esc(x.propertyId) + '/3d#model=' + esc(x.modelId) + '">3D</a>' : '') + '</div>';
|
||||
}
|
||||
if (x.type === 'mail') return '<div>In a mail to ' + fmt.character(x.characterId, x.characterName) + '</div>';
|
||||
return '<div>In ' + (item && item.characterName ? fmt.character(x.characterId, item.characterName) + '\'s ' : 'the creator\'s ') + esc(inventoryName(x.inventory)) + '</div>';
|
||||
}).join('') || '<span class="text-body-secondary" title="Not placed on a property, not in the mail and not in its creator\'s inventories: traded, sold or deleted">Not found</span>';
|
||||
}
|
||||
// The builds that use the open assembly: a server-side DataTable, made once and reloaded for each assembly. A
|
||||
// linked build (refs.highlight) opens on the page holding it, marked.
|
||||
@@ -522,8 +541,7 @@
|
||||
column('Account', function (b) { return b.accountId ? '<a href="/accounts/' + esc(b.accountId) + '">' + esc(b.accountName || b.accountId) + '</a>' : ''; }, 'account'),
|
||||
column('State', function (b) { return badge(b.state) + errorText(b); }, 'state'),
|
||||
column('Made', function (b) { return '<span class="small">' + esc(madeText(b)) + '</span>'; }),
|
||||
column('Where it is', function (b) { return whereText(b.where); }),
|
||||
column('', function (b) { return '<div class="text-end"><a class="btn btn-sm btn-outline-secondary" href="/ugc_search?q=' + encodeURIComponent('id:' + b.id) + '">Find</a></div>'; })
|
||||
column('Where it is', function (b) { return whereText(b.where, b); })
|
||||
];
|
||||
var refTable = null;
|
||||
function loadRefs() {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% if can.accounts_view %}<a href="/accounts" class="list-group-item list-group-item-action{% if current_page == "accounts" %} active{% endif %}">Accounts</a>{% endif %}
|
||||
{% if can.characters_view %}<a href="/characters" class="list-group-item list-group-item-action{% if current_page == "characters" %} active{% endif %}">Characters</a>{% endif %}
|
||||
{% if can.properties_view %}<a href="/properties" class="list-group-item list-group-item-action{% if current_page == "properties" %} active{% endif %}">Properties</a>{% endif %}
|
||||
{% if can.properties_view %}<a href="/ugc_search" class="list-group-item list-group-item-action{% if current_page == "ugc_search" %} active{% endif %}">UGC Search</a>{% endif %}
|
||||
{% if can.properties_view %}<a href="/ugc" class="list-group-item list-group-item-action{% if current_page == "ugc" %} active{% endif %}">UGC</a>{% endif %}
|
||||
</div>
|
||||
|
||||
{% set inboxPages = ["moderation", "player_reports", "bug_reports"] %}
|
||||
@@ -120,8 +120,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "webhooks", "client_assets", "ugc"] %}
|
||||
{% if can.settings or can.permissions_manage or can.grants_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files or can.ugc_manage %}
|
||||
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "webhooks", "client_assets"] %}
|
||||
{% if can.settings or can.permissions_manage or can.grants_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files %}
|
||||
<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>
|
||||
@@ -134,7 +134,6 @@
|
||||
{% 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.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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}UGC Search - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="ugcSearchPage" data-query="{{ query }}" data-can-accounts="{% if can.accounts_view %}1{% endif %}">
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1">UGC Search</h2>
|
||||
<p class="text-body-secondary mb-0">Find the models, cars and rockets players built, what the UGC server made of them and where they are.
|
||||
A number finds ids (the UGC / blueprint id, a placed model's object id, a property, character or account id) and LOTs;
|
||||
text finds creators' character and account names, property names and the names players gave their models.
|
||||
Search one field with <code>owner:</code>, <code>property:</code>, <code>model:</code>, <code>id:</code> or <code>lot:</code>.</p>
|
||||
</div>
|
||||
<form class="mb-3" id="ugcSearchForm" role="search">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" id="ugcSearchInput" placeholder="Name, id or LOT" aria-label="Search players' creations" autocomplete="off" maxlength="100">
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="ugcSearchStatus" class="text-body-secondary small mb-2"></div>
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0 table-stack">
|
||||
<thead><tr><th></th><th>Creation</th><th>Made by</th><th>Where</th><th>UGC server</th></tr></thead>
|
||||
<tbody id="ugcSearchRows"><tr><td colspan="5" class="text-body-secondary">Type something to search.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/ugc-links.js"></script>
|
||||
<script src="/js/ugc-search.js"></script>
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,8 @@
|
||||
<h2 class="mb-1">UGC Server</h2>
|
||||
<p class="text-body-secondary mb-0">The UGC server makes a mesh and an icon for every model players build, and icons for their cars and rockets,
|
||||
and serves them to the game client. New and changed models are picked up by themselves; a model that fails is tried a few times, then left
|
||||
as failed until it's made again from here. See <code>docs/UgcServer.md</code>.</p>
|
||||
as failed until it's made again from here. The search finds creations by owner, account, property, the name players gave them, LOT or id,
|
||||
and the List view says where each one is. See <code>docs/UgcServer.md</code>.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3" id="counts"></div>
|
||||
@@ -75,7 +76,7 @@
|
||||
<div id="gallery" class="ugc-gallery mb-2"></div>
|
||||
<div class="card d-none" id="listCard"><div class="card-body p-2">
|
||||
<div id="modelTableBox"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcModelTable">
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Saved</th><th>Size</th><th>File</th><th></th></tr></thead></table></div>
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Saved</th><th>Size</th><th>File</th><th>Where</th><th></th></tr></thead></table></div>
|
||||
<div id="assemblyTableBox" class="d-none"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcAssemblyTable">
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Size</th><th>File</th><th>Where</th><th></th></tr></thead></table></div>
|
||||
</div></div>
|
||||
@@ -172,7 +173,7 @@
|
||||
<div class="card-header py-2 small fw-semibold d-flex flex-wrap align-items-center gap-2">References <span class="text-body-secondary fw-normal">the builds that use these modules</span>
|
||||
<input type="search" class="form-control form-control-sm ms-auto" style="max-width: 16rem" id="refSearch" placeholder="Owner, account, property or id" aria-label="Search the builds"></div>
|
||||
<div class="card-body p-2"><table class="table table-sm small align-middle mb-0 w-100" id="refTable">
|
||||
<thead><tr><th>Build (blueprint id)</th><th>Owner</th><th>Account</th><th>State</th><th>Made</th><th>Where it is</th><th></th></tr></thead></table></div>
|
||||
<thead><tr><th>Build (blueprint id)</th><th>Owner</th><th>Account</th><th>State</th><th>Made</th><th>Where it is</th></tr></thead></table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
@@ -1073,7 +1073,8 @@ are time taken, so less is better.
|
||||
The property page's model list shows each model's icon: for a player-built model (or a car or rocket) the icon the UGC
|
||||
server made of it (the item's icon until it has), with its UGC state and a link to it on the UGC page. The character
|
||||
page's inventories (models, vault models and the brick building ones included) show creations the same way.
|
||||
**UGC Search** (`/ugc_search`, `properties_view`) finds creations and where they are: see docs/UgcServer.md.
|
||||
The **UGC** page (`/ugc`, `properties_view`) finds creations by owner, account, property, name, LOT or id and says where they are (it
|
||||
replaced the UGC Search page): see docs/UgcServer.md.
|
||||
|
||||
**Today's Top Properties** (Properties page, permission `feature_properties`, default GM 5). The game's news screen has
|
||||
four "Today's Top Properties" slots, one per small property world: Block Yard, Nimbus Rock, Chantey Shanty and Raven
|
||||
|
||||
@@ -777,7 +777,7 @@ it sends every 5 seconds with its workers, totals and storage), in the `server`
|
||||
comes back, in the System Log (`UgcServer_*.log`) and crash dumps (`Crash_UgcServer_<start time>_<pid>.log` in `dump_folder`), and in
|
||||
Prometheus (`darkflame_ugc_up`, `darkflame_ugc_items`, `darkflame_server_ugc_*{server="ugc"}`). See docs/Dashboard.md.
|
||||
|
||||
The UGC Server page (`/ugc`, Server Admin menu; `properties_view` to look, the new `ugc_manage` permission to make
|
||||
The UGC Server page (`/ugc`, **UGC** in the menu under Properties; `properties_view` to look, the new `ugc_manage` permission to make
|
||||
things again and to save icon values) reads the database: counts per state for models and for cars and rockets, and the
|
||||
items as a gallery of their icons or a list. Player models are listed one by one (owner, state, attempts, last attempt,
|
||||
bricks and triangles, file name, failure reason). Cars and rockets are listed as **assemblies**, one per combination of
|
||||
@@ -786,24 +786,26 @@ columns and sorts (assemblies have no Saved): Icon, ID and Owner (an assembly's
|
||||
many other owners), State (an assembly is made when any build of it is), Made, Took, CPU and RAM (an assembly's: the
|
||||
latest make of any of its builds, and the cost of the make the UGC server did for it; builds that shared the made icon
|
||||
cost nothing), Size (bricks and triangles; an assembly's module count), File (the file name; an assembly's build type,
|
||||
named after the type's assembly object in `ModularBuildComponent`, and its modules) and, for assemblies, how many builds
|
||||
and owners use it.
|
||||
named after the type's assembly object in `ModularBuildComponent`, and its modules) and Where: for a model where it is
|
||||
(below), for an assembly how many builds and owners use it. Owners link to their character and, with `accounts_view`,
|
||||
account.
|
||||
|
||||
Both lists are paged on the server (`GET /api/ugc?kind=model|modular&q=&state=&type=&sort=&page=&size=`, with the
|
||||
total), with numbered pages, first and last, a page to jump to and a page size kept per user. The search box takes
|
||||
total; `where=1` adds each model's whereabouts; while searching, `matches` says how many of each kind match, shown on
|
||||
the kind buttons), with numbered pages, first and last, a page to jump to and a page size kept per user. The search box takes
|
||||
plain text (names, owners, ids) or field prefixes: `owner:`, `account:`, `property:`, `name:`, `lot:` or `module:` (a
|
||||
LOT, or for assemblies a module's name), `id:`, `state:` and `kind:` (a car or rocket type). Models sort by newest,
|
||||
oldest, most bricks, most triangles, recently made, slowest, most CPU, most RAM, most triangles saved, owner or file
|
||||
name; assemblies the same (most modules for most bricks, no triangles or saved) and by most builds. The kind,
|
||||
search, filters, sort, page and view are kept in the address, so Back and Forward and shared links work. The search
|
||||
is the UGC search's SQL (`UgcLookupSql`, the same on MySQL and SQLite, `IUgcLookup::ListUgc`); assemblies are grouped
|
||||
is `UgcLookupSql` (the same on MySQL and SQLite, `IUgcLookup::ListUgc`); assemblies are grouped
|
||||
from the builds (`UgcAssemblies`). Buttons make one item, the failed ones or everything again (these only reset the
|
||||
columns; the UGC server picks the rows up).
|
||||
|
||||
An assembly opens with its modules, the icon editor and **References**: the builds that use it (`GET
|
||||
/api/ugc/assembly/builds?modules=&q=&page=`), with owner character and account, state and where each is (placed on a
|
||||
property, in a mail, in its creator's inventories; the same lookup as the UGC search), paged and searchable. A link
|
||||
to a build, `/ugc?item=<build id>&kind=modular` (what the UGC search and the character pages link to), opens its
|
||||
property, in a mail, in its creator's inventories; the same lookup as the models' Where), paged and searchable. A link
|
||||
to a build, `/ugc?item=<build id>&kind=modular` (what the property and character pages link to), opens its
|
||||
assembly (`GET /api/ugc/assembly/of/<id>`) with that build highlighted in References.
|
||||
|
||||
**The icon editor** (on every opened item, and per type under **Icon presets per type**, which opens each type on an
|
||||
@@ -848,16 +850,17 @@ were removed, vertices, shapes, timings, with the change since the version befor
|
||||
|
||||
### Finding creations and showing them elsewhere
|
||||
|
||||
**UGC Search** (`/ugc_search`, in the menu under Properties, `properties_view`; `?q=` fills the search in) finds
|
||||
players' creations with `GET /api/ugc_links/search?q=`. A number matches the UGC / blueprint id, a placed model's
|
||||
object id, the property it is placed on, the creator's character or account id, or a LOT (a model placed as that LOT,
|
||||
or a car or rocket with that module); text matches the creator's character and account names, property names, the
|
||||
name and description a player gave a placed model and the upload's file name. `owner:`, `property:`, `model:`, `id:`
|
||||
and `lot:` search one field. Each result has its icon, state, creator and where it is: placed on a property (from
|
||||
`properties_contents`), attached to a mail (a car's or rocket's subkey, a model item's blueprint in the attachment's
|
||||
config) or in its creator's inventories (their saved XML); anything else is "Not found" (traded, sold or deleted).
|
||||
The UGC page's search is the one search for creations (it replaced the UGC Search page; `/ugc_search?q=` redirects
|
||||
to `/ugc?view=list&q=`). A number matches the UGC / blueprint id, a placed model's object id, the property it is placed
|
||||
on, the creator's character or account id, or a LOT (a model placed as that LOT, or a car or rocket with that module);
|
||||
text matches the creator's character and account names, property names, the name and description a player gave a
|
||||
placed model and the upload's file name. A model's Where (`UgcLinks::Whereabouts`) is where it is: placed on a property
|
||||
(from `properties_contents`, with the name the player gave it there and a link to that model in the property's 3D view),
|
||||
attached to a mail (a model item's blueprint in the attachment's config; for a car's or rocket's build, its subkey) or
|
||||
in its creator's inventories (their saved XML, looked at for the 25 first creators on a page); anything else is "Not
|
||||
found" (traded, sold or deleted). `GET /api/ugc_links/search?q=` (API) answers the same search for both kinds at once.
|
||||
|
||||
Results, the property page and the character page link to a creation on the UGC page as
|
||||
The property page and the character page link to a creation on the UGC page as
|
||||
`/ugc?item=<id>&kind=model|modular`, for the page to open that item.
|
||||
|
||||
What the UGC server made shows on the pages that show a creation, to whoever may view that page:
|
||||
|
||||
Reference in New Issue
Block a user