diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index d50c30e7f..26e959a18 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -223,6 +223,13 @@ namespace { return std::nullopt; } + // An id from a request body, as a string or a number; nullopt when it's missing or not one + std::optional BodyId(const nlohmann::json& body) { + const auto it = body.find("id"); + if (it == body.end()) return std::nullopt; + return GeneralUtils::TryParse(it->is_string() ? it->get() : it->dump()); + } + 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; @@ -348,7 +355,7 @@ namespace UgcRoutes { const auto body = ParseBody(context); if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON"); const auto kind = body->value("kind", std::string("model")) == "modular" ? std::string("modular") : std::string("model"); - const auto id = GeneralUtils::TryParse((*body)["id"].is_string() ? (*body)["id"].get() : (*body)["id"].dump()); + const auto id = BodyId(*body); if (!id) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid id"); const auto after = body->value("after", std::string("now")); Audit(context, "ugc_delete_files", "Deleted the UGC files of " + kind + " " + std::to_string(*id) + " (" + after + ")"); @@ -439,7 +446,7 @@ namespace UgcRoutes { const auto body = ParseBody(context); if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON"); const bool model = body->value("kind", std::string()) == "model"; - ProxyAdmin(reply, context, "/admin/preview", { { "kind", model ? "model" : "modular" }, { "id", (*body)["id"].is_string() ? (*body)["id"].get() : std::string("0") }, + ProxyAdmin(reply, context, "/admin/preview", { { "kind", model ? "model" : "modular" }, { "id", std::to_string(BodyId(*body).value_or(0)) }, { "modules", body->value("modules", std::string()) }, { "values", nlohmann::json::parse(UgcIconParams::ToJson(UgcIconParams::Parse(body->value("values", nlohmann::json::object()).dump()))) } }); }); @@ -463,7 +470,7 @@ namespace UgcRoutes { target = UgcIconParams::CombinationTarget(key); what = "the icon of the module combination " + key; } else { - const auto id = GeneralUtils::TryParse((*body)["id"].is_string() ? (*body)["id"].get() : (*body)["id"].dump()); + const auto id = BodyId(*body); if (!id) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid id"); target = UgcIconParams::ModelTarget(*id); what = "the icon of model " + std::to_string(*id); @@ -500,7 +507,7 @@ namespace UgcRoutes { const bool modular = body->value("kind", "model") == "modular"; std::optional id; if (body->contains("id")) { - id = GeneralUtils::TryParse((*body)["id"].is_string() ? (*body)["id"].get() : (*body)["id"].dump()); + id = BodyId(*body); if (!id) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid id"); } const bool failedOnly = body->value("failedOnly", false); diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index e7a148025..afc4f8c21 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -195,8 +195,8 @@ ['nifColumn', 'lxfmlColumn', 'meshControls'].forEach(function (x) { $(x).classList.toggle('d-none', !model); }); $('statsBox').innerHTML = model ? '' : '
Modules: ' + esc(item.modules || '') + ' (combination ' + esc(item.combination || '') + '). The game client puts cars and rockets together itself; only the icon is made, once per combination of modules.
'; - $('framingCard').classList.toggle('d-none', model || !canManage); - if (!model && canManage) openFraming(item); + $('framingCard').classList.toggle('d-none', !canManage); + if (canManage) openFraming(item, model); bootstrap.Modal.getOrCreateInstance($('previewModal')).show(); if (!model) return; @@ -320,55 +320,67 @@ $('purgeButton').addEventListener('click', function () { purge(false); }); $('purgeAllButton').addEventListener('click', function () { purge(true); }); - // ---- car and rocket icon framing ---- + // ---- icon framing and light, for any item; the controls come from the server's list of parameters ---- - var FRAMING = [ - ['yaw', 'Angle around', -180, 180, 1], ['pitch', 'Angle above', -89, 89, 1], ['fov', 'Field of view', 5, 90, 0.5], ['margin', 'Border (zoom)', 0.5, 3, 0.01], - ['offsetX', 'Shift right', -0.5, 0.5, 0.01], ['offsetY', 'Shift up', -0.5, 0.5, 0.01], ['sunYaw', 'Sun around', -180, 180, 1], ['sunPitch', 'Sun above', -10, 90, 1] - ]; - var framingItem = null, typeFraming = {}, previewTimer = null, previewUrl = null; - $('framingControls').innerHTML = FRAMING.map(function (f) { - return '
' + - '
'; - }).join(''); + var PARAMS = [], framingItem = null, framingKind = '', previewTimer = null, previewUrl = null; + function stepDigits(step) { return step >= 1 ? 0 : 2; } + function buildControls(params) { + PARAMS = params; + $('framingControls').innerHTML = params.map(function (p) { + return '
'; + }).join(''); + } function framingValues() { var out = {}; - FRAMING.forEach(function (f) { out[f[0]] = parseFloat($('framing_' + f[0]).value); }); + PARAMS.forEach(function (p) { out[p.key] = parseFloat($('icon_' + p.key).value); }); return out; } - function setFraming(values) { - FRAMING.forEach(function (f) { - if (values[f[0]] === undefined || values[f[0]] === null) return; - $('framing_' + f[0]).value = values[f[0]]; - $('framing_' + f[0] + '_value').textContent = (+values[f[0]]).toFixed(f[4] < 1 ? 2 : 0); + function setValues(values) { + if (!values) return; + PARAMS.forEach(function (p) { + if (values[p.key] === undefined || values[p.key] === null) return; + $('icon_' + p.key).value = values[p.key]; + $('icon_' + p.key + '_value').textContent = (+values[p.key]).toFixed(stepDigits(p.step)) + (p.unit === 'degrees' ? '\u00b0' : ''); }); } - function loadFramingType(useCombination) { - if (!framingItem) return; - var type = $('framingType').value; - return api.get('/api/ugc/framing?buildType=' + type + '&modules=' + encodeURIComponent(framingItem.modules || '')).then(function (d) { - if (!d.success) return; - typeFraming = d.type; - setFraming(d.type); - if (useCombination && d.combination) setFraming(d.combination); - $('framingState').textContent = d.combination && useCombination ? 'This combination has its own framing.' : 'The ' + d.name + ' framing.'; + function itemQuery() { + return framingItem.model ? 'kind=model&id=' + encodeURIComponent(framingItem.id) : 'kind=modular&modules=' + encodeURIComponent(framingItem.modules || ''); + } + function itemBody(extra) { + var body = framingItem.model ? { kind: 'model', id: framingItem.id } : { kind: 'modular', modules: framingItem.modules }; + for (var k in extra) body[k] = extra[k]; + return body; + } + function loadValues() { + return api.get('/api/ugc/icon/settings?' + itemQuery()).then(function (d) { + if (!d.success) { $('framingState').textContent = d.error || 'Failed'; return; } + framingKind = d.kind; + setValues(d.settings); + setValues(d.preset); + setValues(d.own); + $('framingState').textContent = d.own ? 'This item has its own values.' : d.preset ? 'The kind\'s preset.' : 'The default settings.'; renderPreview(); }); } - function openFraming(item) { - framingItem = item; - // The build type from what the UGC server stored with the icon, when it has - fetchJson(fileUrl('modular', item.id, 'combo.json')).then(function (combo) { - if (combo && (combo.buildType === 3 || combo.buildType === 6)) $('framingType').value = String(combo.buildType); - loadFramingType(true); + function openFraming(item, model) { + framingItem = { id: item.id, modules: item.modules, model: model }; + var start = PARAMS.length ? Promise.resolve() : api.get('/api/ugc/icon/params').then(function (d) { + if (!d.success) return; + buildControls(d.params); + window.ugcIconKinds = d.kinds; + }); + start.then(loadValues).then(function () { + var label = (window.ugcIconKinds || []).find(function (k) { return k.kind === framingKind; }); + $('framingKind').textContent = label ? label.label : framingKind; }); } function renderPreview() { if (!framingItem) return; var img = $('framingPreview'); img.style.opacity = 0.5; - fetch('/api/ugc/framing/preview', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, - body: JSON.stringify({ modules: framingItem.modules, framing: framingValues() }) }).then(function (r) { + fetch('/api/ugc/icon/preview', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, + body: JSON.stringify(itemBody({ values: framingValues() })) }).then(function (r) { if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'HTTP ' + r.status); }); return r.blob(); }).then(function (blob) { @@ -378,37 +390,32 @@ img.style.opacity = 1; }).catch(function (e) { img.style.opacity = 1; $('framingState').textContent = 'No preview: ' + e.message; }); } + function saved(d) { toast(d.success ? d.message : (d.error || 'Failed'), d.success ? 'success' : 'danger'); } $('framingControls').addEventListener('input', function (e) { - var f = FRAMING.find(function (x) { return 'framing_' + x[0] === e.target.id; }); - if (f) $('framing_' + f[0] + '_value').textContent = (+e.target.value).toFixed(f[4] < 1 ? 2 : 0); + var p = PARAMS.find(function (x) { return 'icon_' + x.key === e.target.id; }); + if (p) $('icon_' + p.key + '_value').textContent = (+e.target.value).toFixed(stepDigits(p.step)) + (p.unit === 'degrees' ? '\u00b0' : ''); clearTimeout(previewTimer); previewTimer = setTimeout(renderPreview, 350); }); - $('framingType').addEventListener('change', function () { loadFramingType(false); }); $('framingPreviewButton').addEventListener('click', renderPreview); $('framingReset').addEventListener('click', function () { - if (!framingItem) return; - if (!confirm('Go back to the type\'s framing? This combination\'s own framing, if it has one, is removed.')) return; - api.post('/api/ugc/framing/save', { scope: 'combination', modules: framingItem.modules, framing: null }).then(function () { loadFramingType(false); }); + if (!framingItem || !confirm('Remove this item\'s own values and go back to its kind\'s?')) return; + api.post('/api/ugc/icon/save', itemBody({ scope: 'item', values: null })).then(function (d) { saved(d); loadValues(); }); }); $('framingSaveType').addEventListener('click', function () { - var name = $('framingType').selectedOptions[0].textContent.toLowerCase(); - if (!confirm('Use this framing for every ' + name + ' icon? Icons already made keep theirs until they are drawn again.')) return; - api.post('/api/ugc/framing/save', { scope: 'type', buildType: +$('framingType').value, framing: framingValues() }).then(function (d) { - toast(d.success ? d.message : (d.error || 'Failed'), d.success ? 'success' : 'danger'); - }); + if (!confirm('Use these values for every icon of this kind? Icons already made keep theirs until they are drawn again.')) return; + api.post('/api/ugc/icon/save', { scope: 'kind', kind: framingKind, values: framingValues() }).then(saved); }); $('framingSaveCombo').addEventListener('click', function () { if (!framingItem) return; - api.post('/api/ugc/framing/save', { scope: 'combination', modules: framingItem.modules, framing: framingValues() }).then(function (d) { - toast(d.success ? d.message : (d.error || 'Failed'), d.success ? 'success' : 'danger'); - if (d.success) $('framingState').textContent = 'This combination has its own framing.'; + api.post('/api/ugc/icon/save', itemBody({ scope: 'item', values: framingValues() })).then(function (d) { + saved(d); + if (d.success) $('framingState').textContent = 'This item has its own values.'; }); }); $('framingRegenerate').addEventListener('click', function () { - var name = $('framingType').selectedOptions[0].textContent.toLowerCase(); - if (!confirm('Draw every stored ' + name + ' icon again with the saved framings?')) return; - api.post('/api/ugc/framing/regenerate', { buildType: +$('framingType').value }).then(function (d) { + if (!confirm('Draw every stored icon of this kind again with the saved values? (Only icons are drawn.)')) return; + api.post('/api/ugc/icon/regenerate', { kind: framingKind }).then(function (d) { toast(d.success ? d.queued + ' icon' + (d.queued === 1 ? '' : 's') + ' queued' : (d.error || 'Failed'), d.success ? 'success' : 'danger'); }); }); diff --git a/dDashboardServer/templates/ugc.jinja2 b/dDashboardServer/templates/ugc.jinja2 index c8a511762..15200147d 100644 --- a/dDashboardServer/templates/ugc.jinja2 +++ b/dDashboardServer/templates/ugc.jinja2 @@ -109,8 +109,8 @@
-
Icon framing -
+
Icon framing and light +
@@ -119,10 +119,10 @@
- - + + - +
diff --git a/dUgcServer/UgcIconParams.cpp b/dUgcServer/UgcIconParams.cpp index ef102a17b..4def9619d 100644 --- a/dUgcServer/UgcIconParams.cpp +++ b/dUgcServer/UgcIconParams.cpp @@ -23,14 +23,14 @@ namespace UgcIconParams { { "offsetY", "icon_offset_y", "Shift up", "", -0.5f, 0.5f, 0.01f, 0.0f, "Share of the icon's height.", [](Options& o, float v) { o.offsetY = v; } }, { "sunYaw", "icon_sun_yaw", "Sun around", "degrees", -180, 180, 1, 21.0f, "", [](Options& o, float v) { o.sunYawDegrees = v; } }, { "sunPitch", "icon_sun_pitch", "Sun above", "degrees", -10, 90, 1, 50.3f, "", [](Options& o, float v) { o.sunPitchDegrees = v; } }, - { "sunStrength", "icon_sun_strength", "Sun strength", "", 0, 10, 0.05f, 2.0f, "", [](Options& o, float v) { o.sunStrength = v; } }, - { "ambient", "icon_ambient", "World light", "", 0, 2, 0.01f, 1.0f, "What every face gets.", [](Options& o, float v) { o.ambient = v; } }, + { "sunStrength", "icon_sun_light", "Sun strength", "", 0, 10, 0.05f, 2.0f, "", [](Options& o, float v) { o.sunStrength = v; } }, + { "ambient", "icon_world_light", "World light", "", 0, 2, 0.01f, 1.0f, "What every face gets.", [](Options& o, float v) { o.ambient = v; } }, { "fill", "icon_fill", "Fill light", "", 0, 5, 0.05f, 0.8f, "A light from the camera.", [](Options& o, float v) { o.fill = v; } }, { "specular", "icon_specular", "Highlights", "", 0, 2, 0.05f, 0.2f, "The sun's highlight on the plastic.", [](Options& o, float v) { o.specular = v; } }, { "shininess", "icon_shininess", "Highlight tightness", "", 1, 200, 1, 40.0f, "", [](Options& o, float v) { o.shininess = v; } }, { "exposure", "icon_exposure", "Exposure", "", 0.1f, 4, 0.05f, 2.6f, "Brightness of everything.", [](Options& o, float v) { o.exposure = v; } }, { "contrast", "icon_contrast", "Contrast", "", 0.5f, 1.5f, 0.01f, 1.0f, "", [](Options& o, float v) { o.contrast = v; } }, - { "shadows", "icon_shadows", "Shadows", "", 0, 1, 0.05f, 0.4f, "How much the sun's shadows darken.", [](Options& o, float v) { o.shadows = v; } }, + { "shadows", "icon_shadow_strength", "Shadows", "", 0, 1, 0.05f, 0.4f, "How much the sun's shadows darken.", [](Options& o, float v) { o.shadows = v; } }, { "aoStrength", "icon_ao_strength", "Ambient occlusion", "", 0, 1, 0.05f, 0.0f, "Occlusion worked out for the icon. Player models' meshes have theirs baked in already.", [](Options& o, float v) { o.ao.strength = v; o.ao.enabled = v > 0.0f; } }, }; diff --git a/resources/ugcconfig.ini b/resources/ugcconfig.ini index 61c7bcbd0..01f6b638d 100644 --- a/resources/ugcconfig.ini +++ b/resources/ugcconfig.ini @@ -83,8 +83,8 @@ glow_strength=6 icon_size=128 # How icons are framed and lit: icon_yaw, icon_pitch, icon_fov, icon_margin, icon_offset_x, icon_offset_y, -# icon_sun_yaw, icon_sun_pitch, icon_sun_strength, icon_ambient, icon_fill, icon_specular, icon_shininess, -# icon_exposure, icon_contrast, icon_shadows, icon_ao_strength. Their defaults (the framing from LU Toolbox's icon +# icon_sun_yaw, icon_sun_pitch, icon_sun_light, icon_world_light, icon_fill, icon_specular, icon_shininess, +# icon_exposure, icon_contrast, icon_shadow_strength, icon_ao_strength. Their defaults (the framing from LU Toolbox's icon # renderer, the light matched to the game's own model icons) and ranges are listed once in the server # (UgcIconParams) and on the dashboard's settings page; set one here only to change it. The dashboard's icon editor # also keeps presets for player models and each car or rocket type, and overrides for single items, in the database.