diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 4ba502a87..c305631d3 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -250,6 +250,13 @@ namespace { return GeneralUtils::TryParse(it->is_string() ? it->get() : it->dump()); } + // Sums over what the UGC server has made (IUgc::GetUgcProcessTotals) + nlohmann::json Totals(const IUgc::ProcessTotals& t) { + return { { "made", t.made }, { "timed", t.timed }, { "ms", t.milliseconds }, { "cpuMs", t.cpuMilliseconds }, { "maxMs", t.maxMilliseconds }, + { "averageMs", t.timed ? t.milliseconds / t.timed : 0 }, { "memoryKbAverage", t.memoryKbAverage }, { "memoryKbMax", t.memoryKbMax }, + { "bricks", t.bricks }, { "triangles", t.triangles }, { "trianglesBefore", t.trianglesBefore }, { "trianglesAfter", t.trianglesAfter } }; + } + nlohmann::json Counts(const std::vector>& counts) { nlohmann::json out = nlohmann::json::object(); for (const auto state : magic_enum::enum_values()) out[IUgc::ProcessStateName(state)] = 0; @@ -351,6 +358,7 @@ namespace UgcRoutes { } } 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(page + 1) * size < total }, { "kinds", kinds }, { "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } }); }); @@ -605,7 +613,8 @@ namespace UgcRoutes { const auto value = UgcSetting(param.setting); settings[param.key] = std::clamp(value ? GeneralUtils::TryParse(*value).value_or(param.defaultValue) : param.defaultValue, param.min, param.max); } - JsonSuccess(reply, { { "kind", kind }, { "target", target }, { "settings", settings }, { "preset", StoredValues(UgcIconParams::KindTarget(kind)) }, + JsonSuccess(reply, { { "kind", kind }, { "target", target }, { "settings", settings }, { "preset", kind == UgcIconParams::ModelKind() ? nlohmann::json() : StoredValues(UgcIconParams::KindTarget(kind)) }, + { "presets", kind != UgcIconParams::ModelKind() }, { "own", target.empty() ? nlohmann::json(nullptr) : StoredValues(target) } }); }); @@ -659,6 +668,9 @@ namespace UgcRoutes { bool known = false; for (const auto& entry : IconKinds()) known = known || entry["kind"] == kind; if (!known) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Unknown kind"); + if (kind == UgcIconParams::ModelKind()) { + return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Player models have no shared icon preset: each is fitted to its icon. Change one model's icon instead."); + } target = UgcIconParams::KindTarget(kind); what = "the " + kind + " icon preset"; } else if (body->value("kind", std::string()) == "modular") { diff --git a/dDashboardServer/static/js/ugc-icon-editor.js b/dDashboardServer/static/js/ugc-icon-editor.js index afb6612ce..45a9075d7 100644 --- a/dDashboardServer/static/js/ugc-icon-editor.js +++ b/dDashboardServer/static/js/ugc-icon-editor.js @@ -94,7 +94,11 @@ if (!d.success) throw new Error(d.error || 'No settings'); layers = { kind: d.kind, settings: d.settings, preset: d.preset, own: d.own }; $('framingKind').textContent = kindLabel(); - setAll(layered(3), d.own ? 'This item has its own values.' : d.preset ? 'The type’s preset.' : 'The default settings.'); + // Player models have no shared preset: each is fitted to its icon; only its own values change it + var shared = d.presets !== false; + $('framingSaveType').classList.toggle('d-none', !shared); + $('framingResetType').classList.toggle('d-none', !shared); + setAll(layered(3), d.own ? 'This item has its own values.' : d.preset ? 'The type’s preset.' : shared ? 'The default settings.' : 'Fitted to the icon from the default settings.'); }); } diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index 9c08271dc..974684ef1 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -78,6 +78,23 @@ fmt.badge(c.done + ' made', 'success') + ' ' + fmt.badge(c.pending + ' waiting', 'secondary') + ' ' + fmt.badge(c.failed + ' failed', c.failed ? 'danger' : 'secondary') + (c.empty ? ' ' + fmt.badge(c.empty + ' empty', 'light') : '') + ''; } + // What making them all took, and what it saved (the rows' recorded stats; ones made before a stat existed don't count) + function totalsCard(title, t) { + if (!t || !t.made) return ''; + var saved = t.trianglesBefore ? t.trianglesBefore - t.trianglesAfter : 0; + var rows = [ + ['Time spent making', duration(t.ms) + (t.timed < t.made ? ' (' + esc(t.timed) + ' of ' + esc(t.made) + ' timed)' : '')], + ['CPU time', duration(t.cpuMs)], + ['Average / slowest', duration(t.averageMs) + ' / ' + duration(t.maxMs)], + ['Memory (est.) average / most', mb(t.memoryKbAverage * 1024) + ' / ' + mb(t.memoryKbMax * 1024)] + ]; + if (t.bricks) rows.push(['Bricks', Number(t.bricks).toLocaleString()]); + if (t.triangles) rows.push(['Triangles (most detailed level)', Number(t.triangles).toLocaleString()]); + if (saved) rows.push(['Triangles saved', Number(saved).toLocaleString() + ' (' + (saved / t.trianglesBefore * 100).toFixed(1) + '% of ' + Number(t.trianglesBefore).toLocaleString() + ')']); + return '
' + esc(title) + ': totals
' + + '' + rows.map(function (r) { return ''; }).join('') + + '
' + r[0] + '' + r[1] + '
'; + } function owner(i) { return i.characterName ? '' + esc(i.characterName) + '' : '' + esc(i.characterId) + ''; } @@ -215,7 +232,8 @@ } $('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); + $('counts').innerHTML = countCard('Models', d.counts.model) + countCard('Cars and rockets (builds)', d.counts.modular) + + (d.totals ? totalsCard('Models', d.totals.model) + totalsCard('Cars and rockets', d.totals.modular) : ''); } function filterQuery() { return 'kind=' + list.kind + '&q=' + encodeURIComponent(list.q) + '&state=' + list.state + '&type=' + encodeURIComponent(list.kind === 'modular' ? list.type : ''); @@ -646,6 +664,8 @@ function loadPresets() { UgcIconEditor.kinds().then(function (kinds) { + // Player models have no shared preset (each is fitted to its icon): only cars and rockets have presets + kinds = kinds.filter(function (k) { return k.kind !== 'model'; }); $('presetsList').innerHTML = kinds.map(function (k) { var sample = k.sample ? (k.kind === 'model' ? 'model ' + k.sample : 'modules ' + k.sample + (k.sampleBuilds ? ' (' + k.sampleBuilds + ' builds)' : '')) : 'nothing made yet'; return '