Compare commits

..

1 Commits

Author SHA1 Message Date
David Markowitz
2cc74d7b12 feat: InventoryComponent debug info 2025-10-06 01:50:03 -07:00
45 changed files with 456 additions and 2405 deletions

View File

@@ -1,29 +0,0 @@
# GitHub Copilot Instructions
* c++20 standard, please use the latest features except NO modules.
* use `.contains` for searching in associative containers
* use const as much as possible. If it can be const, it should be made const
* DO NOT USE const_cast EVER.
* use `cstdint` bitwidth types ALWAYS for integral types.
* NEVER use std::wstring. If wide strings are necessary, use std::u16string with conversion utilties in GeneralUtils.h.
* Functions are ALWAYS PascalCase.
* local variables are camelCase
* NEVER use snake case
* indentation is TABS, not SPACES.
* TABS are 4 spaces by default
* Use trailing braces ALWAYS
* global variables are prefixed with `g_`
* if global variables or functions are needed, they should be located in an anonymous namespace
* Use `GeneralUtils::TryParse` for ANY parsing of strings to integrals.
* Use brace initialization when possible.
* ALWAYS default initialize variables.
* Pointers should be avoided unless necessary. Use references when the pointer has been checked and should not be null
* headers should be as compact as possible. Do NOT include extra data that isnt needed.
* Remember to include logs (LOG macro uses printf style logging) while putting verbose logs under LOG_DEBUG.
* NEVER USE `RakNet::BitStream::ReadBit`
* NEVER assume pointers are good, always check if they are null. Once a pointer is checked and is known to be non-null, further accesses no longer need checking
* Be wary of TOCTOU. Prevent all possible issues relating to TOCTOU.
* new memory allocations should never be used unless absolutely necessary.
* new for reconstruction of objects is allowed
* Prefer following the format of the file over correct formatting. Consistency over correctness.
* When using auto, ALWAYS put a * for pointers.

View File

@@ -5,43 +5,13 @@
#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;
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
const auto err = doc.Parse(data.data());
if (err != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
return toReturn;
}
@@ -50,6 +20,7 @@ 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;
}
@@ -78,19 +49,16 @@ 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) 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 (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 (z < lowest.z) lowest.z = z;
if (highest.x < x) highest.x = x;
@@ -119,19 +87,13 @@ 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 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;
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;
std::stringstream stream;
for (int i = 0; i < 9; i++) {
stream << split[i];
@@ -166,345 +128,3 @@ 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;
// Track used groups to avoid processing them twice
std::unordered_set<tinyxml2::XMLElement*> usedGroups;
// 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) {
// Skip if this group was already processed as part of another group
if (usedGroups.find(groupRoot) != usedGroups.end()) continue;
// Helper to collect all partRefs in a group's subtree
std::function<void(const tinyxml2::XMLElement*, std::unordered_set<std::string>&)> collectParts = [&](const tinyxml2::XMLElement* g, std::unordered_set<std::string>& partRefs) {
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, partRefs);
};
// Collect all groups that need to be merged into this output
std::vector<tinyxml2::XMLElement*> groupsToInclude{ groupRoot };
usedGroups.insert(groupRoot);
// Build initial sets of bricks and boneRefs from the starting group
std::unordered_set<std::string> partRefs;
collectParts(groupRoot, partRefs);
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
// and check if those rigid systems' bricks span other groups
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++;
// First, expand rigid systems based on current boneRefsIncluded
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;
}
}
}
}
// Second, check if the newly included bricks span any other groups
// If so, merge those groups into the current output
for (auto* otherGroup : groupRoots) {
if (usedGroups.find(otherGroup) != usedGroups.end()) continue;
// Collect partRefs from this other group
std::unordered_set<std::string> otherPartRefs;
collectParts(otherGroup, otherPartRefs);
// Check if any of these partRefs correspond to bricks we've already included
bool spansOtherGroup = false;
for (const auto& pref : otherPartRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref && bricksIncluded.find(std::string(bref)) != bricksIncluded.end()) {
spansOtherGroup = true;
break;
}
}
}
if (spansOtherGroup) {
// Merge this group into the current output
usedGroups.insert(otherGroup);
groupsToInclude.push_back(otherGroup);
changed = true;
// Add all partRefs, boneRefs, and bricks from this group
for (const auto& pref : otherPartRefs) {
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));
}
}
}
}
}
}
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 all merged groups' XML)
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsToInclude);
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,7 +6,6 @@
#include <string>
#include <string_view>
#include <vector>
#include "NiPoint3.h"
@@ -19,7 +18,6 @@ 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

@@ -81,9 +81,6 @@ public:
[[nodiscard]]
AssetStream GetFile(const char* name) const;
[[nodiscard]]
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
private:
void LoadPackIndex();

View File

@@ -84,8 +84,6 @@
#include "GhostComponent.h"
#include "AchievementVendorComponent.h"
#include "VanityUtilities.h"
#include "ObjectIDManager.h"
#include "ePlayerFlag.h"
// Table includes
#include "CDComponentsRegistryTable.h"
@@ -194,10 +192,7 @@ Entity::~Entity() {
}
void Entity::Initialize() {
RegisterMsg<GameMessages::RequestServerObjectInfo>(this, &Entity::MsgRequestServerObjectInfo);
RegisterMsg<GameMessages::DropClientLoot>(this, &Entity::MsgDropClientLoot);
RegisterMsg<GameMessages::GetFactionTokenType>(this, &Entity::MsgGetFactionTokenType);
RegisterMsg<GameMessages::PickupItem>(this, &Entity::MsgPickupItem);
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &Entity::MsgRequestServerObjectInfo);
/**
* Setup trigger
*/
@@ -292,7 +287,7 @@ void Entity::Initialize() {
AddComponent<LUPExhibitComponent>(lupExhibitID);
}
const auto racingControlID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
const auto racingControlID =compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
if (racingControlID > 0) {
AddComponent<RacingControlComponent>(racingControlID);
}
@@ -1668,7 +1663,7 @@ void Entity::AddLootItem(const Loot::Info& info) const {
auto* const characterComponent = GetComponent<CharacterComponent>();
if (!characterComponent) return;
LOG("Player %llu has been allowed to pickup %i with id %llu", m_ObjectID, info.lot, info.id);
auto& droppedLoot = characterComponent->GetDroppedLoot();
droppedLoot[info.id] = info;
}
@@ -2280,73 +2275,3 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
if (client) GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, client->GetSystemAddress());
return true;
}
bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) {
auto& dropLootMsg = static_cast<GameMessages::DropClientLoot&>(msg);
if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) {
Loot::Info info{
.id = dropLootMsg.lootID,
.lot = dropLootMsg.item,
.count = dropLootMsg.count,
};
AddLootItem(info);
}
if (dropLootMsg.item == LOT_NULL && dropLootMsg.currency != 0) {
RegisterCoinDrop(dropLootMsg.currency);
}
return true;
}
bool Entity::MsgGetFlag(GameMessages::GameMsg& msg) {
auto& flagMsg = static_cast<GameMessages::GetFlag&>(msg);
if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID);
return true;
}
bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) {
auto& tokenMsg = static_cast<GameMessages::GetFactionTokenType&>(msg);
GameMessages::GetFlag getFlagMsg{};
getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8318;
getFlagMsg.flagID = ePlayerFlag::SENTINEL_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8319;
getFlagMsg.flagID = ePlayerFlag::PARADOX_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8320;
getFlagMsg.flagID = ePlayerFlag::VENTURE_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8321;
LOG("Returning token type %i", tokenMsg.tokenType);
return tokenMsg.tokenType != LOT_NULL;
}
bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) {
auto& pickupItemMsg = static_cast<GameMessages::PickupItem&>(msg);
if (GetObjectID() == pickupItemMsg.lootOwnerID) {
PickupItem(pickupItemMsg.lootID);
} else {
auto* const characterComponent = GetComponent<CharacterComponent>();
if (!characterComponent) return false;
auto& droppedLoot = characterComponent->GetDroppedLoot();
const auto it = droppedLoot.find(pickupItemMsg.lootID);
if (it != droppedLoot.end()) {
CDObjectsTable* objectsTable = CDClientManager::GetTable<CDObjectsTable>();
const CDObjects& object = objectsTable->GetByID(it->second.lot);
if (object.id != 0 && object.type == "Powerup") {
return false; // Let powerups be duplicated
}
}
droppedLoot.erase(pickupItemMsg.lootID);
}
return true;
}

View File

@@ -176,10 +176,6 @@ public:
void AddComponent(eReplicaComponentType componentId, Component* component);
bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg);
bool MsgDropClientLoot(GameMessages::GameMsg& msg);
bool MsgGetFlag(GameMessages::GameMsg& msg);
bool MsgGetFactionTokenType(GameMessages::GameMsg& msg);
bool MsgPickupItem(GameMessages::GameMsg& msg);
// This is expceted to never return nullptr, an assert checks this.
CppScripts::Script* const GetScript() const;
@@ -346,12 +342,6 @@ public:
RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1));
}
template<typename T>
inline void RegisterMsg(auto* self, const auto handler) {
T msg;
RegisterMsg(msg.msgId, self, handler);
}
/**
* @brief The observable for player entity position updates.
*/
@@ -610,5 +600,5 @@ auto Entity::GetComponents() const {
template<typename... T>
auto Entity::GetComponentsMut() const {
return std::tuple{ GetComponent<T>()... };
return std::tuple{GetComponent<T>()...};
}

View File

@@ -9,16 +9,6 @@ Team::Team() {
lootOption = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
}
LWOOBJID Team::GetNextLootOwner() {
lootRound++;
if (lootRound >= members.size()) {
lootRound = 0;
}
return members[lootRound];
}
TeamManager::TeamManager() {
}

View File

@@ -4,8 +4,6 @@
struct Team {
Team();
LWOOBJID GetNextLootOwner();
LWOOBJID teamID = LWOOBJID_EMPTY;
char lootOption = 0;
std::vector<LWOOBJID> members{};

View File

@@ -45,6 +45,33 @@ ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Comp
m_ActivityID = parent->GetVar<int32_t>(u"activityID");
LoadActivityData(m_ActivityID);
}
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
if (destroyableComponent) {
// First lookup the loot matrix id for this component id.
CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) {return (entry.LootMatrixIndex == destroyableComponent->GetLootMatrixID()); });
uint32_t startingLMI = 0;
// If we have one, set the starting loot matrix id to that.
if (activityRewards.size() > 0) {
startingLMI = activityRewards[0].LootMatrixIndex;
}
if (startingLMI > 0) {
// We may have more than 1 loot matrix index to use depending ont the size of the team that is looting the activity.
// So this logic will get the rest of the loot matrix indices for this activity.
std::vector<CDActivityRewards> objectTemplateActivities = activityRewardsTable->Query([=](CDActivityRewards entry) {return (activityRewards[0].objectTemplate == entry.objectTemplate); });
for (const auto& item : objectTemplateActivities) {
if (item.activityRating > 0 && item.activityRating < 5) {
m_ActivityLootMatrices.insert({ item.activityRating, item.LootMatrixIndex });
}
}
}
}
}
void ActivityComponent::LoadActivityData(const int32_t activityId) {
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
@@ -671,6 +698,10 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
}
}
auto& lootMatrices = activityInfo.PushDebug("Loot Matrices");
for (const auto& [activityRating, lootMatrixID] : m_ActivityLootMatrices) {
lootMatrices.PushDebug<AMFIntValue>("Loot Matrix " + std::to_string(activityRating)) = lootMatrixID;
}
activityInfo.PushDebug<AMFIntValue>("ActivityID") = m_ActivityID;
return true;
}

View File

@@ -341,6 +341,12 @@ public:
*/
void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; };
/**
* Returns the LMI that this activity points to for a team size
* @param teamSize the team size to get the LMI for
* @return the LMI that this activity points to for a team size
*/
uint32_t GetLootMatrixForTeamSize(uint32_t teamSize) { return m_ActivityLootMatrices[teamSize]; }
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
@@ -364,6 +370,11 @@ private:
*/
std::vector<ActivityPlayer*> m_ActivityPlayers;
/**
* LMIs for team sizes
*/
std::unordered_map<uint32_t, uint32_t> m_ActivityLootMatrices;
/**
* The activity id
*/

View File

@@ -694,8 +694,6 @@ void DestroyableComponent::NotifySubscribers(Entity* attacker, uint32_t damage)
}
void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType, const std::u16string& deathType, uint32_t skillID) {
if (m_IsDead) return;
//check if hardcore mode is enabled
if (Game::entityManager->GetHardcoreMode()) {
DoHardcoreModeDrops(source);
@@ -708,7 +706,6 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
Game::entityManager->SerializeEntity(m_Parent);
}
m_IsDead = true;
m_KillerID = source;
auto* owner = Game::entityManager->GetEntity(source);
@@ -756,7 +753,36 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
//NANI?!
if (!isPlayer) {
if (owner != nullptr) {
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
auto* team = TeamManager::Instance()->GetTeam(owner->GetObjectID());
if (team != nullptr && m_Parent->GetComponent<BaseCombatAIComponent>() != nullptr) {
LWOOBJID specificOwner = LWOOBJID_EMPTY;
auto* scriptedActivityComponent = m_Parent->GetComponent<ScriptedActivityComponent>();
uint32_t teamSize = team->members.size();
uint32_t lootMatrixId = GetLootMatrixID();
if (scriptedActivityComponent) {
lootMatrixId = scriptedActivityComponent->GetLootMatrixForTeamSize(teamSize);
}
if (team->lootOption == 0) { // Round robin
specificOwner = TeamManager::Instance()->GetNextLootOwner(team);
auto* member = Game::entityManager->GetEntity(specificOwner);
if (member) Loot::DropLoot(member, m_Parent->GetObjectID(), lootMatrixId, GetMinCoins(), GetMaxCoins());
} else {
for (const auto memberId : team->members) { // Free for all
auto* member = Game::entityManager->GetEntity(memberId);
if (member == nullptr) continue;
Loot::DropLoot(member, m_Parent->GetObjectID(), lootMatrixId, GetMinCoins(), GetMaxCoins());
}
}
} else { // drop loot for non team user
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
}
}
} else {
//Check if this zone allows coin drops
@@ -773,15 +799,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
coinsTotal -= coinsToLose;
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = m_Parent->GetObjectID();
lootMsg.ownerID = m_Parent->GetObjectID();
lootMsg.currency = coinsToLose;
lootMsg.spawnPos = m_Parent->GetPosition();
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
lootMsg.Send(m_Parent->GetSystemAddress());
Loot::DropLoot(m_Parent, m_Parent->GetObjectID(), -1, coinsToLose, coinsToLose);
character->SetCoins(coinsTotal, eLootSourceType::PICKUP);
}
}
@@ -1025,8 +1043,8 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
auto maxHealth = GetMaxHealth();
const auto uscoreMultiplier = Game::entityManager->GetHardcoreUscoreEnemiesMultiplier();
const bool isUscoreReducedLot =
Game::entityManager->GetHardcoreUscoreReducedLots().contains(lot) ||
Game::entityManager->GetHardcoreUscoreReduced();
Game::entityManager->GetHardcoreUscoreReducedLots().contains(lot) ||
Game::entityManager->GetHardcoreUscoreReduced();
const auto uscoreReduction = isUscoreReducedLot ? Game::entityManager->GetHardcoreUscoreReduction() : 1.0f;
int uscore = maxHealth * Game::entityManager->GetHardcoreUscoreEnemiesMultiplier() * uscoreReduction;

View File

@@ -471,8 +471,6 @@ public:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnSetFaction(GameMessages::GameMsg& msg);
void SetIsDead(const bool value) { m_IsDead = value; }
static Implementation<bool, const Entity*> IsEnemyImplentation;
static Implementation<bool, const Entity*> IsFriendImplentation;

View File

@@ -443,7 +443,7 @@ Item* InventoryComponent::FindItemBySubKey(LWOOBJID id, eInventoryType inventory
}
}
bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) {
bool InventoryComponent::HasSpaceForLoot(const std::unordered_map<LOT, int32_t>& loot) {
std::unordered_map<eInventoryType, int32_t> spaceOffset{};
uint32_t slotsNeeded = 0;

View File

@@ -22,7 +22,6 @@
#include "eInventoryType.h"
#include "eReplicaComponentType.h"
#include "eLootSourceType.h"
#include "Loot.h"
class Entity;
class ItemSet;
@@ -201,7 +200,7 @@ public:
* @param loot a map of items to add and how many to add
* @return whether the entity has enough space for all the items
*/
bool HasSpaceForLoot(const Loot::Return& loot);
bool HasSpaceForLoot(const std::unordered_map<LOT, int32_t>& loot);
/**
* Equips an item in the specified slot

View File

@@ -31,8 +31,6 @@ MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) :
m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue();
RegisterMsg<GetObjectReportInfo>(this, &MissionComponent::OnGetObjectReportInfo);
RegisterMsg<GameMessages::GetMissionState>(this, &MissionComponent::OnGetMissionState);
RegisterMsg<GameMessages::MissionNeedsLot>(this, &MissionComponent::OnMissionNeedsLot);
}
//! Destructor
@@ -735,15 +733,3 @@ bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
return true;
}
bool MissionComponent::OnGetMissionState(GameMessages::GameMsg& msg) {
auto misState = static_cast<GameMessages::GetMissionState&>(msg);
misState.missionState = GetMissionState(misState.missionID);
return true;
}
bool MissionComponent::OnMissionNeedsLot(GameMessages::GameMsg& msg) {
const auto& needMsg = static_cast<GameMessages::MissionNeedsLot&>(msg);
return RequiresItem(needMsg.item);
}

View File

@@ -172,8 +172,6 @@ public:
void ResetMission(const int32_t missionId);
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetMissionState(GameMessages::GameMsg& msg);
bool OnMissionNeedsLot(GameMessages::GameMsg& msg);
/**
* All the missions owned by this entity, mapped by mission ID
*/

View File

@@ -24,7 +24,6 @@ ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Comp
m_userModelID = m_Parent->GetVarAs<LWOOBJID>(u"userModelID");
RegisterMsg<RequestUse>(this, &ModelComponent::OnRequestUse);
RegisterMsg<ResetModelToDefaults>(this, &ModelComponent::OnResetModelToDefaults);
RegisterMsg<GetObjectReportInfo>(this, &ModelComponent::OnGetObjectReportInfo);
}
bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
@@ -339,19 +338,3 @@ void ModelComponent::RemoveAttack() {
set.Send();
}
}
bool ModelComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
if (!reportMsg.info) return false;
auto& cmptInfo = reportMsg.info->PushDebug("Model Behaviors (Mutable)");
cmptInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
cmptInfo.PushDebug<AMFStringValue>("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name";
cmptInfo.PushDebug<AMFBoolValue>("Has Unique Name") = false;
cmptInfo.PushDebug<AMFStringValue>("UGID (from item)") = std::to_string(m_userModelID);
cmptInfo.PushDebug<AMFStringValue>("UGID") = std::to_string(m_userModelID);
cmptInfo.PushDebug<AMFStringValue>("Description") = "";
cmptInfo.PushDebug<AMFIntValue>("Behavior Count") = m_Behaviors.size();
return true;
}

View File

@@ -34,7 +34,6 @@ public:
bool OnRequestUse(GameMessages::GameMsg& msg);
bool OnResetModelToDefaults(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;

View File

@@ -48,7 +48,6 @@ namespace {
{ REQUEST_USE, []() { return std::make_unique<RequestUse>(); }},
{ REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique<RequestServerObjectInfo>(); } },
{ SHOOTING_GALLERY_FIRE, []() { return std::make_unique<ShootingGalleryFire>(); } },
{ PICKUP_ITEM, []() { return std::make_unique<PickupItem>(); } },
};
};
@@ -282,6 +281,11 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
break;
}
case MessageType::Game::PICKUP_ITEM: {
GameMessages::HandlePickupItem(inStream, entity);
break;
}
case MessageType::Game::RESURRECT: {
GameMessages::HandleResurrect(inStream, entity);
break;

View File

@@ -978,7 +978,6 @@ void GameMessages::SendResurrect(Entity* entity) {
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent != nullptr && entity->GetLOT() == 1) {
destroyableComponent->SetIsDead(false);
auto* levelComponent = entity->GetComponent<LevelProgressionComponent>();
if (levelComponent) {
int32_t healthToRestore = levelComponent->GetLevel() >= 45 ? 8 : 4;
@@ -1103,6 +1102,52 @@ void GameMessages::SendDropClientLoot(Entity* entity, const LWOOBJID& sourceID,
finalPosition = NiPoint3(static_cast<float>(spawnPos.GetX() + sin_v), spawnPos.GetY(), static_cast<float>(spawnPos.GetZ() + cos_v));
}
//Write data to packet & send:
CBITSTREAM;
CMSGHEADER;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::DROP_CLIENT_LOOT);
bitStream.Write(bUsePosition);
bitStream.Write(finalPosition != NiPoint3Constant::ZERO);
if (finalPosition != NiPoint3Constant::ZERO) bitStream.Write(finalPosition);
bitStream.Write(currency);
bitStream.Write(item);
bitStream.Write(lootID);
bitStream.Write(owner);
bitStream.Write(sourceID);
bitStream.Write(spawnPos != NiPoint3Constant::ZERO);
if (spawnPos != NiPoint3Constant::ZERO) bitStream.Write(spawnPos);
auto* team = TeamManager::Instance()->GetTeam(owner);
// Currency and powerups should not sync
if (team != nullptr && currency == 0) {
CDObjectsTable* objectsTable = CDClientManager::GetTable<CDObjectsTable>();
const CDObjects& object = objectsTable->GetByID(item);
if (object.type != "Powerup") {
for (const auto memberId : team->members) {
auto* member = Game::entityManager->GetEntity(memberId);
if (member == nullptr) continue;
SystemAddress sysAddr = member->GetSystemAddress();
SEND_PACKET;
}
return;
}
}
SystemAddress sysAddr = entity->GetSystemAddress();
SEND_PACKET;
}
void GameMessages::SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme) {
@@ -2520,6 +2565,9 @@ 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.
@@ -2533,6 +2581,23 @@ 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();
@@ -2549,120 +2614,85 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
std::istringstream sd0DataStream(str);
Sd0 sd0(sd0DataStream);
// Uncompress the data, split, and nornmalize the model
// Uncompress the data and normalize the position
const auto asStr = sd0.GetAsStringUncompressed();
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr);
if (Game::config->GetValue("save_lxfmls") == "1") {
// save using localId to avoid conflicts
std::ofstream outFile("debug_lxfml_uncompressed_" + std::to_string(localId) + ".lxfml");
outFile << asStr;
outFile.close();
}
// 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());
auto splitLxfmls = Lxfml::Split(asStr);
LOG_DEBUG("Split into %zu models", splitLxfmls.size());
//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>(splitLxfmls.size());
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
std::vector<LWOOBJID> blueprintIDs;
std::vector<LWOOBJID> modelIDs;
bitStream.Write(newSd0Size);
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());
}
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
SEND_PACKET;
// 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;
//Now we have to construct this object:
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);
EntityInfo info;
info.lot = 14;
info.pos = newCenter;
info.rot = {};
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
//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]);
}
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);
}
}
@@ -5687,6 +5717,27 @@ void GameMessages::HandleModularBuildMoveAndEquip(RakNet::BitStream& inStream, E
inv->MoveItemToInventory(item, eInventoryType::MODELS, 1, false, true);
}
void GameMessages::HandlePickupItem(RakNet::BitStream& inStream, Entity* entity) {
LWOOBJID lootObjectID;
LWOOBJID playerID;
inStream.Read(lootObjectID);
inStream.Read(playerID);
entity->PickupItem(lootObjectID);
auto* team = TeamManager::Instance()->GetTeam(entity->GetObjectID());
if (team != nullptr) {
for (const auto memberId : team->members) {
auto* member = Game::entityManager->GetEntity(memberId);
if (member == nullptr || memberId == playerID) continue;
SendTeamPickupItem(lootObjectID, lootObjectID, playerID, member->GetSystemAddress());
}
}
}
void GameMessages::HandleResurrect(RakNet::BitStream& inStream, Entity* entity) {
bool immediate = inStream.ReadBit();
@@ -6270,11 +6321,6 @@ namespace GameMessages {
return Game::entityManager->SendMessage(*this);
}
bool GameMsg::Send(const LWOOBJID _target) {
target = _target;
return Send();
}
void GameMsg::Send(const SystemAddress& sysAddr) const {
CBITSTREAM;
CMSGHEADER;
@@ -6442,49 +6488,4 @@ namespace GameMessages {
stream.Write(emoteID);
stream.Write(targetID);
}
void DropClientLoot::Serialize(RakNet::BitStream& stream) const {
stream.Write(bUsePosition);
stream.Write(finalPosition != NiPoint3Constant::ZERO);
if (finalPosition != NiPoint3Constant::ZERO) stream.Write(finalPosition);
stream.Write(currency);
stream.Write(item);
stream.Write(lootID);
stream.Write(ownerID);
stream.Write(sourceID);
stream.Write(spawnPos != NiPoint3Constant::ZERO);
if (spawnPos != NiPoint3Constant::ZERO) stream.Write(spawnPos);
}
bool PickupItem::Deserialize(RakNet::BitStream& stream) {
if (!stream.Read(lootID)) return false;
if (!stream.Read(lootOwnerID)) return false;
return true;
}
void PickupItem::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* team = TeamManager::Instance()->GetTeam(entity.GetObjectID());
LOG("Has team %i picking up %llu:%llu", team != nullptr, lootID, lootOwnerID);
if (team) {
for (const auto memberId : team->members) {
this->Send(memberId);
TeamPickupItem teamPickupMsg{};
teamPickupMsg.target = lootID;
teamPickupMsg.lootID = lootID;
teamPickupMsg.lootOwnerID = lootOwnerID;
const auto* const memberEntity = Game::entityManager->GetEntity(memberId);
if (memberEntity) teamPickupMsg.Send(memberEntity->GetSystemAddress());
}
} else {
entity.PickupItem(lootID);
}
}
void TeamPickupItem::Serialize(RakNet::BitStream& stream) const {
stream.Write(lootID);
stream.Write(lootOwnerID);
}
}

View File

@@ -43,7 +43,6 @@ enum class eQuickBuildState : uint32_t;
enum class BehaviorSlot : int32_t;
enum class eVendorTransactionResult : uint32_t;
enum class eReponseMoveItemBetweenInventoryTypeCode : int32_t;
enum class eMissionState : int;
enum class eCameraTargetCyclingMode : int32_t {
ALLOW_CYCLE_TEAMMATES,
@@ -58,7 +57,6 @@ namespace GameMessages {
// Sends a message to the entity manager to route to the target
bool Send();
bool Send(const LWOOBJID _target);
// Sends the message to the specified client or
// all clients if UNASSIGNED_SYSTEM_ADDRESS is specified
@@ -852,9 +850,9 @@ namespace GameMessages {
struct EmotePlayed : public GameMsg {
EmotePlayed() : GameMsg(MessageType::Game::EMOTE_PLAYED), emoteID(0), targetID(0) {}
void Serialize(RakNet::BitStream& stream) const override;
int32_t emoteID;
LWOOBJID targetID;
};
@@ -872,65 +870,5 @@ namespace GameMessages {
bool bIgnoreChecks{ false };
};
struct DropClientLoot : public GameMsg {
DropClientLoot() : GameMsg(MessageType::Game::DROP_CLIENT_LOOT) {}
void Serialize(RakNet::BitStream& stream) const override;
LWOOBJID sourceID{ LWOOBJID_EMPTY };
LOT item{ LOT_NULL };
int32_t currency{};
NiPoint3 spawnPos{};
NiPoint3 finalPosition{};
int32_t count{};
bool bUsePosition{};
LWOOBJID lootID{ LWOOBJID_EMPTY };
LWOOBJID ownerID{ LWOOBJID_EMPTY };
};
struct GetMissionState : public GameMsg {
GetMissionState() : GameMsg(MessageType::Game::GET_MISSION_STATE) {}
int32_t missionID{};
eMissionState missionState{};
bool cooldownInfoRequested{};
bool cooldownFinished{};
};
struct GetFlag : public GameMsg {
GetFlag() : GameMsg(MessageType::Game::GET_FLAG) {}
uint32_t flagID{};
bool flag{};
};
struct GetFactionTokenType : public GameMsg {
GetFactionTokenType() : GameMsg(MessageType::Game::GET_FACTION_TOKEN_TYPE) {}
LOT tokenType{ LOT_NULL };
};
struct MissionNeedsLot : public GameMsg {
MissionNeedsLot() : GameMsg(MessageType::Game::MISSION_NEEDS_LOT) {}
LOT item{};
};
struct PickupItem : public GameMsg {
PickupItem() : GameMsg(MessageType::Game::PICKUP_ITEM) {}
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
bool Deserialize(RakNet::BitStream& stream) override;
LWOOBJID lootID{};
LWOOBJID lootOwnerID{};
};
struct TeamPickupItem : public GameMsg {
TeamPickupItem() : GameMsg(MessageType::Game::TEAM_PICKUP_ITEM) {}
void Serialize(RakNet::BitStream& stream) const override;
LWOOBJID lootID{};
LWOOBJID lootOwnerID{};
};
};
#endif // GAMEMESSAGES_H

View File

@@ -343,9 +343,9 @@ void Item::UseNonEquip(Item* item) {
if (this->GetPreconditionExpression()->Check(playerInventoryComponent->GetParent())) {
auto* entityParent = playerInventoryComponent->GetParent();
// Roll the loot for all the packages then see if it all fits. If it fits, give it to the player, otherwise don't.
Loot::Return rolledLoot{};
std::unordered_map<LOT, int32_t> rolledLoot{};
for (auto& pack : packages) {
const auto thisPackage = Loot::RollLootMatrix(entityParent, pack.LootMatrixIndex);
auto thisPackage = Loot::RollLootMatrix(entityParent, pack.LootMatrixIndex);
for (auto& loot : thisPackage) {
// If we already rolled this lot, add it to the existing one, otherwise create a new entry.
auto existingLoot = rolledLoot.find(loot.first);
@@ -356,7 +356,6 @@ void Item::UseNonEquip(Item* item) {
}
}
}
if (playerInventoryComponent->HasSpaceForLoot(rolledLoot)) {
Loot::GiveLoot(playerInventoryComponent->GetParent(), rolledLoot, eLootSourceType::CONSUMPTION);
item->SetCount(item->GetCount() - 1);

View File

@@ -18,27 +18,131 @@
#include "MissionComponent.h"
#include "eMissionState.h"
#include "eReplicaComponentType.h"
#include "TeamManager.h"
#include "CDObjectsTable.h"
#include "ObjectIDManager.h"
namespace {
std::unordered_set<uint32_t> CachedMatrices;
constexpr float g_MAX_DROP_RADIUS = 700.0f;
}
struct LootDropInfo {
CDLootTable table{};
uint32_t count{ 0 };
};
void Loot::CacheMatrix(uint32_t matrixIndex) {
if (CachedMatrices.contains(matrixIndex)) return;
std::map<LOT, LootDropInfo> RollLootMatrix(uint32_t matrixIndex) {
CachedMatrices.insert(matrixIndex);
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
std::map<LOT, LootDropInfo> drops;
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
for (const auto& entry : matrix) {
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
}
}
}
std::unordered_map<LOT, int32_t> Loot::RollLootMatrix(Entity* player, uint32_t matrixIndex) {
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
auto* missionComponent = player->GetComponent<MissionComponent>();
std::unordered_map<LOT, int32_t> drops;
if (missionComponent == nullptr) return drops;
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
for (const auto& entry : matrix) {
if (GeneralUtils::GenerateRandomNumber<float>(0, 1) < entry.percent) { // GetTable
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
uint32_t dropCount = GeneralUtils::GenerateRandomNumber<uint32_t>(entry.minToDrop, entry.maxToDrop);
for (uint32_t i = 0; i < dropCount; ++i) {
uint32_t maxRarity = 1;
float rarityRoll = GeneralUtils::GenerateRandomNumber<float>(0, 1);
for (const auto& rarity : rarityTable) {
if (rarity.randmax >= rarityRoll) {
maxRarity = rarity.rarity;
} else {
break;
}
}
bool rarityFound = false;
std::vector<CDLootTable> possibleDrops;
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
if (rarity == maxRarity) {
possibleDrops.push_back(loot);
rarityFound = true;
} else if (rarity < maxRarity && !rarityFound) {
possibleDrops.push_back(loot);
maxRarity = rarity;
}
}
if (possibleDrops.size() > 0) {
const auto& drop = possibleDrops[GeneralUtils::GenerateRandomNumber<uint32_t>(0, possibleDrops.size() - 1)];
// filter out uneeded mission items
if (drop.MissionDrop && !missionComponent->RequiresItem(drop.itemid))
continue;
LOT itemID = drop.itemid;
// convert faction token proxy
if (itemID == 13763) {
if (missionComponent->GetMissionState(545) == eMissionState::COMPLETE)
itemID = 8318; // "Assembly Token"
else if (missionComponent->GetMissionState(556) == eMissionState::COMPLETE)
itemID = 8321; // "Venture League Token"
else if (missionComponent->GetMissionState(567) == eMissionState::COMPLETE)
itemID = 8319; // "Sentinels Token"
else if (missionComponent->GetMissionState(578) == eMissionState::COMPLETE)
itemID = 8320; // "Paradox Token"
}
if (itemID == 13763) {
continue;
} // check if we aren't in faction
// drops[itemID]++; this should work?
if (drops.find(itemID) == drops.end()) {
drops.insert({ itemID, 1 });
} else {
++drops[itemID];
}
}
}
}
}
for (const auto& drop : drops) {
LOG("Player %llu has rolled %i of item %i from loot matrix %i", player->GetObjectID(), drop.second, drop.first, matrixIndex);
}
return drops;
}
std::unordered_map<LOT, int32_t> Loot::RollLootMatrix(uint32_t matrixIndex) {
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
std::unordered_map<LOT, int32_t> drops;
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
@@ -77,12 +181,14 @@ std::map<LOT, LootDropInfo> RollLootMatrix(uint32_t matrixIndex) {
}
}
if (!possibleDrops.empty()) {
if (possibleDrops.size() > 0) {
const auto& drop = possibleDrops[GeneralUtils::GenerateRandomNumber<uint32_t>(0, possibleDrops.size() - 1)];
auto& info = drops[drop.itemid];
if (info.count == 0) info.table = drop;
info.count++;
if (drops.find(drop.itemid) == drops.end()) {
drops.insert({ drop.itemid, 1 });
} else {
++drops[drop.itemid];
}
}
}
}
@@ -91,395 +197,15 @@ std::map<LOT, LootDropInfo> RollLootMatrix(uint32_t matrixIndex) {
return drops;
}
// Generates a 'random' final position for the loot drop based on its input spawn position.
void CalcFinalDropPos(GameMessages::DropClientLoot& lootMsg) {
lootMsg.bUsePosition = true;
//Calculate where the loot will go:
uint16_t degree = GeneralUtils::GenerateRandomNumber<uint16_t>(0, 360);
double rad = degree * 3.14 / 180;
double sin_v = sin(rad) * 4.2;
double cos_v = cos(rad) * 4.2;
const auto [x, y, z] = lootMsg.spawnPos;
lootMsg.finalPosition = NiPoint3(static_cast<float>(x + sin_v), y, static_cast<float>(z + cos_v));
}
// Visually drop the loot to all team members, though only the lootMsg.ownerID can pick it up
void DistrbuteMsgToTeam(const GameMessages::DropClientLoot& lootMsg, const Team& team) {
for (const auto memberClient : team.members) {
const auto* const memberEntity = Game::entityManager->GetEntity(memberClient);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
// The following 8 functions are all ever so slightly different such that combining them
// would make the logic harder to follow. Please read the comments!
// Given a faction token proxy LOT to drop, drop 1 token for each player on a team, or the provided player.
// token drops are always given to every player on the team.
void DropFactionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
GameMessages::GetFactionTokenType factionTokenType{};
factionTokenType.target = playerID;
// If we're not in a faction, this message will return false
if (factionTokenType.Send()) {
lootMsg.item = factionTokenType.tokenType;
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop on the player
lootMsg.Send();
// Visually drop it for the player
lootMsg.Send(player.GetSystemAddress());
}
}
// Drops 1 token for each player on a team
// token drops are always given to every player on the team.
void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::GetFactionTokenType factionTokenType{};
factionTokenType.target = member;
// If we're not in a faction, this message will return false
if (factionTokenType.Send()) {
lootMsg.item = factionTokenType.tokenType;
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop on this team member
lootMsg.Send();
// Show the rewards on all connected members of the team. Only the loot owner will be able to pick the tokens up.
DistrbuteMsgToTeam(lootMsg, team);
}
}
}
// Drop the power up with no owner
// Power ups can be picked up by anyone on a team, however unlike actual loot items,
// if multiple clients say they picked one up, we let them pick it up.
void DropPowerupLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
CalcFinalDropPos(lootMsg);
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.ownerID = playerID;
lootMsg.target = playerID;
// Register the drop on the player
lootMsg.Send();
// Visually drop it for the player
lootMsg.Send(player.GetSystemAddress());
}
// Drop the power up with no owner
// Power ups can be picked up by anyone on a team, however unlike actual loot items,
// if multiple clients say they picked one up, we let them pick it up.
void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.ownerID = LWOOBJID_EMPTY; // By setting ownerID to empty, any client that gets this DropClientLoot message can pick up the item.
CalcFinalDropPos(lootMsg);
// We want to drop the powerups as the same ID and the same position to all members of the team
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
lootMsg.target = member;
// By sending this message with the same ID to all players on the team, all players on the team are allowed to pick it up.
lootMsg.Send();
// No need to send to all members in a loop since that will happen by using the outer loop above and also since there is no owner
// sending to all will do nothing.
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
// Drops a mission item for a player
// If the player does not need this item, it will not be dropped.
void DropMissionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
GameMessages::MissionNeedsLot needMsg{};
needMsg.item = lootMsg.item;
const auto playerID = player.GetObjectID();
needMsg.target = playerID;
// Will return false if the item is not required
if (needMsg.Send()) {
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop with the player
lootMsg.Send();
// Visually drop the loot to be picked up
lootMsg.Send(player.GetSystemAddress());
}
}
// Check if the item needs to be dropped for anyone on the team
// Only players who need the item will have it dropped
void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
GameMessages::MissionNeedsLot needMsg{};
needMsg.item = lootMsg.item;
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
needMsg.target = member;
// Will return false if the item is not required
if (needMsg.Send()) {
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop with the player
lootMsg.Send();
DistrbuteMsgToTeam(lootMsg, team);
}
}
}
// Drop a regular piece of loot.
// Most items will go through this.
// A player will always get a drop that goes through this function
void DropRegularLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
CalcFinalDropPos(lootMsg);
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
// Register the drop with the player
lootMsg.Send();
// Visually drop the loot to be picked up
lootMsg.Send(player.GetSystemAddress());
}
// Drop a regular piece of loot.
// Most items will go through this.
// Finds the next loot owner on the team the is in range of the kill and gives them this reward.
void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) {
auto earningPlayer = LWOOBJID_EMPTY;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
GameMessages::GetPosition memberPosMsg{};
// Find the next loot owner. Eventually this will run into the `player` passed into this function, since those will
// have the same ID, this loop will only ever run at most 4 times.
do {
earningPlayer = team.GetNextLootOwner();
memberPosMsg.target = earningPlayer;
memberPosMsg.Send();
} while (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS);
if (team.lootOption == 0 /* Shared loot */) {
lootMsg.target = earningPlayer;
lootMsg.ownerID = earningPlayer;
lootMsg.Send();
} else /* Free for all loot */ {
lootMsg.ownerID = LWOOBJID_EMPTY;
// By sending the loot with NO owner and to ALL members of the team,
// its a first come, first serve with who picks the item up.
for (const auto ffaMember : team.members) {
lootMsg.target = ffaMember;
lootMsg.Send();
}
}
DistrbuteMsgToTeam(lootMsg, team);
}
void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDropInfo>& rolledItems, uint32_t minCoins, uint32_t maxCoins) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
const auto playerID = player->GetObjectID();
if (!player || !player->IsPlayer()) {
LOG("Trying to drop loot for non-player %llu:%i", playerID, player->GetLOT());
return;
}
// TODO should be scene based instead of radius based
// drop loot to either single player or team
// powerups never have an owner when dropped
// for every player on the team in a radius of 700 (arbitrary value, not lore)
// if shared loot, drop everything but tokens to the next team member that gets loot,
// then tokens to everyone (1 token drop in a 3 person team means everyone gets a token)
// if Free for all, drop everything with NO owner, except tokens which follow the same logic as above
auto* team = TeamManager::Instance()->GetTeam(playerID);
GameMessages::GetPosition posMsg;
posMsg.target = source;
posMsg.Send();
const auto spawnPosition = posMsg.pos;
auto* const objectsTable = CDClientManager::GetTable<CDObjectsTable>();
constexpr LOT TOKEN_PROXY = 13763;
// Go through the drops 1 at a time to drop them
for (auto it = rolledItems.begin(); it != rolledItems.end(); it++) {
auto& [lootLot, info] = *it;
for (int i = 0; i < info.count; i++) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = lootLot;
lootMsg.count = 1;
lootMsg.currency = 0;
const CDObjects& object = objectsTable->GetByID(lootLot);
if (lootLot == TOKEN_PROXY) {
team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(*player, lootMsg);
} else if (info.table.MissionDrop) {
team ? DropMissionLoot(*team, lootMsg) : DropMissionLoot(*player, lootMsg);
} else if (object.type == "Powerup") {
team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(*player, lootMsg);
} else {
team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(*player, lootMsg);
}
}
}
// Coin roll is divided up between the members, rounded up, then dropped for each player
const uint32_t coinRoll = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
const auto droppedCoins = team ? std::ceil(coinRoll / team->members.size()) : coinRoll;
if (team) {
for (auto member : team->members) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.currency = droppedCoins;
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
} else {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.currency = droppedCoins;
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
lootMsg.Send(player->GetSystemAddress());
}
}
void Loot::DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam, bool forceFfa) {
auto* const team = useTeam ? TeamManager::Instance()->GetTeam(player.GetObjectID()) : nullptr;
char oldTeamLoot{};
if (team && forceFfa) {
oldTeamLoot = team->lootOption;
team->lootOption = 1;
}
auto* const objectsTable = CDClientManager::GetTable<CDObjectsTable>();
const CDObjects& object = objectsTable->GetByID(lootMsg.item);
constexpr LOT TOKEN_PROXY = 13763;
if (lootMsg.item == TOKEN_PROXY) {
team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(player, lootMsg);
} else if (object.type == "Powerup") {
team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(player, lootMsg);
} else {
team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(player, lootMsg);
}
if (team) team->lootOption = oldTeamLoot;
}
void Loot::CacheMatrix(uint32_t matrixIndex) {
if (CachedMatrices.contains(matrixIndex)) return;
CachedMatrices.insert(matrixIndex);
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
for (const auto& entry : matrix) {
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
}
}
}
Loot::Return Loot::RollLootMatrix(Entity* player, uint32_t matrixIndex) {
auto* const missionComponent = player ? player->GetComponent<MissionComponent>() : nullptr;
Loot::Return toReturn;
const auto drops = ::RollLootMatrix(matrixIndex);
// if no mission component, just convert the map and skip checking if its a mission drop
if (!missionComponent) {
for (const auto& [lot, info] : drops) toReturn[lot] = info.count;
} else {
for (const auto& [lot, info] : drops) {
const auto& itemInfo = info.table;
// filter out uneeded mission items
if (itemInfo.MissionDrop && !missionComponent->RequiresItem(itemInfo.itemid))
continue;
LOT itemLot = lot;
// convert faction token proxy
if (itemLot == 13763) {
if (missionComponent->GetMissionState(545) == eMissionState::COMPLETE)
itemLot = 8318; // "Assembly Token"
else if (missionComponent->GetMissionState(556) == eMissionState::COMPLETE)
itemLot = 8321; // "Venture League Token"
else if (missionComponent->GetMissionState(567) == eMissionState::COMPLETE)
itemLot = 8319; // "Sentinels Token"
else if (missionComponent->GetMissionState(578) == eMissionState::COMPLETE)
itemLot = 8320; // "Paradox Token"
}
if (itemLot == 13763) {
continue;
} // check if we aren't in faction
toReturn[itemLot] = info.count;
}
}
if (player) {
for (const auto& [lot, count] : toReturn) {
LOG("Player %llu has rolled %i of item %i from loot matrix %i", player->GetObjectID(), count, lot, matrixIndex);
}
}
return toReturn;
}
void Loot::GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType) {
player = player->GetOwner(); // If the owner is overwritten, we collect that here
const auto result = RollLootMatrix(player, matrixIndex);
std::unordered_map<LOT, int32_t> result = RollLootMatrix(player, matrixIndex);
GiveLoot(player, result, lootSourceType);
}
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType) {
void Loot::GiveLoot(Entity* player, std::unordered_map<LOT, int32_t>& result, eLootSourceType lootSourceType) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
@@ -534,9 +260,34 @@ void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex,
if (!inventoryComponent)
return;
const auto result = ::RollLootMatrix(matrixIndex);
std::unordered_map<LOT, int32_t> result = RollLootMatrix(player, matrixIndex);
::DropLoot(player, source, result, minCoins, maxCoins);
DropLoot(player, source, result, minCoins, maxCoins);
}
void Loot::DropLoot(Entity* player, const LWOOBJID source, std::unordered_map<LOT, int32_t>& result, uint32_t minCoins, uint32_t maxCoins) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
if (!inventoryComponent)
return;
GameMessages::GetPosition posMsg;
posMsg.target = source;
posMsg.Send();
const auto spawnPosition = posMsg.pos;
for (const auto& pair : result) {
for (int i = 0; i < pair.second; ++i) {
GameMessages::SendDropClientLoot(player, source, pair.first, 0, spawnPosition, 1);
}
}
uint32_t coins = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
GameMessages::SendDropClientLoot(player, source, LOT_NULL, coins, spawnPosition);
}
void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) {

View File

@@ -6,25 +6,20 @@
class Entity;
namespace GameMessages {
struct DropClientLoot;
};
namespace Loot {
struct Info {
LWOOBJID id = 0;
LOT lot = 0;
int32_t count = 0;
uint32_t count = 0;
};
using Return = std::map<LOT, int32_t>;
Loot::Return RollLootMatrix(Entity* player, uint32_t matrixIndex);
std::unordered_map<LOT, int32_t> RollLootMatrix(Entity* player, uint32_t matrixIndex);
std::unordered_map<LOT, int32_t> RollLootMatrix(uint32_t matrixIndex);
void CacheMatrix(const uint32_t matrixIndex);
void GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType = eLootSourceType::NONE);
void GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType = eLootSourceType::NONE);
void GiveLoot(Entity* player, std::unordered_map<LOT, int32_t>& result, eLootSourceType lootSourceType = eLootSourceType::NONE);
void GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0);
void DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins);
void DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam = false, bool forceFfa = false);
void DropLoot(Entity* player, const LWOOBJID source, std::unordered_map<LOT, int32_t>& result, uint32_t minCoins, uint32_t maxCoins);
void DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0);
};

View File

@@ -52,7 +52,7 @@
#include "eInventoryType.h"
#include "ePlayerFlag.h"
#include "StringifiedEnum.h"
#include "BinaryPathFinder.h"
namespace DEVGMCommands {
void SetGMLevel(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -368,20 +368,6 @@ namespace DEVGMCommands {
}
}
void HandleMacro(Entity& entity, const SystemAddress& sysAddr, std::istream& inStream) {
if (inStream.good()) {
std::string line;
while (std::getline(inStream, line)) {
// Do this in two separate calls to catch both \n and \r\n
line.erase(std::remove(line.begin(), line.end(), '\n'), line.end());
line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16(line), &entity, sysAddr);
}
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?");
}
}
void RunMacro(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
@@ -390,16 +376,24 @@ namespace DEVGMCommands {
if (splitArgs[0].find("/") != std::string::npos) return;
if (splitArgs[0].find("\\") != std::string::npos) return;
const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer";
auto infile = Game::assetManager->GetFile("macros/" + splitArgs[0] + ".scm");
auto resServerInFile = std::ifstream(resServerPath / "macros" / (splitArgs[0] + ".scm"));
if (!infile.good() && !resServerInFile.good()) {
auto infile = Game::assetManager->GetFile(("macros/" + splitArgs[0] + ".scm").c_str());
if (!infile) {
ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?");
return;
}
HandleMacro(*entity, sysAddr, infile);
HandleMacro(*entity, sysAddr, resServerInFile);
if (infile.good()) {
std::string line;
while (std::getline(infile, line)) {
// Do this in two separate calls to catch both \n and \r\n
line.erase(std::remove(line.begin(), line.end(), '\n'), line.end());
line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16(line), entity, sysAddr);
}
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?");
}
}
void AddMission(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -1314,7 +1308,7 @@ namespace DEVGMCommands {
for (uint32_t i = 0; i < loops; i++) {
while (true) {
const auto lootRoll = Loot::RollLootMatrix(nullptr, lootMatrixIndex.value());
auto lootRoll = Loot::RollLootMatrix(lootMatrixIndex.value());
totalRuns += 1;
bool doBreak = false;
for (const auto& kv : lootRoll) {
@@ -1479,20 +1473,15 @@ namespace DEVGMCommands {
void Inspect(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
const auto idParsed = GeneralUtils::TryParse<LWOOBJID>(splitArgs[0]);
// First try to get the object by its ID if provided.
// Second try to get the object by player name.
// Lastly assume we were passed a component or LDF and try to find the closest entity with that component or LDF.
Entity* closest = nullptr;
if (idParsed) closest = Game::entityManager->GetEntity(idParsed.value());
float closestDistance = 0.0f;
std::u16string ldf;
bool isLDF = false;
if (!closest) closest = PlayerManager::GetPlayer(splitArgs[0]);
closest = PlayerManager::GetPlayer(splitArgs[0]);
if (!closest) {
auto component = GeneralUtils::TryParse<eReplicaComponentType>(splitArgs[0]);
if (!component) {

View File

@@ -15,6 +15,11 @@ void ScriptedPowerupSpawner::OnTimerDone(Entity* self, std::string message) {
const auto itemLOT = self->GetVar<LOT>(u"lootLOT");
// Build drop table
std::unordered_map<LOT, int32_t> drops;
drops.emplace(itemLOT, 1);
// Spawn the required number of powerups
auto* owner = Game::entityManager->GetEntity(self->GetSpawnerID());
if (owner != nullptr) {
@@ -23,19 +28,8 @@ void ScriptedPowerupSpawner::OnTimerDone(Entity* self, std::string message) {
if (renderComponent != nullptr) {
renderComponent->PlayEffect(0, u"cast", "N_cast");
}
GameMessages::GetPosition posMsg{};
posMsg.target = self->GetObjectID();
posMsg.Send();
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = owner->GetObjectID();
lootMsg.ownerID = owner->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.spawnPos = posMsg.pos;
lootMsg.item = itemLOT;
lootMsg.count = 1;
lootMsg.currency = 0;
Loot::DropItem(*owner, lootMsg, true, true);
Loot::DropLoot(owner, self->GetObjectID(), drops, 0, 0);
}
// Increment the current cycle

View File

@@ -10,21 +10,8 @@ void AgPicnicBlanket::OnUse(Entity* self, Entity* user) {
return;
self->SetVar<bool>(u"active", true);
GameMessages::GetPosition posMsg{};
posMsg.target = self->GetObjectID();
posMsg.Send();
for (int32_t i = 0; i < 3; i++) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = user->GetObjectID();
lootMsg.ownerID = user->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.item = 935;
lootMsg.count = 1;
lootMsg.spawnPos = posMsg.pos;
lootMsg.currency = 0;
Loot::DropItem(*user, lootMsg, true);
}
auto lootTable = std::unordered_map<LOT, int32_t>{ {935, 3} };
Loot::DropLoot(user, self->GetObjectID(), lootTable, 0, 0);
self->AddCallbackTimer(5.0f, [self]() {
self->SetVar<bool>(u"active", false);

View File

@@ -415,7 +415,9 @@ void SGCannon::SpawnNewModel(Entity* self) {
}
if (lootMatrix != 0) {
const auto toDrop = Loot::RollLootMatrix(player, lootMatrix);
std::unordered_map<LOT, int32_t> toDrop = {};
toDrop = Loot::RollLootMatrix(player, lootMatrix);
for (const auto [lot, count] : toDrop) {
GameMessages::SetModelToBuild modelToBuild{};
modelToBuild.modelLot = lot;

View File

@@ -1166,36 +1166,32 @@ void HandlePacket(Packet* packet) {
LOG("Couldn't find property ID for zone %i, clone %i", zoneId, cloneId);
goto noBBB;
}
// 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) {
for (auto& bbbModel : Database::Get()->GetUgcModels(propertyId)) {
LOG("Getting lxfml ugcID: %llu", bbbModel.id);
bbbModel.lxfmlData.seekg(0, std::ios::end);
size_t lxfmlSize = bbbModel.lxfmlData.tellg();
bbbModel.lxfmlData.seekg(0);
// write data
//Send message:
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

@@ -47,7 +47,7 @@ client_net_version=171022
# Turn to 0 to default teams to use the live accurate Shared Loot (0) by default as opposed to Free for All (1)
# This is used in both Chat and World servers.
default_team_loot=0
default_team_loot=1
# event gating for login response and luz gating
event_1=Talk_Like_A_Pirate

View File

@@ -102,6 +102,3 @@ hardcore_disabled_worlds=
# Keeps this percentage of a players' coins on death in hardcore
hardcore_coin_keep=
# save pre-split lxfmls to disk for debugging
save_lxfmls=0

View File

@@ -10,7 +10,6 @@ set(DCOMMONTEST_SOURCES
"TestLUString.cpp"
"TestLUWString.cpp"
"dCommonDependencies.cpp"
"LxfmlTests.cpp"
)
add_subdirectory(dEnumsTests)
@@ -33,8 +32,6 @@ 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

@@ -1,20 +0,0 @@
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"
"group_issue.lxfml"
"complex_grouping.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

@@ -1,132 +0,0 @@
<?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="3001">
<Part refID="0" designID="3001" materials="23">
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-56.400001525878906">
</Bone>
</Part>
</Brick>
<Brick refID="1" designID="3001">
<Part refID="1" designID="3001" materials="23">
<Bone refID="1" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,433.91998291015625,-56.400001525878906">
</Bone>
</Part>
</Brick>
<Brick refID="2" designID="3001">
<Part refID="2" designID="3001" materials="23">
<Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,435.8399658203125,-56.399993896484375">
</Bone>
</Part>
</Brick>
<Brick refID="3" designID="3001">
<Part refID="3" designID="3001" materials="23">
<Bone refID="3" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-56.399993896484375">
</Bone>
</Part>
</Brick>
<Brick refID="4" designID="3001">
<Part refID="4" designID="3001" materials="23">
<Bone refID="4" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-51.600002288818359">
</Bone>
</Part>
</Brick>
<Brick refID="5" designID="3001">
<Part refID="5" designID="3001" materials="23">
<Bone refID="5" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,433.91998291015625,-51.600002288818359">
</Bone>
</Part>
</Brick>
<Brick refID="6" designID="3001">
<Part refID="6" designID="3001" materials="23">
<Bone refID="6" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-51.600002288818359">
</Bone>
</Part>
</Brick>
<Brick refID="7" designID="3001">
<Part refID="7" designID="3001" materials="23">
<Bone refID="7" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,435.8399658203125,-51.600002288818359">
</Bone>
</Part>
</Brick>
<Brick refID="8" designID="3001">
<Part refID="8" designID="3001" materials="23">
<Bone refID="8" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-50.000003814697266">
</Bone>
</Part>
</Brick>
<Brick refID="9" designID="3001">
<Part refID="9" designID="3001" materials="23">
<Bone refID="9" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,433.91998291015625,-50.000003814697266">
</Bone>
</Part>
</Brick>
<Brick refID="10" designID="3001">
<Part refID="10" designID="3001" materials="23">
<Bone refID="10" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-50.000003814697266">
</Bone>
</Part>
</Brick>
<Brick refID="11" designID="3001">
<Part refID="11" designID="3001" materials="23">
<Bone refID="11" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,435.8399658203125,-50.000003814697266">
</Bone>
</Part>
</Brick>
<Brick refID="12" designID="3001">
<Part refID="12" designID="3001" materials="23">
<Bone refID="12" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-54.800003051757813">
</Bone>
</Part>
</Brick>
<Brick refID="13" designID="3001">
<Part refID="13" designID="3001" materials="23">
<Bone refID="13" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,433.91998291015625,-54.800003051757813">
</Bone>
</Part>
</Brick>
<Brick refID="14" designID="3001">
<Part refID="14" designID="3001" materials="23">
<Bone refID="14" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,435.8399658203125,-54.800003051757813">
</Bone>
</Part>
</Brick>
<Brick refID="15" designID="3001">
<Part refID="15" designID="3001" materials="23">
<Bone refID="15" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-54.800003051757813">
</Bone>
</Part>
</Brick>
</Bricks>
<RigidSystems>
<RigidSystem>
<Rigid refID="0" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-56.400001525878906" boneRefs="0,1,2,3"/>
</RigidSystem>
<RigidSystem>
<Rigid refID="1" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-51.600002288818359" boneRefs="4,5,6,7"/>
</RigidSystem>
<RigidSystem>
<Rigid refID="2" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,432.95999145507812,-50.000003814697266" boneRefs="8,9,10,11"/>
</RigidSystem>
<RigidSystem>
<Rigid refID="3" transformation="1,0,0,0,1,0,0,0,1,-35.599998474121094,434.87997436523437,-54.800003051757813" boneRefs="12,13,14,15"/>
</RigidSystem>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="5,9"/>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="3,15">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="4,8"/>
</Group>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="6,10"/>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="14,2">
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="7,11"/>
</Group>
</GroupSystem>
</GroupSystems>
</LXFML>

View File

@@ -1,50 +0,0 @@
<?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

@@ -1,11 +0,0 @@
<?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

@@ -1,48 +0,0 @@
<?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="3001">
<Part refID="0" designID="3001" materials="23">
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,-8.3999996185302734,433.91998291015625,-62.800003051757813">
</Bone>
</Part>
</Brick>
<Brick refID="1" designID="3001">
<Part refID="1" designID="3001" materials="23">
<Bone refID="1" transformation="1,0,0,0,1,0,0,0,1,-8.4000005722045898,432.95999145507812,-62.800003051757813">
</Bone>
</Part>
</Brick>
<Brick refID="2" designID="3001">
<Part refID="2" designID="3001" materials="23">
<Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,-8.3999996185302734,433.91998291015625,-64.400001525878906">
</Bone>
</Part>
</Brick>
<Brick refID="3" designID="3001">
<Part refID="3" designID="3001" materials="23">
<Bone refID="3" transformation="1,0,0,0,1,0,0,0,1,-8.4000005722045898,432.95999145507812,-64.400001525878906">
</Bone>
</Part>
</Brick>
</Bricks>
<RigidSystems>
<RigidSystem>
<Rigid refID="0" transformation="1,0,0,0,1,0,0,0,1,-8.3999996185302734,433.91998291015625,-62.800003051757813" boneRefs="0,1"/>
</RigidSystem>
<RigidSystem>
<Rigid refID="1" transformation="1,0,0,0,1,0,0,0,1,-8.3999996185302734,433.91998291015625,-64.400001525878906" boneRefs="2,3"/>
</RigidSystem>
</RigidSystems>
<GroupSystems>
<GroupSystem>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="3,1"/>
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="0,2"/>
</GroupSystem>
</GroupSystems>
</LXFML>

View File

@@ -1,20 +0,0 @@
<?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

@@ -1,11 +0,0 @@
<?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

@@ -1,44 +0,0 @@
<?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

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

View File

@@ -1,11 +0,0 @@
<?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

@@ -1,336 +0,0 @@
<?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

@@ -1,11 +0,0 @@
<?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

@@ -1,413 +0,0 @@
#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());
};
// Helper function to test splitting functionality
static void TestSplitUsesAllBricksAndNoDuplicatesHelper(const std::string& filename) {
// Read the LXFML file
std::string data = ReadFile(filename);
ASSERT_FALSE(data.empty()) << "Failed to read " << filename << " from build directory";
std::cout << "\n=== Testing LXFML splitting for: " << filename << " ===" << std::endl;
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0) << "Split results should not be empty for " << filename;
std::cout << "Split produced " << results.size() << " output(s)" << std::endl;
// parse original to count bricks
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS) << "Failed to parse " << filename;
DocumentReader reader(doc);
auto lxfml = reader["LXFML"];
ASSERT_TRUE(lxfml) << "No LXFML element found in " << filename;
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;
std::cout << "Original file contains " << originalBricks.size() << " bricks: ";
for (const auto& brick : originalBricks) {
std::cout << brick << " ";
}
std::cout << std::endl;
int splitIndex = 0;
std::filesystem::path baseFilename = std::filesystem::path(filename).stem();
for (const auto& res : results) {
splitIndex++;
std::cout << "\n--- Split " << splitIndex << " ---" << std::endl;
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);
}
}
}
// Collect and display bricks in this split
std::vector<std::string> splitBricks;
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);
splitBricks.push_back(ref);
}
}
std::cout << "Contains " << splitBricks.size() << " bricks: ";
for (const auto& brick : splitBricks) {
std::cout << brick << " ";
}
std::cout << std::endl;
// Count rigid systems and groups
int rigidCount = 0;
if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) {
for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidCount++;
}
}
int groupCount = 0;
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")) {
groupCount++;
}
}
}
std::cout << "Contains " << rigidCount << " rigid systems and " << groupCount << " groups" << std::endl;
}
// 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 << " in " << filename;
}
// 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 << " in " << filename;
}
// Ensure all original rigid systems and groups were used exactly once
ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch in " << filename;
for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits in " << filename;
ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch in " << filename;
for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits in " << filename;
}
TEST(LxfmlTests, SplitGroupIssueFile) {
// Specific test for the group issue file
TestSplitUsesAllBricksAndNoDuplicatesHelper("group_issue.lxfml");
}
TEST(LxfmlTests, SplitTestFile) {
// Specific test for the larger test file
TestSplitUsesAllBricksAndNoDuplicatesHelper("test.lxfml");
}
TEST(LxfmlTests, SplitComplexGroupingFile) {
// Test for the complex grouping file - should produce only one split
// because all groups are connected via rigid systems
std::string data = ReadFile("complex_grouping.lxfml");
ASSERT_FALSE(data.empty()) << "Failed to read complex_grouping.lxfml from build directory";
std::cout << "\n=== Testing complex grouping file ===" << std::endl;
auto results = Lxfml::Split(data);
ASSERT_GT(results.size(), 0) << "Split results should not be empty";
// The complex grouping file should produce exactly ONE split
// because all groups share bricks through rigid systems
if (results.size() != 1) {
FAIL() << "Complex grouping file produced " << results.size()
<< " splits instead of 1 (all groups should be merged)";
}
std::cout << "✓ Correctly produced 1 merged split" << std::endl;
// Verify the split contains all the expected elements
tinyxml2::XMLDocument doc;
ASSERT_EQ(doc.Parse(results[0].lxfml.c_str()), tinyxml2::XML_SUCCESS);
auto* lxfml = doc.FirstChildElement("LXFML");
ASSERT_NE(lxfml, nullptr);
// Count bricks
int brickCount = 0;
if (auto* bricks = lxfml->FirstChildElement("Bricks")) {
for (auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
brickCount++;
}
}
std::cout << "Contains " << brickCount << " bricks" << std::endl;
// Count rigid systems
int rigidCount = 0;
if (auto* rigidSystems = lxfml->FirstChildElement("RigidSystems")) {
for (auto* rs = rigidSystems->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidCount++;
}
}
std::cout << "Contains " << rigidCount << " rigid systems" << std::endl;
EXPECT_GT(rigidCount, 0) << "Should contain rigid systems";
// Count groups
int groupCount = 0;
if (auto* groupSystems = lxfml->FirstChildElement("GroupSystems")) {
for (auto* gs = groupSystems->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) {
groupCount++;
}
}
}
std::cout << "Contains " << groupCount << " groups" << std::endl;
EXPECT_GT(groupCount, 1) << "Should contain multiple groups (all merged into one split)";
}
// 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";
}
}
}
}