diff --git a/dDatabase/GameDatabase/ITables/IPropertyContents.h b/dDatabase/GameDatabase/ITables/IPropertyContents.h index 0ff729ee9..f18903090 100644 --- a/dDatabase/GameDatabase/ITables/IPropertyContents.h +++ b/dDatabase/GameDatabase/ITables/IPropertyContents.h @@ -1,6 +1,7 @@ #ifndef __IPROPERTIESCONTENTS__H__ #define __IPROPERTIESCONTENTS__H__ +#include #include #include #include @@ -22,7 +23,7 @@ public: LWOOBJID placedBy{}; }; - // Inserts a new UGC model into the database. + // Inserts a new UGC model into the database. unsplitId is the save it was split from (InsertUnsplitUgcModel), if any. virtual void InsertNewUgcModel( std::stringstream& sd0Data, const uint64_t blueprintId, @@ -30,7 +31,11 @@ public: const LWOOBJID characterId, // Unix seconds before which the UGC server doesn't make it (0: right away). The owner's other models waiting // to be made wait until then too (they may be edited again). - const int64_t processAfter) = 0; + const int64_t processAfter, + const std::optional unsplitId = std::nullopt) = 0; + + // Keeps a brick by brick save as the client sent it, before it is split into models. + virtual void InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) = 0; // Get the property models for the given property id. virtual std::vector GetPropertyModels(const LWOOBJID& propertyId) = 0; diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp b/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp index 903d8ccfa..4ae347c61 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp @@ -107,6 +107,7 @@ void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) { ExecuteDelete("DELETE FROM properties_contents WHERE property_id IN (SELECT id FROM properties WHERE owner_id=?);", characterId); ExecuteDelete("DELETE FROM properties WHERE owner_id=?;", characterId); ExecuteDelete("DELETE FROM ugc WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM ugc_unsplit WHERE character_id=?;", characterId); ExecuteDelete("DELETE FROM activity_log WHERE character_id=?;", characterId); ExecuteDelete("DELETE FROM mail WHERE receiver_id=?;", characterId); ExecuteDelete("DELETE FROM ignore_list WHERE player_id=? OR ignored_player_id=?;", characterId, characterId); diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 65f229ec3..bd7ecd0db 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -404,7 +404,9 @@ public: const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, - const int64_t processAfter) override; + const int64_t processAfter, + const std::optional unsplitId) override; + void InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) override; std::vector GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override; std::optional GetMail(const uint64_t mailId) override; uint32_t GetUnreadMailCount(const LWOOBJID characterId) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 2b1f42122..3c84214bb 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -64,6 +64,8 @@ std::vector MySQLDatabase::GetAllUgcModels() { void MySQLDatabase::RemoveUnreferencedUgcModels() { ExecuteDelete("DELETE FROM ugc WHERE id NOT IN (SELECT ugc_id FROM properties_contents WHERE ugc_id IS NOT NULL);"); + // An unsplit save is kept while any model split from it is + ExecuteDelete("DELETE FROM ugc_unsplit WHERE id NOT IN (SELECT unsplit_id FROM ugc WHERE unsplit_id IS NOT NULL);"); } void MySQLDatabase::InsertNewUgcModel( @@ -71,10 +73,11 @@ void MySQLDatabase::InsertNewUgcModel( const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, - const int64_t processAfter) { + const int64_t processAfter, + const std::optional unsplitId) { const std::istream stream(sd0Data.rdbuf()); ExecuteInsert( - "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`, `process_after`, `queued`) VALUES (?,?,?,?,?,?,?,?,1)", + "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`, `process_after`, `queued`, `unsplit_id`) VALUES (?,?,?,?,?,?,?,?,1,?)", blueprintId, accountId, characterId, @@ -82,15 +85,22 @@ void MySQLDatabase::InsertNewUgcModel( &stream, false, "weedeater.lxfml", - processAfter + processAfter, + unsplitId ); // The owner is still building: their other models waiting for their quiet period wait longer if (processAfter > 0) ExecuteUpdate("UPDATE ugc SET process_after = ? WHERE character_id = ? AND is_optimized = 0 AND process_after > 0 AND process_after < ?;", processAfter, characterId, processAfter); } +void MySQLDatabase::InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) { + const std::istream stream(sd0Data.rdbuf()); + ExecuteInsert("INSERT INTO `ugc_unsplit`(`id`, `account_id`, `character_id`, `lxfml`) VALUES (?,?,?,?)", id, accountId, characterId, &stream); +} + void MySQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM ugc WHERE id = ?;", modelId); ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); + ExecuteDelete("DELETE FROM ugc_unsplit WHERE id NOT IN (SELECT unsplit_id FROM ugc WHERE unsplit_id IS NOT NULL);"); } void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml, const int64_t processAfter) { diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp index 645998e0b..e10508d90 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp @@ -71,6 +71,7 @@ void SQLiteDatabase::DeleteCharacter(const LWOOBJID characterId) { ExecuteDelete("DELETE FROM properties_contents WHERE property_id IN (SELECT id FROM properties WHERE owner_id=?);", characterId); ExecuteDelete("DELETE FROM properties WHERE owner_id=?;", characterId); ExecuteDelete("DELETE FROM ugc WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM ugc_unsplit WHERE character_id=?;", characterId); ExecuteDelete("DELETE FROM activity_log WHERE character_id=?;", characterId); ExecuteDelete("DELETE FROM mail WHERE receiver_id=?;", characterId); ExecuteDelete("DELETE FROM ignore_list WHERE player_id=? OR ignored_player_id=?;", characterId, characterId); diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index a9d9b87a0..287e69bfb 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -388,7 +388,9 @@ public: const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, - const int64_t processAfter) override; + const int64_t processAfter, + const std::optional unsplitId) override; + void InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) override; std::vector GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override; std::optional GetMail(const uint64_t mailId) override; uint32_t GetUnreadMailCount(const LWOOBJID characterId) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index fde02c6d6..d017944df 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -65,6 +65,8 @@ std::vector SQLiteDatabase::GetAllUgcModels() { void SQLiteDatabase::RemoveUnreferencedUgcModels() { ExecuteDelete("DELETE FROM ugc WHERE id NOT IN (SELECT ugc_id FROM properties_contents WHERE ugc_id IS NOT NULL);"); + // An unsplit save is kept while any model split from it is + ExecuteDelete("DELETE FROM ugc_unsplit WHERE id NOT IN (SELECT unsplit_id FROM ugc WHERE unsplit_id IS NOT NULL);"); } void SQLiteDatabase::InsertNewUgcModel( @@ -72,10 +74,11 @@ void SQLiteDatabase::InsertNewUgcModel( const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, - const int64_t processAfter) { + const int64_t processAfter, + const std::optional unsplitId) { const std::istream stream(sd0Data.rdbuf()); ExecuteInsert( - "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`, `process_after`, `queued`) VALUES (?,?,?,?,?,?,?,?,1)", + "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`, `process_after`, `queued`, `unsplit_id`) VALUES (?,?,?,?,?,?,?,?,1,?)", blueprintId, accountId, characterId, @@ -83,15 +86,22 @@ void SQLiteDatabase::InsertNewUgcModel( &stream, false, "weedeater.lxfml", - processAfter + processAfter, + unsplitId ); // The owner is still building: their other models waiting for their quiet period wait longer if (processAfter > 0) ExecuteUpdate("UPDATE ugc SET process_after = ? WHERE character_id = ? AND is_optimized = 0 AND process_after > 0 AND process_after < ?;", processAfter, characterId, processAfter); } +void SQLiteDatabase::InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) { + const std::istream stream(sd0Data.rdbuf()); + ExecuteInsert("INSERT INTO `ugc_unsplit`(`id`, `account_id`, `character_id`, `lxfml`) VALUES (?,?,?,?)", id, accountId, characterId, &stream); +} + void SQLiteDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM ugc WHERE id = ?;", modelId); ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); + ExecuteDelete("DELETE FROM ugc_unsplit WHERE id NOT IN (SELECT unsplit_id FROM ugc WHERE unsplit_id IS NOT NULL);"); } void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml, const int64_t processAfter) { diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index 6e993a4fa..bfab9a1cd 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -192,7 +192,11 @@ void TestSQLDatabase::InsertNewMail(const MailInfo& mail) { } -void TestSQLDatabase::InsertNewUgcModel(std::stringstream& sd0Data, const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, const int64_t processAfter) { +void TestSQLDatabase::InsertNewUgcModel(std::stringstream& sd0Data, const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, const int64_t processAfter, const std::optional unsplitId) { + +} + +void TestSQLDatabase::InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) { } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index c8a4de4c4..1cb50685a 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -68,7 +68,9 @@ class TestSQLDatabase : public GameDatabase { const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, - const int64_t processAfter) override; + const int64_t processAfter, + const std::optional unsplitId) override; + void InsertUnsplitUgcModel(std::stringstream& sd0Data, const LWOOBJID id, const uint32_t accountId, const LWOOBJID characterId) override; std::vector GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override; std::optional GetMail(const uint64_t mailId) override; uint32_t GetUnreadMailCount(const LWOOBJID characterId) override; diff --git a/dGame/dUtilities/BrickByBrick.cpp b/dGame/dUtilities/BrickByBrick.cpp index 534249265..f69efcb17 100644 --- a/dGame/dUtilities/BrickByBrick.cpp +++ b/dGame/dUtilities/BrickByBrick.cpp @@ -115,6 +115,12 @@ namespace { outFile << lxfml; } + // Keep the save as the client sent it before splitting it; every model made from it points back to it + const auto accountId = character->GetParentUser()->GetAccountID(); + const LWOOBJID unsplitId = ObjectIDManager::GetPersistentID(); + std::stringstream unsplitData(sd0Data); + Database::Get()->InsertUnsplitUgcModel(unsplitData, unsplitId, accountId, character->GetID()); + // A quiet period before the UGC server makes the models (ugc_debounce_seconds, sharedconfig.ini): a model // the owner keeps editing isn't made for every save (docs/UgcServer.md) const auto debounce = Game::config ? GeneralUtils::TryParse(Game::config->GetValue("ugc_debounce_seconds")).value_or(120) : 120; @@ -125,7 +131,7 @@ namespace { Sd0 model = sd0; model.FromData(reinterpret_cast(part.lxfml.data()), part.lxfml.size()); auto stream = model.GetAsStream(); - Database::Get()->InsertNewUgcModel(stream, blueprintId, character->GetParentUser()->GetAccountID(), character->GetID(), processAfter); + Database::Get()->InsertNewUgcModel(stream, blueprintId, accountId, character->GetID(), processAfter, unsplitId); auto& entry = saved.emplace_back(); entry.modelId = modelId; diff --git a/migrations/dlu/mysql/106_ugc_unsplit.sql b/migrations/dlu/mysql/106_ugc_unsplit.sql new file mode 100644 index 000000000..68030e34c --- /dev/null +++ b/migrations/dlu/mysql/106_ugc_unsplit.sql @@ -0,0 +1,11 @@ +-- Every brick by brick save as the client sent it (sd0), before the server splits it into one model per group of +-- connected bricks; each model made from it points back to it +CREATE TABLE IF NOT EXISTS ugc_unsplit ( + id BIGINT NOT NULL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id), + character_id BIGINT NOT NULL REFERENCES charinfo(id), + lxfml MEDIUMBLOB NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE ugc ADD COLUMN unsplit_id BIGINT NULL DEFAULT NULL; diff --git a/migrations/dlu/sqlite/89_ugc_unsplit.sql b/migrations/dlu/sqlite/89_ugc_unsplit.sql new file mode 100644 index 000000000..e7d2e50f9 --- /dev/null +++ b/migrations/dlu/sqlite/89_ugc_unsplit.sql @@ -0,0 +1,11 @@ +-- Every brick by brick save as the client sent it (sd0), before the server splits it into one model per group of +-- connected bricks; each model made from it points back to it +CREATE TABLE IF NOT EXISTS ugc_unsplit ( + id BIGINT NOT NULL PRIMARY KEY, + account_id INTEGER NOT NULL REFERENCES accounts(id), + character_id BIGINT NOT NULL REFERENCES charinfo(id), + lxfml BLOB NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE ugc ADD COLUMN unsplit_id BIGINT NULL DEFAULT NULL;