diff --git a/dDashboardServer/routes/MaintenanceRoutes.cpp b/dDashboardServer/routes/MaintenanceRoutes.cpp index 45575bdfb..aaf700a3f 100644 --- a/dDashboardServer/routes/MaintenanceRoutes.cpp +++ b/dDashboardServer/routes/MaintenanceRoutes.cpp @@ -281,4 +281,23 @@ void RegisterMaintenanceRoutes() { BroadcastTableChanged("properties", std::to_string(*propertyId)); JsonSuccess(reply, { {"models", created}, {"message", "Imported " + std::to_string(created.size()) + " model(s)"} }); }); + + Route(eHTTPMethod::POST, "/api/properties/:id/remove_models", Perm("properties_import"), + "Delete every model placed on a property (they don't go back to the owner). Body: {confirm: the property ID}", + [](HTTPReply& reply, const HTTPContext& context) { + const auto propertyId = PathId(context.path, 2); + const auto body = ParseBody(context); + if (!propertyId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid ID"); + if (!body || body->value("confirm", std::string()) != std::to_string(*propertyId)) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Confirm with the property ID"); + const auto property = Database::Get()->GetPropertyInfo(*propertyId); + if (!property) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "Property not found"); + // A loaded property keeps its own list of models and saves it back + if (PropertyInstanceRunning(property->cloneId)) return JsonError(reply, eHTTPStatusCode::CONFLICT, "This property is loaded in a world right now; try again when nobody is on it"); + + const auto models = Database::Get()->GetPropertyModels(*propertyId); + for (const auto& model : models) Database::Get()->RemoveModel(model.id); + Audit(context, "remove_property_models", std::to_string(models.size()) + " model(s) from property " + std::to_string(*propertyId)); + BroadcastTableChanged("properties", std::to_string(*propertyId)); + JsonSuccess(reply, { {"removed", models.size()}, {"message", "Removed " + std::to_string(models.size()) + " model(s)"} }); + }); } diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 9980d810d..317e780a8 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -682,11 +682,19 @@ namespace UgcRoutes { }); Route(eHTTPMethod::POST, "/api/ugc/reprocess", Perm("ugc_manage"), - "Have the UGC server make items again. Body: {kind: model|modular, id} for one, {kind, failedOnly: true} for the failed ones, {kind} for all", + "Have the UGC server make items again. Body: {kind: model|modular, id} for one, {kind: model, property} for every model placed on a property, {kind, failedOnly: true} for the failed ones, {kind} for all", [](HTTPReply& reply, const HTTPContext& context) { const auto body = ParseBody(context); if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON"); const bool modular = body->value("kind", "model") == "modular"; + if (!modular && body->contains("property")) { + const auto property = GeneralUtils::TryParse((*body)["property"].is_string() ? (*body)["property"].get() : (*body)["property"].dump()); + if (!property) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid property"); + const auto changed = Database::Get()->ResetPropertyUgcModelProcessing(*property); + Audit(context, "ugc_reprocess", "Queued " + std::to_string(changed) + " model(s) on property " + std::to_string(*property) + " to be made again"); + BroadcastTableChanged("ugc"); + return JsonSuccess(reply, { { "message", std::to_string(changed) + " model" + (changed == 1 ? "" : "s") + " will be made again" } }); + } std::optional id; if (body->contains("id")) { id = BodyId(*body); diff --git a/dDashboardServer/templates/property-view.jinja2 b/dDashboardServer/templates/property-view.jinja2 index 12f74852c..dddec8500 100644 --- a/dDashboardServer/templates/property-view.jinja2 +++ b/dDashboardServer/templates/property-view.jinja2 @@ -20,6 +20,9 @@ {% endif %} Open 3D view + {% if can.ugc_manage %} + + {% endif %} {% if can.reports_view %} Economy & map {% endif %} @@ -88,6 +91,11 @@
+
+
+ + Deletes every model placed on this property; they don't go back to the owner. Not possible while the property is loaded in a world. +
{% endif %} @@ -264,6 +272,21 @@ $('#modelsTable').DataTable({ language: { emptyTable: 'Nothing placed on this property' } }); +const reprocessModelsBtn = document.getElementById('reprocessModelsBtn'); +if (reprocessModelsBtn) reprocessModelsBtn.addEventListener('click', () => { + reprocessModelsBtn.disabled = true; + api.action('/api/ugc/reprocess', { kind: 'model', property: propertyId }).then((d) => { toast(d.message, 'success'); load(); }) + .catch(() => {}).finally(() => { reprocessModelsBtn.disabled = false; }); +}); + +const removeModelsBtn = document.getElementById('removeModelsBtn'); +if (removeModelsBtn) removeModelsBtn.addEventListener('click', () => { + if (window.prompt('This deletes every model on the property. Type the property ID (' + propertyId + ') to confirm:') !== propertyId) return; + removeModelsBtn.disabled = true; + api.action('/api/properties/' + propertyId + '/remove_models', { confirm: propertyId }).then((d) => { toast(d.message, 'success'); load(); }) + .catch(() => {}).finally(() => { removeModelsBtn.disabled = false; }); +}); + const importBtn = document.getElementById('importBtn'); if (importBtn) importBtn.addEventListener('click', async () => { const file = document.getElementById('importFile').files[0]; diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index 0c2510804..f61d9c957 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -110,6 +110,9 @@ public: // with `failedOnly`). Returns how many rows changed. virtual uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) = 0; + // Sets every model placed on a property back to pending with no attempts. Returns how many rows changed. + virtual uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) = 0; + // A page of models (all, or those in `state`), the newest first; `search` (when not empty) matches the model's id // exactly or part of its owner's name virtual std::vector GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) = 0; diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index ef40b7938..ac536a47f 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -437,6 +437,7 @@ public: std::vector GetModularBuildsWithoutCombination(const uint32_t limit) override; std::optional GetUgcProcessInfo(const LWOOBJID id) override; uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override; + uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override; std::vector GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) override; std::vector> GetUgcProcessCounts() override; std::vector GetModularBuildsToProcess(const uint32_t limit) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 7db4a49f7..286876da6 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -144,6 +144,11 @@ uint64_t MySQLDatabase::ResetUgcModelProcessing(const std::optional id return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;"); } +uint64_t MySQLDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) { + return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0 " + "WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId); +} + std::vector MySQLDatabase::GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) { const std::string select = "SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after " diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index eac981d29..24379524c 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -421,6 +421,7 @@ public: std::vector GetModularBuildsWithoutCombination(const uint32_t limit) override; std::optional GetUgcProcessInfo(const LWOOBJID id) override; uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override; + uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override; std::vector GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) override; std::vector> GetUgcProcessCounts() override; std::vector GetModularBuildsToProcess(const uint32_t limit) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 3655871ef..b893d7fb7 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -145,6 +145,11 @@ uint64_t SQLiteDatabase::ResetUgcModelProcessing(const std::optional i return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;"); } +uint64_t SQLiteDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) { + return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0 " + "WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId); +} + std::vector SQLiteDatabase::GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) { const std::string select = "SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after " diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index e77c331f5..5a93c7581 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -419,6 +419,7 @@ class TestSQLDatabase : public GameDatabase { std::vector GetModularBuildsWithoutCombination(const uint32_t limit) override { return {}; } std::optional GetUgcProcessInfo(const LWOOBJID id) override { return {}; } uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override { return 0; } + uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override { return 0; } std::vector GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) override { return {}; } std::vector> GetUgcProcessCounts() override { return {}; } std::vector GetModularBuildsToProcess(const uint32_t limit) override { return {}; } diff --git a/tests/dDatabaseTests/DatabaseParityTests.cpp b/tests/dDatabaseTests/DatabaseParityTests.cpp index 3c8dd8afe..bf3e76e35 100644 --- a/tests/dDatabaseTests/DatabaseParityTests.cpp +++ b/tests/dDatabaseTests/DatabaseParityTests.cpp @@ -756,6 +756,13 @@ TEST_F(ParitySeeded, UgcModel) { db.SetUgcModelProcessed(1152921510000500001LL, IUgc::eProcessState::DONE, 1, "", true); return json{ changed, infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), infoJson(db.GetUgcProcessInfo(1)) }; }); + Both("ResetPropertyUgcModelProcessing", [&](GameDatabase& db) { + IPropertyContents::Model model; + model.id = 1152921510000400009LL; model.lot = 14; model.ugcId = 1152921510000500001LL; + db.InsertNewPropertyModel(PROP1, model, "Objects_14_name"); + const auto changed = db.ResetPropertyUgcModelProcessing(PROP1); + return json{ changed, infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), db.ResetPropertyUgcModelProcessing(1) }; + }); Both("Modular build processing", [&](GameDatabase& db) { db.InsertUgcBuild("1:4713+1:4714+1:4715", 1152921510000500002LL, CHAR_ALICE); json out = json::array();