diff --git a/dCommon/dEnums/MessageType/Master.h b/dCommon/dEnums/MessageType/Master.h index b6054d7f..1529ca51 100644 --- a/dCommon/dEnums/MessageType/Master.h +++ b/dCommon/dEnums/MessageType/Master.h @@ -3,9 +3,7 @@ namespace MessageType { enum class Master : uint32_t { - REQUEST_PERSISTENT_ID = 1, - REQUEST_PERSISTENT_ID_RESPONSE, - REQUEST_ZONE_TRANSFER, + REQUEST_ZONE_TRANSFER = 1, REQUEST_ZONE_TRANSFER_RESPONSE, SERVER_INFO, REQUEST_SESSION_KEY, diff --git a/dCommon/dEnums/eCharacterVersion.h b/dCommon/dEnums/eCharacterVersion.h index 477467b8..15610c3a 100644 --- a/dCommon/dEnums/eCharacterVersion.h +++ b/dCommon/dEnums/eCharacterVersion.h @@ -19,7 +19,8 @@ enum class eCharacterVersion : uint32_t { // Fixes nexus force explorer missions NJ_JAYMISSIONS, NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories - UP_TO_DATE, // will become PET_IDS + PET_IDS, // Fixes pet ids in player inventories + UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS }; #endif //!__ECHARACTERVERSION__H__ diff --git a/dDatabase/GameDatabase/ITables/IObjectIdTracker.h b/dDatabase/GameDatabase/ITables/IObjectIdTracker.h index cbe34b6d..6344f2e7 100644 --- a/dDatabase/GameDatabase/ITables/IObjectIdTracker.h +++ b/dDatabase/GameDatabase/ITables/IObjectIdTracker.h @@ -6,14 +6,19 @@ class IObjectIdTracker { public: + // Only the first 48 bits of the ids are the id, the last 16 bits are reserved for flags. + struct Range { + uint64_t minID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags. + uint64_t maxID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags. + }; + // Get the current persistent id. - virtual std::optional GetCurrentPersistentId() = 0; + virtual std::optional GetCurrentPersistentId() = 0; // Insert the default persistent id. virtual void InsertDefaultPersistentId() = 0; - // Update the persistent id. - virtual void UpdatePersistentId(const uint32_t newId) = 0; + virtual Range GetPersistentIdRange() = 0; }; #endif //!__IOBJECTIDTRACKER__H__ diff --git a/dDatabase/GameDatabase/ITables/IProperty.h b/dDatabase/GameDatabase/ITables/IProperty.h index 78f58284..c4957e5b 100644 --- a/dDatabase/GameDatabase/ITables/IProperty.h +++ b/dDatabase/GameDatabase/ITables/IProperty.h @@ -39,6 +39,9 @@ public: std::vector entries; }; + // Get the property info for the given property id. + virtual std::optional GetPropertyInfo(const LWOOBJID id) = 0; + // Get the property info for the given property id. virtual std::optional GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) = 0; diff --git a/dDatabase/GameDatabase/ITables/IPropertyContents.h b/dDatabase/GameDatabase/ITables/IPropertyContents.h index b2bb1a97..9a16d5d7 100644 --- a/dDatabase/GameDatabase/ITables/IPropertyContents.h +++ b/dDatabase/GameDatabase/ITables/IPropertyContents.h @@ -45,6 +45,6 @@ public: virtual void RemoveModel(const LWOOBJID& modelId) = 0; // Gets a model by ID - virtual Model GetModel(const LWOOBJID modelID) = 0; + virtual std::optional GetModel(const LWOOBJID modelID) = 0; }; #endif //!__IPROPERTIESCONTENTS__H__ diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index cbc770b8..9f6d2ef1 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -29,5 +29,7 @@ public: // Inserts a new UGC model into the database. virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0; + + virtual std::optional GetUgcModel(const LWOOBJID ugcId) = 0; }; #endif //!__IUGC__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index e355ce62..456ab5fa 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -98,9 +98,9 @@ public: void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override; void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override; void SetMasterInfo(const IServers::MasterInfo& info) override; - std::optional GetCurrentPersistentId() override; + std::optional GetCurrentPersistentId() override; + IObjectIdTracker::Range GetPersistentIdRange() override; void InsertDefaultPersistentId() override; - void UpdatePersistentId(const uint32_t id) override; std::optional GetDonationTotal(const uint32_t activityId) override; std::optional IsPlaykeyActive(const int32_t playkeyId) override; std::vector GetUgcModels(const LWOOBJID& propertyId) override; @@ -127,7 +127,9 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; - IPropertyContents::Model GetModel(const LWOOBJID modelID) override; + std::optional GetModel(const LWOOBJID modelID) override; + std::optional GetUgcModel(const LWOOBJID ugcId) override; + std::optional GetPropertyInfo(const LWOOBJID id) override; sql::PreparedStatement* CreatePreppedStmt(const std::string& query); private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/ObjectIdTracker.cpp b/dDatabase/GameDatabase/MySQL/Tables/ObjectIdTracker.cpp index f22cd855..462a0edf 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/ObjectIdTracker.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/ObjectIdTracker.cpp @@ -1,17 +1,42 @@ #include "MySQLDatabase.h" -std::optional MySQLDatabase::GetCurrentPersistentId() { +std::optional MySQLDatabase::GetCurrentPersistentId() { auto result = ExecuteSelect("SELECT last_object_id FROM object_id_tracker"); if (!result->next()) { return std::nullopt; } - return result->getUInt("last_object_id"); + return result->getUInt64("last_object_id"); } void MySQLDatabase::InsertDefaultPersistentId() { ExecuteInsert("INSERT INTO object_id_tracker VALUES (1);"); } -void MySQLDatabase::UpdatePersistentId(const uint32_t newId) { - ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = ?;", newId); +IObjectIdTracker::Range MySQLDatabase::GetPersistentIdRange() { + IObjectIdTracker::Range range; + auto prevCommit = GetAutoCommit(); + SetAutoCommit(false); + + // THIS MUST ABSOLUTELY NOT FAIL. These IDs are expected to be unique. As such a transactional select is used to safely get a range + // of IDs that will never be used again. A separate feature could track unused IDs and recycle them, but that is not implemented. + ExecuteCustomQuery("START TRANSACTION;"); + // 200 seems like a good range to reserve at once. Only way this would be noticable is if a player + // added hundreds of items at once. + // Doing the update first ensures that all other connections are blocked from accessing this table until we commit. + auto result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;"); + // If no rows were updated, it means the table is empty, so we need to insert the default row first. + if (result == 0) { + InsertDefaultPersistentId(); + result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;"); + } + + // At this point all connections are waiting on us to finish the transaction, so we can safely select the ID we just set. + auto selectRes = ExecuteSelect("SELECT last_object_id FROM object_id_tracker;"); + selectRes->next(); + range.maxID = selectRes->getUInt64("last_object_id"); + range.minID = range.maxID - 199; + + ExecuteCustomQuery("COMMIT;"); + SetAutoCommit(prevCommit); + return range; } diff --git a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp index 662070df..18916e24 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp @@ -1,6 +1,23 @@ #include "MySQLDatabase.h" #include "ePropertySortType.h" +IProperty::Info ReadPropertyInfo(UniqueResultSet& result) { + IProperty::Info info; + info.id = result->getUInt64("id"); + info.ownerId = result->getInt64("owner_id"); + info.cloneId = result->getUInt64("clone_id"); + info.name = result->getString("name").c_str(); + info.description = result->getString("description").c_str(); + info.privacyOption = result->getInt("privacy_option"); + info.rejectionReason = result->getString("rejection_reason").c_str(); + info.lastUpdatedTime = result->getUInt("last_updated"); + info.claimedTime = result->getUInt("time_claimed"); + info.reputation = result->getUInt("reputation"); + info.modApproved = result->getUInt("mod_approved"); + info.performanceCost = result->getFloat("performance_cost"); + return info; +} + std::optional MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { std::optional result; std::string query; @@ -117,19 +134,7 @@ std::optional MySQLDatabase::GetProperties(co while (properties->next()) { if (!result) result = IProperty::PropertyEntranceResult(); - auto& entry = result->entries.emplace_back(); - entry.id = properties->getUInt64("id"); - entry.ownerId = properties->getInt64("owner_id"); - entry.cloneId = properties->getUInt64("clone_id"); - entry.name = properties->getString("name").c_str(); - entry.description = properties->getString("description").c_str(); - entry.privacyOption = properties->getInt("privacy_option"); - entry.rejectionReason = properties->getString("rejection_reason").c_str(); - entry.lastUpdatedTime = properties->getUInt("last_updated"); - entry.claimedTime = properties->getUInt("time_claimed"); - entry.reputation = properties->getUInt("reputation"); - entry.modApproved = properties->getUInt("mod_approved"); - entry.performanceCost = properties->getFloat("performance_cost"); + result->entries.push_back(ReadPropertyInfo(properties)); } return result; @@ -144,21 +149,7 @@ std::optional MySQLDatabase::GetPropertyInfo(const LWOMAPID map return std::nullopt; } - IProperty::Info toReturn; - toReturn.id = propertyEntry->getUInt64("id"); - toReturn.ownerId = propertyEntry->getInt64("owner_id"); - toReturn.cloneId = propertyEntry->getUInt64("clone_id"); - toReturn.name = propertyEntry->getString("name").c_str(); - toReturn.description = propertyEntry->getString("description").c_str(); - toReturn.privacyOption = propertyEntry->getInt("privacy_option"); - toReturn.rejectionReason = propertyEntry->getString("rejection_reason").c_str(); - toReturn.lastUpdatedTime = propertyEntry->getUInt("last_updated"); - toReturn.claimedTime = propertyEntry->getUInt("time_claimed"); - toReturn.reputation = propertyEntry->getUInt("reputation"); - toReturn.modApproved = propertyEntry->getUInt("mod_approved"); - toReturn.performanceCost = propertyEntry->getFloat("performance_cost"); - - return toReturn; + return ReadPropertyInfo(propertyEntry); } void MySQLDatabase::UpdatePropertyModerationInfo(const IProperty::Info& info) { @@ -195,3 +186,15 @@ void MySQLDatabase::InsertNewProperty(const IProperty::Info& info, const uint32_ zoneId.GetMapID() ); } + +std::optional MySQLDatabase::GetPropertyInfo(const LWOOBJID id) { + auto propertyEntry = ExecuteSelect( + "SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost " + "FROM properties WHERE id = ?;", id); + + if (!propertyEntry->next()) { + return std::nullopt; + } + + return ReadPropertyInfo(propertyEntry); +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp index 44394518..cecea68b 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp @@ -64,26 +64,27 @@ void MySQLDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } -IPropertyContents::Model MySQLDatabase::GetModel(const LWOOBJID modelID) { +std::optional MySQLDatabase::GetModel(const LWOOBJID modelID) { auto result = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); - IPropertyContents::Model model{}; + std::optional model = std::nullopt; while (result->next()) { - model.id = result->getUInt64("id"); - model.lot = static_cast(result->getUInt("lot")); - model.position.x = result->getFloat("x"); - model.position.y = result->getFloat("y"); - model.position.z = result->getFloat("z"); - model.rotation.w = result->getFloat("rw"); - model.rotation.x = result->getFloat("rx"); - model.rotation.y = result->getFloat("ry"); - model.rotation.z = result->getFloat("rz"); - model.ugcId = result->getUInt64("ugc_id"); - model.behaviors[0] = result->getUInt64("behavior_1"); - model.behaviors[1] = result->getUInt64("behavior_2"); - model.behaviors[2] = result->getUInt64("behavior_3"); - model.behaviors[3] = result->getUInt64("behavior_4"); - model.behaviors[4] = result->getUInt64("behavior_5"); + model = IPropertyContents::Model{}; + model->id = result->getUInt64("id"); + model->lot = static_cast(result->getUInt("lot")); + model->position.x = result->getFloat("x"); + model->position.y = result->getFloat("y"); + model->position.z = result->getFloat("z"); + model->rotation.w = result->getFloat("rw"); + model->rotation.x = result->getFloat("rx"); + model->rotation.y = result->getFloat("ry"); + model->rotation.z = result->getFloat("rz"); + model->ugcId = result->getUInt64("ugc_id"); + model->behaviors[0] = result->getUInt64("behavior_1"); + model->behaviors[1] = result->getUInt64("behavior_2"); + model->behaviors[2] = result->getUInt64("behavior_3"); + model->behaviors[3] = result->getUInt64("behavior_4"); + model->behaviors[4] = result->getUInt64("behavior_5"); } return model; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 031ae6c7..06865c5e 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -1,5 +1,17 @@ #include "MySQLDatabase.h" +IUgc::Model ReadModel(UniqueResultSet& result) { + IUgc::Model model; + + // blob is owned by the query, so we need to do a deep copy :/ + std::unique_ptr blob(result->getBlob("lxfml")); + model.lxfmlData << blob->rdbuf(); + model.id = result->getUInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); + + return model; +} + std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto result = ExecuteSelect( "SELECT lxfml, u.id as ugcID, pc.id as modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", @@ -8,14 +20,7 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) std::vector toReturn; while (result->next()) { - IUgc::Model model; - - // blob is owned by the query, so we need to do a deep copy :/ - std::unique_ptr blob(result->getBlob("lxfml")); - model.lxfmlData << blob->rdbuf(); - model.id = result->getUInt64("ugcID"); - model.modelID = result->getUInt64("modelID"); - toReturn.push_back(std::move(model)); + toReturn.push_back(ReadModel(result)); } return toReturn; @@ -27,14 +32,7 @@ std::vector MySQLDatabase::GetAllUgcModels() { std::vector models; models.reserve(result->rowsCount()); while (result->next()) { - IUgc::Model model; - model.id = result->getInt64("ugcID"); - model.modelID = result->getUInt64("modelID"); - - // blob is owned by the query, so we need to do a deep copy :/ - std::unique_ptr blob(result->getBlob("lxfml")); - model.lxfmlData << blob->rdbuf(); - models.push_back(std::move(model)); + models.push_back(ReadModel(result)); } return models; @@ -45,7 +43,7 @@ void MySQLDatabase::RemoveUnreferencedUgcModels() { } void MySQLDatabase::InsertNewUgcModel( - std:: stringstream& sd0Data, // cant be const sad + std::stringstream& sd0Data, // cant be const sad const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId) { @@ -71,3 +69,14 @@ void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstrea const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } + +std::optional MySQLDatabase::GetUgcModel(const LWOOBJID ugcId) { + auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE u.id = ?", ugcId); + + std::optional toReturn = std::nullopt; + if (result->next()) { + toReturn = ReadModel(result); + } + + return toReturn; +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 54dc39d3..3b6dc643 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -96,9 +96,9 @@ public: void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override; void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override; void SetMasterInfo(const IServers::MasterInfo& info) override; - std::optional GetCurrentPersistentId() override; + std::optional GetCurrentPersistentId() override; + IObjectIdTracker::Range GetPersistentIdRange() override; void InsertDefaultPersistentId() override; - void UpdatePersistentId(const uint32_t id) override; std::optional GetDonationTotal(const uint32_t activityId) override; std::optional IsPlaykeyActive(const int32_t playkeyId) override; std::vector GetUgcModels(const LWOOBJID& propertyId) override; @@ -125,7 +125,9 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; - IPropertyContents::Model GetModel(const LWOOBJID modelID) override; + std::optional GetModel(const LWOOBJID modelID) override; + std::optional GetUgcModel(const LWOOBJID ugcId) override; + std::optional GetPropertyInfo(const LWOOBJID id) override; private: CppSQLite3Statement CreatePreppedStmt(const std::string& query); diff --git a/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp b/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp index af8014dd..aef1d267 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp @@ -1,17 +1,40 @@ #include "SQLiteDatabase.h" -std::optional SQLiteDatabase::GetCurrentPersistentId() { +std::optional SQLiteDatabase::GetCurrentPersistentId() { auto [_, result] = ExecuteSelect("SELECT last_object_id FROM object_id_tracker"); if (result.eof()) { return std::nullopt; } - return result.getIntField("last_object_id"); + return result.getInt64Field("last_object_id"); } void SQLiteDatabase::InsertDefaultPersistentId() { ExecuteInsert("INSERT INTO object_id_tracker VALUES (1);"); } -void SQLiteDatabase::UpdatePersistentId(const uint32_t newId) { - ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = ?;", newId); +IObjectIdTracker::Range SQLiteDatabase::GetPersistentIdRange() { + IObjectIdTracker::Range range; + auto prevCommit = GetAutoCommit(); + SetAutoCommit(false); + + // THIS MUST ABSOLUTELY NOT FAIL. These IDs are expected to be unique. As such a transactional select is used to safely get a range + // of IDs that will never be used again. A separate feature could track unused IDs and recycle them, but that is not implemented. + ExecuteCustomQuery("BEGIN TRANSACTION;"); + // 200 seems like a good range to reserve at once. Only way this would be noticable is if a player + // added hundreds of items at once. + // Doing the update first ensures that all other connections are blocked from accessing this table until we commit. + auto result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;"); + if (result == 0) { + InsertDefaultPersistentId(); + result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;"); + } + + // At this point all connections are waiting on us to finish the transaction, so we can safely select the ID we just set. + auto [_, selectResult] = ExecuteSelect("SELECT last_object_id FROM object_id_tracker;"); + range.maxID = selectResult.getInt64Field("last_object_id"); + range.minID = range.maxID - 199; + + ExecuteCustomQuery("COMMIT;"); + SetAutoCommit(prevCommit); + return range; } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp index 195fee2a..67fc57b3 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp @@ -1,6 +1,23 @@ #include "SQLiteDatabase.h" #include "ePropertySortType.h" +IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) { + IProperty::Info toReturn; + toReturn.id = propertyEntry.getInt64Field("id"); + toReturn.ownerId = propertyEntry.getInt64Field("owner_id"); + toReturn.cloneId = propertyEntry.getInt64Field("clone_id"); + toReturn.name = propertyEntry.getStringField("name"); + toReturn.description = propertyEntry.getStringField("description"); + toReturn.privacyOption = propertyEntry.getIntField("privacy_option"); + toReturn.rejectionReason = propertyEntry.getStringField("rejection_reason"); + toReturn.lastUpdatedTime = propertyEntry.getIntField("last_updated"); + toReturn.claimedTime = propertyEntry.getIntField("time_claimed"); + toReturn.reputation = propertyEntry.getIntField("reputation"); + toReturn.modApproved = propertyEntry.getIntField("mod_approved"); + toReturn.performanceCost = propertyEntry.getFloatField("performance_cost"); + return toReturn; +} + std::optional SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { std::optional result; std::string query; @@ -118,19 +135,7 @@ std::optional SQLiteDatabase::GetProperties(c auto& [_, properties] = propertiesRes; if (!properties.eof() && !result.has_value()) result = IProperty::PropertyEntranceResult(); while (!properties.eof()) { - auto& entry = result->entries.emplace_back(); - entry.id = properties.getInt64Field("id"); - entry.ownerId = properties.getInt64Field("owner_id"); - entry.cloneId = properties.getInt64Field("clone_id"); - entry.name = properties.getStringField("name"); - entry.description = properties.getStringField("description"); - entry.privacyOption = properties.getIntField("privacy_option"); - entry.rejectionReason = properties.getStringField("rejection_reason"); - entry.lastUpdatedTime = properties.getIntField("last_updated"); - entry.claimedTime = properties.getIntField("time_claimed"); - entry.reputation = properties.getIntField("reputation"); - entry.modApproved = properties.getIntField("mod_approved"); - entry.performanceCost = properties.getFloatField("performance_cost"); + result->entries.push_back(ReadPropertyInfo(properties)); properties.nextRow(); } @@ -146,21 +151,7 @@ std::optional SQLiteDatabase::GetPropertyInfo(const LWOMAPID ma return std::nullopt; } - IProperty::Info toReturn; - toReturn.id = propertyEntry.getInt64Field("id"); - toReturn.ownerId = propertyEntry.getInt64Field("owner_id"); - toReturn.cloneId = propertyEntry.getInt64Field("clone_id"); - toReturn.name = propertyEntry.getStringField("name"); - toReturn.description = propertyEntry.getStringField("description"); - toReturn.privacyOption = propertyEntry.getIntField("privacy_option"); - toReturn.rejectionReason = propertyEntry.getStringField("rejection_reason"); - toReturn.lastUpdatedTime = propertyEntry.getIntField("last_updated"); - toReturn.claimedTime = propertyEntry.getIntField("time_claimed"); - toReturn.reputation = propertyEntry.getIntField("reputation"); - toReturn.modApproved = propertyEntry.getIntField("mod_approved"); - toReturn.performanceCost = propertyEntry.getFloatField("performance_cost"); - - return toReturn; + return ReadPropertyInfo(propertyEntry); } void SQLiteDatabase::UpdatePropertyModerationInfo(const IProperty::Info& info) { @@ -197,3 +188,15 @@ void SQLiteDatabase::InsertNewProperty(const IProperty::Info& info, const uint32 zoneId.GetMapID() ); } + +std::optional SQLiteDatabase::GetPropertyInfo(const LWOOBJID id) { + auto [_, propertyEntry] = ExecuteSelect( + "SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost " + "FROM properties WHERE id = ?;", id); + + if (propertyEntry.eof()) { + return std::nullopt; + } + + return ReadPropertyInfo(propertyEntry); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp index a04f3536..5251c534 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp @@ -64,27 +64,28 @@ void SQLiteDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } -IPropertyContents::Model SQLiteDatabase::GetModel(const LWOOBJID modelID) { +std::optional SQLiteDatabase::GetModel(const LWOOBJID modelID) { auto [_, result] = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); - IPropertyContents::Model model{}; + std::optional model = std::nullopt; if (!result.eof()) { do { - model.id = result.getInt64Field("id"); - model.lot = static_cast(result.getIntField("lot")); - model.position.x = result.getFloatField("x"); - model.position.y = result.getFloatField("y"); - model.position.z = result.getFloatField("z"); - model.rotation.w = result.getFloatField("rw"); - model.rotation.x = result.getFloatField("rx"); - model.rotation.y = result.getFloatField("ry"); - model.rotation.z = result.getFloatField("rz"); - model.ugcId = result.getInt64Field("ugc_id"); - model.behaviors[0] = result.getInt64Field("behavior_1"); - model.behaviors[1] = result.getInt64Field("behavior_2"); - model.behaviors[2] = result.getInt64Field("behavior_3"); - model.behaviors[3] = result.getInt64Field("behavior_4"); - model.behaviors[4] = result.getInt64Field("behavior_5"); + model = IPropertyContents::Model{}; + model->id = result.getInt64Field("id"); + model->lot = static_cast(result.getIntField("lot")); + model->position.x = result.getFloatField("x"); + model->position.y = result.getFloatField("y"); + model->position.z = result.getFloatField("z"); + model->rotation.w = result.getFloatField("rw"); + model->rotation.x = result.getFloatField("rx"); + model->rotation.y = result.getFloatField("ry"); + model->rotation.z = result.getFloatField("rz"); + model->ugcId = result.getInt64Field("ugc_id"); + model->behaviors[0] = result.getInt64Field("behavior_1"); + model->behaviors[1] = result.getInt64Field("behavior_2"); + model->behaviors[2] = result.getInt64Field("behavior_3"); + model->behaviors[3] = result.getInt64Field("behavior_4"); + model->behaviors[4] = result.getInt64Field("behavior_5"); } while (result.nextRow()); } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 31a6a2e7..0649bb9f 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -1,5 +1,17 @@ #include "SQLiteDatabase.h" +IUgc::Model ReadModel(CppSQLite3Query& result) { + IUgc::Model model; + + int blobSize{}; + const auto* blob = result.getBlobField("lxfml", blobSize); + model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); + + return model; +} + std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto [_, result] = ExecuteSelect( "SELECT lxfml, u.id AS ugcID, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", @@ -8,14 +20,7 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId std::vector toReturn; while (!result.eof()) { - IUgc::Model model; - - int blobSize{}; - const auto* blob = result.getBlobField("lxfml", blobSize); - model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); - model.id = result.getInt64Field("ugcID"); - model.modelID = result.getInt64Field("modelID"); - toReturn.push_back(std::move(model)); + toReturn.push_back(ReadModel(result)); result.nextRow(); } @@ -27,14 +32,7 @@ std::vector SQLiteDatabase::GetAllUgcModels() { std::vector models; while (!result.eof()) { - IUgc::Model model; - model.id = result.getInt64Field("ugcID"); - model.modelID = result.getInt64Field("modelID"); - - int blobSize{}; - const auto* blob = result.getBlobField("lxfml", blobSize); - model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); - models.push_back(std::move(model)); + models.push_back(ReadModel(result)); result.nextRow(); } @@ -72,3 +70,14 @@ void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstre const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } + +std::optional SQLiteDatabase::GetUgcModel(const LWOOBJID ugcId) { + auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.id = u.id WHERE u.id = ?;", ugcId); + + std::optional toReturn = std::nullopt; + if (!result.eof()) { + toReturn = ReadModel(result); + } + + return toReturn; +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index f9c9fb09..1cd48ad9 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -244,7 +244,7 @@ void TestSQLDatabase::SetMasterInfo(const IServers::MasterInfo& info) { } -std::optional TestSQLDatabase::GetCurrentPersistentId() { +std::optional TestSQLDatabase::GetCurrentPersistentId() { return {}; } @@ -252,10 +252,6 @@ void TestSQLDatabase::InsertDefaultPersistentId() { } -void TestSQLDatabase::UpdatePersistentId(const uint32_t id) { - -} - std::optional TestSQLDatabase::GetDonationTotal(const uint32_t activityId) { return {}; } @@ -304,3 +300,6 @@ void TestSQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGame } +IObjectIdTracker::Range TestSQLDatabase::GetPersistentIdRange() { + return {}; +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 87e08e91..2c7890dd 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -75,9 +75,9 @@ class TestSQLDatabase : public GameDatabase { void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override; void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override; void SetMasterInfo(const IServers::MasterInfo& info) override; - std::optional GetCurrentPersistentId() override; + std::optional GetCurrentPersistentId() override; + IObjectIdTracker::Range GetPersistentIdRange() override; void InsertDefaultPersistentId() override; - void UpdatePersistentId(const uint32_t id) override; std::optional GetDonationTotal(const uint32_t activityId) override; std::optional IsPlaykeyActive(const int32_t playkeyId) override; std::vector GetUgcModels(const LWOOBJID& propertyId) override; @@ -105,7 +105,9 @@ class TestSQLDatabase : public GameDatabase { uint32_t GetAccountCount() override { return 0; }; bool IsNameInUse(const std::string_view name) override { return false; }; - IPropertyContents::Model GetModel(const LWOOBJID modelID) override { return {}; } + std::optional GetModel(const LWOOBJID modelID) override { return {}; } + std::optional GetPropertyInfo(const LWOOBJID id) override { return {}; } + std::optional GetUgcModel(const LWOOBJID ugcId) override { return {}; } }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/ModelNormalizeMigration.cpp b/dDatabase/ModelNormalizeMigration.cpp index b415ef57..45fcdb82 100644 --- a/dDatabase/ModelNormalizeMigration.cpp +++ b/dDatabase/ModelNormalizeMigration.cpp @@ -10,7 +10,7 @@ void ModelNormalizeMigration::Run() { for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) { const auto model = Database::Get()->GetModel(modelID); // only BBB models (lot 14) and models with a position of NiPoint3::ZERO need to have their position fixed. - if (model.position != NiPoint3Constant::ZERO || model.lot != 14) continue; + if (!model || model->position != NiPoint3Constant::ZERO || model->lot != 14) continue; Sd0 sd0(lxfmlData); const auto asStr = sd0.GetAsStringUncompressed(); @@ -23,7 +23,7 @@ void ModelNormalizeMigration::Run() { LOG("Updated model %llu to have a center of %f %f %f", modelID, newCenter.x, newCenter.y, newCenter.z); sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); auto asStream = sd0.GetAsStream(); - Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors); + Database::Get()->UpdateModel(model->id, newCenter, model->rotation, model->behaviors); Database::Get()->UpdateUgcModelData(id, asStream); } Database::Get()->SetAutoCommit(oldCommit); @@ -35,15 +35,15 @@ void ModelNormalizeMigration::RunAfterFirstPart() { for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) { const auto model = Database::Get()->GetModel(modelID); // only BBB models (lot 14) need to have their position fixed from the above blunder - if (model.lot != 14) continue; + if (!model || model->lot != 14) continue; Sd0 sd0(lxfmlData); const auto asStr = sd0.GetAsStringUncompressed(); - const auto [newLxfml, newCenter] = Lxfml::NormalizePositionAfterFirstPart(asStr, model.position); + const auto [newLxfml, newCenter] = Lxfml::NormalizePositionAfterFirstPart(asStr, model->position); sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); auto asStream = sd0.GetAsStream(); - Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors); + Database::Get()->UpdateModel(model->id, newCenter, model->rotation, model->behaviors); Database::Get()->UpdateUgcModelData(id, asStream); } Database::Get()->SetAutoCommit(oldCommit); @@ -55,16 +55,16 @@ void ModelNormalizeMigration::RunBrickBuildGrid() { for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) { const auto model = Database::Get()->GetModel(modelID); // only BBB models (lot 14) need to have their position fixed from the above blunder - if (model.lot != 14) continue; + if (!model || model->lot != 14) continue; Sd0 sd0(lxfmlData); const auto asStr = sd0.GetAsStringUncompressed(); - const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr, model.position); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr, model->position); sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); LOG("Updated model %llu to have a center of %f %f %f", modelID, newCenter.x, newCenter.y, newCenter.z); auto asStream = sd0.GetAsStream(); - Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors); + Database::Get()->UpdateModel(model->id, newCenter, model->rotation, model->behaviors); Database::Get()->UpdateUgcModelData(id, asStream); } Database::Get()->SetAutoCommit(oldCommit); diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index 82afb063..343ce28d 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -30,6 +30,7 @@ #include "BitStreamUtils.h" #include "CheatDetection.h" #include "CharacterComponent.h" +#include "eCharacterVersion.h" UserManager* UserManager::m_Address = nullptr; @@ -324,79 +325,77 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) LOG("AccountID: %i is creating a character with name: %s (temporary: %s)", u->GetAccountID(), name.c_str(), predefinedName.c_str()); } - //Now that the name is ok, we can get an objectID from Master: - ObjectIDManager::RequestPersistentID([=, this](uint32_t persistentID) { - LWOOBJID objectID = persistentID; - GeneralUtils::SetBit(objectID, eObjectBits::CHARACTER); - if (Database::Get()->GetCharacterInfo(objectID)) { - LOG("Character object id unavailable, check object_id_tracker!"); - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE); - return; - } + //Now that the name is ok, we can get a persistent ObjectID: + LWOOBJID objectID = ObjectIDManager::GetPersistentID(); + const uint32_t maxRetries = 100; + uint32_t tries = 0; + while (Database::Get()->GetCharacterInfo(objectID) && tries < maxRetries) { + tries++; + LOG("Found a duplicate character %llu, getting a new objectID", objectID); + objectID = ObjectIDManager::GetPersistentID(); + } - std::stringstream xml; - xml << ""; + if (tries >= maxRetries) { + LOG("Failed to get a unique objectID for new character after %i tries, aborting char creation for account %i", maxRetries, u->GetAccountID()); + WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE); + return; + } - xml << ""; + std::stringstream xml; + xml << ""; - xml << "GetAccountID() << "\" cc=\"0\" gm=\"0\" ft=\"0\" llog=\"" << time(NULL) << "\" "; - xml << "ls=\"0\" lzx=\"-626.5847\" lzy=\"613.3515\" lzz=\"-28.6374\" lzrx=\"0.0\" lzry=\"0.7015\" lzrz=\"0.0\" lzrw=\"0.7126\" "; - xml << "stt=\"0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;\">"; - xml << ""; + xml << ""; - xml << ""; + xml << "GetAccountID() << "\" cc=\"0\" gm=\"0\" ft=\"0\" llog=\"" << time(NULL) << "\" "; + xml << "ls=\"0\" lzx=\"-626.5847\" lzy=\"613.3515\" lzz=\"-28.6374\" lzrx=\"0.0\" lzry=\"0.7015\" lzrz=\"0.0\" lzrw=\"0.7126\" "; + xml << "stt=\"0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;\">"; + xml << ""; - xml << ""; + xml << ""; - xml << ""; + xml << ""; - LWOOBJID lwoidforshirt = ObjectIDManager::GenerateRandomObjectID(); - LWOOBJID lwoidforpants; + xml << ""; - do { - lwoidforpants = ObjectIDManager::GenerateRandomObjectID(); - } while (lwoidforpants == lwoidforshirt); //Make sure we don't have the same ID for both shirt and pants + LWOOBJID lwoidforshirt = ObjectIDManager::GetPersistentID(); + LWOOBJID lwoidforpants = ObjectIDManager::GetPersistentID(); - GeneralUtils::SetBit(lwoidforshirt, eObjectBits::CHARACTER); - GeneralUtils::SetBit(lwoidforpants, eObjectBits::CHARACTER); + xml << ""; + xml << ""; - xml << ""; - xml << ""; + xml << ""; - xml << ""; + //Check to see if our name was pre-approved: + bool nameOk = IsNamePreapproved(name); + if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true; - //Check to see if our name was pre-approved: - bool nameOk = IsNamePreapproved(name); - if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true; + // If predefined name is invalid, change it to be their object id + // that way more than one player can create characters if the predefined name files are not provided + auto assignedPredefinedName = predefinedName; + if (assignedPredefinedName == "INVALID") { + std::stringstream nameObjID; + nameObjID << "minifig" << objectID; + assignedPredefinedName = nameObjID.str(); + } - // If predefined name is invalid, change it to be their object id - // that way more than one player can create characters if the predefined name files are not provided - auto assignedPredefinedName = predefinedName; - if (assignedPredefinedName == "INVALID") { - std::stringstream nameObjID; - nameObjID << "minifig" << objectID; - assignedPredefinedName = nameObjID.str(); - } + std::string_view nameToAssign = !name.empty() && nameOk ? name : assignedPredefinedName; + std::string pendingName = !name.empty() && !nameOk ? name : ""; - std::string_view nameToAssign = !name.empty() && nameOk ? name : assignedPredefinedName; - std::string pendingName = !name.empty() && !nameOk ? name : ""; + ICharInfo::Info info; + info.name = nameToAssign; + info.pendingName = pendingName; + info.id = objectID; + info.accountId = u->GetAccountID(); - ICharInfo::Info info; - info.name = nameToAssign; - info.pendingName = pendingName; - info.id = objectID; - info.accountId = u->GetAccountID(); + Database::Get()->InsertNewCharacter(info); - Database::Get()->InsertNewCharacter(info); + //Now finally insert our character xml: + Database::Get()->InsertCharacterXml(objectID, xml.str()); - //Now finally insert our character xml: - Database::Get()->InsertCharacterXml(objectID, xml.str()); - - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::SUCCESS); - UserManager::RequestCharacterList(sysAddr); - }); + WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::SUCCESS); + UserManager::RequestCharacterList(sysAddr); } void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) { diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 4b50ece2..e22becad 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -1786,3 +1786,9 @@ void InventoryComponent::LoadGroupXml(const tinyxml2::XMLElement& groups) { groupElement = groupElement->NextSiblingElement("grp"); } } + +void InventoryComponent::RegenerateItemIDs() { + for (auto* const inventory : m_Inventories | std::views::values) { + inventory->RegenerateItemIDs(); + } +} diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index d67cf3c1..a1441655 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -407,6 +407,9 @@ public: void FixInvisibleItems(); + // Used to migrate a character version, no need to call outside of that context + void RegenerateItemIDs(); + ~InventoryComponent() override; private: diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 5081db6c..df57aef7 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -479,9 +479,19 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) { return; } - LWOOBJID petSubKey = ObjectIDManager::GenerateRandomObjectID(); + LWOOBJID petSubKey = ObjectIDManager::GetPersistentID(); + const uint32_t maxTries = 100; + uint32_t tries = 0; + while (Database::Get()->GetPetNameInfo(petSubKey) && tries < maxTries) { + tries++; + LOG("Found a duplicate pet %llu, getting a new subKey", petSubKey); + petSubKey = ObjectIDManager::GetPersistentID(); + } - GeneralUtils::SetBit(petSubKey, eObjectBits::CHARACTER); + if (tries >= maxTries) { + LOG("Failed to get a unique pet subKey after %i tries, aborting pet creation for player %i", maxTries, tamer->GetCharacter() ? tamer->GetCharacter()->GetID() : -1); + return; + } m_DatabaseId = petSubKey; diff --git a/dGame/dComponents/PropertyEntranceComponent.cpp b/dGame/dComponents/PropertyEntranceComponent.cpp index 783de9a9..d2e595cf 100644 --- a/dGame/dComponents/PropertyEntranceComponent.cpp +++ b/dGame/dComponents/PropertyEntranceComponent.cpp @@ -135,7 +135,7 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl const auto owner = propertyEntry.ownerId; const auto otherCharacter = Database::Get()->GetCharacterInfo(owner); if (!otherCharacter.has_value()) { - LOG("Failed to find property owner name for %u!", owner); + LOG("Failed to find property owner name for %llu!", owner); continue; } auto& entry = entries.emplace_back(); diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 06792897..c6edbf6d 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -170,7 +170,7 @@ void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::s info.name = propertyName; info.description = propertyDescription; info.lastUpdatedTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - + Database::Get()->UpdateLastSave(info); Database::Get()->UpdatePropertyDetails(info); @@ -203,14 +203,22 @@ bool PropertyManagementComponent::Claim(const LWOOBJID playerId) { auto prop_path = zone->GetPath(m_Parent->GetVarAsString(u"propertyName")); - if (prop_path){ + if (prop_path) { if (!prop_path->property.displayName.empty()) name = prop_path->property.displayName; description = prop_path->property.displayDesc; } SetOwnerId(playerId); - propertyId = ObjectIDManager::GenerateRandomObjectID(); + // Due to legacy IDs being random + propertyId = ObjectIDManager::GetPersistentID(); + const uint32_t maxTries = 100; + uint32_t tries = 0; + while (Database::Get()->GetPropertyInfo(propertyId) && tries < maxTries) { + tries++; + LOG("Found a duplicate property %llu, getting a new propertyId", propertyId); + propertyId = ObjectIDManager::GetPersistentID(); + } IProperty::Info info; info.id = propertyId; @@ -374,46 +382,45 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N node->position = position; node->rotation = rotation; - ObjectIDManager::RequestPersistentID([this, node, modelLOT, entity, position, rotation, originalRotation](uint32_t persistentId) { - SpawnerInfo info{}; + SpawnerInfo info{}; - info.templateID = modelLOT; - info.nodes = { node }; - info.templateScale = 1.0f; - info.activeOnLoad = true; - info.amountMaintained = 1; - info.respawnTime = 10; + info.templateID = modelLOT; + info.nodes = { node }; + info.templateScale = 1.0f; + info.activeOnLoad = true; + info.amountMaintained = 1; + info.respawnTime = 10; - info.emulated = true; - info.emulator = Game::entityManager->GetZoneControlEntity()->GetObjectID(); + info.emulated = true; + info.emulator = Game::entityManager->GetZoneControlEntity()->GetObjectID(); - info.spawnerID = persistentId; - GeneralUtils::SetBit(info.spawnerID, eObjectBits::CLIENT); + info.spawnerID = ObjectIDManager::GetPersistentID(); + GeneralUtils::SetBit(info.spawnerID, eObjectBits::CLIENT); - const auto spawnerId = Game::zoneManager->MakeSpawner(info); + const auto spawnerId = Game::zoneManager->MakeSpawner(info); - auto* spawner = Game::zoneManager->GetSpawner(spawnerId); + auto* spawner = Game::zoneManager->GetSpawner(spawnerId); - info.nodes[0]->config.push_back(new LDFData(u"modelBehaviors", 0)); - info.nodes[0]->config.push_back(new LDFData(u"userModelID", info.spawnerID)); - info.nodes[0]->config.push_back(new LDFData(u"modelType", 2)); - info.nodes[0]->config.push_back(new LDFData(u"propertyObjectID", true)); - info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); + info.nodes[0]->config.push_back(new LDFData(u"modelBehaviors", 0)); + info.nodes[0]->config.push_back(new LDFData(u"userModelID", info.spawnerID)); + info.nodes[0]->config.push_back(new LDFData(u"modelType", 2)); + info.nodes[0]->config.push_back(new LDFData(u"propertyObjectID", true)); + info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); - auto* model = spawner->Spawn(); - auto* modelComponent = model->GetComponent(); - if (modelComponent) modelComponent->Pause(); + auto* model = spawner->Spawn(); + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Pause(); - models.insert_or_assign(model->GetObjectID(), spawnerId); + models.insert_or_assign(model->GetObjectID(), spawnerId); - GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), position, m_Parent->GetObjectID(), 14, originalRotation); + GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), position, m_Parent->GetObjectID(), 14, originalRotation); - GameMessages::SendUGCEquipPreCreateBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), 0, spawnerId); + GameMessages::SendUGCEquipPreCreateBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), 0, spawnerId); - GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); + + Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity); - Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity); - }); // Progress place model missions auto missionComponent = entity->GetComponent(); if (missionComponent != nullptr) missionComponent->Progress(eMissionTaskType::PLACE_MODEL, 0); @@ -693,7 +700,7 @@ void PropertyManagementComponent::Save() { // save the behaviors of the model for (const auto& [behaviorId, behaviorStr] : modelBehaviors) { if (behaviorStr.empty() || behaviorId == -1 || behaviorId == 0) continue; - IBehaviors::Info info { + IBehaviors::Info info{ .behaviorId = behaviorId, .characterId = character->GetID(), .behaviorInfo = behaviorStr @@ -821,7 +828,7 @@ void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMess if (!model) continue; auto* const modelComponent = model->GetComponent(); if (!modelComponent) continue; - + modelComponent->OnChatMessageReceived(sMessage); } } diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 168469c9..c7108e50 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -2555,7 +2555,7 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent uint32_t sd0Size; inStream.Read(sd0Size); - std::shared_ptr sd0Data(new char[sd0Size]); + std::unique_ptr sd0Data(new char[sd0Size]); if (sd0Data == nullptr) return; @@ -2579,115 +2579,121 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent //Now, the cave of dragons: - //We runs this in async because the http library here is blocking, meaning it'll halt the thread. - //But we don't want the server to go unresponsive, because then the client would disconnect. - //We need to get a new ID for our model first: - ObjectIDManager::RequestPersistentID([=](uint32_t newID) { - if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return; - LWOOBJID newIDL = newID; - GeneralUtils::SetBit(newIDL, eObjectBits::CHARACTER); + if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return; + const uint32_t maxRetries = 100; + uint32_t retries = 0; + bool blueprintIDExists = true; + bool modelExists = true; - LWOOBJID blueprintID = ObjectIDManager::GenerateRandomObjectID(); - GeneralUtils::SetBit(blueprintID, eObjectBits::CHARACTER); + // Legacy logic to check for old random IDs (regenerating these is not really feasible) + // Probably good to have this anyway in case someone messes with the last_object_id or it gets reset somehow + LWOOBJID newIDL = LWOOBJID_EMPTY; + LWOOBJID blueprintID = LWOOBJID_EMPTY; + do { + if (newIDL != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID); + newIDL = ObjectIDManager::GetPersistentID(); + blueprintID = ObjectIDManager::GetPersistentID(); + ++retries; + blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value(); + modelExists = Database::Get()->GetModel(newIDL).has_value(); + } while ((blueprintIDExists || modelExists) && retries < maxRetries); - //We need to get the propertyID: (stolen from Wincent's propertyManagementComp) - const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); + //We need to get the propertyID: (stolen from Wincent's propertyManagementComp) + const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); - const auto zoneId = worldId.GetMapID(); - const auto cloneId = worldId.GetCloneID(); + const auto zoneId = worldId.GetMapID(); + const auto cloneId = worldId.GetCloneID(); - auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); - LWOOBJID propertyId = LWOOBJID_EMPTY; - if (propertyInfo) propertyId = propertyInfo->id; + auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); + LWOOBJID propertyId = LWOOBJID_EMPTY; + if (propertyInfo) propertyId = propertyInfo->id; - // Save the binary data to the Sd0 buffer - std::string str(sd0Data.get(), sd0Size); - std::istringstream sd0DataStream(str); - Sd0 sd0(sd0DataStream); + // Save the binary data to the Sd0 buffer + std::string str(sd0Data.get(), sd0Size); + std::istringstream sd0DataStream(str); + Sd0 sd0(sd0DataStream); - // Uncompress the data and normalize the position - const auto asStr = sd0.GetAsStringUncompressed(); - const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + // Uncompress the data and normalize the position + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); - // Recompress the data and save to the database - sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); - auto sd0AsStream = sd0.GetAsStream(); - Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); + // Recompress the data and save to the database + sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); + auto sd0AsStream = sd0.GetAsStream(); + Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); - //Insert into the db as a BBB model: - IPropertyContents::Model model; - model.id = newIDL; - model.ugcId = blueprintID; - model.position = newCenter; - model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f); - model.lot = 14; - Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); + //Insert into the db as a BBB model: + IPropertyContents::Model model; + model.id = newIDL; + model.ugcId = blueprintID; + model.position = newCenter; + model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f); + model.lot = 14; + Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); - /* - Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream. - (or you uncomment the lxfml decomp stuff above) - */ + /* + Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream. + (or you uncomment the lxfml decomp stuff above) + */ - ////Send off to UGC for processing, if enabled: - //if (Game::config->GetValue("ugc_remote") == "1") { - // std::string ugcIP = Game::config->GetValue("ugc_ip"); - // int ugcPort = std::stoi(Game::config->GetValue("ugc_port")); + // //Send off to UGC for processing, if enabled: + // if (Game::config->GetValue("ugc_remote") == "1") { + // std::string ugcIP = Game::config->GetValue("ugc_ip"); + // int ugcPort = std::stoi(Game::config->GetValue("ugc_port")); - // httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^ + // httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^ - // //Send out a request: - // std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml"; - // cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml"); + // //Send out a request: + // std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml"; + // cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml"); - // //When the "put" above returns, it means that the UGC HTTP server is done processing our model & - // //the nif, hkx and checksum files are ready to be downloaded from cache. - //} + // //When the "put" above returns, it means that the UGC HTTP server is done processing our model & + // //the nif, hkx and checksum files are ready to be downloaded from cache. + // } - //Tell the client their model is saved: (this causes us to actually pop out of our current state): - const auto& newSd0 = sd0.GetAsVector(); - uint32_t sd0Size{}; - for (const auto& chunk : newSd0) sd0Size += chunk.size(); - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); - bitStream.Write(localId); - bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); - bitStream.Write(1); - bitStream.Write(blueprintID); + //Tell the client their model is saved: (this causes us to actually pop out of our current state): + const auto& newSd0 = sd0.GetAsVector(); + uint32_t newSd0Size{}; + for (const auto& chunk : newSd0) newSd0Size += chunk.size(); + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); + bitStream.Write(localId); + bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); + bitStream.Write(1); + bitStream.Write(blueprintID); - bitStream.Write(sd0Size); + bitStream.Write(newSd0Size); - for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); + for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); - SEND_PACKET; + SEND_PACKET; - //Now we have to construct this object: + //Now we have to construct this object: - EntityInfo info; - info.lot = 14; - info.pos = newCenter; - info.rot = {}; - info.spawner = nullptr; - info.spawnerID = entity->GetObjectID(); - info.spawnerNodeID = 0; + EntityInfo info; + info.lot = 14; + info.pos = newCenter; + info.rot = {}; + info.spawner = nullptr; + info.spawnerID = entity->GetObjectID(); + info.spawnerNodeID = 0; - info.settings.push_back(new LDFData(u"blueprintid", blueprintID)); - info.settings.push_back(new LDFData(u"componentWhitelist", 1)); - info.settings.push_back(new LDFData(u"modelType", 2)); - info.settings.push_back(new LDFData(u"propertyObjectID", true)); - info.settings.push_back(new LDFData(u"userModelID", newIDL)); + info.settings.push_back(new LDFData(u"blueprintid", blueprintID)); + info.settings.push_back(new LDFData(u"componentWhitelist", 1)); + info.settings.push_back(new LDFData(u"modelType", 2)); + info.settings.push_back(new LDFData(u"propertyObjectID", true)); + info.settings.push_back(new LDFData(u"userModelID", newIDL)); - Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); - if (newEntity) { - Game::entityManager->ConstructEntity(newEntity); + Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); + if (newEntity) { + Game::entityManager->ConstructEntity(newEntity); - //Make sure the propMgmt doesn't delete our model after the server dies - //Trying to do this after the entity is constructed. Shouldn't really change anything but - //there was an issue with builds not appearing since it was placed above ConstructEntity. - PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), newIDL); - } - - }); + //Make sure the propMgmt doesn't delete our model after the server dies + //Trying to do this after the entity is constructed. Shouldn't really change anything but + //there was an issue with builds not appearing since it was placed above ConstructEntity. + PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), newIDL); + } } void GameMessages::HandlePropertyEntranceSync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { @@ -5526,55 +5532,52 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* } } - ObjectIDManager::RequestPersistentID([=](uint32_t newId) { - LOG("Build finished"); - GameMessages::SendFinishArrangingWithItem(character, entity->GetObjectID()); // kick them from modular build - GameMessages::SendModularBuildEnd(character); // i dont know if this does anything but DLUv2 did it + LOG("Build finished"); + GameMessages::SendFinishArrangingWithItem(character, entity->GetObjectID()); // kick them from modular build + GameMessages::SendModularBuildEnd(character); // i dont know if this does anything but DLUv2 did it - //inv->UnequipItem(inv->GetItemStackByLOT(6086, eInventoryType::ITEMS)); // take off the thinking cap - //Game::entityManager->SerializeEntity(entity); + //inv->UnequipItem(inv->GetItemStackByLOT(6086, eInventoryType::ITEMS)); // take off the thinking cap + //Game::entityManager->SerializeEntity(entity); - const auto moduleAssembly = new LDFData(u"assemblyPartLOTs", modules); + const auto moduleAssembly = new LDFData(u"assemblyPartLOTs", modules); - std::vector config; - config.push_back(moduleAssembly); + std::vector config; + config.push_back(moduleAssembly); - LWOOBJID newIdBig = newId; - GeneralUtils::SetBit(newIdBig, eObjectBits::CHARACTER); + LWOOBJID newID = ObjectIDManager::GetPersistentID(); - if (count == 3) { - inv->AddItem(6416, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newIdBig); - } else if (count == 7) { - inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newIdBig); + if (count == 3) { + inv->AddItem(6416, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); + } else if (count == 7) { + inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newID); + } + + auto* pCharacter = character->GetCharacter(); + Database::Get()->InsertUgcBuild(GeneralUtils::UTF16ToWTF8(modules), newID, pCharacter ? std::optional(character->GetCharacter()->GetID()) : std::nullopt); + + auto* missionComponent = character->GetComponent(); + + if (entity->GetLOT() != 9980 || Game::server->GetZoneID() != 1200) { + if (missionComponent != nullptr) { + missionComponent->Progress(eMissionTaskType::SCRIPT, entity->GetLOT(), entity->GetObjectID()); + if (count >= 7 && everyPieceSwapped) missionComponent->Progress(eMissionTaskType::RACING, LWOOBJID_EMPTY, static_cast(eRacingTaskParam::MODULAR_BUILDING)); } + } - auto* pCharacter = character->GetCharacter(); - Database::Get()->InsertUgcBuild(GeneralUtils::UTF16ToWTF8(modules), newIdBig, pCharacter ? std::optional(character->GetCharacter()->GetID()) : std::nullopt); + ScriptComponent* script = static_cast(entity->GetComponent(eReplicaComponentType::SCRIPT)); - auto* missionComponent = character->GetComponent(); + entity->GetScript()->OnModularBuildExit(entity, character, count >= 3, modList); - if (entity->GetLOT() != 9980 || Game::server->GetZoneID() != 1200) { - if (missionComponent != nullptr) { - missionComponent->Progress(eMissionTaskType::SCRIPT, entity->GetLOT(), entity->GetObjectID()); - if (count >= 7 && everyPieceSwapped) missionComponent->Progress(eMissionTaskType::RACING, LWOOBJID_EMPTY, static_cast(eRacingTaskParam::MODULAR_BUILDING)); - } - } + // Move remaining temp models back to models + std::vector items; - ScriptComponent* script = static_cast(entity->GetComponent(eReplicaComponentType::SCRIPT)); + for (const auto& pair : temp->GetItems()) { + items.push_back(pair.second); + } - entity->GetScript()->OnModularBuildExit(entity, character, count >= 3, modList); - - // Move remaining temp models back to models - std::vector items; - - for (const auto& pair : temp->GetItems()) { - items.push_back(pair.second); - } - - for (auto* item : items) { - inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false); - } - }); + for (auto* item : items) { + inv->MoveItemToInventory(item, eInventoryType::MODELS, item->GetCount(), false); + } } } diff --git a/dGame/dInventory/Inventory.cpp b/dGame/dInventory/Inventory.cpp index 35222bea..98e34f8d 100644 --- a/dGame/dInventory/Inventory.cpp +++ b/dGame/dInventory/Inventory.cpp @@ -5,8 +5,11 @@ #include "InventoryComponent.h" #include "eItemType.h" #include "eReplicaComponentType.h" +#include "ObjectIDManager.h" +#include "eObjectBits.h" #include "CDComponentsRegistryTable.h" +#include std::vector Inventory::m_GameMasterRestrictedItems = { 1727, // GM Only - JetPack @@ -317,3 +320,16 @@ Inventory::~Inventory() { items.clear(); } + +void Inventory::RegenerateItemIDs() { + std::map newItems{}; + for (auto* const item : items | std::views::values) { + const auto oldID = item->GetId(); + const auto newID = item->GenerateID(); + LOG("Updating item ID from %llu to %llu", oldID, newID); + newItems.insert_or_assign(newID, item); + } + + // We don't want to delete the item pointers, we're just moving from map to map + items = newItems; +} diff --git a/dGame/dInventory/Inventory.h b/dGame/dInventory/Inventory.h index 5e0ac8d6..f07ec68e 100644 --- a/dGame/dInventory/Inventory.h +++ b/dGame/dInventory/Inventory.h @@ -158,6 +158,8 @@ public: */ void DeleteAllItems(); + void RegenerateItemIDs(); + ~Inventory(); private: diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 1a12c25f..89f8fcc1 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -98,21 +98,12 @@ Item::Item( this->preconditions = new PreconditionExpression(this->info->reqPrecondition); this->subKey = subKey; - LWOOBJID id = ObjectIDManager::GenerateRandomObjectID(); - - GeneralUtils::SetBit(id, eObjectBits::CHARACTER); - - const auto type = static_cast(info->itemType); - - if (type == eItemType::MOUNT) { - GeneralUtils::SetBit(id, eObjectBits::CLIENT); - } - - this->id = id; + auto* const inventoryComponent = inventory->GetComponent(); + GenerateID(); inventory->AddManagedItem(this); - auto* entity = inventory->GetComponent()->GetParent(); + auto* entity = inventoryComponent->GetParent(); GameMessages::SendAddItemToInventoryClientSync(entity, entity->GetSystemAddress(), this, id, showFlyingLoot, static_cast(this->count), subKey, lootSourceType); if (isModMoveAndEquip) { @@ -120,7 +111,7 @@ Item::Item( LOG("Move and equipped (%i) from (%i)", this->lot, this->inventory->GetType()); - Game::entityManager->SerializeEntity(inventory->GetComponent()->GetParent()); + Game::entityManager->SerializeEntity(inventoryComponent->GetParent()); } } @@ -573,3 +564,28 @@ void Item::LoadConfigXml(const tinyxml2::XMLElement& i) { config.push_back(LDFBaseData::DataFromString(value)); } } + +LWOOBJID Item::GenerateID() { + auto* const inventoryComponent = inventory->GetComponent(); + const bool isPlayer = inventoryComponent->GetParent()->IsPlayer(); + LWOOBJID id{}; + + // Only players and non-proxy items get persistent IDs (since they are the only ones that will persist between worlds) + if (isPlayer && parent == LWOOBJID_EMPTY) { + id = ObjectIDManager::GetPersistentID(); + } else { + id = ObjectIDManager::GenerateObjectID(); + GeneralUtils::SetBit(id, eObjectBits::SPAWNED); + GeneralUtils::SetBit(id, eObjectBits::CLIENT); + } + + LOG("Parent %i lot %u Generated id %u:%llu", parent, GetLot(), static_cast(id), id); + const auto type = static_cast(info->itemType); + + if (type == eItemType::MOUNT) { + GeneralUtils::SetBit(id, eObjectBits::CLIENT); + } + + this->id = id; + return id; +} diff --git a/dGame/dInventory/Item.h b/dGame/dInventory/Item.h index 72ff264c..846a7aa7 100644 --- a/dGame/dInventory/Item.h +++ b/dGame/dInventory/Item.h @@ -228,6 +228,8 @@ public: void LoadConfigXml(const tinyxml2::XMLElement& i); + LWOOBJID GenerateID(); + private: /** * The object ID of this item diff --git a/dGame/dPropertyBehaviors/ControlBehaviors.cpp b/dGame/dPropertyBehaviors/ControlBehaviors.cpp index 1a142273..70a32981 100644 --- a/dGame/dPropertyBehaviors/ControlBehaviors.cpp +++ b/dGame/dPropertyBehaviors/ControlBehaviors.cpp @@ -35,25 +35,21 @@ void ControlBehaviors::RequestUpdatedID(ControlBehaviorContext& context) { BehaviorMessageBase msgBase{ context.arguments }; const auto oldBehaviorID = msgBase.GetBehaviorId(); - ObjectIDManager::RequestPersistentID( - [context, oldBehaviorID](uint32_t persistentId) { - if (!context) { - LOG("Model to update behavior ID for is null. Cannot update ID."); - return; - } - LWOOBJID persistentIdBig = persistentId; - GeneralUtils::SetBit(persistentIdBig, eObjectBits::CHARACTER); - // This updates the behavior ID of the behavior should this be a new behavior - AMFArrayValue args; + if (!context) { + LOG("Model to update behavior ID for is null. Cannot update ID."); + return; + } + LWOOBJID persistentIdBig = ObjectIDManager::GetPersistentID(); + // This updates the behavior ID of the behavior should this be a new behavior + AMFArrayValue args; - args.Insert("behaviorID", std::to_string(persistentIdBig)); - args.Insert("objectID", std::to_string(context.modelComponent->GetParent()->GetObjectID())); + args.Insert("behaviorID", std::to_string(persistentIdBig)); + args.Insert("objectID", std::to_string(context.modelComponent->GetParent()->GetObjectID())); - GameMessages::SendUIMessageServerToSingleClient(context.modelOwner, context.modelOwner->GetSystemAddress(), "UpdateBehaviorID", args); - context.modelComponent->UpdatePendingBehaviorId(persistentIdBig, oldBehaviorID); + GameMessages::SendUIMessageServerToSingleClient(context.modelOwner, context.modelOwner->GetSystemAddress(), "UpdateBehaviorID", args); + context.modelComponent->UpdatePendingBehaviorId(persistentIdBig, oldBehaviorID); - ControlBehaviors::Instance().SendBehaviorListToClient(context); - }); + ControlBehaviors::Instance().SendBehaviorListToClient(context); } void ControlBehaviors::SendBehaviorListToClient(const ControlBehaviorContext& context) { diff --git a/dGame/dUtilities/ObjectIDManager.cpp b/dGame/dUtilities/ObjectIDManager.cpp index a30ede05..2eda10e2 100644 --- a/dGame/dUtilities/ObjectIDManager.cpp +++ b/dGame/dUtilities/ObjectIDManager.cpp @@ -5,47 +5,38 @@ #include "Database.h" #include "Logger.h" #include "Game.h" +#include "eObjectBits.h" - //! The persistent ID request -struct PersistentIDRequest { - PersistentIDRequest(const uint64_t& requestID, const std::function& callback) : requestID(requestID), callback(callback) {} - uint64_t requestID; - - std::function callback; -}; +// proxy items and dropped loot get SPAWNED + CLIENT flag and need to have the ChangeObjectWorldState message sent to them +// should the spawners from vanity also have the CLIENT flag? namespace { - std::vector Requests; //!< All outstanding persistent ID requests - uint64_t CurrentRequestID = 0; //!< The current request ID - uint32_t CurrentObjectID = uint32_t(1152921508165007067); //!< The current object ID - std::uniform_int_distribution Uni(10000000, INT32_MAX); + // Start the range in a way that it when first called it will fetch some new persistent IDs + std::optional CurrentRange = std::nullopt; + uint32_t CurrentObjectID = uint32_t(1152921508165007067); // The current object ID (this should really start at the highest current ID in the world, then increment from there) }; -//! Requests a persistent ID -void ObjectIDManager::RequestPersistentID(const std::function callback) { - const auto& request = Requests.emplace_back(++CurrentRequestID, callback); +uint64_t ObjectIDManager::GetPersistentID() { + if (!CurrentRange.has_value() || CurrentRange->minID > CurrentRange->maxID) { + CurrentRange = Database::Get()->GetPersistentIdRange(); + // We're getting close to being out of IDs in this range, log a warning + const auto WARNING_RANGE = 70368744100000ULL; + if (CurrentRange->minID >= 70368744100000ULL) { + LOG("WARNING: Your server is running low on persistent IDs, please consider an ID squash in the near future."); + } - MasterPackets::SendPersistentIDRequest(Game::server, request.requestID); + LOG("Reserved object ID range: %llu - %llu", CurrentRange->minID, CurrentRange->maxID); + } + + const auto usedID = CurrentRange->minID++; + auto toReturn = usedID; + // Any IDs gotten from persistent IDs use the CHARACTER bit + GeneralUtils::SetBit(toReturn, eObjectBits::CHARACTER); + LOG("Using ID: %llu:%llu", toReturn, usedID); + return toReturn; } -//! Handles a persistent ID response -void ObjectIDManager::HandleRequestPersistentIDResponse(const uint64_t requestID, const uint32_t persistentID) { - auto it = std::find_if(Requests.begin(), Requests.end(), [requestID](const PersistentIDRequest& request) { - return request.requestID == requestID; - }); - - if (it == Requests.end()) return; - - it->callback(persistentID); - Requests.erase(it); -} - -//! Handles cases where we have to get a unique object ID synchronously -uint32_t ObjectIDManager::GenerateRandomObjectID() { - return Uni(Game::randomEngine); -} - -//! Generates an object ID server-sided (used for regular entities like smashables) +// Generates an object ID server-sided (used for regular entities like smashables) uint32_t ObjectIDManager::GenerateObjectID() { return ++CurrentObjectID; } diff --git a/dGame/dUtilities/ObjectIDManager.h b/dGame/dUtilities/ObjectIDManager.h index 7650e4cc..4e2f245f 100644 --- a/dGame/dUtilities/ObjectIDManager.h +++ b/dGame/dUtilities/ObjectIDManager.h @@ -5,36 +5,25 @@ #include #include -/*! - \file ObjectIDManager.h - \brief A manager for handling object ID generation +/** + * There are 2 types of IDs: + * Persistent IDs - These are used for anything that needs to be persist between worlds. + * Ephemeral IDs - These are used for any objects that only need to be unique for this world session. */ -//! The Object ID Manager namespace ObjectIDManager { - //! Requests a persistent ID - /*! - \param callback The callback function + + /** + * @brief Returns a Persistent ID with the CHARACTER bit set. + * + * @return uint64_t A unique persistent ID with the CHARACTER bit set. */ - void RequestPersistentID(const std::function callback); + uint64_t GetPersistentID(); - - //! Handles a persistent ID response - /*! - \param requestID The request ID - \param persistentID The persistent ID - */ - void HandleRequestPersistentIDResponse(const uint64_t requestID, const uint32_t persistentID); - - //! Generates an object ID server-sided - /*! - \return A generated object ID + /** + * @brief Generates an ephemeral object ID for non-persistent objects. + * + * @return uint32_t */ uint32_t GenerateObjectID(); - - //! Generates a random object ID server-sided - /*! - \return A generated object ID - */ - uint32_t GenerateRandomObjectID(); }; diff --git a/dMasterServer/CMakeLists.txt b/dMasterServer/CMakeLists.txt index 2e2b4dd9..ec313ab0 100644 --- a/dMasterServer/CMakeLists.txt +++ b/dMasterServer/CMakeLists.txt @@ -1,6 +1,5 @@ set(DMASTERSERVER_SOURCES "InstanceManager.cpp" - "PersistentIDManager.cpp" "Start.cpp" ) diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 34d44ea8..32a2cb56 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -35,7 +35,6 @@ #include "Game.h" #include "InstanceManager.h" #include "MasterPackets.h" -#include "PersistentIDManager.h" #include "FdbToSqlite.h" #include "BitStreamUtils.h" #include "Start.h" @@ -360,7 +359,6 @@ int main(int argc, char** argv) { Database::Get()->SetMasterInfo(info); //Create additional objects here: - PersistentIDManager::Initialize(); Game::im = new InstanceManager(Game::server->GetIP()); //Get CDClient initial information @@ -534,17 +532,6 @@ void HandlePacket(Packet* packet) { if (static_cast(packet->data[1]) == ServiceType::MASTER) { switch (static_cast(packet->data[3])) { - case MessageType::Master::REQUEST_PERSISTENT_ID: { - LOG("A persistent ID req"); - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); - uint64_t requestID = 0; - inStream.Read(requestID); - - uint32_t objID = PersistentIDManager::GeneratePersistentID(); - MasterPackets::SendPersistentIDResponse(Game::server, packet->systemAddress, requestID, objID); - break; - } case MessageType::Master::REQUEST_ZONE_TRANSFER: { LOG("Received zone transfer req"); @@ -882,9 +869,6 @@ int ShutdownSequence(int32_t signal) { LOG("Triggered master shutdown"); } - PersistentIDManager::SaveToDatabase(); - LOG("Saved ObjectIDTracker to DB"); - // A server might not be finished spinning up yet, remove all of those here. for (const auto& instance : Game::im->GetInstances()) { if (!instance) continue; diff --git a/dMasterServer/PersistentIDManager.cpp b/dMasterServer/PersistentIDManager.cpp deleted file mode 100644 index 1b3a1c0b..00000000 --- a/dMasterServer/PersistentIDManager.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "PersistentIDManager.h" - -// Custom Classes -#include "Database.h" -#include "Logger.h" -#include "Game.h" - -namespace { - uint32_t CurrentPersistentID = 1; //!< The highest current persistent ID in use -}; - -//! Initializes the manager -void PersistentIDManager::Initialize() { - try { - auto lastObjectId = Database::Get()->GetCurrentPersistentId(); - - if (!lastObjectId) { - Database::Get()->InsertDefaultPersistentId(); - } else { - CurrentPersistentID = lastObjectId.value(); - } - - if (CurrentPersistentID <= 0) { - LOG("Invalid persistent object ID in database. Aborting to prevent bad id generation."); - throw std::runtime_error("Invalid persistent object ID in database. Aborting to prevent bad id generation."); - } - } catch (std::exception& e) { - LOG("Unable to fetch max persistent object ID in use. This will cause issues. Aborting to prevent collisions."); - LOG("Error: %s", e.what()); - throw e; - } -} - -//! Generates a new persistent ID -uint32_t PersistentIDManager::GeneratePersistentID() { - uint32_t toReturn = ++CurrentPersistentID; - - SaveToDatabase(); - - return toReturn; -} - -void PersistentIDManager::SaveToDatabase() { - Database::Get()->UpdatePersistentId(CurrentPersistentID); -} diff --git a/dMasterServer/PersistentIDManager.h b/dMasterServer/PersistentIDManager.h deleted file mode 100644 index 916ee33a..00000000 --- a/dMasterServer/PersistentIDManager.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -// C++ -#include - -/*! - \file PersistentIDManager.h - \brief A manager that handles requests for object IDs - */ - - //! The Object ID Manager -namespace PersistentIDManager { - //! Initializes the manager - void Initialize(); - - //! Generates a new persistent ID - /*! - \return The new persistent ID - */ - uint32_t GeneratePersistentID(); - - void SaveToDatabase(); -}; diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index 36a23f4f..aac49929 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -8,23 +8,6 @@ #include -void MasterPackets::SendPersistentIDRequest(dServer* server, uint64_t requestID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_PERSISTENT_ID); - bitStream.Write(requestID); - server->SendToMaster(bitStream); -} - -void MasterPackets::SendPersistentIDResponse(dServer* server, const SystemAddress& sysAddr, uint64_t requestID, uint32_t objID) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_PERSISTENT_ID_RESPONSE); - - bitStream.Write(requestID); - bitStream.Write(objID); - - server->Send(bitStream, sysAddr, false); -} - void MasterPackets::SendZoneTransferRequest(dServer* server, uint64_t requestID, bool mythranShift, uint32_t zoneID, uint32_t cloneID) { RakNet::BitStream bitStream; BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_ZONE_TRANSFER); diff --git a/dNet/MasterPackets.h b/dNet/MasterPackets.h index 93fd158e..68f184eb 100644 --- a/dNet/MasterPackets.h +++ b/dNet/MasterPackets.h @@ -8,9 +8,6 @@ class dServer; namespace MasterPackets { - void SendPersistentIDRequest(dServer* server, uint64_t requestID); //Called from the World server - void SendPersistentIDResponse(dServer* server, const SystemAddress& sysAddr, uint64_t requestID, uint32_t objID); - void SendZoneTransferRequest(dServer* server, uint64_t requestID, bool mythranShift, uint32_t zoneID, uint32_t cloneID); void SendZoneTransferResponse(dServer* server, const SystemAddress& sysAddr, uint64_t requestID, bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, const std::string& serverIP, uint32_t serverPort); @@ -22,8 +19,6 @@ namespace MasterPackets { void SendZoneRequestPrivate(dServer* server, uint64_t requestID, bool mythranShift, const std::string& password); void SendWorldReady(dServer* server, LWOMAPID zoneId, LWOINSTANCEID instanceId); - - void HandleSetSessionKey(Packet* packet); } #endif // MASTERPACKETS_H diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index d291be8b..b57acd58 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -8,7 +8,6 @@ #include "CharacterComponent.h" #include "SimplePhysicsComponent.h" #include "MovementAIComponent.h" -#include "ObjectIDManager.h" #include "MissionComponent.h" #include "Loot.h" #include "InventoryComponent.h" diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 3a22bbf0..964a4bb9 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -35,7 +35,6 @@ #include "CDClientManager.h" #include "CDClientDatabase.h" #include "GeneralUtils.h" -#include "ObjectIDManager.h" #include "ZoneInstanceManager.h" #include "dChatFilter.h" #include "ClientPackets.h" @@ -676,15 +675,6 @@ void HandleMasterPacket(Packet* packet) { if (packet->length < 2) return; if (static_cast(packet->data[1]) != ServiceType::MASTER || packet->length < 4) return; switch (static_cast(packet->data[3])) { - case MessageType::Master::REQUEST_PERSISTENT_ID_RESPONSE: { - CINSTREAM_SKIP_HEADER; - uint64_t requestID; - inStream.Read(requestID); - uint32_t objectID; - inStream.Read(objectID); - ObjectIDManager::HandleRequestPersistentIDResponse(requestID, objectID); - break; - } case MessageType::Master::SESSION_KEY_RESPONSE: { //Read our session key and to which user it belongs: @@ -1047,82 +1037,90 @@ void HandlePacket(Packet* packet) { auto version = levelComponent->GetCharacterVersion(); LOG("Updating character from version %s", StringifiedEnum::ToString(version).data()); - switch (version) { - case eCharacterVersion::RELEASE: - // TODO: Implement, super low priority - [[fallthrough]]; - case eCharacterVersion::LIVE: - LOG("Updating Character Flags"); - c->SetRetroactiveFlags(); - levelComponent->SetCharacterVersion(eCharacterVersion::PLAYER_FACTION_FLAGS); - [[fallthrough]]; - case eCharacterVersion::PLAYER_FACTION_FLAGS: - LOG("Updating Vault Size"); - player->RetroactiveVaultSize(); - levelComponent->SetCharacterVersion(eCharacterVersion::VAULT_SIZE); - [[fallthrough]]; - case eCharacterVersion::VAULT_SIZE: - LOG("Updaing Speedbase"); - levelComponent->SetRetroactiveBaseSpeed(); - levelComponent->SetCharacterVersion(eCharacterVersion::SPEED_BASE); - [[fallthrough]]; - case eCharacterVersion::SPEED_BASE: { - LOG("Removing lots from NJ Jay missions bugged at foss"); - // https://explorer.lu/missions/1789 - const auto* mission = missionComponent->GetMission(1789); - if (mission && mission->IsComplete()) { - inventoryComponent->RemoveItem(14474, 1, eInventoryType::ITEMS); - inventoryComponent->RemoveItem(14474, 1, eInventoryType::VAULT_ITEMS); - } - // https://explorer.lu/missions/1927 - mission = missionComponent->GetMission(1927); - if (mission && mission->IsComplete()) { - inventoryComponent->RemoveItem(14493, 1, eInventoryType::ITEMS); - inventoryComponent->RemoveItem(14493, 1, eInventoryType::VAULT_ITEMS); - } - levelComponent->SetCharacterVersion(eCharacterVersion::NJ_JAYMISSIONS); - [[fallthrough]]; - } - case eCharacterVersion::NJ_JAYMISSIONS: { - LOG("Fixing Nexus Force Explorer missions"); - auto missions = { 502 /* Pet Cove */, 593/* Nimbus Station */, 938/* Avant Gardens */, 284/* Gnarled Forest */, 754/* Forbidden Valley */ }; - bool complete = true; - for (auto missionID : missions) { - auto* mission = missionComponent->GetMission(missionID); - if (!mission || !mission->IsComplete()) { - complete = false; + if (version < eCharacterVersion::UP_TO_DATE) { + switch (version) { + case eCharacterVersion::RELEASE: + // TODO: Implement, super low priority + [[fallthrough]]; + case eCharacterVersion::LIVE: + LOG("Updating Character Flags"); + c->SetRetroactiveFlags(); + levelComponent->SetCharacterVersion(eCharacterVersion::PLAYER_FACTION_FLAGS); + [[fallthrough]]; + case eCharacterVersion::PLAYER_FACTION_FLAGS: + LOG("Updating Vault Size"); + player->RetroactiveVaultSize(); + levelComponent->SetCharacterVersion(eCharacterVersion::VAULT_SIZE); + [[fallthrough]]; + case eCharacterVersion::VAULT_SIZE: + LOG("Updaing Speedbase"); + levelComponent->SetRetroactiveBaseSpeed(); + levelComponent->SetCharacterVersion(eCharacterVersion::SPEED_BASE); + [[fallthrough]]; + case eCharacterVersion::SPEED_BASE: { + LOG("Removing lots from NJ Jay missions bugged at foss"); + // https://explorer.lu/missions/1789 + const auto* mission = missionComponent->GetMission(1789); + if (mission && mission->IsComplete()) { + inventoryComponent->RemoveItem(14474, 1, eInventoryType::ITEMS); + inventoryComponent->RemoveItem(14474, 1, eInventoryType::VAULT_ITEMS); } - } - - if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); - levelComponent->SetCharacterVersion(eCharacterVersion::NEXUS_FORCE_EXPLORER); - [[fallthrough]]; - } - case eCharacterVersion::NEXUS_FORCE_EXPLORER: { - LOG("Fixing pet IDs"); - - // First copy the original ids - const auto pets = inventoryComponent->GetPetsMut(); - - // Then clear the pets so we can re-add them with the updated IDs - auto& invPets = inventoryComponent->GetPetsMut(); - invPets.clear(); - for (auto& [id, databasePet] : pets) { - const auto originalID = id; - const auto newId = GeneralUtils::ClearBit(id, 32); // Persistent bit that didn't exist - LOG("New ID %llu", newId); - auto* item = inventoryComponent->FindItemBySubKey(originalID); - if (item) { - LOG("item subkey %llu", item->GetSubKey()); - item->SetSubKey(newId); - invPets[newId] = databasePet; + // https://explorer.lu/missions/1927 + mission = missionComponent->GetMission(1927); + if (mission && mission->IsComplete()) { + inventoryComponent->RemoveItem(14493, 1, eInventoryType::ITEMS); + inventoryComponent->RemoveItem(14493, 1, eInventoryType::VAULT_ITEMS); } + levelComponent->SetCharacterVersion(eCharacterVersion::NJ_JAYMISSIONS); + [[fallthrough]]; + } + case eCharacterVersion::NJ_JAYMISSIONS: { + LOG("Fixing Nexus Force Explorer missions"); + auto missions = { 502 /* Pet Cove */, 593/* Nimbus Station */, 938/* Avant Gardens */, 284/* Gnarled Forest */, 754/* Forbidden Valley */ }; + bool complete = true; + for (auto missionID : missions) { + auto* mission = missionComponent->GetMission(missionID); + if (!mission || !mission->IsComplete()) { + complete = false; + } + } + + if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); + levelComponent->SetCharacterVersion(eCharacterVersion::NEXUS_FORCE_EXPLORER); + [[fallthrough]]; + } + case eCharacterVersion::NEXUS_FORCE_EXPLORER: { + LOG("Fixing pet IDs"); + + // First copy the original ids + const auto pets = inventoryComponent->GetPetsMut(); + + // Then clear the pets so we can re-add them with the updated IDs + auto& invPets = inventoryComponent->GetPetsMut(); + invPets.clear(); + for (auto& [id, databasePet] : pets) { + const auto originalID = id; + const auto newId = GeneralUtils::ClearBit(id, 32); // Persistent bit that didn't exist + LOG("New ID %llu", newId); + auto* item = inventoryComponent->FindItemBySubKey(originalID); + if (item) { + LOG("item subkey %llu", item->GetSubKey()); + item->SetSubKey(newId); + invPets[newId] = databasePet; + } + } + levelComponent->SetCharacterVersion(eCharacterVersion::PET_IDS); + [[fallthrough]]; + } + case eCharacterVersion::PET_IDS: { + LOG("Regenerating item ids"); + inventoryComponent->RegenerateItemIDs(); + levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); + [[fallthrough]]; + } + case eCharacterVersion::UP_TO_DATE: + break; } - levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); - [[fallthrough]]; - } - case eCharacterVersion::UP_TO_DATE: - break; } // Update the characters xml to ensure the update above is not only saved, but so the client picks up on the changes. diff --git a/migrations/dlu/mysql/26_update_property_contents_ids.sql b/migrations/dlu/mysql/26_update_property_contents_ids.sql new file mode 100644 index 00000000..7e36924e --- /dev/null +++ b/migrations/dlu/mysql/26_update_property_contents_ids.sql @@ -0,0 +1 @@ +UPDATE `properties_contents` SET `id` = `id` | 0x1000000000000000; diff --git a/migrations/dlu/sqlite/9_update_property_contents_ids.sql b/migrations/dlu/sqlite/9_update_property_contents_ids.sql new file mode 100644 index 00000000..7e36924e --- /dev/null +++ b/migrations/dlu/sqlite/9_update_property_contents_ids.sql @@ -0,0 +1 @@ +UPDATE `properties_contents` SET `id` = `id` | 0x1000000000000000;