From e159d7b768ec37781be6803ffb79afb7b2071f69 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:08:16 -0500 Subject: [PATCH] feat(dashboard): the cars and rockets table has the models table's columns The UGC page's assemblies (cars and rockets) list had its own columns. It now has the models' columns and sorts, minus Saved: ID and Owner (the newest build and its creator, and how many other owners), State, Made, Took, CPU and RAM (the latest make of any build, and the cost of the make the UGC server did for the combination; builds that shared the made icon cost nothing), Size (the module count), File (the build type and its modules) and Where (how many builds and owners use it, opening References). The gallery sorts the same way. Check: UGC page, Cars and rockets, List: every column sorts both ways on the server, and the gallery's sort list has the same sorts. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/UgcAssemblies.h | 49 +++++++++++++++++++++++- dDashboardServer/routes/UgcRoutes.cpp | 50 ++++++++++++++++--------- dDashboardServer/static/js/ugc.js | 44 +++++++++++++++------- dDashboardServer/templates/ugc.jinja2 | 2 +- docs/UgcServer.md | 13 +++++-- tests/dWebTests/UgcLookupTests.cpp | 49 ++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 39 deletions(-) diff --git a/dDashboardServer/routes/UgcAssemblies.h b/dDashboardServer/routes/UgcAssemblies.h index 45c423dc6..ceed38a84 100644 --- a/dDashboardServer/routes/UgcAssemblies.h +++ b/dDashboardServer/routes/UgcAssemblies.h @@ -33,15 +33,33 @@ namespace UgcAssemblies { IUgc::eProcessState state{ IUgc::eProcessState::PENDING }; LWOOBJID iconBuild{}; // a build whose icon is made (the newest made one, else the newest) std::string error; // a failed build's error, when none is made + // The newest build's creator + LWOOBJID characterId{}; + std::string characterName; + uint32_t accountId{}; + std::string accountName; + // The combination's last make: when (the latest any build of it was made or attempted) and what it cost (the + // build the UGC server made it for; the builds that shared it after it was made cost nothing) + int64_t processedAt{}; + uint32_t processMs{}; + uint32_t processCpuMs{}; + uint32_t processMemoryKb{}; }; - enum class eSort : uint8_t { NEWEST, OLDEST, REFERENCES, NAME }; + // The models list's sorts (IUgcLookup::eSort) that assemblies have: MODULES is the Size column (the most modules first) + enum class eSort : uint8_t { NEWEST, OLDEST, REFERENCES, NAME, OWNER, MADE, SLOWEST, CPU, MEMORY, MODULES }; inline std::optional ParseSort(std::string_view text) { if (text.empty() || text == "newest") return eSort::NEWEST; if (text == "oldest") return eSort::OLDEST; if (text == "references" || text == "uses") return eSort::REFERENCES; if (text == "name") return eSort::NAME; + if (text == "owner") return eSort::OWNER; + if (text == "made") return eSort::MADE; + if (text == "slowest") return eSort::SLOWEST; + if (text == "cpu") return eSort::CPU; + if (text == "memory") return eSort::MEMORY; + if (text == "modules" || text == "bricks") return eSort::MODULES; return std::nullopt; } @@ -94,6 +112,22 @@ namespace UgcAssemblies { } } if (!made) assembly.iconBuild = assembly.builds.front(); + const auto& newest = *byId[assembly.builds.front()]; + assembly.characterId = newest.characterId; + assembly.characterName = newest.characterName; + assembly.accountId = newest.accountId; + assembly.accountName = newest.accountName; + int64_t costAt = -1; + for (const auto id : assembly.builds) { + const auto& build = *byId[id]; + assembly.processedAt = std::max(assembly.processedAt, build.processedAt); + if (build.processMs > 0 && build.processedAt > costAt) { + costAt = build.processedAt; + assembly.processMs = build.processMs; + assembly.processCpuMs = build.processCpuMs; + assembly.processMemoryKb = build.processMemoryKb; + } + } assembly.state = made ? IUgc::eProcessState::DONE : waiting ? IUgc::eProcessState::PENDING : failed ? IUgc::eProcessState::FAILED : IUgc::eProcessState::EMPTY; if (made) assembly.error.clear(); out.push_back(std::move(assembly)); @@ -141,14 +175,25 @@ namespace UgcAssemblies { // reverse: the sort's other direction (the order is flipped as a whole) inline void Sort(std::vector& list, eSort sort, const std::map& modules, bool reverse = false) { + // The most first, ties the newest first + const auto most = [](auto x, auto y, const Assembly& a, const Assembly& b) { return x != y ? x > y : a.builds.front() > b.builds.front(); }; std::stable_sort(list.begin(), list.end(), [&](const Assembly& a, const Assembly& b) { switch (sort) { case eSort::OLDEST: return a.builds.back() < b.builds.back(); - case eSort::REFERENCES: return a.builds.size() != b.builds.size() ? a.builds.size() > b.builds.size() : a.builds.front() > b.builds.front(); + case eSort::REFERENCES: return most(a.builds.size(), b.builds.size(), a, b); case eSort::NAME: { const auto an = Lower(Name(a, modules)), bn = Lower(Name(b, modules)); return an != bn ? an < bn : a.key < b.key; } + case eSort::OWNER: { + const auto an = Lower(a.characterName), bn = Lower(b.characterName); + return an != bn ? an < bn : a.builds.front() > b.builds.front(); + } + case eSort::MADE: return most(a.processedAt, b.processedAt, a, b); + case eSort::SLOWEST: return most(a.processMs, b.processMs, a, b); + case eSort::CPU: return most(a.processCpuMs, b.processCpuMs, a, b); + case eSort::MEMORY: return most(a.processMemoryKb, b.processMemoryKb, a, b); + case eSort::MODULES: return most(a.lots.size(), b.lots.size(), a, b); default: return a.builds.front() > b.builds.front(); } }); diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index c5b97ec2f..863c5ac74 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -283,8 +283,9 @@ namespace UgcRoutes { "A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use " "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|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, " - "kinds, ugcPublicUrl, canManage}", + "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}", [](HTTPReply& reply, const HTTPContext& context) { const bool modular = QueryValue(context.queryString, "kind") == "modular"; auto parsed = ParseListSearch(QueryValue(context.queryString, "q").substr(0, 100)); @@ -294,10 +295,10 @@ namespace UgcRoutes { const auto size = std::clamp(GeneralUtils::TryParse(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"; - nlohmann::json items = nlohmann::json::array(); - uint64_t total = 0; const auto kinds = IconKinds(); - if (!modular) { + + // A page of models, and how many match + const auto models = [&](uint32_t offset, uint32_t limit) { IUgcLookup::UgcListQuery query; query.search = parsed.search; query.state = parsed.state; @@ -305,12 +306,12 @@ namespace UgcRoutes { { "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES }, { "slowest", IUgcLookup::eSort::SLOWEST }, { "made", IUgcLookup::eSort::MADE }, { "cpu", IUgcLookup::eSort::CPU }, { "memory", IUgcLookup::eSort::MEMORY }, { "savings", IUgcLookup::eSort::SAVINGS } }; if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second; query.reverse = reverse; - query.offset = page * size; - query.limit = size; - const auto [entries, count] = Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODEL, query); - total = count; - for (const auto& entry : entries) items.push_back(EntryJson(entry)); - } else { + query.offset = offset; + query.limit = limit; + return Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODEL, query); + }; + // The assemblies matching the search and filters, sorted + const auto assemblies = [&]() { UgcAssemblies::Filter filter; filter.state = parsed.state; if (!parsed.kind.empty()) { @@ -336,12 +337,23 @@ namespace UgcRoutes { if (parsed.search.number) filter.moduleLot = static_cast(*parsed.search.number); } } - auto assemblies = *CachedAssemblies(); - std::erase_if(assemblies, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); }); - UgcAssemblies::Sort(assemblies, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules, reverse); - total = assemblies.size(); - for (size_t i = static_cast(page) * size; i < assemblies.size() && i < static_cast(page + 1) * size; i++) { - const auto& a = assemblies[i]; + auto list = *CachedAssemblies(); + std::erase_if(list, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); }); + UgcAssemblies::Sort(list, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules, reverse); + return list; + }; + + nlohmann::json items = nlohmann::json::array(); + uint64_t total = 0; + if (!modular) { + const auto [entries, count] = models(page * size, size); + total = count; + for (const auto& entry : entries) items.push_back(EntryJson(entry)); + } else { + const auto list = assemblies(); + total = list.size(); + for (size_t i = static_cast(page) * size; i < list.size() && i < static_cast(page + 1) * size; i++) { + const auto& a = list[i]; std::string label, kind; for (const auto& k : kinds) { if (k.contains("buildType") && k["buildType"] == a.buildType) { @@ -354,7 +366,9 @@ namespace UgcRoutes { items.push_back({ { "id", a.key }, { "key", a.key }, { "modules", ldf }, { "moduleList", ModulesJson(a.lots) }, { "buildType", a.buildType }, { "kind", kind }, { "kindLabel", label }, { "state", IUgc::ProcessStateName(a.state) }, { "error", a.error }, { "uses", a.builds.size() }, { "owners", a.owners.size() }, { "iconBuild", std::to_string(a.iconBuild) }, { "newestBuild", std::to_string(a.builds.front()) }, - { "storageId", std::to_string(UgcModularKey::StorageId(a.key)) } }); + { "storageId", std::to_string(UgcModularKey::StorageId(a.key)) }, { "characterId", std::to_string(a.characterId) }, + { "characterName", a.characterName }, { "accountId", a.accountId }, { "accountName", a.accountName }, { "processedAt", a.processedAt }, + { "processMs", a.processMs }, { "processCpuMs", a.processCpuMs }, { "processMemoryKb", a.processMemoryKb } }); } } JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } }, diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index 974684ef1..620173f2e 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -12,7 +12,7 @@ var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] }; var SORTS = { model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['cpu', 'Most CPU'], ['memory', 'Most RAM'], ['savings', 'Most triangles saved'], ['owner', 'Owner'], ['name', 'File name']], - modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['name', 'Name']] + modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['modules', 'Most modules'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['cpu', 'Most CPU'], ['memory', 'Most RAM'], ['owner', 'Owner'], ['name', 'Name']] }; // The list's state, as in the address: ?kind=&q=&state=&type=&sort=&page= (from 1)&view=, and the open item (&item=, &build=) var list = { kind: 'model', q: '', state: '', type: '', sort: 'newest', page: 0, view: 'gallery' }; @@ -157,6 +157,15 @@ return { data: null, title: title, orderable: !!sort, sortKey: sort, desc: !!desc, orderSequence: desc ? ['desc', 'asc'] : ['asc', 'desc'], render: function (x, type, i) { return render(i); } }; } + // When it was made and what that cost: the same columns for models and assemblies + function madeColumns() { + return [ + column('Made', function (i) { return '' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + ''; }, 'made', true), + column('Took', function (i) { return '' + (i.processMs ? esc(duration(i.processMs)) : '') + ''; }, 'slowest', true), + column('CPU', function (i) { return '' + (i.processCpuMs ? esc(duration(i.processCpuMs)) : '') + ''; }, 'cpu', true), + column('RAM (est.)', function (i) { return '' + (i.processMemoryKb ? '~' + esc(megabytes(i.processMemoryKb)) : '') + ''; }, 'memory', true) + ]; + } var COLUMNS = { model: [ column('Icon', function (i) { return iconImg(i, 48); }), @@ -164,11 +173,8 @@ column('Owner', owner, 'owner'), column('State', function (i) { return badge(i.state) + (i.attempts ? ' ' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '' : '') + waitBadge(i); - }), - column('Made', function (i) { return '' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + ''; }, 'made', true), - column('Took', function (i) { return '' + (i.processMs ? esc(duration(i.processMs)) : '') + ''; }, 'slowest', true), - column('CPU', function (i) { return '' + (i.processCpuMs ? esc(duration(i.processCpuMs)) : '') + ''; }, 'cpu', true), - column('RAM (est.)', function (i) { return '' + (i.processMemoryKb ? '~' + esc(megabytes(i.processMemoryKb)) : '') + ''; }, 'memory', true), + }) + ].concat(madeColumns(), [ column('Saved', function (i) { if (!i.trianglesBefore) return ''; var removed = i.trianglesBefore - i.triangles, share = removed / i.trianglesBefore * 100; @@ -178,17 +184,27 @@ column('Size', function (i) { return '' + (i.bricks ? esc(i.bricks) + ' bricks
' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '
'; }, 'bricks', true), column('File', function (i) { return '' + esc(fileName(i)) + '' + errorText(i); }, 'name'), column('', function (i) { return '
' + actions(i, i.id) + '
'; }) - ], + ]), + // The models' columns but Saved: an assembly's ID and owner are its newest build's, its size its modules, its + // file its type and modules, and it is where the builds that use it are (References) modular: [ column('Icon', function (i) { return iconImg(i, 48); }), - column('Newest build', function (i) { return '' + esc(i.newestBuild) + ''; }, 'newest', true), - column('Type', function (i) { return esc(i.kindLabel || i.buildType); }), - column('Modules', function (i) { return '' + esc(moduleNames(i)) + '
' + esc(i.key) + '
' + 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('ID', function (i) { return '' + esc(i.newestBuild) + ''; }, 'newest', true), + column('Owner', function (i) { + return owner(i) + (i.owners > 1 ? '
and ' + esc(i.owners - 1) + ' other' + (i.owners === 2 ? '' : 's') + '
' : ''); + }, 'owner'), + column('State', function (i) { return badge(i.state); }) + ].concat(madeColumns(), [ + column('Size', function (i) { return '' + esc((i.moduleList || []).length) + ' modules'; }, 'modules', true), + column('File', function (i) { + return '' + esc(i.kindLabel || i.buildType) + ': ' + esc(moduleNames(i)) + '
' + esc(i.key) + '
' + errorText(i); + }, 'name'), + column('Where', function (i) { + return 'Used by ' + esc(i.uses) + ' build' + (i.uses === 1 ? '' : 's') + ' of ' + esc(i.owners) + ' owner' + (i.owners === 1 ? '' : 's') + + '
'; + }, 'references', true), 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) { diff --git a/dDashboardServer/templates/ugc.jinja2 b/dDashboardServer/templates/ugc.jinja2 index 9eb394feb..744fb9d70 100644 --- a/dDashboardServer/templates/ugc.jinja2 +++ b/dDashboardServer/templates/ugc.jinja2 @@ -77,7 +77,7 @@
IconIDOwnerStateMadeTookCPURAM (est.)SavedSizeFile
-
IconNewest buildTypeModulesStateBuildsOwners
+ IconIDOwnerStateMadeTookCPURAM (est.)SizeFileWhere
diff --git a/docs/UgcServer.md b/docs/UgcServer.md index b7a2497c1..04f6114ed 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -781,15 +781,20 @@ The UGC Server page (`/ugc`, Server Admin menu; `properties_view` to look, the n 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 -modules however many builds use it (the UGC server makes one icon per combination): its icon, build type (named after -the type's assembly object in `ModularBuildComponent`), its modules with their names and icons from the CDClient, its -state (made when any build of it is) and how many builds and owners use it. +modules however many builds use it (the UGC server makes one icon per combination). The List view gives both the same +columns and sorts (assemblies have no Saved): Icon, ID and Owner (an assembly's newest build and its creator, and how +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. 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 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, owner or file name; assemblies by newest, oldest, most builds or name. The kind, +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 from the builds (`UgcAssemblies`). Buttons make one item, the failed ones or everything again (these only reset the diff --git a/tests/dWebTests/UgcLookupTests.cpp b/tests/dWebTests/UgcLookupTests.cpp index e2e92420e..18306f6c3 100644 --- a/tests/dWebTests/UgcLookupTests.cpp +++ b/tests/dWebTests/UgcLookupTests.cpp @@ -283,6 +283,55 @@ TEST(UgcAssemblies, GroupsBuildsByTheirModules) { EXPECT_FALSE(UgcAssemblies::ParseSort("nonsense")); } +// An assembly has the models' columns: its newest build's owner, and its combination's make (when, and what the make +// cost: the build it was made for, not the ones that shared it after) +TEST(UgcAssemblies, HasTheModelsColumns) { + using State = IUgc::eProcessState; + const auto build = [](LWOOBJID id, const std::string& owner, int64_t at, uint32_t ms) { + IUgcLookup::UgcEntry e; + e.kind = IUgcLookup::eUgcKind::MODULAR; + e.id = id; + e.characterId = id * 10; + e.characterName = owner; + e.accountId = static_cast(id); + e.accountName = owner + "Account"; + e.detail = id < 20 ? "1:4713+1:4714+1:4715" : "1:8129+1:8130"; + e.state = State::DONE; + e.processedAt = at; + e.processMs = ms; + e.processCpuMs = ms / 2; + e.processMemoryKb = ms * 10; + return e; + }; + const std::map modules = { + { 4713, { 3, "Nose" } }, { 4714, { 3, "Cockpit" } }, { 4715, { 3, "Engine" } }, { 8129, { 6, "Chassis" } }, { 8130, { 6, "Wheels" } }, + }; + auto list = UgcAssemblies::Group({ build(10, "Zed", 100, 900), build(11, "Amy", 300, 0), build(20, "Bob", 200, 50) }, modules); + 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->characterName, "Amy"); // the newest build's + EXPECT_EQ(rocket->characterId, 110); + EXPECT_EQ(rocket->accountName, "AmyAccount"); + EXPECT_EQ(rocket->processedAt, 300); // the latest + EXPECT_EQ(rocket->processMs, 900u); // build 11 shared the made icon and cost nothing + EXPECT_EQ(rocket->processCpuMs, 450u); + EXPECT_EQ(rocket->processMemoryKb, 9000u); + + UgcAssemblies::Sort(list, UgcAssemblies::eSort::OWNER, modules); + EXPECT_EQ(list.front().characterName, "Amy"); + UgcAssemblies::Sort(list, UgcAssemblies::eSort::MADE, modules); + EXPECT_EQ(list.front().processedAt, 300); + UgcAssemblies::Sort(list, UgcAssemblies::eSort::SLOWEST, modules); + EXPECT_EQ(list.front().processMs, 900u); + UgcAssemblies::Sort(list, UgcAssemblies::eSort::SLOWEST, modules, true); + EXPECT_EQ(list.front().processMs, 50u); + UgcAssemblies::Sort(list, UgcAssemblies::eSort::MODULES, modules); + EXPECT_EQ(list.front().lots.size(), 3u); + EXPECT_EQ(UgcAssemblies::ParseSort("cpu"), UgcAssemblies::eSort::CPU); + EXPECT_EQ(UgcAssemblies::ParseSort("memory"), UgcAssemblies::eSort::MEMORY); + EXPECT_EQ(UgcAssemblies::ParseSort("bricks"), UgcAssemblies::eSort::MODULES); +} + TEST_F(UgcLookupSqlTests, FindsByIds) { EXPECT_EQ(Find("1000", false), (std::set{ 1000 })); // the blueprint id EXPECT_EQ(Find("7000", false), (std::set{ 1000 })); // the placed model's object id