';
}
// 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 '
' + r[0] + '
' + r[1] + '
'; }).join('') +
'
';
}
function owner(i) {
return i.characterName ? '' + esc(i.characterName) + '' : '' + esc(i.characterId) + '';
}
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)
function iconId(i) { return list.kind === 'modular' ? i.iconBuild : i.id; }
function iconImg(i, size) {
if (i.state !== 'done') return '' + esc((STATES[i.state] || [i.state])[0]) + '';
return '';
}
function moduleNames(i) { return (i.moduleList || []).map(function (m) { return m.name || m.lot; }).join(', '); }
// A duration in the largest units that fit: "850 ms", "12.4 s", "2 min 5 s", "1 h 4 min", "2 d 3 h"
function duration(ms) {
ms = Math.round(ms);
if (ms < 1000) return ms + ' ms';
if (ms < 60000) return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + ' s';
var s = Math.round(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60), d = Math.floor(h / 24);
if (d) return d + ' d' + (h % 24 ? ' ' + (h % 24) + ' h' : '');
if (h) return h + ' h' + (m % 60 ? ' ' + (m % 60) + ' min' : '');
return m + ' min' + (s % 60 ? ' ' + (s % 60) + ' s' : '');
}
function megabytes(kb) { return kb >= 1048576 ? (kb / 1048576).toFixed(1) + ' GB' : kb >= 1024 ? Math.round(kb / 1024) + ' MB' : kb + ' KB'; }
// What the last make cost: its time, the worker's CPU time and the memory the UGC server estimated for it
function costText(i) {
if (!i.processMs) return '';
return 'took ' + duration(i.processMs) + (i.processCpuMs ? ', CPU ' + duration(i.processCpuMs) : '') + (i.processMemoryKb ? ', ~' + megabytes(i.processMemoryKb) + ' RAM (est.)' : '');
}
// When it was made and what that cost (the cost is only known for makes since it was recorded)
function madeText(i) {
if (!i.processedAt) return '';
return fmt.unix(i.processedAt) + (costText(i) ? ' \u00b7 ' + costText(i) : '');
}
// A model's file as the player named it: its name with the upload's extension (".lxfml"), else the upload's name
function fileName(i) {
var file = i.detail || '';
if (!i.modelName) return file;
var dot = file.lastIndexOf('.');
return i.modelName + (dot > 0 ? file.slice(dot) : '');
}
function waitBadge(i) {
return i.state === 'pending' && i.processAfter > Date.now() / 1000 ? ' after ' + esc(fmt.unix(i.processAfter)) + '' : '';
}
function tile(i) {
var title = list.kind === 'modular' ? esc(i.kindLabel || 'Build type ' + i.buildType) : (i.characterName ? esc(i.characterName) : '' + esc(i.characterId) + '');
var sub = list.kind === 'modular' ? esc(moduleNames(i)) : esc(i.id);
var extra = list.kind === 'modular' ? ' ' + fmt.badge(i.uses + ' build' + (i.uses === 1 ? '' : 's'), 'info') : waitBadge(i);
var tip = i.error || (list.kind === 'modular' ? moduleNames(i) : i.id) + (madeText(i) ? '\nMade ' + madeText(i) : '');
return '
' + errorText(i); }, 'name'),
column('State', function (i) { return badge(i.state); }),
column('Builds', function (i) { return esc(i.uses); }, 'references', true),
column('Owners', function (i) { return esc(i.owners); }),
column('', function (i) { return '
' + actions(i, i.iconBuild) + '
'; })
]
};
// The sort a DataTables order asks for, as /api/ugc's sort= and reverse=
function orderQuery(columns, order) {
var o = order && order[0], c = o && columns[o.column];
if (!c || !c.sortKey) return 'sort=newest';
return 'sort=' + c.sortKey + ((o.dir === 'desc') !== c.desc ? '&reverse=1' : '');
}
// Numbered pages with first and last, around the current one
function pager(el, page, pages, onPage) {
if (pages <= 1) { el.innerHTML = ''; return; }
var shown = [];
for (var p = Math.max(0, page - 2); p <= Math.min(pages - 1, page + 2); p++) shown.push(p);
var li = function (p, label, disabled, active) {
return '
' : '');
}).catch(function () { box.innerHTML = fmt.badge('Unknown', 'secondary') + ' Couldn\'t ask the dashboard for the UGC server\'s status.'; });
}
function remake(body, question) {
if (question && !confirm(question)) return;
body.kind = list.kind;
api.post('/api/ugc/reprocess', body).then(function (d) {
if (!d.success) { toast(d.error || 'Failed', 'danger'); return; }
toast(d.message, 'success');
load();
});
}
// ---- the open item ----
function statsTable(now, before) {
if (!now) return '
No stats for this model (made before the UGC server wrote them).
';
var lods = now.lods || [], old = before && before.lods || [];
var diff = function (a, b) {
if (b === undefined || b === null || a === b) return '';
var d = a - b;
return ' (' + (d > 0 ? '+' : '') + d + ')';
};
var rows = lods.map(function (l, i) {
var o = old[i] || {};
var removed = l.opaqueBefore ? (100 - l.opaqueAfter / l.opaqueBefore * 100).toFixed(0) + '%' : '';
return '
Modules (combination ' + esc(item.key) + ', used by ' + esc(item.uses) + ' build' + (item.uses === 1 ? '' : 's') +
'). The game client puts cars and rockets together itself; the UGC server only draws the icon, once per combination.
' + modulesHtml(item) : '';
$('referencesCard').classList.toggle('d-none', !modular);
if (modular) {
refs = { key: item.key, page: 0, q: '', highlight: build || '' };
$('refSearch').value = '';
loadRefs();
}
UgcIconEditor.open(modular ? { model: false, modules: item.modules || item.key.replace(/-/g, '+'), label: item.key } : { model: true, id: id });
bootstrap.Modal.getOrCreateInstance($('previewModal')).show();
if (modular) return;
Promise.all([fetchJson(fileUrl('model', id, 'stats.json')), fetchJson(fileUrl('model', id, 'previous.stats.json'))]).then(function (s) {
if (!current || current.id !== id) return;
current.stats = s[0];
current.statsLoaded = true;
$('statsBox').innerHTML = statsTable(s[0], s[1]);
var lods = (s[0] && s[0].lods) || [{ lod: 0 }];
$('lodSelect').innerHTML = lods.map(function (l, n) { return ''; }).join('');
$('versionSelect').querySelector('[value=previous]').disabled = !s[1];
$('versionSelect').value = 'current';
loadMesh(true);
});
var nifBox = $('nifViewer'), lxfmlBox = $('lxfmlViewer');
import('/js/ugc-viewer.js').then(function (module) {
if (!nifViewer) {
nifViewer = module.createNifViewer(nifBox);
nifViewer.setWireframe($('wireframeSwitch').checked);
nifViewer.setVertexColors($('colorsSwitch').checked);
}
if (current && current.id === id && current.statsLoaded) loadMesh(true);
}).catch(function (e) { nifBox.textContent = 'The 3D view could not load: ' + e.message; });
lxfmlBox.textContent = 'Loading…';
import('/js/lddviewer.js').then(function (module) {
if (lxfmlViewer && lxfmlViewer.dispose) lxfmlViewer.dispose();
lxfmlBox.textContent = '';
lxfmlViewer = module.createViewer(lxfmlBox, {});
return lxfmlViewer.load([{ id: id, name: 'Model ' + id, position: [0, 0, 0], rotation: [0, 0, 0, 1], url: '/api/ugc/' + id + '/lxfml' }], 1);
}).catch(function (e) { lxfmlBox.textContent = 'The 3D view could not load: ' + e.message; });
}
function fetchJson(url) {
return fetch(url, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
function readUrlItem() {
var p = new URLSearchParams(location.search), item = p.get('item');
return item ? { id: item, build: p.get('build') || '' } : null;
}
// ---- an assembly's references: the builds that use it, and where they are ----
var refs = null, refTimer = null;
function whereText(w) {
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 ' + esc(x.characterName || x.characterId) + '';
return 'In the creator\'s ' + esc(x.inventory || 'inventory');
}).join(' ') || 'Not found placed, mailed or with its creator';
}
// 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.
var REF_COLUMNS = [
column('Build (blueprint id)', function (b) { return '' + esc(b.id) + ''; }, 'id', true),
column('Owner', owner, 'owner'),
column('Account', function (b) { return b.accountId ? '' + esc(b.accountName || b.accountId) + '' : ''; }, 'account'),
column('State', function (b) { return badge(b.state) + errorText(b); }, 'state'),
column('Made', function (b) { return '' + esc(madeText(b)) + ''; }),
column('Where it is', function (b) { return whereText(b.where); }),
column('', function (b) { return '