diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp index 5aef091a..b5a2df9b 100644 --- a/dCommon/BrickByBrickFix.cpp +++ b/dCommon/BrickByBrickFix.cpp @@ -8,6 +8,7 @@ #include "Database.h" #include "Game.h" +#include "Sd0.h" #include "ZCompression.h" #include "Logger.h" @@ -44,10 +45,10 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() { } // Ignore the valgrind warning about uninitialized values. These are discarded later when we know the actual uncompressed size. - std::unique_ptr uncompressedChunk(new uint8_t[ZCompression::MAX_SD0_CHUNK_SIZE]); + std::unique_ptr uncompressedChunk(new uint8_t[Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE]); int32_t err{}; int32_t actualUncompressedSize = ZCompression::Decompress( - compressedChunk.get(), chunkSize, uncompressedChunk.get(), ZCompression::MAX_SD0_CHUNK_SIZE, err); + compressedChunk.get(), chunkSize, uncompressedChunk.get(), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); if (actualUncompressedSize != -1) { uint32_t previousSize = completeUncompressedModel.size(); @@ -117,7 +118,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() { } std::string outputString(sd0ConvertedModel.get(), oldLxfmlSizeWithHeader); - std::istringstream outputStringStream(outputString); + std::stringstream outputStringStream(outputString); try { Database::Get()->UpdateUgcModelData(model.id, outputStringStream); diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index 18fda0ed..74432e0f 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -16,6 +16,9 @@ set(DCOMMON_SOURCES "BrickByBrickFix.cpp" "BinaryPathFinder.cpp" "FdbToSqlite.cpp" + "TinyXmlUtils.cpp" + "Sd0.cpp" + "Lxfml.cpp" ) # Workaround for compiler bug where the optimized code could result in a memcpy of 0 bytes, even though that isnt possible. diff --git a/dCommon/Logger.h b/dCommon/Logger.h index 5754d9ac..3a1771e6 100644 --- a/dCommon/Logger.h +++ b/dCommon/Logger.h @@ -29,8 +29,8 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) { // they will not be valid constexpr and will be evaluated at runtime instead of compile time! // The full string is still stored in the binary, however the offset of the filename in the absolute paths // is used in the instruction instead of the start of the absolute path. -#define LOG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->Log(str, message, ##__VA_ARGS__); } while(0) -#define LOG_DEBUG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->LogDebug(str, message, ##__VA_ARGS__); } while(0) +#define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0) +#define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0) // Writer class for writing data to files. class Writer { diff --git a/dCommon/Lxfml.cpp b/dCommon/Lxfml.cpp new file mode 100644 index 00000000..f713bcab --- /dev/null +++ b/dCommon/Lxfml.cpp @@ -0,0 +1,115 @@ +#include "Lxfml.h" + +#include "GeneralUtils.h" +#include "StringifiedEnum.h" +#include "TinyXmlUtils.h" + +#include + +Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { + Result toReturn; + tinyxml2::XMLDocument doc; + const auto err = doc.Parse(data.data()); + if (err != tinyxml2::XML_SUCCESS) { + LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); + return toReturn; + } + + TinyXmlUtils::DocumentReader reader(doc); + std::map transformations; + + auto lxfml = reader["LXFML"]; + if (!lxfml) { + LOG("Failed to find LXFML element."); + return toReturn; + } + + // First get all the positions of bricks + for (const auto& brick : lxfml["Bricks"]) { + const auto* part = brick.FirstChildElement("Part"); + if (part) { + const auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) transformations[refID] = transformation; + } + } + } + } + + // These points are well out of bounds for an actual player + NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f }; + NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f }; + + // Calculate the lowest and highest points on the entire model + for (const auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value(); + auto y = GeneralUtils::TryParse(split[10]).value(); + auto z = GeneralUtils::TryParse(split[11]).value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; + if (z < lowest.z) lowest.z = z; + + if (highest.x < x) highest.x = x; + if (highest.y < y) highest.y = y; + if (highest.z < z) highest.z = z; + } + + auto delta = (highest - lowest) / 2.0f; + auto newRootPos = lowest + delta; + + // Clamp the Y to the lowest point on the model + newRootPos.y = lowest.y; + + // Adjust all positions to account for the new origin + for (auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x; + auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y; + auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z; + std::stringstream stream; + for (int i = 0; i < 9; i++) { + stream << split[i]; + stream << ','; + } + stream << x << ',' << y << ',' << z; + transformation = stream.str(); + } + + // Finally write the new transformation back into the lxfml + for (auto& brick : lxfml["Bricks"]) { + auto* part = brick.FirstChildElement("Part"); + if (part) { + auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) { + bone->SetAttribute("transformation", transformations[refID].c_str()); + } + } + } + } + } + + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + toReturn.lxfml = printer.CStr(); + toReturn.center = newRootPos; + return toReturn; +} diff --git a/dCommon/Lxfml.h b/dCommon/Lxfml.h new file mode 100644 index 00000000..3f5f4d4a --- /dev/null +++ b/dCommon/Lxfml.h @@ -0,0 +1,23 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef LXFML_H +#define LXFML_H + +#include +#include + +#include "NiPoint3.h" + +namespace Lxfml { + struct Result { + std::string lxfml; + NiPoint3 center; + }; + + // Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0. + // Returns a struct of its new center and the updated LXFML containing these edits. + [[nodiscard]] Result NormalizePosition(const std::string_view data); +}; + +#endif //!LXFML_H diff --git a/dCommon/Sd0.cpp b/dCommon/Sd0.cpp new file mode 100644 index 00000000..5b97cb75 --- /dev/null +++ b/dCommon/Sd0.cpp @@ -0,0 +1,150 @@ +#include "Sd0.h" + +#include +#include + +#include "BinaryIO.h" + +#include "Game.h" +#include "Logger.h" + +#include "ZCompression.h" + +// Insert header if on first buffer +void WriteHeader(Sd0::BinaryBuffer& chunk) { + chunk.push_back(Sd0::SD0_HEADER[0]); + chunk.push_back(Sd0::SD0_HEADER[1]); + chunk.push_back(Sd0::SD0_HEADER[2]); + chunk.push_back(Sd0::SD0_HEADER[3]); + chunk.push_back(Sd0::SD0_HEADER[4]); +} + +// Write the size of the buffer to a chunk +void WriteSize(Sd0::BinaryBuffer& chunk, uint32_t chunkSize) { + for (int i = 0; i < 4; i++) { + char toPush = chunkSize & 0xff; + chunkSize = chunkSize >> 8; + chunk.push_back(toPush); + } +} + +int32_t GetDataOffset(bool firstBuffer) { + return firstBuffer ? 9 : 4; +} + +Sd0::Sd0(std::istream& buffer) { + char header[5]{}; + + // Check if this is an sd0 buffer. It's possible we may be handed a zlib buffer directly due to old code so check for that too. + if (!BinaryIO::BinaryRead(buffer, header) || memcmp(header, SD0_HEADER, sizeof(header)) != 0) { + LOG("Failed to read SD0 header %i %i %i %i %i %i %i", buffer.good(), buffer.tellg(), header[0], header[1], header[2], header[3], header[4]); + LOG_DEBUG("This may be a zlib buffer directly? Trying again assuming its a zlib buffer."); + auto& firstChunk = m_Chunks.emplace_back(); + WriteHeader(firstChunk); + buffer.seekg(0, std::ios::end); + uint32_t bufferSize = buffer.tellg(); + buffer.seekg(0, std::ios::beg); + WriteSize(firstChunk, bufferSize); + firstChunk.resize(firstChunk.size() + bufferSize); + auto* dataStart = reinterpret_cast(firstChunk.data() + GetDataOffset(true)); + if (!buffer.read(dataStart, bufferSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", bufferSize, m_Chunks.size() - 1); + } + return; + } + + while (buffer && buffer.peek() != std::istream::traits_type::eof()) { + uint32_t chunkSize{}; + if (!BinaryIO::BinaryRead(buffer, chunkSize)) { + LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size()); + break; + } + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + // Insert header if on first buffer + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, chunkSize); + + chunk.resize(chunkSize + dataOffset); + auto* dataStart = reinterpret_cast(chunk.data() + dataOffset); + if (!buffer.read(dataStart, chunkSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", chunkSize, m_Chunks.size() - 1); + break; + } + } +} + +void Sd0::FromData(const uint8_t* data, size_t bufferSize) { + const auto originalBufferSize = bufferSize; + if (bufferSize == 0) return; + + m_Chunks.clear(); + while (bufferSize > 0) { + const auto numToCopy = std::min(MAX_UNCOMPRESSED_CHUNK_SIZE, bufferSize); + const auto* startOffset = data + originalBufferSize - bufferSize; + bufferSize -= numToCopy; + std::array compressedChunk; + const auto compressedSize = ZCompression::Compress( + startOffset, numToCopy, + compressedChunk.data(), compressedChunk.size()); + + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, compressedSize); + + chunk.resize(compressedSize + dataOffset); + memcpy(chunk.data() + dataOffset, compressedChunk.data(), compressedSize); + } + +} + +std::string Sd0::GetAsStringUncompressed() const { + std::string toReturn; + bool first = true; + uint32_t totalSize{}; + for (const auto& chunk : m_Chunks) { + auto dataOffset = GetDataOffset(first); + first = false; + const auto chunkSize = chunk.size(); + + auto oldSize = toReturn.size(); + toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE); + int32_t error{}; + const auto uncompressedSize = ZCompression::Decompress( + chunk.data() + dataOffset, chunkSize - dataOffset, + reinterpret_cast(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE, + error); + + totalSize += uncompressedSize; + } + + toReturn.resize(totalSize); + return toReturn; +} + +std::stringstream Sd0::GetAsStream() const { + std::stringstream toReturn; + + for (const auto& chunk : m_Chunks) { + toReturn.write(reinterpret_cast(chunk.data()), chunk.size()); + } + + return toReturn; +} + +const std::vector& Sd0::GetAsVector() const { + return m_Chunks; +} diff --git a/dCommon/Sd0.h b/dCommon/Sd0.h new file mode 100644 index 00000000..40bf9930 --- /dev/null +++ b/dCommon/Sd0.h @@ -0,0 +1,42 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef SD0_H +#define SD0_H + +#include +#include + +// Sd0 is comprised of multiple zlib compressed buffers stored in a row. +// The format starts with a SD0 header (see SD0_HEADER) followed by the size of a zlib buffer, and then the zlib buffer itself. +// This repeats until end of file +class Sd0 { +public: + using BinaryBuffer = std::vector; + + static inline const char* SD0_HEADER = "sd0\x01\xff"; + + /** + * @brief Max size of an inflated sd0 zlib chunk + */ + static constexpr inline size_t MAX_UNCOMPRESSED_CHUNK_SIZE = 1024 * 256; + + // Read the input buffer into an internal chunk stream to be used later + Sd0(std::istream& buffer); + + // Uncompresses the entire Sd0 buffer and returns it as a string + [[nodiscard]] std::string GetAsStringUncompressed() const; + + // Gets the Sd0 buffer as a stream in its raw compressed form + [[nodiscard]] std::stringstream GetAsStream() const; + + // Gets the Sd0 buffer as a vector in its raw compressed form + [[nodiscard]] const std::vector& GetAsVector() const; + + // Compress data into a Sd0 buffer + void FromData(const uint8_t* data, size_t bufferSize); +private: + std::vector m_Chunks{}; +}; + +#endif //!SD0_H diff --git a/dCommon/TinyXmlUtils.cpp b/dCommon/TinyXmlUtils.cpp new file mode 100644 index 00000000..9fe88eb7 --- /dev/null +++ b/dCommon/TinyXmlUtils.cpp @@ -0,0 +1,37 @@ +#include "TinyXmlUtils.h" + +#include + +using namespace TinyXmlUtils; + +Element DocumentReader::operator[](const std::string_view elem) const { + return Element(m_Doc.FirstChildElement(elem.empty() ? nullptr : elem.data()), elem); +} + +Element::Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem) : + m_IteratedName{ elem }, + m_Elem{ xmlElem } { +} + +Element Element::operator[](const std::string_view elem) const { + const auto* usedElem = elem.empty() ? nullptr : elem.data(); + auto* toReturn = m_Elem ? m_Elem->FirstChildElement(usedElem) : nullptr; + return Element(toReturn, m_IteratedName); +} + +ElementIterator Element::begin() { + return ElementIterator(m_Elem ? m_Elem->FirstChildElement() : nullptr); +} + +ElementIterator Element::end() { + return ElementIterator(nullptr); +} + +ElementIterator::ElementIterator(tinyxml2::XMLElement* elem) : + m_CurElem{ elem } { +} + +ElementIterator& ElementIterator::operator++() { + if (m_CurElem) m_CurElem = m_CurElem->NextSiblingElement(); + return *this; +} diff --git a/dCommon/TinyXmlUtils.h b/dCommon/TinyXmlUtils.h new file mode 100644 index 00000000..e7740f04 --- /dev/null +++ b/dCommon/TinyXmlUtils.h @@ -0,0 +1,66 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef TINYXMLUTILS_H +#define TINYXMLUTILS_H + +#include + +#include "DluAssert.h" + +#include + +namespace TinyXmlUtils { + // See cstdlib for iterator technicalities + struct ElementIterator { + ElementIterator(tinyxml2::XMLElement* elem); + + ElementIterator& operator++(); + [[nodiscard]] tinyxml2::XMLElement* operator->() { DluAssert(m_CurElem); return m_CurElem; } + [[nodiscard]] tinyxml2::XMLElement& operator*() { DluAssert(m_CurElem); return *m_CurElem; } + + bool operator==(const ElementIterator& other) const { return other.m_CurElem == m_CurElem; } + + private: + tinyxml2::XMLElement* m_CurElem{ nullptr }; + }; + + // Wrapper class to act as an iterator over xml elements. + // All the normal rules that apply to Iterators in the std library apply here. + class Element { + public: + Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem); + + // The first child element of this element. + [[nodiscard]] ElementIterator begin(); + + // Always returns an ElementIterator which points to nullptr. + // TinyXml2 return NULL when you've reached the last child element so + // you can't do any funny one past end logic here. + [[nodiscard]] ElementIterator end(); + + // Get a child element + [[nodiscard]] Element operator[](const std::string_view elem) const; + [[nodiscard]] Element operator[](const char* elem) const { return operator[](std::string_view(elem)); }; + + // Whether or not data exists for this element + operator bool() const { return m_Elem != nullptr; } + + [[nodiscard]] const tinyxml2::XMLElement* operator->() const { return m_Elem; } + private: + const char* GetElementName() const { return m_IteratedName.empty() ? nullptr : m_IteratedName.c_str(); } + const std::string m_IteratedName; + tinyxml2::XMLElement* m_Elem; + }; + + class DocumentReader { + public: + DocumentReader(tinyxml2::XMLDocument& doc) : m_Doc{ doc } {} + + [[nodiscard]] Element operator[](const std::string_view elem) const; + private: + tinyxml2::XMLDocument& m_Doc; + }; +}; + +#endif //!TINYXMLUTILS_H diff --git a/dCommon/ZCompression.h b/dCommon/ZCompression.h index 84e8a9b4..22a5ff86 100644 --- a/dCommon/ZCompression.h +++ b/dCommon/ZCompression.h @@ -8,11 +8,5 @@ namespace ZCompression { int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst); int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr); - - /** - * @brief Max size of an inflated sd0 zlib chunk - * - */ - constexpr uint32_t MAX_SD0_CHUNK_SIZE = 1024 * 256; } diff --git a/dCommon/dClient/Pack.cpp b/dCommon/dClient/Pack.cpp index 04d07f12..800cfa19 100644 --- a/dCommon/dClient/Pack.cpp +++ b/dCommon/dClient/Pack.cpp @@ -1,6 +1,7 @@ #include "Pack.h" #include "BinaryIO.h" +#include "Sd0.h" #include "ZCompression.h" Pack::Pack(const std::filesystem::path& filePath) { @@ -106,7 +107,7 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons pos += size; // Move pointer position the amount of bytes read to the right int32_t err; - currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), ZCompression::MAX_SD0_CHUNK_SIZE, err); + currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); free(chunk); } diff --git a/dDatabase/CMakeLists.txt b/dDatabase/CMakeLists.txt index 42bdb983..56ed4df7 100644 --- a/dDatabase/CMakeLists.txt +++ b/dDatabase/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(CDClientDatabase) add_subdirectory(GameDatabase) -add_library(dDatabase STATIC "MigrationRunner.cpp") +add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp") add_custom_target(conncpp_dylib ${CMAKE_COMMAND} -E copy $ ${PROJECT_BINARY_DIR}) diff --git a/dDatabase/GameDatabase/ITables/IPropertyContents.h b/dDatabase/GameDatabase/ITables/IPropertyContents.h index 0d8d8b5c..e965e05c 100644 --- a/dDatabase/GameDatabase/ITables/IPropertyContents.h +++ b/dDatabase/GameDatabase/ITables/IPropertyContents.h @@ -22,7 +22,7 @@ public: // Inserts a new UGC model into the database. virtual void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) = 0; @@ -34,9 +34,17 @@ public: virtual void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) = 0; // Update the model position and rotation for the given property id. - virtual void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array behaviorIDs) { + std::array, 5> behaviors; + for (int32_t i = 0; i < behaviors.size(); i++) behaviors[i].first = behaviorIDs[i]; + UpdateModel(modelID, position, rotation, behaviors); + } // Remove the model for the given property id. virtual void RemoveModel(const LWOOBJID& modelId) = 0; + + // Gets a model by ID + virtual Model GetModel(const LWOOBJID modelID) = 0; }; #endif //!__IPROPERTIESCONTENTS__H__ diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index 024636ac..cbc770b8 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -12,6 +12,7 @@ public: struct Model { std::stringstream lxfmlData; LWOOBJID id{}; + LWOOBJID modelID{}; }; // Gets all UGC models for the given property id. @@ -27,6 +28,6 @@ public: virtual void DeleteUgcModelData(const LWOOBJID& modelId) = 0; // Inserts a new UGC model into the database. - virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) = 0; + virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0; }; #endif //!__IUGC__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index e998f488..9b0b38a6 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -48,7 +48,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -75,14 +75,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -127,6 +127,7 @@ 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; sql::PreparedStatement* CreatePreppedStmt(const std::string& query); private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp index 05998785..fe63fc49 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp @@ -52,14 +52,39 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr } } -void MySQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void MySQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void MySQLDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model MySQLDatabase::GetModel(const LWOOBJID modelID) { + auto result = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + 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->getInt("behavior_1"); + model.behaviors[1] = result->getInt("behavior_2"); + model.behaviors[2] = result->getInt("behavior_3"); + model.behaviors[3] = result->getInt("behavior_4"); + model.behaviors[4] = result->getInt("behavior_5"); + } + + return model; +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 3b62a51b..2d2655f4 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto result = ExecuteSelect( - "SELECT lxfml, u.id 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;", + "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;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) // 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("id"); + model.id = result->getUInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); toReturn.push_back(std::move(model)); } @@ -21,13 +22,14 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) } std::vector MySQLDatabase::GetAllUgcModels() { - auto result = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + 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 pc.lot = 14 AND pc.ugc_id IS NOT NULL;"); std::vector models; models.reserve(result->rowsCount()); while (result->next()) { IUgc::Model model; - model.id = result->getInt64("id"); + 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")); @@ -43,7 +45,7 @@ void MySQLDatabase::RemoveUnreferencedUgcModels() { } void MySQLDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std:: stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -65,7 +67,7 @@ void MySQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 15e0176f..f6c545af 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -46,7 +46,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -73,14 +73,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -125,6 +125,7 @@ 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; private: CppSQLite3Statement CreatePreppedStmt(const std::string& query); diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp index 6a8d7028..960e1113 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp @@ -52,14 +52,41 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP } } -void SQLiteDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void SQLiteDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void SQLiteDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model SQLiteDatabase::GetModel(const LWOOBJID modelID) { + auto [_, result] = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + 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.getIntField("behavior_1"); + model.behaviors[1] = result.getIntField("behavior_2"); + model.behaviors[2] = result.getIntField("behavior_3"); + model.behaviors[3] = result.getIntField("behavior_4"); + model.behaviors[4] = result.getIntField("behavior_5"); + } while (result.nextRow()); + } + + return model; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 048b53ab..c6410a42 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto [_, result] = ExecuteSelect( - "SELECT lxfml, u.id 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;", + "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;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); toReturn.push_back(std::move(model)); result.nextRow(); } @@ -22,12 +23,13 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId } std::vector SQLiteDatabase::GetAllUgcModels() { - auto [_, result] = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + 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;"); std::vector models; while (!result.eof()) { IUgc::Model model; - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); @@ -44,7 +46,7 @@ void SQLiteDatabase::RemoveUnreferencedUgcModels() { } void SQLiteDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std::stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -66,7 +68,7 @@ void SQLiteDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index 733f281b..01b3961f 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -60,7 +60,7 @@ void TestSQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { } -void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { } @@ -168,7 +168,7 @@ void TestSQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const I } -void TestSQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void TestSQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { } @@ -192,7 +192,7 @@ void TestSQLDatabase::InsertNewMail(const MailInfo& mail) { } -void TestSQLDatabase::InsertNewUgcModel(std::istringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { +void TestSQLDatabase::InsertNewUgcModel(std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index e9cf8acb..8016f333 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -25,7 +25,7 @@ class TestSQLDatabase : public GameDatabase { void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -52,14 +52,14 @@ class TestSQLDatabase : public GameDatabase { std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -105,6 +105,7 @@ 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 {}; } }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index e6dfb042..8cdd17ae 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -7,6 +7,7 @@ #include "GeneralUtils.h" #include "Logger.h" #include "BinaryPathFinder.h" +#include "ModelNormalizeMigration.h" #include @@ -35,7 +36,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->CreateMigrationHistoryTable(); // has to be here because when moving the files to the new folder, the migration_history table is not updated so it will run them all again. - + const auto migrationFolder = Database::GetMigrationFolder(); if (!Database::Get()->IsMigrationRun("17_migration_for_migrations.sql") && migrationFolder == "mysql") { LOG("Running migration: 17_migration_for_migrations.sql"); @@ -45,6 +46,7 @@ void MigrationRunner::RunMigrations() { std::string finalSQL = ""; bool runSd0Migrations = false; + bool runNormalizeMigrations = false; for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); @@ -57,6 +59,8 @@ void MigrationRunner::RunMigrations() { LOG("Running migration: %s", migration.name.c_str()); if (migration.name == "5_brick_model_sd0.sql") { runSd0Migrations = true; + } else if (migration.name.ends_with("_normalize_model_positions.sql")) { + runNormalizeMigrations = true; } else { finalSQL.append(migration.data.c_str()); } @@ -64,7 +68,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->InsertMigration(migration.name); } - if (finalSQL.empty() && !runSd0Migrations) { + if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations) { LOG("Server database is up to date."); return; } @@ -88,6 +92,10 @@ void MigrationRunner::RunMigrations() { uint32_t numberOfTruncatedModels = BrickByBrickFix::TruncateBrokenBrickByBrickXml(); LOG("%i models were truncated from the database.", numberOfTruncatedModels); } + + if (runNormalizeMigrations) { + ModelNormalizeMigration::Run(); + } } void MigrationRunner::RunSQLiteMigrations() { diff --git a/dDatabase/ModelNormalizeMigration.cpp b/dDatabase/ModelNormalizeMigration.cpp new file mode 100644 index 00000000..b8215733 --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.cpp @@ -0,0 +1,30 @@ +#include "ModelNormalizeMigration.h" + +#include "Database.h" +#include "Lxfml.h" +#include "Sd0.h" + +void ModelNormalizeMigration::Run() { + const auto oldCommit = Database::Get()->GetAutoCommit(); + Database::Get()->SetAutoCommit(false); + 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; + + Sd0 sd0(lxfmlData); + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + if (newCenter == NiPoint3Constant::ZERO) { + LOG("Failed to update model %llu due to failure reading xml."); + continue; + } + + 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()->UpdateUgcModelData(id, asStream); + } + Database::Get()->SetAutoCommit(oldCommit); +} diff --git a/dDatabase/ModelNormalizeMigration.h b/dDatabase/ModelNormalizeMigration.h new file mode 100644 index 00000000..000781cd --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.h @@ -0,0 +1,11 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef MODELNORMALIZEMIGRATION_H +#define MODELNORMALIZEMIGRATION_H + +namespace ModelNormalizeMigration { + void Run(); +}; + +#endif //!MODELNORMALIZEMIGRATION_H diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index c95af3d7..ad0c0b1c 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -99,7 +99,7 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE } // Exclude the zone control object from any flags - if (!controller && info.lot != 14) { + if (!controller) { // The client flags means the client should render the entity GeneralUtils::SetBit(id, eObjectBits::CLIENT); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 1f60017b..d1f2c587 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -102,6 +102,8 @@ #include "CDComponentsRegistryTable.h" #include "CDObjectsTable.h" #include "eItemType.h" +#include "Lxfml.h" +#include "Sd0.h" void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) { CBITSTREAM; @@ -2574,18 +2576,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch? */ - ////Decompress the SD0 from the client so we can process the lxfml properly - //uint8_t* outData = new uint8_t[327680]; - //int32_t error; - //int32_t size = ZCompression::Decompress(inData, lxfmlSize, outData, 327680, error); - - //if (size == -1) { - // LOG("Failed to decompress LXFML: (%i)", error); - // return; - //} - // - //std::string lxfml(reinterpret_cast(outData), size); //std::string version of the decompressed data! - //Now, the cave of dragons: //We runs this in async because the http library here is blocking, meaning it'll halt the thread. @@ -2613,16 +2603,25 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent LWOOBJID propertyId = LWOOBJID_EMPTY; if (propertyInfo) propertyId = propertyInfo->id; - //Insert into ugc: + // Save the binary data to the Sd0 buffer std::string str(sd0Data.get(), sd0Size); std::istringstream sd0DataStream(str); - Database::Get()->InsertNewUgcModel(sd0DataStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); + Sd0 sd0(sd0DataStream); + + // 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, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); //Insert into the db as a BBB model: IPropertyContents::Model model; model.id = newIDL; model.ugcId = blueprintIDSmall; - model.position = NiPoint3Constant::ZERO; + 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"); @@ -2648,6 +2647,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent //} //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, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(localId); @@ -2655,9 +2657,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent bitStream.Write(1); bitStream.Write(blueprintID); - bitStream.Write(sd0Size); + bitStream.Write(sd0Size); - bitStream.WriteAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); + for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); SEND_PACKET; @@ -2665,7 +2667,7 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent EntityInfo info; info.lot = 14; - info.pos = {}; + info.pos = newCenter; info.rot = {}; info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index d88d33ce..8d189864 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1153,6 +1153,8 @@ void HandlePacket(Packet* packet) { GeneralUtils::SetBit(blueprintID, eObjectBits::CHARACTER); GeneralUtils::SetBit(blueprintID, eObjectBits::PERSISTENT); + // Workaround for not having a UGC server to get model LXFML onto the client so it + // can generate the physics and nif for the object. CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes @@ -1452,7 +1454,6 @@ void WorldShutdownProcess(uint32_t zoneId) { if (PropertyManagementComponent::Instance() != nullptr) { LOG("Saving ALL property data for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); PropertyManagementComponent::Instance()->Save(); - Database::Get()->RemoveUnreferencedUgcModels(); LOG("ALL property data saved for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); } diff --git a/migrations/dlu/mysql/19_normalize_model_positions.sql b/migrations/dlu/mysql/19_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/mysql/19_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ diff --git a/migrations/dlu/sqlite/2_normalize_model_positions.sql b/migrations/dlu/sqlite/2_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/sqlite/2_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */