feat: lxfml splitting for bbb (#1877)

* LXFML SPLITTING
Included test file

* move base to global namespace

* wip need to test

* update last fixes

* update world sending bbb to be more efficient

* Address feedback form Emo in doscord

* Make LXFML class for robust and add more tests to edge cases and malformed data

* get rid of the string copy and make the deep clone have a recursive limit

* cleanup tests

* fix test file locations

* fix file path

* KISS

* add cmakelists

* fix typos

* NL @ EOF

* tabs and split out to func

* naming standard
This commit is contained in:
Aaron Kimbrell
2025-10-10 23:07:16 -05:00
committed by GitHub
parent cbdd5d9bc6
commit ce28834dce
16 changed files with 1267 additions and 118 deletions

View File

@@ -5,13 +5,43 @@
#include "TinyXmlUtils.h"
#include <ranges>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <sstream>
namespace {
// The base LXFML xml file to use when creating new models.
std::string g_base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
</Bricks>
<RigidSystems>
</RigidSystems>
<GroupSystems>
<GroupSystem>
</GroupSystem>
</GroupSystems>
</LXFML>)";
}
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
Result toReturn;
// Handle empty or invalid input
if (data.empty()) {
return toReturn;
}
tinyxml2::XMLDocument doc;
const auto err = doc.Parse(data.data());
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
return toReturn;
}
@@ -20,7 +50,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
auto lxfml = reader["LXFML"];
if (!lxfml) {
LOG("Failed to find LXFML element.");
return toReturn;
}
@@ -49,16 +78,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
// 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<float>(split[9]).value();
auto y = GeneralUtils::TryParse<float>(split[10]).value();
auto z = GeneralUtils::TryParse<float>(split[11]).value();
if (x < lowest.x) lowest.x = x;
if (y < lowest.y) lowest.y = y;
if (split.size() < 12) continue;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue;
auto x = xOpt.value();
auto y = yOpt.value();
auto z = zOpt.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;
@@ -87,13 +119,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
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<float>(split[9]).value() - newRootPos.x + curPosition.x;
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) {
continue;
}
auto x = xOpt.value() - newRootPos.x + curPosition.x;
auto y = yOpt.value() - newRootPos.y + curPosition.y;
auto z = zOpt.value() - newRootPos.z + curPosition.z;
std::stringstream stream;
for (int i = 0; i < 9; i++) {
stream << split[i];
@@ -128,3 +166,285 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
toReturn.center = newRootPos;
return toReturn;
}
// Deep-clone an XMLElement (attributes, text, and child elements) into a target document
// with maximum depth protection to prevent infinite loops
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc, int maxDepth = 100) {
if (!src || maxDepth <= 0) return nullptr;
auto* dst = dstDoc.NewElement(src->Name());
// copy attributes
for (const tinyxml2::XMLAttribute* attr = src->FirstAttribute(); attr; attr = attr->Next()) {
dst->SetAttribute(attr->Name(), attr->Value());
}
// copy children (elements and text)
for (const tinyxml2::XMLNode* child = src->FirstChild(); child; child = child->NextSibling()) {
if (const tinyxml2::XMLElement* childElem = child->ToElement()) {
// Recursively clone child elements with decremented depth
auto* clonedChild = CloneElementDeep(childElem, dstDoc, maxDepth - 1);
if (clonedChild) dst->InsertEndChild(clonedChild);
} else if (const tinyxml2::XMLText* txt = child->ToText()) {
auto* n = dstDoc.NewText(txt->Value());
dst->InsertEndChild(n);
} else if (const tinyxml2::XMLComment* c = child->ToComment()) {
auto* n = dstDoc.NewComment(c->Value());
dst->InsertEndChild(n);
}
}
return dst;
}
std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
std::vector<Result> results;
// Handle empty or invalid input
if (data.empty()) {
return results;
}
// Prevent processing extremely large inputs that could cause hangs
if (data.size() > 10000000) { // 10MB limit
return results;
}
tinyxml2::XMLDocument doc;
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
return results;
}
auto* lxfml = doc.FirstChildElement("LXFML");
if (!lxfml) {
return results;
}
// Build maps: partRef -> Part element, partRef -> Brick element, boneRef -> partRef, brickRef -> Brick element
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToPart;
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToBrick;
std::unordered_map<std::string, std::string> boneRefToPartRef;
std::unordered_map<std::string, tinyxml2::XMLElement*> brickByRef;
auto* bricksParent = lxfml->FirstChildElement("Bricks");
if (bricksParent) {
for (auto* brick = bricksParent->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
const char* brickRef = brick->Attribute("refID");
if (brickRef) brickByRef.emplace(std::string(brickRef), brick);
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
const char* partRef = part->Attribute("refID");
if (partRef) {
partRefToPart.emplace(std::string(partRef), part);
partRefToBrick.emplace(std::string(partRef), brick);
}
auto* bone = part->FirstChildElement("Bone");
if (bone) {
const char* boneRef = bone->Attribute("refID");
if (boneRef) boneRefToPartRef.emplace(std::string(boneRef), partRef ? std::string(partRef) : std::string());
}
}
}
}
// Collect RigidSystem elements
std::vector<tinyxml2::XMLElement*> rigidSystems;
auto* rigidSystemsParent = lxfml->FirstChildElement("RigidSystems");
if (rigidSystemsParent) {
for (auto* rs = rigidSystemsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidSystems.push_back(rs);
}
}
// Collect top-level groups (immediate children of GroupSystem)
std::vector<tinyxml2::XMLElement*> groupRoots;
auto* groupSystemsParent = lxfml->FirstChildElement("GroupSystems");
if (groupSystemsParent) {
for (auto* gs = groupSystemsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* group = gs->FirstChildElement("Group"); group; group = group->NextSiblingElement("Group")) {
groupRoots.push_back(group);
}
}
}
// Track used bricks and rigidsystems
std::unordered_set<std::string> usedBrickRefs;
std::unordered_set<tinyxml2::XMLElement*> usedRigidSystems;
// Helper to create output document from sets of brick refs and rigidsystem pointers
auto makeOutput = [&](const std::unordered_set<std::string>& bricksToInclude, const std::vector<tinyxml2::XMLElement*>& rigidSystemsToInclude, const std::vector<tinyxml2::XMLElement*>& groupsToInclude = {}) {
tinyxml2::XMLDocument outDoc;
outDoc.Parse(g_base.c_str());
auto* outRoot = outDoc.FirstChildElement("LXFML");
auto* outBricks = outRoot->FirstChildElement("Bricks");
auto* outRigidSystems = outRoot->FirstChildElement("RigidSystems");
auto* outGroupSystems = outRoot->FirstChildElement("GroupSystems");
// clone and insert bricks
for (const auto& bref : bricksToInclude) {
auto it = brickByRef.find(bref);
if (it == brickByRef.end()) continue;
tinyxml2::XMLElement* cloned = CloneElementDeep(it->second, outDoc);
if (cloned) outBricks->InsertEndChild(cloned);
}
// clone and insert rigidsystems
for (auto* rsPtr : rigidSystemsToInclude) {
tinyxml2::XMLElement* cloned = CloneElementDeep(rsPtr, outDoc);
if (cloned) outRigidSystems->InsertEndChild(cloned);
}
// clone and insert group(s) if requested
if (outGroupSystems && !groupsToInclude.empty()) {
// clear default children
while (outGroupSystems->FirstChild()) outGroupSystems->DeleteChild(outGroupSystems->FirstChild());
// create a GroupSystem element and append requested groups
auto* newGS = outDoc.NewElement("GroupSystem");
for (auto* gptr : groupsToInclude) {
tinyxml2::XMLElement* clonedG = CloneElementDeep(gptr, outDoc);
if (clonedG) newGS->InsertEndChild(clonedG);
}
outGroupSystems->InsertEndChild(newGS);
}
// Print to string
tinyxml2::XMLPrinter printer;
outDoc.Print(&printer);
// Normalize position and compute center using existing helper
std::string xmlString = printer.CStr();
if (xmlString.size() > 5000000) { // 5MB limit for normalization
Result emptyResult;
emptyResult.lxfml = xmlString;
return emptyResult;
}
auto normalized = NormalizePosition(xmlString, curPosition);
return normalized;
};
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
for (auto* groupRoot : groupRoots) {
// collect all partRefs in this group's subtree
std::unordered_set<std::string> partRefs;
std::function<void(const tinyxml2::XMLElement*)> collectParts = [&](const tinyxml2::XMLElement* g) {
if (!g) return;
const char* partAttr = g->Attribute("partRefs");
if (partAttr) {
for (auto& tok : GeneralUtils::SplitString(partAttr, ',')) partRefs.insert(tok);
}
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child);
};
collectParts(groupRoot);
// Build initial sets of bricks and boneRefs
std::unordered_set<std::string> bricksIncluded;
std::unordered_set<std::string> boneRefsIncluded;
for (const auto& pref : partRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
// Iteratively include any RigidSystems that reference any boneRefsIncluded
bool changed = true;
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
int maxIterations = 1000; // Safety limit to prevent infinite loops
int iteration = 0;
while (changed && iteration < maxIterations) {
changed = false;
iteration++;
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
// parse boneRefs of this rigid system (from its <Rigid> children)
bool intersects = false;
std::vector<std::string> rsBoneRefs;
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
rsBoneRefs.push_back(tok);
if (boneRefsIncluded.find(tok) != boneRefsIncluded.end()) intersects = true;
}
}
if (!intersects) continue;
// include this rigid system and all boneRefs it references
usedRigidSystems.insert(rs);
rigidSystemsToInclude.push_back(rs);
for (const auto& br : rsBoneRefs) {
boneRefsIncluded.insert(br);
auto bpIt = boneRefToPartRef.find(br);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref && bricksIncluded.insert(std::string(bref)).second) changed = true;
}
}
}
}
}
if (iteration >= maxIterations) {
// Iteration limit reached, stop processing to prevent infinite loops
// The file is likely malformed, so just skip further processing
return results;
}
// include bricks from bricksIncluded into used set
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
// make output doc and push result (include this group's XML)
std::vector<tinyxml2::XMLElement*> groupsVec{ groupRoot };
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsVec);
results.push_back(normalized);
}
// 2) Process remaining RigidSystems (each becomes its own file)
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
std::unordered_set<std::string> bricksIncluded;
// collect boneRefs referenced by this rigid system
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
auto bpIt = boneRefToPartRef.find(tok);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
}
}
}
// mark used
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
usedRigidSystems.insert(rs);
std::vector<tinyxml2::XMLElement*> rsVec{ rs };
auto normalized = makeOutput(bricksIncluded, rsVec);
results.push_back(normalized);
}
// 3) Any remaining bricks not included become their own files
for (const auto& [bref, brickPtr] : brickByRef) {
if (usedBrickRefs.find(bref) != usedBrickRefs.end()) continue;
std::unordered_set<std::string> bricksIncluded{ bref };
auto normalized = makeOutput(bricksIncluded, {});
results.push_back(normalized);
usedBrickRefs.insert(bref);
}
return results;
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include <string_view>
#include <vector>
#include "NiPoint3.h"
@@ -18,6 +19,7 @@ namespace Lxfml {
// 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, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
// these are only for the migrations due to a bug in one of the implementations.
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);

View File

@@ -2566,9 +2566,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
inStream.Read(timeTaken);
/*
Disabled this, as it's kinda silly to do this roundabout way of storing plaintext lxfml, then recompressing
it to send it back to the client.
On DLU we had agreed that bricks wouldn't be taken anyway, but if your server decides otherwise, feel free to
comment this back out and add the needed code to get the bricks used from lxfml and take them from the inventory.
@@ -2582,23 +2579,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
//We need to get a new ID for our model first:
if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return;
const uint32_t maxRetries = 100;
uint32_t retries = 0;
bool blueprintIDExists = true;
bool modelExists = true;
// 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();
@@ -2615,85 +2595,112 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
std::istringstream sd0DataStream(str);
Sd0 sd0(sd0DataStream);
// Uncompress the data and normalize the position
// Uncompress the data, split, and nornmalize the model
const auto asStr = sd0.GetAsStringUncompressed();
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr);
auto splitLxfmls = Lxfml::Split(asStr);
LOG_DEBUG("Split into %zu models", splitLxfmls.size());
// Recompress the data and save to the database
sd0.FromData(reinterpret_cast<const uint8_t*>(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");
/*
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"));
// 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");
// //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 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<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(splitLxfmls.size());
bitStream.Write(newSd0Size);
std::vector<LWOOBJID> blueprintIDs;
std::vector<LWOOBJID> modelIDs;
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
// 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
const uint32_t maxRetries = 100;
uint32_t retries = 0;
bool blueprintIDExists = true;
bool modelExists = true;
LWOOBJID newID = LWOOBJID_EMPTY;
LWOOBJID blueprintID = LWOOBJID_EMPTY;
do {
if (newID != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID);
newID = ObjectIDManager::GetPersistentID();
blueprintID = ObjectIDManager::GetPersistentID();
++retries;
blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value();
modelExists = Database::Get()->GetModel(newID).has_value();
} while ((blueprintIDExists || modelExists) && retries < maxRetries);
blueprintIDs.push_back(blueprintID);
modelIDs.push_back(newID);
// Save each model to the database
sd0.FromData(reinterpret_cast<const uint8_t*>(splitLxfmls[i].lxfml.data()), splitLxfmls[i].lxfml.size());
auto sd0AsStream = sd0.GetAsStream();
Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
// Insert the new property model
IPropertyContents::Model model;
model.id = newID;
model.ugcId = blueprintID;
model.position = splitLxfmls[i].center;
model.rotation = QuatUtils::IDENTITY;
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)
*/
// 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 ^
// //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.
// }
// Write the ID and data to the response packet
bitStream.Write(blueprintID);
const auto& newSd0 = sd0.GetAsVector();
uint32_t newSd0Size{};
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
bitStream.Write(newSd0Size);
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
}
SEND_PACKET;
//Now we have to construct this object:
// Create entities for each model
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
EntityInfo info;
info.lot = 14;
info.pos = splitLxfmls[i].center;
info.rot = QuatUtils::IDENTITY;
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<LWOOBJID>(u"blueprintid", blueprintIDs[i]));
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
info.settings.push_back(new LDFData<int>(u"modelType", 2));
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", modelIDs[i]));
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity) {
Game::entityManager->ConstructEntity(newEntity);
info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintID));
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
info.settings.push_back(new LDFData<int>(u"modelType", 2));
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", newIDL));
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(), modelIDs[i]);
}
}
}

View File

@@ -1166,32 +1166,36 @@ void HandlePacket(Packet* packet) {
LOG("Couldn't find property ID for zone %i, clone %i", zoneId, cloneId);
goto noBBB;
}
for (auto& bbbModel : Database::Get()->GetUgcModels(propertyId)) {
// 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.
auto bbbModels = Database::Get()->GetUgcModels(propertyId);
if (bbbModels.empty()) {
LOG("No BBB models found for property %llu", propertyId);
goto noBBB;
}
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(bbbModels.size());
for (auto& bbbModel : bbbModels) {
LOG("Getting lxfml ugcID: %llu", bbbModel.id);
bbbModel.lxfmlData.seekg(0, std::ios::end);
size_t lxfmlSize = bbbModel.lxfmlData.tellg();
bbbModel.lxfmlData.seekg(0);
//Send message:
// write data
LWOOBJID blueprintID = bbbModel.id;
// 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, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(lxfmlSize);
bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(bbbModel.lxfmlData.str().c_str()), lxfmlSize);
SystemAddress sysAddr = packet->systemAddress;
SEND_PACKET;
}
SystemAddress sysAddr = packet->systemAddress;
SEND_PACKET;
}
noBBB:

View File

@@ -10,6 +10,7 @@ set(DCOMMONTEST_SOURCES
"TestLUString.cpp"
"TestLUWString.cpp"
"dCommonDependencies.cpp"
"LxfmlTests.cpp"
)
add_subdirectory(dEnumsTests)
@@ -32,6 +33,8 @@ target_link_libraries(dCommonTests ${COMMON_LIBRARIES} GTest::gtest_main)
# Copy test files to testing directory
add_subdirectory(TestBitStreams)
file(COPY ${TESTBITSTREAMS} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
add_subdirectory(LxfmlTestFiles)
file(COPY ${LXFMLTESTFILES} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
# Discover the tests
gtest_discover_tests(dCommonTests)

View File

@@ -0,0 +1,18 @@
set(LXFMLTESTFILES
"deeply_nested.lxfml"
"empty_transform.lxfml"
"invalid_transform.lxfml"
"mixed_invalid_transform.lxfml"
"mixed_valid_invalid.lxfml"
"non_numeric_transform.lxfml"
"no_bricks.lxfml"
"test.lxfml"
"too_few_values.lxfml"
)
# Get the folder name and prepend it to the files above
get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME)
list(TRANSFORM LXFMLTESTFILES PREPEND "${thisFolderName}/")
# Export our list of files
set(LXFMLTESTFILES ${LXFMLTESTFILES} PARENT_SCOPE)

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
<Brick refID="0" designID="3001">
<Part refID="0" designID="3001" materials="23">
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/>
</Part>
</Brick>
</Bricks>
<RigidSystems>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0">
<Group partRefs="0"/>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</Group>
</GroupSystem>
</GroupSystems>
</LXFML>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta></Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679">
<Bone refID="0" transformation=""/>
</Part>
</Brick>
</Bricks>
</LXFML>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679" materials="23">
<Bone refID="0" transformation="invalid,matrix,with,text,values,here,not,numbers,at,all,fails,parse"/>
</Part>
</Brick>
<Brick refID="1" designID="41533">
<Part refID="1" designID="41533" materials="23">
<Bone refID="1" transformation="1,2,3"/>
</Part>
</Brick>
</Bricks>
</LXFML>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta></Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679">
<Bone refID="0" transformation="1,0,invalid,0,1,0,0,0,1,10,20,30"/>
</Part>
</Brick>
</Bricks>
</LXFML>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679" materials="23">
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/>
</Part>
</Brick>
<Brick refID="1" designID="41533">
<Part refID="1" designID="41533" materials="23">
<Bone refID="1" transformation="invalid,transform,here,bad,values,foo,bar,baz,qux,0,0,0"/>
</Part>
</Brick>
<Brick refID="2" designID="74340">
<Part refID="2" designID="3679" materials="23">
<Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,10,20,30"/>
</Part>
</Brick>
<Brick refID="3" designID="41533">
<Part refID="3" designID="41533" materials="23">
<Bone refID="3" transformation="1,2,3"/>
</Part>
</Brick>
</Bricks>
<RigidSystems>
<RigidSystem>
<Rigid boneRefs="0,2"/>
</RigidSystem>
<RigidSystem>
<Rigid boneRefs="1,3"/>
</RigidSystem>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group partRefs="0,2"/>
<Group partRefs="1,3"/>
</GroupSystem>
</GroupSystems>
</LXFML>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta></Meta>
</LXFML>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta></Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679">
<Bone refID="0" transformation="a,b,c,d,e,f,g,h,i,j,k,l"/>
</Part>
</Brick>
</Bricks>
</LXFML>

View File

@@ -0,0 +1,336 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679" materials="23">
<Bone refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.7959,4.83179,1.36732"/>
</Part>
<Part refID="1" designID="3680" materials="23">
<Bone refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.8444,4.54236,1.49497"/>
</Part>
</Brick>
<Brick refID="1" designID="41533">
<Part refID="2" designID="41533" materials="23">
<Bone refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
</Part>
</Brick>
<Brick refID="2" designID="74340">
<Part refID="3" designID="3679" materials="23">
<Bone refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
</Part>
<Part refID="4" designID="3680" materials="23">
<Bone refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.6174,2.99808,3.31576"/>
</Part>
</Brick>
<Brick refID="3" designID="41533">
<Part refID="5" designID="41533" materials="23">
<Bone refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,12.3419,1.74316,5.0089"/>
</Part>
</Brick>
<Brick refID="4" designID="3614">
<Part refID="6" designID="3614" materials="23">
<Bone refID="6" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,13.1227,1.67822,5.36752"/>
</Part>
</Brick>
<Brick refID="5" designID="3004">
<Part refID="7" designID="3004" materials="23,0,0,0" decoration="0,0,0">
<Bone refID="7" transformation="0,0,-1,0,1,0,1,0,0,22,0.320038,10.8"/>
</Part>
</Brick>
<Brick refID="6" designID="3036">
<Part refID="8" designID="3036" materials="23">
<Bone refID="8" transformation="1,0,0,0,1,0,0,0,1,18,3.05176e-05,12.4"/>
</Part>
</Brick>
<Brick refID="7" designID="3036">
<Part refID="9" designID="3036" materials="23">
<Bone refID="9" transformation="1,0,0,0,1,0,0,0,1,14.8,0.320038,13.2"/>
</Part>
</Brick>
<Brick refID="8" designID="3036">
<Part refID="10" designID="3036" materials="23">
<Bone refID="10" transformation="1,0,0,0,1,0,0,0,1,13.2,0.640045,11.6"/>
</Part>
</Brick>
<Brick refID="9" designID="6106">
<Part refID="11" designID="6106" materials="23">
<Bone refID="11" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,0.960052,6.8"/>
</Part>
</Brick>
<Brick refID="10" designID="6106">
<Part refID="12" designID="6106" materials="23">
<Bone refID="12" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,13.2,1.28006,6.8"/>
</Part>
</Brick>
<Brick refID="11" designID="6106">
<Part refID="13" designID="6106" materials="23">
<Bone refID="13" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,1.60007,6.8"/>
</Part>
</Brick>
<Brick refID="12" designID="3730">
<Part refID="14" designID="3730" materials="23">
<Bone refID="14" transformation="-1,0,0,0,1,0,0,0,-1,13.2,1.92007,6.8"/>
</Part>
</Brick>
<Brick refID="13" designID="73587">
<Part refID="15" designID="4592" materials="23">
<Bone refID="15" transformation="1,0,0,0,1,0,0,0,1,15.6,1.92007,6.8"/>
</Part>
<Part refID="16" designID="4593" materials="23">
<Bone refID="16" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,15.6,2.34009,6.8"/>
</Part>
</Brick>
<Brick refID="14" designID="3688">
<Part refID="17" designID="3688" materials="23">
<Bone refID="17" transformation="1,0,0,0,1,0,0,0,1,3.6,0.960022,15.6"/>
</Part>
</Brick>
<Brick refID="15" designID="4085">
<Part refID="18" designID="4085" materials="23">
<Bone refID="18" transformation="1,0,0,0,1,0,0,0,1,5.2,0.960022,15.6"/>
</Part>
</Brick>
<Brick refID="16" designID="3046">
<Part refID="19" designID="3046" materials="23">
<Bone refID="19" transformation="1,0,0,0,1,0,0,0,1,4.4,3.05176e-05,14.8"/>
</Part>
</Brick>
<Brick refID="17" designID="73587">
<Part refID="20" designID="4592" materials="23">
<Bone refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-21.2,3.05176e-05,-9.2"/>
</Part>
<Part refID="21" designID="4593" materials="23">
<Bone refID="21" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-21.2,0.420044,-9.2"/>
</Part>
</Brick>
<Brick refID="18" designID="74340">
<Part refID="22" designID="3679" materials="23">
<Bone refID="22" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
</Part>
<Part refID="23" designID="3680" materials="23">
<Bone refID="23" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-15.8842,2.99805,-6.01737"/>
</Part>
</Brick>
<Brick refID="19" designID="41533">
<Part refID="24" designID="41533" materials="23">
<Bone refID="24" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
</Part>
</Brick>
<Brick refID="20" designID="41533">
<Part refID="25" designID="41533" materials="23">
<Bone refID="25" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-14.1911,1.74313,-6.74193"/>
</Part>
</Brick>
<Brick refID="21" designID="3730">
<Part refID="26" designID="3730" materials="23">
<Bone refID="26" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-12.4,1.92004,-7.60001"/>
</Part>
</Brick>
<Brick refID="22" designID="6106">
<Part refID="27" designID="6106" materials="23">
<Bone refID="27" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.60004,-6.80001"/>
</Part>
</Brick>
<Brick refID="23" designID="6106">
<Part refID="28" designID="6106" materials="23">
<Bone refID="28" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.28003,-7.60001"/>
</Part>
</Brick>
<Brick refID="24" designID="6106">
<Part refID="29" designID="6106" materials="23">
<Bone refID="29" transformation="0,0,-1,0,1,0,1,0,0,-12.4,0.960022,-6.80001"/>
</Part>
</Brick>
<Brick refID="25" designID="3036">
<Part refID="30" designID="3036" materials="23">
<Bone refID="30" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-7.6,0.640015,-7.60001"/>
</Part>
</Brick>
<Brick refID="26" designID="3036">
<Part refID="31" designID="3036" materials="23">
<Bone refID="31" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6,0.320007,-9.2"/>
</Part>
</Brick>
<Brick refID="27" designID="3036">
<Part refID="32" designID="3036" materials="23">
<Bone refID="32" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6.8,0,-12.4"/>
</Part>
</Brick>
<Brick refID="28" designID="3004">
<Part refID="33" designID="3004" materials="23,0,0,0" decoration="0,0,0">
<Bone refID="33" transformation="-1,0,0,0,0.99999994039535522,0,0,0,-1,-8.40001,0.320007,-16.4"/>
</Part>
</Brick>
<Brick refID="29" designID="73587">
<Part refID="34" designID="4592" materials="23">
<Bone refID="34" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-12.4,1.92004,-10"/>
</Part>
<Part refID="35" designID="4593" materials="23">
<Bone refID="35" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-12.4,2.34006,-10"/>
</Part>
</Brick>
<Brick refID="30" designID="3614">
<Part refID="36" designID="3614" materials="23">
<Bone refID="36" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-13.8325,1.67819,-7.52268"/>
</Part>
</Brick>
<Brick refID="31" designID="74340">
<Part refID="37" designID="3679" materials="23">
<Bone refID="37" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.8327,4.83176,-5.19592"/>
</Part>
<Part refID="38" designID="3680" materials="23">
<Bone refID="38" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.705,4.54233,-5.24437"/>
</Part>
</Brick>
<Brick refID="32" designID="3046">
<Part refID="39" designID="3046" materials="23">
<Bone refID="39" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-4.4,0,1.2"/>
</Part>
</Brick>
<Brick refID="33" designID="4085">
<Part refID="40" designID="4085" materials="23">
<Bone refID="40" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.6,0.959991,0.399994"/>
</Part>
</Brick>
<Brick refID="34" designID="3688">
<Part refID="41" designID="3688" materials="23">
<Bone refID="41" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.60001,0.959991,2"/>
</Part>
</Brick>
<Brick refID="35" designID="73587">
<Part refID="42" designID="4592" materials="23">
<Bone refID="42" transformation="1,0,0,0,1,0,0,0,1,7.60001,6.10352e-05,-9.2"/>
</Part>
<Part refID="43" designID="4593" materials="23">
<Bone refID="43" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,7.6,0.420074,-9.2"/>
</Part>
</Brick>
</Bricks>
<RigidSystems>
<RigidSystem>
<Rigid refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,5.9959182739257812,459.87173461914062,69.367317199707031" boneRefs="0"/>
<Rigid refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.0443649291992187,459.58230590820312,69.494972229003906" boneRefs="1"/>
<Rigid refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.7689208984375,458.32742309570312,71.188117980957031" boneRefs="2,3"/>
<Rigid refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.8173675537109375,458.03802490234375,71.315757751464844" boneRefs="4"/>
<Rigid refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,7.54193115234375,456.78311157226562,73.008895874023438" boneRefs="5"/>
<Rigid refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,8.3226776123046875,456.71817016601562,73.367523193359375" boneRefs="6"/>
<Rigid refID="6" transformation="0,0,-1,0,1,0,1,0,0,17.19999885559082,455.3599853515625,78.799995422363281" boneRefs="7,8,9,10,11,12,13,14,15"/>
<Rigid refID="7" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,10.800004959106445,457.38003540039062,74.800003051757813" boneRefs="16"/>
<Joint type="hinge">
<RigidRef rigidRef="0" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="1" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="1" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
<RigidRef rigidRef="2" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="2" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="3" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
<RigidRef rigidRef="3" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="5" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="ball">
<RigidRef rigidRef="6" a="-1,0,0" z="0,1,0" t="4.799992561340332,1.7600365877151489,-9.1999912261962891"/>
<RigidRef rigidRef="5" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="6" a="1,0,0" z="0,1,0" t="3.9999923706054687,2.0200366973876953,-6.3999929428100586"/>
<RigidRef rigidRef="7" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="8" transformation="1,0,0,0,1,0,0,0,1,-1.1999999284744263,455.99996948242187,83.599998474121094" boneRefs="17"/>
<Rigid refID="9" transformation="1,0,0,0,1,0,0,0,1,0.40000009536743164,455.99996948242187,83.600006103515625" boneRefs="18,19"/>
<Joint type="hinge">
<RigidRef rigidRef="9" a="0,1,0" z="0,0,1" t="-0.80000007152557373,0,-0.8000030517578125"/>
<RigidRef rigidRef="8" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="10" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-26.000003814697266,455.03997802734375,58.799995422363281" boneRefs="20"/>
<Rigid refID="11" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-26.000003814697266,455.45999145507812,58.799995422363281" boneRefs="21"/>
<Joint type="hinge">
<RigidRef rigidRef="10" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
<RigidRef rigidRef="11" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="12" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-20.811885833740234,458.327392578125,62.031078338623047" boneRefs="22,24"/>
<Rigid refID="13" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-20.684246063232422,458.03799438476562,61.982631683349609" boneRefs="23"/>
<Rigid refID="14" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.991107940673828,456.7830810546875,61.258068084716797" boneRefs="25"/>
<Rigid refID="15" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-17.200000762939453,456.95999145507812,60.399990081787109" boneRefs="26,27,28,29,30,31,32,33,34"/>
<Rigid refID="16" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.632480621337891,456.7181396484375,60.477321624755859" boneRefs="36"/>
<Rigid refID="17" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-17.200000762939453,457.3800048828125,57.999996185302734" boneRefs="35"/>
<Rigid refID="18" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.632686614990234,459.8717041015625,62.804080963134766" boneRefs="37"/>
<Rigid refID="19" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.505031585693359,459.582275390625,62.755634307861328" boneRefs="38"/>
<Joint type="hinge">
<RigidRef rigidRef="12" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="13" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="12" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
<RigidRef rigidRef="19" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="13" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="16" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
</Joint>
<Joint type="ball">
<RigidRef rigidRef="15" a="0,0,-1" z="0,1,0" t="0.40000000596046448,0.15999999642372131,0.80000001192092896"/>
<RigidRef rigidRef="16" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="15" a="0,0,1" z="0,1,0" t="-2.3999977111816406,0.41999998688697815,0"/>
<RigidRef rigidRef="17" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
<Joint type="hinge">
<RigidRef rigidRef="18" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
<RigidRef rigidRef="19" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-9.2000007629394531,455.03994750976562,69.199996948242188" boneRefs="39,40"/>
<Rigid refID="21" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-8.4000053405761719,455.99993896484375,70" boneRefs="41"/>
<Joint type="hinge">
<RigidRef rigidRef="20" a="0,1,0" z="0,0,1" t="0,0.95999997854232788,0"/>
<RigidRef rigidRef="21" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
</Joint>
</RigidSystem>
<RigidSystem>
<Rigid refID="22" transformation="1,0,0,0,1,0,0,0,1,2.8000054359436035,455.04000854492187,58.799999237060547" boneRefs="42"/>
<Rigid refID="23" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,2.8000044822692871,455.46002197265625,58.799999237060547" boneRefs="43"/>
<Joint type="hinge">
<RigidRef rigidRef="22" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
<RigidRef rigidRef="23" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
</Joint>
</RigidSystem>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="15,16">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="19,18,17">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="11,12,13">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="10,9,7,8"/>
</Group>
</Group>
</Group>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="25,34,35,20,21,31,37,38,30,24,39,22,23,32,41,26,28,27,36,40,29,33"/>
</GroupSystem>
</GroupSystems>
</LXFML>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<LXFML versionMajor="5" versionMinor="0">
<Meta></Meta>
<Bricks>
<Brick refID="0" designID="74340">
<Part refID="0" designID="3679">
<Bone refID="0" transformation="1,0,0,0,1,0"/>
</Part>
</Brick>
</Bricks>
</LXFML>

View File

@@ -0,0 +1,297 @@
#include "gtest/gtest.h"
#include "Lxfml.h"
#include "TinyXmlUtils.h"
#include "dCommonDependencies.h"
#include <fstream>
#include <sstream>
#include <unordered_set>
#include <filesystem>
using namespace TinyXmlUtils;
static std::string ReadFile(const std::string& filename) {
std::ifstream in(filename, std::ios::in | std::ios::binary);
if (!in.is_open()) {
return "";
}
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
std::string SerializeElement(tinyxml2::XMLElement* elem) {
tinyxml2::XMLPrinter p;
elem->Accept(&p);
return std::string(p.CStr());
};
TEST(LxfmlTests, SplitUsesAllBricksAndNoDuplicates) {
// Read the test.lxfml file copied to build directory by CMake
std::string data = ReadFile("test.lxfml");
ASSERT_FALSE(data.empty()) << "Failed to read test.lxfml from build directory";
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0);
// parse original to count bricks
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS);
DocumentReader reader(doc);
auto lxfml = reader["LXFML"];
ASSERT_TRUE(lxfml);
std::unordered_set<std::string> originalRigidSet;
if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
originalRigidSet.insert(SerializeElement(rs));
}
}
std::unordered_set<std::string> originalGroupSet;
if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) {
for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
// collect this group and nested groups
std::function<void(tinyxml2::XMLElement*)> collectGroups = [&](tinyxml2::XMLElement* grp) {
originalGroupSet.insert(SerializeElement(grp));
for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child);
};
collectGroups(g);
}
}
}
std::unordered_set<std::string> originalBricks;
for (const auto& brick : lxfml["Bricks"]) {
const auto* ref = brick.Attribute("refID");
if (ref) originalBricks.insert(ref);
}
ASSERT_GT(originalBricks.size(), 0);
// Collect bricks across all results and ensure no duplicates and all used
std::unordered_set<std::string> usedBricks;
// Track used rigid systems and groups (serialized strings)
std::unordered_set<std::string> usedRigidSet;
std::unordered_set<std::string> usedGroupSet;
for (const auto& res : results) {
tinyxml2::XMLDocument outDoc;
ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS);
DocumentReader outReader(outDoc);
auto outLxfml = outReader["LXFML"];
ASSERT_TRUE(outLxfml);
// collect rigid systems in this output
if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
auto s = SerializeElement(rs);
// no duplicate allowed across outputs
ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits";
usedRigidSet.insert(s);
}
}
// collect groups in this output
if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) {
for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
std::function<void(tinyxml2::XMLElement*)> collectGroupsOut = [&](tinyxml2::XMLElement* grp) {
auto s = SerializeElement(grp);
ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits";
usedGroupSet.insert(s);
for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child);
};
collectGroupsOut(g);
}
}
}
for (const auto& brick : outLxfml["Bricks"]) {
const auto* ref = brick.Attribute("refID");
if (ref) {
// no duplicate allowed
ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref;
usedBricks.insert(ref);
}
}
}
// Every original brick must be used in one of the outputs
for (const auto& bref : originalBricks) {
ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref;
}
// And usedBricks should not contain anything outside original
for (const auto& ub : usedBricks) {
ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub;
}
// Ensure all original rigid systems and groups were used exactly once
ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch";
for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits";
ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch";
for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits";
}
// Tests for invalid input handling - now working with the improved Split function
TEST(LxfmlTests, InvalidLxfmlHandling) {
// Test LXFML with invalid transformation matrices
std::string invalidTransformData = ReadFile("invalid_transform.lxfml");
ASSERT_FALSE(invalidTransformData.empty()) << "Failed to read invalid_transform.lxfml from build directory";
// The Split function should handle invalid transformation matrices gracefully
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(invalidTransformData);
}) << "Split should not crash on invalid transformation matrices";
// Function should handle invalid transforms gracefully, possibly returning empty or partial results
// The exact behavior depends on how the function handles invalid numeric parsing
}
TEST(LxfmlTests, EmptyLxfmlHandling) {
// Test with completely empty input
std::string emptyData = "";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(emptyData);
}) << "Split should not crash on empty input";
EXPECT_EQ(results.size(), 0) << "Empty input should return empty results";
}
TEST(LxfmlTests, EmptyTransformHandling) {
// Test LXFML with empty transformation matrix
std::string testData = ReadFile("empty_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read empty_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on empty transformation matrix";
// The function should handle empty transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, TooFewValuesTransformHandling) {
// Test LXFML with too few transformation values (needs 12, has fewer)
std::string testData = ReadFile("too_few_values.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read too_few_values.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on transformation matrix with too few values";
// The function should handle incomplete transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, NonNumericTransformHandling) {
// Test LXFML with non-numeric transformation values
std::string testData = ReadFile("non_numeric_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read non_numeric_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on non-numeric transformation values";
// The function should handle non-numeric transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, MixedInvalidTransformHandling) {
// Test LXFML with mixed valid/invalid transformation values within a matrix
std::string testData = ReadFile("mixed_invalid_transform.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read mixed_invalid_transform.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on mixed valid/invalid transformation values";
// The function should handle mixed valid/invalid transforms gracefully
// May return empty results or skip invalid bricks
}
TEST(LxfmlTests, NoBricksHandling) {
// Test LXFML with no Bricks section (should return empty gracefully)
std::string testData = ReadFile("no_bricks.lxfml");
ASSERT_FALSE(testData.empty()) << "Failed to read no_bricks.lxfml from build directory";
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(testData);
}) << "Split should not crash on LXFML with no Bricks section";
// Should return empty results gracefully when no bricks are present
EXPECT_EQ(results.size(), 0) << "LXFML with no bricks should return empty results";
}
TEST(LxfmlTests, MixedValidInvalidTransformsHandling) {
// Test LXFML with mix of valid and invalid transformation data
std::string mixedValidData = ReadFile("mixed_valid_invalid.lxfml");
ASSERT_FALSE(mixedValidData.empty()) << "Failed to read mixed_valid_invalid.lxfml from build directory";
// The Split function should handle mixed valid/invalid transforms gracefully
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(mixedValidData);
}) << "Split should not crash on mixed valid/invalid transforms";
// Should process valid bricks and handle invalid ones gracefully
if (results.size() > 0) {
EXPECT_NO_FATAL_FAILURE({
for (size_t i = 0; i < results.size(); ++i) {
// Each result should have valid LXFML structure
tinyxml2::XMLDocument doc;
auto parseResult = doc.Parse(results[i].lxfml.c_str());
EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS)
<< "Result " << i << " should produce valid XML";
if (parseResult == tinyxml2::XML_SUCCESS) {
auto* lxfml = doc.FirstChildElement("LXFML");
EXPECT_NE(lxfml, nullptr) << "Result " << i << " should have LXFML root element";
}
}
}) << "Mixed valid/invalid transform processing should not cause fatal errors";
}
}
TEST(LxfmlTests, DeepCloneDepthProtection) {
// Test that deep cloning has protection against excessive nesting
std::string deeplyNestedLxfml = ReadFile("deeply_nested.lxfml");
ASSERT_FALSE(deeplyNestedLxfml.empty()) << "Failed to read deeply_nested.lxfml from build directory";
// The Split function should handle deeply nested structures without hanging
std::vector<Lxfml::Result> results;
EXPECT_NO_FATAL_FAILURE({
results = Lxfml::Split(deeplyNestedLxfml);
}) << "Split should not hang or crash on deeply nested XML structures";
// Should still produce valid output despite depth limitations
EXPECT_GT(results.size(), 0) << "Should produce at least one result even with deep nesting";
if (results.size() > 0) {
// Verify the result is still valid XML
tinyxml2::XMLDocument doc;
auto parseResult = doc.Parse(results[0].lxfml.c_str());
EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS) << "Result should still be valid XML";
if (parseResult == tinyxml2::XML_SUCCESS) {
auto* lxfml = doc.FirstChildElement("LXFML");
EXPECT_NE(lxfml, nullptr) << "Result should have LXFML root element";
// Verify that bricks are still included despite group nesting issues
auto* bricks = lxfml->FirstChildElement("Bricks");
EXPECT_NE(bricks, nullptr) << "Bricks element should be present";
if (bricks) {
auto* brick = bricks->FirstChildElement("Brick");
EXPECT_NE(brick, nullptr) << "At least one brick should be present";
}
}
}
}