mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-12-20 11:59:40 -06:00
Compare commits
18 Commits
object-deb
...
loot-rewor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34236b900 | ||
|
|
bb654752ca | ||
| c2dba31f70 | |||
|
|
74630b56c8 | ||
|
|
8384126783 | ||
|
|
409053e7af | ||
|
|
28c6bfcbd7 | ||
|
|
fd6029ae10 | ||
|
|
ff645a6662 | ||
|
|
e051229fb6 | ||
| ce28834dce | |||
|
|
cbdd5d9bc6 | ||
|
|
62ac65c520 | ||
|
|
5d5bce53d0 | ||
|
|
5791c55a9e | ||
|
|
17d0c45382 | ||
|
|
7dbbef81ac | ||
|
|
06958cb9cd |
29
.github/copilot-instructions.md
vendored
Normal file
29
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
@@ -3,7 +3,7 @@
|
||||
// C++
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "dPlatforms.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "DluAssert.h"
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
@@ -305,7 +306,7 @@ namespace GeneralUtils {
|
||||
template<typename Container>
|
||||
inline Container::value_type GetRandomElement(const Container& container) {
|
||||
DluAssert(!container.empty());
|
||||
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
|
||||
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,13 +5,43 @@
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
// The base LXFML xml file to use when creating new models.
|
||||
std::string g_base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
</Bricks>
|
||||
<RigidSystems>
|
||||
</RigidSystems>
|
||||
<GroupSystems>
|
||||
<GroupSystem>
|
||||
</GroupSystem>
|
||||
</GroupSystems>
|
||||
</LXFML>)";
|
||||
}
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
|
||||
Result toReturn;
|
||||
|
||||
// Handle empty or invalid input
|
||||
if (data.empty()) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto err = doc.Parse(data.data());
|
||||
// Use length-based parsing to avoid expensive string copy
|
||||
const auto err = doc.Parse(data.data(), data.size());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -20,7 +50,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
|
||||
auto lxfml = reader["LXFML"];
|
||||
if (!lxfml) {
|
||||
LOG("Failed to find LXFML element.");
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -49,16 +78,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
// Calculate the lowest and highest points on the entire model
|
||||
for (const auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (split.size() < 12) continue;
|
||||
|
||||
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
|
||||
|
||||
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue;
|
||||
|
||||
auto x = xOpt.value();
|
||||
auto y = yOpt.value();
|
||||
auto z = zOpt.value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
|
||||
if (highest.x < x) highest.x = x;
|
||||
@@ -87,13 +119,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
for (auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
|
||||
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
|
||||
|
||||
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) {
|
||||
continue;
|
||||
}
|
||||
auto x = xOpt.value() - newRootPos.x + curPosition.x;
|
||||
auto y = yOpt.value() - newRootPos.y + curPosition.y;
|
||||
auto z = zOpt.value() - newRootPos.z + curPosition.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
@@ -128,3 +166,345 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
@@ -18,6 +19,7 @@ namespace Lxfml {
|
||||
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
|
||||
// Returns a struct of its new center and the updated LXFML containing these edits.
|
||||
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
|
||||
// these are only for the migrations due to a bug in one of the implementations.
|
||||
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);
|
||||
|
||||
@@ -81,6 +81,9 @@ public:
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const char* name) const;
|
||||
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
|
||||
|
||||
private:
|
||||
void LoadPackIndex();
|
||||
|
||||
|
||||
@@ -50,7 +50,10 @@ enum class eMissionState : int {
|
||||
/**
|
||||
* The mission has been completed before and has now been completed again. Used for daily missions.
|
||||
*/
|
||||
COMPLETE_READY_TO_COMPLETE = 12
|
||||
COMPLETE_READY_TO_COMPLETE = 12,
|
||||
|
||||
// The mission is failed (don't know where this is used)
|
||||
FAILED = 16,
|
||||
};
|
||||
|
||||
#endif //!__MISSIONSTATE__H__
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
#include "GhostComponent.h"
|
||||
#include "AchievementVendorComponent.h"
|
||||
#include "VanityUtilities.h"
|
||||
#include "ObjectIDManager.h"
|
||||
#include "ePlayerFlag.h"
|
||||
|
||||
// Table includes
|
||||
#include "CDComponentsRegistryTable.h"
|
||||
@@ -192,7 +194,10 @@ Entity::~Entity() {
|
||||
}
|
||||
|
||||
void Entity::Initialize() {
|
||||
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &Entity::MsgRequestServerObjectInfo);
|
||||
RegisterMsg<GameMessages::RequestServerObjectInfo>(this, &Entity::MsgRequestServerObjectInfo);
|
||||
RegisterMsg<GameMessages::DropClientLoot>(this, &Entity::MsgDropClientLoot);
|
||||
RegisterMsg<GameMessages::GetFactionTokenType>(this, &Entity::MsgGetFactionTokenType);
|
||||
RegisterMsg<GameMessages::PickupItem>(this, &Entity::MsgPickupItem);
|
||||
/**
|
||||
* Setup trigger
|
||||
*/
|
||||
@@ -287,7 +292,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);
|
||||
}
|
||||
@@ -1663,7 +1668,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;
|
||||
}
|
||||
@@ -2247,6 +2252,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
|
||||
response.Insert("objectID", std::to_string(m_ObjectID));
|
||||
response.Insert("serverInfo", true);
|
||||
GameMessages::GetObjectReportInfo info{};
|
||||
info.bVerbose = requestInfo.bVerbose;
|
||||
info.info = response.InsertArray("data");
|
||||
auto& objectInfo = info.info->PushDebug("Object Details");
|
||||
auto* table = CDClientManager::GetTable<CDObjectsTable>();
|
||||
@@ -2260,17 +2266,87 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
|
||||
|
||||
auto& componentDetails = objectInfo.PushDebug("Component Information");
|
||||
for (const auto [id, component] : m_Components) {
|
||||
componentDetails.PushDebug<AMFStringValue>(StringifiedEnum::ToString(id)) = "";
|
||||
componentDetails.PushDebug(StringifiedEnum::ToString(id));
|
||||
}
|
||||
|
||||
auto& configData = objectInfo.PushDebug("Config Data");
|
||||
for (const auto config : m_Settings) {
|
||||
configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString();
|
||||
|
||||
}
|
||||
|
||||
HandleMsg(info);
|
||||
|
||||
auto* client = Game::entityManager->GetEntity(requestInfo.clientId);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ 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;
|
||||
@@ -342,6 +346,12 @@ 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.
|
||||
*/
|
||||
@@ -600,5 +610,5 @@ auto Entity::GetComponents() const {
|
||||
|
||||
template<typename... T>
|
||||
auto Entity::GetComponentsMut() const {
|
||||
return std::tuple{GetComponent<T>()...};
|
||||
return std::tuple{ GetComponent<T>()... };
|
||||
}
|
||||
|
||||
@@ -87,6 +87,8 @@ void EntityManager::ReloadConfig() {
|
||||
auto hcXpReduction = Game::config->GetValue("hardcore_uscore_reduction");
|
||||
m_HardcoreUscoreReduction = hcXpReduction.empty() ? 1.0f : GeneralUtils::TryParse<float>(hcXpReduction).value_or(1.0f);
|
||||
m_HardcoreMode = GetHardcoreDisabledWorlds().contains(Game::zoneManager->GetZoneID().GetMapID()) ? false : m_HardcoreMode;
|
||||
auto hcCoinKeep = Game::config->GetValue("hardcore_coin_keep");
|
||||
m_HardcoreCoinKeep = hcCoinKeep.empty() ? false : GeneralUtils::TryParse<float>(hcCoinKeep).value_or(0.0f);
|
||||
}
|
||||
|
||||
void EntityManager::Initialize() {
|
||||
|
||||
@@ -81,6 +81,7 @@ public:
|
||||
const std::set<LOT>& GetHardcoreUscoreReducedLots() const { return m_HardcoreUscoreReducedLots; };
|
||||
const std::set<LOT>& GetHardcoreUscoreExcludedEnemies() const { return m_HardcoreUscoreExcludedEnemies; };
|
||||
const std::set<LWOMAPID>& GetHardcoreDisabledWorlds() const { return m_HardcoreDisabledWorlds; };
|
||||
float GetHardcoreCoinKeep() const { return m_HardcoreCoinKeep; }
|
||||
|
||||
// Messaging
|
||||
bool SendMessage(GameMessages::GameMsg& msg) const;
|
||||
@@ -125,6 +126,7 @@ private:
|
||||
std::set<LOT> m_HardcoreUscoreReducedLots{};
|
||||
std::set<LOT> m_HardcoreUscoreExcludedEnemies{};
|
||||
std::set<LWOMAPID> m_HardcoreDisabledWorlds{};
|
||||
float m_HardcoreCoinKeep{};
|
||||
};
|
||||
|
||||
#endif // ENTITYMANAGER_H
|
||||
|
||||
@@ -9,6 +9,16 @@ 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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
struct Team {
|
||||
Team();
|
||||
|
||||
LWOOBJID GetNextLootOwner();
|
||||
LWOOBJID teamID = LWOOBJID_EMPTY;
|
||||
char lootOption = 0;
|
||||
std::vector<LWOOBJID> members{};
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "BitStreamUtils.h"
|
||||
#include "CheatDetection.h"
|
||||
#include "CharacterComponent.h"
|
||||
#include "dConfig.h"
|
||||
#include "eCharacterVersion.h"
|
||||
|
||||
UserManager* UserManager::m_Address = nullptr;
|
||||
@@ -92,6 +93,23 @@ void UserManager::Initialize() {
|
||||
StripCR(line);
|
||||
m_PreapprovedNames.push_back(line);
|
||||
}
|
||||
|
||||
// Initialize cached config values and register a handler to update them on config reload
|
||||
// This avoids repeated lookups into dConfig at runtime.
|
||||
if (Game::config) {
|
||||
m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
||||
m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
||||
m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
||||
|
||||
Game::config->AddConfigHandler([this]() {
|
||||
this->m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
||||
this->m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
||||
this->m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
||||
});
|
||||
}
|
||||
else {
|
||||
LOG("Warning: dConfig not initialized before UserManager. Cached config values will not be available.");
|
||||
}
|
||||
}
|
||||
|
||||
UserManager::~UserManager() {
|
||||
@@ -301,7 +319,9 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
inStream.Read(eyes);
|
||||
inStream.Read(mouth);
|
||||
|
||||
const auto name = LUWStringName.GetAsString();
|
||||
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
|
||||
|
||||
const auto name = autoRejectNames ? "" : LUWStringName.GetAsString();
|
||||
std::string predefinedName = GetPredefinedName(firstNameIndex, middleNameIndex, lastNameIndex);
|
||||
|
||||
LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle);
|
||||
@@ -319,6 +339,10 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRejectNames) {
|
||||
LOG("AccountID: %i is muted, forcing use of predefined name", u->GetAccountID());
|
||||
}
|
||||
|
||||
if (name.empty()) {
|
||||
LOG("AccountID: %i is creating a character with predefined name: %s", u->GetAccountID(), predefinedName.c_str());
|
||||
} else {
|
||||
@@ -369,6 +393,7 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
|
||||
//Check to see if our name was pre-approved:
|
||||
bool nameOk = IsNamePreapproved(name);
|
||||
|
||||
if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true;
|
||||
|
||||
// If predefined name is invalid, change it to be their object id
|
||||
@@ -448,9 +473,10 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
|
||||
LUWString LUWStringName;
|
||||
inStream.Read(LUWStringName);
|
||||
const auto newName = LUWStringName.GetAsString();
|
||||
auto newName = LUWStringName.GetAsString();
|
||||
|
||||
Character* character = nullptr;
|
||||
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
|
||||
|
||||
//Check if this user has this character:
|
||||
bool ownsCharacter = CheatDetection::VerifyLwoobjidIsSender(
|
||||
@@ -471,13 +497,30 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
if (!ownsCharacter || !character) {
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
|
||||
} else if (ownsCharacter && character) {
|
||||
if (autoRejectNames) {
|
||||
// Create a random preapproved name (fallback to default if none available)
|
||||
if (!m_FirstNames.empty() && !m_MiddleNames.empty() && !m_LastNames.empty()) {
|
||||
std::string firstName = GeneralUtils::GetRandomElement(m_FirstNames);
|
||||
std::string middleName = GeneralUtils::GetRandomElement(m_MiddleNames);
|
||||
std::string lastName = GeneralUtils::GetRandomElement(m_LastNames);
|
||||
newName = firstName + middleName + lastName;
|
||||
} else {
|
||||
newName = "character" + std::to_string(objectID);
|
||||
}
|
||||
}
|
||||
|
||||
if (newName == character->GetName()) {
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Database::Get()->GetCharacterInfo(newName)) {
|
||||
if (IsNamePreapproved(newName)) {
|
||||
if (autoRejectNames) {
|
||||
Database::Get()->SetCharacterName(objectID, newName);
|
||||
LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str());
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
||||
UserManager::RequestCharacterList(sysAddr);
|
||||
} else if (IsNamePreapproved(newName)) {
|
||||
Database::Get()->SetCharacterName(objectID, newName);
|
||||
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
||||
|
||||
@@ -41,6 +41,11 @@ public:
|
||||
|
||||
size_t GetUserCount() const { return m_Users.size(); }
|
||||
|
||||
// Access cached config values
|
||||
bool GetMuteAutoRejectNames() const { return m_MuteAutoRejectNames; }
|
||||
bool GetMuteRestrictTrade() const { return m_MuteRestrictTrade; }
|
||||
bool GetMuteRestrictMail() const { return m_MuteRestrictMail; }
|
||||
|
||||
private:
|
||||
static UserManager* m_Address; //Singleton
|
||||
std::map<SystemAddress, User*> m_Users;
|
||||
@@ -50,6 +55,11 @@ private:
|
||||
std::vector<std::string> m_MiddleNames;
|
||||
std::vector<std::string> m_LastNames;
|
||||
std::vector<std::string> m_PreapprovedNames;
|
||||
|
||||
// Cached config values that can change on config reload
|
||||
bool m_MuteAutoRejectNames = false;
|
||||
bool m_MuteRestrictTrade = false;
|
||||
bool m_MuteRestrictMail = false;
|
||||
};
|
||||
|
||||
#endif // USERMANAGER_H
|
||||
|
||||
@@ -45,33 +45,6 @@ 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>();
|
||||
@@ -698,10 +671,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -341,12 +341,6 @@ 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);
|
||||
@@ -370,11 +364,6 @@ private:
|
||||
*/
|
||||
std::vector<ActivityPlayer*> m_ActivityPlayers;
|
||||
|
||||
/**
|
||||
* LMIs for team sizes
|
||||
*/
|
||||
std::unordered_map<uint32_t, uint32_t> m_ActivityLootMatrices;
|
||||
|
||||
/**
|
||||
* The activity id
|
||||
*/
|
||||
|
||||
@@ -70,7 +70,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
for (const auto zoneID : m_VisitedLevels) {
|
||||
std::stringstream sstream;
|
||||
sstream << "MapID: " << zoneID.GetMapID() << " CloneID: " << zoneID.GetCloneID();
|
||||
vl.PushDebug<AMFStringValue>(sstream.str()) = "";
|
||||
vl.PushDebug(sstream.str());
|
||||
}
|
||||
|
||||
// visited locations
|
||||
@@ -95,7 +95,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
const int32_t flagId = base + i;
|
||||
std::stringstream stream;
|
||||
stream << "Flag: " << flagId;
|
||||
allFlags.PushDebug<AMFStringValue>(stream.str()) = "";
|
||||
allFlags.PushDebug(stream.str());
|
||||
}
|
||||
flagChunkCopy >>= 1;
|
||||
}
|
||||
|
||||
@@ -694,6 +694,8 @@ 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);
|
||||
@@ -706,6 +708,7 @@ 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);
|
||||
@@ -753,40 +756,11 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
||||
//NANI?!
|
||||
if (!isPlayer) {
|
||||
if (owner != nullptr) {
|
||||
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());
|
||||
}
|
||||
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
|
||||
}
|
||||
} else {
|
||||
//Check if this zone allows coin drops
|
||||
if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) {
|
||||
if (Game::zoneManager->GetPlayerLoseCoinOnDeath() && !Game::entityManager->GetHardcoreMode()) {
|
||||
auto* character = m_Parent->GetCharacter();
|
||||
uint64_t coinsTotal = character->GetCoins();
|
||||
const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin;
|
||||
@@ -799,7 +773,15 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
||||
|
||||
coinsTotal -= coinsToLose;
|
||||
|
||||
Loot::DropLoot(m_Parent, m_Parent->GetObjectID(), -1, coinsToLose, 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());
|
||||
character->SetCoins(coinsTotal, eLootSourceType::PICKUP);
|
||||
}
|
||||
}
|
||||
@@ -1012,13 +994,23 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
|
||||
//get character:
|
||||
auto* chars = m_Parent->GetCharacter();
|
||||
if (chars) {
|
||||
auto coins = chars->GetCoins();
|
||||
auto oldCoins = chars->GetCoins();
|
||||
// Floor this so there arent coins generated from rounding
|
||||
auto coins = static_cast<uint64_t>(oldCoins * Game::entityManager->GetHardcoreCoinKeep());
|
||||
auto coinsToDrop = oldCoins - coins;
|
||||
LOG("Player had %llu coins, will lose %i coins to have %i", oldCoins, coinsToDrop, coins);
|
||||
|
||||
//lose all coins:
|
||||
chars->SetCoins(0, eLootSourceType::NONE);
|
||||
chars->SetCoins(coins, eLootSourceType::NONE);
|
||||
|
||||
//drop all coins:
|
||||
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coins, m_Parent->GetPosition());
|
||||
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
|
||||
while (coinsToDrop > MAX_TO_DROP_PER_GM) {
|
||||
LOG("Dropping 100,000, %llu left", coinsToDrop);
|
||||
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, MAX_TO_DROP_PER_GM, m_Parent->GetPosition());
|
||||
coinsToDrop -= MAX_TO_DROP_PER_GM;
|
||||
}
|
||||
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coinsToDrop, m_Parent->GetPosition());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1033,8 +1025,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;
|
||||
|
||||
@@ -471,6 +471,8 @@ 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;
|
||||
|
||||
|
||||
@@ -39,10 +39,13 @@
|
||||
#include "CDObjectSkillsTable.h"
|
||||
#include "CDSkillBehaviorTable.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Amf3.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
||||
using namespace GameMessages;
|
||||
RegisterMsg<GetObjectReportInfo>(this, &InventoryComponent::OnGetObjectReportInfo);
|
||||
this->m_Dirty = true;
|
||||
this->m_Equipped = {};
|
||||
this->m_Pushed = {};
|
||||
@@ -440,7 +443,7 @@ Item* InventoryComponent::FindItemBySubKey(LWOOBJID id, eInventoryType inventory
|
||||
}
|
||||
}
|
||||
|
||||
bool InventoryComponent::HasSpaceForLoot(const std::unordered_map<LOT, int32_t>& loot) {
|
||||
bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) {
|
||||
std::unordered_map<eInventoryType, int32_t> spaceOffset{};
|
||||
|
||||
uint32_t slotsNeeded = 0;
|
||||
@@ -626,7 +629,7 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
|
||||
for (const auto& pair : this->m_Inventories) {
|
||||
auto* inventory = pair.second;
|
||||
|
||||
static const auto EXCLUDED_INVENTORIES = {VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS};
|
||||
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS };
|
||||
if (std::ranges::find(EXCLUDED_INVENTORIES, inventory->GetType()) != EXCLUDED_INVENTORIES.end()) {
|
||||
continue;
|
||||
}
|
||||
@@ -1793,3 +1796,99 @@ void InventoryComponent::RegenerateItemIDs() {
|
||||
inventory->RegenerateItemIDs();
|
||||
}
|
||||
}
|
||||
|
||||
std::string DebugInvToString(const eInventoryType inv, bool verbose) {
|
||||
switch (inv) {
|
||||
case ITEMS:
|
||||
return "Backpack";
|
||||
case VAULT_ITEMS:
|
||||
return "Bank";
|
||||
case BRICKS:
|
||||
return verbose ? "Bricks" : "Bricks (contents only shown in high-detail report)";
|
||||
case MODELS_IN_BBB:
|
||||
return "Models in BBB";
|
||||
case TEMP_ITEMS:
|
||||
return "Temp Equip";
|
||||
case MODELS:
|
||||
return verbose ? "Model" : "Model (contents only shown in high-detail report)";
|
||||
case TEMP_MODELS:
|
||||
return "Module";
|
||||
case BEHAVIORS:
|
||||
return "B3 Behavior";
|
||||
case PROPERTY_DEEDS:
|
||||
return "Property";
|
||||
case BRICKS_IN_BBB:
|
||||
return "Brick In BBB";
|
||||
case VENDOR:
|
||||
return "Vendor";
|
||||
case VENDOR_BUYBACK:
|
||||
return "BuyBack";
|
||||
case QUEST:
|
||||
return "Quest";
|
||||
case DONATION:
|
||||
return "Donation";
|
||||
case VAULT_MODELS:
|
||||
return "Bank Model";
|
||||
case ITEM_SETS:
|
||||
return "Bank Behavior";
|
||||
case INVALID:
|
||||
return "Invalid";
|
||||
case ALL:
|
||||
return "All";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
auto& report = static_cast<GameMessages::GetObjectReportInfo&>(msg);
|
||||
auto& cmpt = report.info->PushDebug("Inventory");
|
||||
cmpt.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
uint32_t numItems = 0;
|
||||
for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size();
|
||||
cmpt.PushDebug<AMFIntValue>("Inventory Item Count") = numItems;
|
||||
|
||||
auto& itemsInBags = cmpt.PushDebug("Items in bags");
|
||||
for (const auto& [id, inventoryMut] : m_Inventories) {
|
||||
if (!inventoryMut) continue;
|
||||
const auto* const inventory = inventoryMut;
|
||||
auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, report.bVerbose) + " - " + std::to_string(id));
|
||||
for (uint32_t i = 0; i < inventory->GetSize(); i++) {
|
||||
const auto* const item = inventory->FindItemBySlot(i);
|
||||
if (!item) continue;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "%[Objects_" << item->GetLot() << "_name] Slot " << item->GetSlot();
|
||||
auto& slot = curInv.PushDebug(ss.str());
|
||||
slot.PushDebug<AMFStringValue>("Object ID") = std::to_string(item->GetId());
|
||||
slot.PushDebug<AMFIntValue>("LOT") = item->GetLot();
|
||||
if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug<AMFStringValue>("Subkey") = std::to_string(item->GetSubKey());
|
||||
slot.PushDebug<AMFIntValue>("Count") = item->GetCount();
|
||||
slot.PushDebug<AMFIntValue>("Slot") = item->GetSlot();
|
||||
slot.PushDebug<AMFBoolValue>("Bind on pickup") = item->GetInfo().isBOP;
|
||||
slot.PushDebug<AMFBoolValue>("Bind on equip") = item->GetInfo().isBOE;
|
||||
slot.PushDebug<AMFBoolValue>("Is currently bound") = item->GetBound();
|
||||
auto& extra = slot.PushDebug("Extra Info");
|
||||
for (const auto* const setting : item->GetConfig()) {
|
||||
if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& equipped = cmpt.PushDebug("Equipped Items");
|
||||
for (const auto& [location, info] : GetEquippedItems()) {
|
||||
std::stringstream ss;
|
||||
ss << "%[Objects_" << info.lot << "_name]";
|
||||
auto& equipSlot = equipped.PushDebug(ss.str());
|
||||
equipSlot.PushDebug<AMFStringValue>("Location") = location;
|
||||
equipSlot.PushDebug<AMFStringValue>("Object ID") = std::to_string(info.id);
|
||||
equipSlot.PushDebug<AMFIntValue>("Slot") = info.slot;
|
||||
equipSlot.PushDebug<AMFIntValue>("Count") = info.count;
|
||||
auto& extra = equipSlot.PushDebug("Extra Info");
|
||||
for (const auto* const setting : info.config) {
|
||||
if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "eInventoryType.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "Loot.h"
|
||||
|
||||
class Entity;
|
||||
class ItemSet;
|
||||
@@ -200,7 +201,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 std::unordered_map<LOT, int32_t>& loot);
|
||||
bool HasSpaceForLoot(const Loot::Return& loot);
|
||||
|
||||
/**
|
||||
* Equips an item in the specified slot
|
||||
@@ -410,6 +411,8 @@ public:
|
||||
// Used to migrate a character version, no need to call outside of that context
|
||||
void RegenerateItemIDs();
|
||||
|
||||
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
|
||||
|
||||
~InventoryComponent() override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -27,7 +27,12 @@ std::unordered_map<AchievementCacheKey, std::vector<uint32_t>> MissionComponent:
|
||||
|
||||
//! Initializer
|
||||
MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
||||
using namespace GameMessages;
|
||||
m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue();
|
||||
|
||||
RegisterMsg<GetObjectReportInfo>(this, &MissionComponent::OnGetObjectReportInfo);
|
||||
RegisterMsg<GameMessages::GetMissionState>(this, &MissionComponent::OnGetMissionState);
|
||||
RegisterMsg<GameMessages::MissionNeedsLot>(this, &MissionComponent::OnMissionNeedsLot);
|
||||
}
|
||||
|
||||
//! Destructor
|
||||
@@ -622,3 +627,123 @@ void MissionComponent::ResetMission(const int32_t missionId) {
|
||||
m_Missions.erase(missionId);
|
||||
GameMessages::SendResetMissions(m_Parent, m_Parent->GetSystemAddress(), missionId);
|
||||
}
|
||||
|
||||
void PushMissions(const std::map<uint32_t, Mission*>& missions, AMFArrayValue& V, bool verbose) {
|
||||
for (const auto& [id, mission] : missions) {
|
||||
std::stringstream ss;
|
||||
if (!mission) {
|
||||
ss << "Mission ID: " << id;
|
||||
V.PushDebug(ss.str());
|
||||
} else if (!verbose) {
|
||||
ss << "%[Missions_" << id << "_name]" << ", Mission ID";
|
||||
V.PushDebug<AMFIntValue>(ss.str()) = id;
|
||||
} else {
|
||||
ss << "%[Missions_" << id << "_name]" << ", Mission ID: " << id;
|
||||
auto& missionV = V.PushDebug(ss.str());
|
||||
auto& missionInformation = missionV.PushDebug("Mission Information");
|
||||
|
||||
if (mission->IsComplete()) {
|
||||
missionInformation.PushDebug<AMFStringValue>("Time mission last completed") = std::to_string(mission->GetTimestamp());
|
||||
missionInformation.PushDebug<AMFIntValue>("Number of times completed") = mission->GetCompletions();
|
||||
}
|
||||
// Expensive to network this especially when its read from the client anyways
|
||||
// missionInformation.PushDebug("Description").PushDebug("None");
|
||||
// missionInformation.PushDebug("Text").PushDebug("None");
|
||||
|
||||
auto& statusInfo = missionInformation.PushDebug("Mission statuses for local player");
|
||||
if (mission->IsAvalible()) statusInfo.PushDebug("Available");
|
||||
if (mission->IsActive()) statusInfo.PushDebug("Active");
|
||||
if (mission->IsReadyToComplete()) statusInfo.PushDebug("Ready To Complete");
|
||||
if (mission->IsComplete()) statusInfo.PushDebug("Completed");
|
||||
if (mission->IsFailed()) statusInfo.PushDebug("Failed");
|
||||
const auto& clientInfo = mission->GetClientInfo();
|
||||
|
||||
statusInfo.PushDebug<AMFBoolValue>("Is an achievement mission") = mission->IsAchievement();
|
||||
statusInfo.PushDebug<AMFBoolValue>("Is an timed mission") = clientInfo.time_limit > 0;
|
||||
auto& taskInfo = statusInfo.PushDebug("Task Info");
|
||||
taskInfo.PushDebug<AMFIntValue>("Number of tasks in this mission") = mission->GetTasks().size();
|
||||
int32_t i = 0;
|
||||
for (const auto* task : mission->GetTasks()) {
|
||||
auto& thisTask = taskInfo.PushDebug("Task " + std::to_string(i));
|
||||
// Expensive to network this especially when its read from the client anyways
|
||||
// thisTask.PushDebug("Description").PushDebug("%[MissionTasks_" + taskUidStr + "_description]");
|
||||
thisTask.PushDebug<AMFIntValue>("Number done") = std::min(task->GetProgress(), static_cast<uint32_t>(task->GetClientInfo().targetValue));
|
||||
thisTask.PushDebug<AMFIntValue>("Number total needed") = task->GetClientInfo().targetValue;
|
||||
thisTask.PushDebug<AMFIntValue>("Task Type") = task->GetClientInfo().taskType;
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
// auto& chatText = missionInformation.PushDebug("Chat Text for Mission States");
|
||||
// Expensive to network this especially when its read from the client anyways
|
||||
// chatText.PushDebug("Available Text").PushDebug("%[MissionText_" + idStr + "_chat_state_1]");
|
||||
// chatText.PushDebug("Active Text").PushDebug("%[MissionText_" + idStr + "_chat_state_2]");
|
||||
// chatText.PushDebug("Ready-to-Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_3]");
|
||||
// chatText.PushDebug("Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_4]");
|
||||
|
||||
if (clientInfo.time_limit > 0) {
|
||||
missionInformation.PushDebug<AMFIntValue>("Time Limit") = clientInfo.time_limit;
|
||||
missionInformation.PushDebug<AMFDoubleValue>("Time Remaining") = 0;
|
||||
}
|
||||
|
||||
if (clientInfo.offer_objectID != -1) {
|
||||
missionInformation.PushDebug<AMFIntValue>("Offer Object LOT") = clientInfo.offer_objectID;
|
||||
}
|
||||
|
||||
if (clientInfo.target_objectID != -1) {
|
||||
missionInformation.PushDebug<AMFIntValue>("Complete Object LOT") = clientInfo.target_objectID;
|
||||
}
|
||||
|
||||
if (!clientInfo.prereqMissionID.empty()) {
|
||||
missionInformation.PushDebug<AMFStringValue>("Requirement Mission IDs") = clientInfo.prereqMissionID;
|
||||
}
|
||||
|
||||
missionInformation.PushDebug<AMFBoolValue>("Is Repeatable") = clientInfo.repeatable;
|
||||
const bool hasNoOfferer = clientInfo.offer_objectID == -1 || clientInfo.offer_objectID == 0;
|
||||
const bool hasNoCompleter = clientInfo.target_objectID == -1 || clientInfo.target_objectID == 0;
|
||||
missionInformation.PushDebug<AMFBoolValue>("Is Achievement") = hasNoOfferer && hasNoCompleter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
|
||||
auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)");
|
||||
missionInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
// Sort the missions so they are easier to parse and present to the end user
|
||||
std::map<uint32_t, Mission*> achievements;
|
||||
std::map<uint32_t, Mission*> missions;
|
||||
std::map<uint32_t, Mission*> doneMissions;
|
||||
for (const auto [id, mission] : m_Missions) {
|
||||
if (!mission) continue;
|
||||
else if (mission->IsComplete()) doneMissions[id] = mission;
|
||||
else if (mission->IsAchievement()) achievements[id] = mission;
|
||||
else if (mission->IsMission()) missions[id] = mission;
|
||||
}
|
||||
|
||||
// None of these should be empty, but if they are dont print the field
|
||||
if (!achievements.empty() || !missions.empty()) {
|
||||
auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions");
|
||||
PushMissions(achievements, incompleteMissions, reportMsg.bVerbose);
|
||||
PushMissions(missions, incompleteMissions, reportMsg.bVerbose);
|
||||
}
|
||||
|
||||
if (!doneMissions.empty()) {
|
||||
auto& completeMissions = missionInfo.PushDebug("Completed Missions");
|
||||
PushMissions(doneMissions, completeMissions, reportMsg.bVerbose);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -171,6 +171,9 @@ 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
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,7 @@ 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) {
|
||||
@@ -338,3 +339,19 @@ 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;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
|
||||
bool OnRequestUse(GameMessages::GameMsg& msg);
|
||||
bool OnResetModelToDefaults(GameMessages::GameMsg& msg);
|
||||
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
|
||||
|
||||
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
#include "InventoryComponent.h"
|
||||
#include "Item.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "User.h"
|
||||
#include "SwitchComponent.h"
|
||||
#include "DestroyableComponent.h"
|
||||
#include "dpWorld.h"
|
||||
#include "UserManager.h"
|
||||
#include "PetDigServer.h"
|
||||
#include "ObjectIDManager.h"
|
||||
#include "eUnequippableActiveType.h"
|
||||
@@ -21,6 +23,7 @@
|
||||
#include "eUseItemResponse.h"
|
||||
#include "ePlayerFlag.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "dChatFilter.h"
|
||||
@@ -553,18 +556,29 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
|
||||
}
|
||||
|
||||
void PetComponent::RequestSetPetName(std::u16string name) {
|
||||
const bool autoRejectNames = UserManager::Instance()->GetMuteAutoRejectNames();
|
||||
|
||||
if (m_Tamer == LWOOBJID_EMPTY) {
|
||||
if (m_Owner != LWOOBJID_EMPTY) {
|
||||
auto* owner = GetOwner();
|
||||
|
||||
m_ModerationStatus = 1; // Pending
|
||||
m_Name = "";
|
||||
// If auto reject names is on, and the user is muted, force use of predefined name
|
||||
if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
||||
m_ModerationStatus = 2; // Approved
|
||||
std::string forcedName = "Pet";
|
||||
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
|
||||
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
|
||||
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
|
||||
} else {
|
||||
m_ModerationStatus = 1; // Pending
|
||||
m_Name = "";
|
||||
|
||||
//Save our pet's new name to the db:
|
||||
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
|
||||
//Save our pet's new name to the db:
|
||||
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
|
||||
|
||||
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
|
||||
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
|
||||
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
|
||||
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -586,11 +600,21 @@ void PetComponent::RequestSetPetName(std::u16string name) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_ModerationStatus = 1; // Pending
|
||||
m_Name = "";
|
||||
// If auto reject names is on, and the user is muted, force use of predefined name ELSE proceed with normal name check
|
||||
if (autoRejectNames && tamer->GetCharacter() && tamer->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
||||
m_ModerationStatus = 2; // Approved
|
||||
m_Name = "";
|
||||
std::string forcedName = "Pet";
|
||||
|
||||
//Save our pet's new name to the db:
|
||||
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
|
||||
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
|
||||
LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID());
|
||||
} else {
|
||||
m_ModerationStatus = 1; // Pending
|
||||
m_Name = "";
|
||||
|
||||
//Save our pet's new name to the db:
|
||||
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
|
||||
}
|
||||
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ 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>(); } },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -281,11 +282,6 @@ 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;
|
||||
|
||||
@@ -978,6 +978,7 @@ 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;
|
||||
@@ -1102,52 +1103,6 @@ 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) {
|
||||
@@ -2565,9 +2520,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
|
||||
inStream.Read(timeTaken);
|
||||
|
||||
/*
|
||||
Disabled this, as it's kinda silly to do this roundabout way of storing plaintext lxfml, then recompressing
|
||||
it to send it back to the client.
|
||||
|
||||
On DLU we had agreed that bricks wouldn't be taken anyway, but if your server decides otherwise, feel free to
|
||||
comment this back out and add the needed code to get the bricks used from lxfml and take them from the inventory.
|
||||
|
||||
@@ -2581,23 +2533,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
|
||||
|
||||
//We need to get a new ID for our model first:
|
||||
if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return;
|
||||
const uint32_t maxRetries = 100;
|
||||
uint32_t retries = 0;
|
||||
bool blueprintIDExists = true;
|
||||
bool modelExists = true;
|
||||
|
||||
// Legacy logic to check for old random IDs (regenerating these is not really feasible)
|
||||
// Probably good to have this anyway in case someone messes with the last_object_id or it gets reset somehow
|
||||
LWOOBJID newIDL = LWOOBJID_EMPTY;
|
||||
LWOOBJID blueprintID = LWOOBJID_EMPTY;
|
||||
do {
|
||||
if (newIDL != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID);
|
||||
newIDL = ObjectIDManager::GetPersistentID();
|
||||
blueprintID = ObjectIDManager::GetPersistentID();
|
||||
++retries;
|
||||
blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value();
|
||||
modelExists = Database::Get()->GetModel(newIDL).has_value();
|
||||
} while ((blueprintIDExists || modelExists) && retries < maxRetries);
|
||||
|
||||
//We need to get the propertyID: (stolen from Wincent's propertyManagementComp)
|
||||
const auto& worldId = Game::zoneManager->GetZone()->GetZoneID();
|
||||
@@ -2614,85 +2549,120 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
|
||||
std::istringstream sd0DataStream(str);
|
||||
Sd0 sd0(sd0DataStream);
|
||||
|
||||
// Uncompress the data and normalize the position
|
||||
// Uncompress the data, split, and nornmalize the model
|
||||
const auto asStr = sd0.GetAsStringUncompressed();
|
||||
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr);
|
||||
|
||||
// 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());
|
||||
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();
|
||||
}
|
||||
|
||||
//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");
|
||||
auto splitLxfmls = Lxfml::Split(asStr);
|
||||
LOG_DEBUG("Split into %zu models", splitLxfmls.size());
|
||||
|
||||
/*
|
||||
Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream.
|
||||
(or you uncomment the lxfml decomp stuff above)
|
||||
*/
|
||||
|
||||
// //Send off to UGC for processing, if enabled:
|
||||
// if (Game::config->GetValue("ugc_remote") == "1") {
|
||||
// std::string ugcIP = Game::config->GetValue("ugc_ip");
|
||||
// int ugcPort = std::stoi(Game::config->GetValue("ugc_port"));
|
||||
|
||||
// httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^
|
||||
|
||||
// //Send out a request:
|
||||
// std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml";
|
||||
// cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml");
|
||||
|
||||
// //When the "put" above returns, it means that the UGC HTTP server is done processing our model &
|
||||
// //the nif, hkx and checksum files are ready to be downloaded from cache.
|
||||
// }
|
||||
|
||||
//Tell the client their model is saved: (this causes us to actually pop out of our current state):
|
||||
const auto& newSd0 = sd0.GetAsVector();
|
||||
uint32_t newSd0Size{};
|
||||
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
|
||||
bitStream.Write(localId);
|
||||
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
|
||||
bitStream.Write<uint32_t>(1);
|
||||
bitStream.Write(blueprintID);
|
||||
bitStream.Write<uint32_t>(splitLxfmls.size());
|
||||
|
||||
bitStream.Write(newSd0Size);
|
||||
std::vector<LWOOBJID> blueprintIDs;
|
||||
std::vector<LWOOBJID> modelIDs;
|
||||
|
||||
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
|
||||
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
|
||||
// Legacy logic to check for old random IDs (regenerating these is not really feasible)
|
||||
// Probably good to have this anyway in case someone messes with the last_object_id or it gets reset somehow
|
||||
const uint32_t maxRetries = 100;
|
||||
uint32_t retries = 0;
|
||||
bool blueprintIDExists = true;
|
||||
bool modelExists = true;
|
||||
|
||||
LWOOBJID newID = LWOOBJID_EMPTY;
|
||||
LWOOBJID blueprintID = LWOOBJID_EMPTY;
|
||||
do {
|
||||
if (newID != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID);
|
||||
newID = ObjectIDManager::GetPersistentID();
|
||||
blueprintID = ObjectIDManager::GetPersistentID();
|
||||
++retries;
|
||||
blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value();
|
||||
modelExists = Database::Get()->GetModel(newID).has_value();
|
||||
} while ((blueprintIDExists || modelExists) && retries < maxRetries);
|
||||
|
||||
blueprintIDs.push_back(blueprintID);
|
||||
modelIDs.push_back(newID);
|
||||
|
||||
// Save each model to the database
|
||||
sd0.FromData(reinterpret_cast<const uint8_t*>(splitLxfmls[i].lxfml.data()), splitLxfmls[i].lxfml.size());
|
||||
auto sd0AsStream = sd0.GetAsStream();
|
||||
Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
|
||||
|
||||
// Insert the new property model
|
||||
IPropertyContents::Model model;
|
||||
model.id = newID;
|
||||
model.ugcId = blueprintID;
|
||||
model.position = splitLxfmls[i].center;
|
||||
model.rotation = QuatUtils::IDENTITY;
|
||||
model.lot = 14;
|
||||
Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name");
|
||||
|
||||
/*
|
||||
Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream.
|
||||
(or you uncomment the lxfml decomp stuff above)
|
||||
*/
|
||||
|
||||
// Send off to UGC for processing, if enabled:
|
||||
// if (Game::config->GetValue("ugc_remote") == "1") {
|
||||
// std::string ugcIP = Game::config->GetValue("ugc_ip");
|
||||
// int ugcPort = std::stoi(Game::config->GetValue("ugc_port"));
|
||||
|
||||
// httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^
|
||||
|
||||
// //Send out a request:
|
||||
// std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml";
|
||||
// cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml");
|
||||
|
||||
// //When the "put" above returns, it means that the UGC HTTP server is done processing our model &
|
||||
// //the nif, hkx and checksum files are ready to be downloaded from cache.
|
||||
// }
|
||||
|
||||
// Write the ID and data to the response packet
|
||||
bitStream.Write(blueprintID);
|
||||
|
||||
const auto& newSd0 = sd0.GetAsVector();
|
||||
uint32_t newSd0Size{};
|
||||
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
|
||||
bitStream.Write(newSd0Size);
|
||||
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
|
||||
}
|
||||
|
||||
SEND_PACKET;
|
||||
|
||||
//Now we have to construct this object:
|
||||
// Create entities for each model
|
||||
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
|
||||
EntityInfo info;
|
||||
info.lot = 14;
|
||||
info.pos = splitLxfmls[i].center;
|
||||
info.rot = QuatUtils::IDENTITY;
|
||||
info.spawner = nullptr;
|
||||
info.spawnerID = entity->GetObjectID();
|
||||
info.spawnerNodeID = 0;
|
||||
|
||||
EntityInfo info;
|
||||
info.lot = 14;
|
||||
info.pos = newCenter;
|
||||
info.rot = {};
|
||||
info.spawner = nullptr;
|
||||
info.spawnerID = entity->GetObjectID();
|
||||
info.spawnerNodeID = 0;
|
||||
info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintIDs[i]));
|
||||
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
|
||||
info.settings.push_back(new LDFData<int>(u"modelType", 2));
|
||||
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
|
||||
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", modelIDs[i]));
|
||||
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
|
||||
if (newEntity) {
|
||||
Game::entityManager->ConstructEntity(newEntity);
|
||||
|
||||
info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintID));
|
||||
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
|
||||
info.settings.push_back(new LDFData<int>(u"modelType", 2));
|
||||
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
|
||||
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", newIDL));
|
||||
|
||||
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
|
||||
if (newEntity) {
|
||||
Game::entityManager->ConstructEntity(newEntity);
|
||||
|
||||
//Make sure the propMgmt doesn't delete our model after the server dies
|
||||
//Trying to do this after the entity is constructed. Shouldn't really change anything but
|
||||
//there was an issue with builds not appearing since it was placed above ConstructEntity.
|
||||
PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), newIDL);
|
||||
//Make sure the propMgmt doesn't delete our model after the server dies
|
||||
//Trying to do this after the entity is constructed. Shouldn't really change anything but
|
||||
//there was an issue with builds not appearing since it was placed above ConstructEntity.
|
||||
PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), modelIDs[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3243,12 +3213,13 @@ void GameMessages::SendServerTradeUpdate(LWOOBJID objectId, uint64_t coins, cons
|
||||
void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) {
|
||||
// Check if the player has restricted trade access
|
||||
auto* character = entity->GetCharacter();
|
||||
const bool restrictTradeOnMute = UserManager::Instance()->GetMuteRestrictTrade();
|
||||
|
||||
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess)) {
|
||||
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) {
|
||||
// Send a message to the player
|
||||
ChatPackets::SendSystemMessage(
|
||||
sysAddr,
|
||||
u"This character has restricted trade access."
|
||||
u"Your character has restricted trade access."
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -3264,7 +3235,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity*
|
||||
if (invitee != nullptr && invitee->IsPlayer()) {
|
||||
character = invitee->GetCharacter();
|
||||
|
||||
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess)) {
|
||||
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) {
|
||||
// Send a message to the player
|
||||
ChatPackets::SendSystemMessage(
|
||||
sysAddr,
|
||||
@@ -5716,27 +5687,6 @@ 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();
|
||||
|
||||
@@ -6320,6 +6270,11 @@ 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;
|
||||
@@ -6414,6 +6369,7 @@ namespace GameMessages {
|
||||
void RequestServerObjectInfo::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* handlingEntity = Game::entityManager->GetEntity(targetForReport);
|
||||
if (handlingEntity) handlingEntity->HandleMsg(*this);
|
||||
else LOG("Failed to find target %llu", targetForReport);
|
||||
}
|
||||
|
||||
bool RequestUse::Deserialize(RakNet::BitStream& stream) {
|
||||
@@ -6486,4 +6442,49 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ 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,
|
||||
@@ -57,6 +58,7 @@ 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
|
||||
@@ -850,9 +852,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;
|
||||
};
|
||||
@@ -870,5 +872,65 @@ 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
|
||||
|
||||
@@ -324,7 +324,8 @@ Inventory::~Inventory() {
|
||||
void Inventory::RegenerateItemIDs() {
|
||||
std::map<LWOOBJID, Item*> newItems{};
|
||||
for (auto* const item : items | std::views::values) {
|
||||
const bool equipped = item->GetParent() == LWOOBJID_EMPTY && item->IsEquipped();
|
||||
if (item->GetParent() != LWOOBJID_EMPTY) continue; // temp items dont need new ids
|
||||
const bool equipped = item->IsEquipped();
|
||||
if (equipped) item->UnEquip();
|
||||
const auto oldID = item->GetId();
|
||||
const auto newID = item->GenerateID();
|
||||
|
||||
@@ -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.
|
||||
std::unordered_map<LOT, int32_t> rolledLoot{};
|
||||
Loot::Return rolledLoot{};
|
||||
for (auto& pack : packages) {
|
||||
auto thisPackage = Loot::RollLootMatrix(entityParent, pack.LootMatrixIndex);
|
||||
const 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,6 +356,7 @@ void Item::UseNonEquip(Item* item) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playerInventoryComponent->HasSpaceForLoot(rolledLoot)) {
|
||||
Loot::GiveLoot(playerInventoryComponent->GetParent(), rolledLoot, eLootSourceType::CONSUMPTION);
|
||||
item->SetCount(item->GetCount() - 1);
|
||||
|
||||
@@ -270,6 +270,12 @@ bool Mission::IsReadyToComplete() const {
|
||||
return m_State == eMissionState::READY_TO_COMPLETE || m_State == eMissionState::COMPLETE_READY_TO_COMPLETE;
|
||||
}
|
||||
|
||||
bool Mission::IsFailed() const {
|
||||
const auto underlying = GeneralUtils::ToUnderlying(m_State);
|
||||
const auto target = GeneralUtils::ToUnderlying(eMissionState::FAILED);
|
||||
return (underlying & target) != 0;
|
||||
}
|
||||
|
||||
void Mission::MakeReadyToComplete() {
|
||||
SetMissionState(m_Completions == 0 ? eMissionState::READY_TO_COMPLETE : eMissionState::COMPLETE_READY_TO_COMPLETE);
|
||||
}
|
||||
|
||||
@@ -244,6 +244,8 @@ public:
|
||||
|
||||
const std::set<uint32_t>& GetTestedMissions() const;
|
||||
|
||||
bool IsFailed() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Progresses all the newly accepted tasks for this mission after it has been accepted to reflect the state of the
|
||||
|
||||
@@ -18,131 +18,27 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
void Loot::CacheMatrix(uint32_t matrixIndex) {
|
||||
if (CachedMatrices.contains(matrixIndex)) return;
|
||||
struct LootDropInfo {
|
||||
CDLootTable table{};
|
||||
uint32_t count{ 0 };
|
||||
};
|
||||
|
||||
CachedMatrices.insert(matrixIndex);
|
||||
std::map<LOT, LootDropInfo> 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>();
|
||||
|
||||
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;
|
||||
std::map<LOT, LootDropInfo> drops;
|
||||
|
||||
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
|
||||
|
||||
@@ -181,14 +77,12 @@ std::unordered_map<LOT, int32_t> Loot::RollLootMatrix(uint32_t matrixIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
if (possibleDrops.size() > 0) {
|
||||
if (!possibleDrops.empty()) {
|
||||
const auto& drop = possibleDrops[GeneralUtils::GenerateRandomNumber<uint32_t>(0, possibleDrops.size() - 1)];
|
||||
|
||||
if (drops.find(drop.itemid) == drops.end()) {
|
||||
drops.insert({ drop.itemid, 1 });
|
||||
} else {
|
||||
++drops[drop.itemid];
|
||||
}
|
||||
auto& info = drops[drop.itemid];
|
||||
if (info.count == 0) info.table = drop;
|
||||
info.count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,15 +91,395 @@ std::unordered_map<LOT, int32_t> Loot::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
|
||||
|
||||
std::unordered_map<LOT, int32_t> result = RollLootMatrix(player, matrixIndex);
|
||||
const auto result = RollLootMatrix(player, matrixIndex);
|
||||
|
||||
GiveLoot(player, result, lootSourceType);
|
||||
}
|
||||
|
||||
void Loot::GiveLoot(Entity* player, std::unordered_map<LOT, int32_t>& result, eLootSourceType lootSourceType) {
|
||||
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType) {
|
||||
player = player->GetOwner(); // if the owner is overwritten, we collect that here
|
||||
|
||||
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
@@ -260,34 +534,9 @@ void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex,
|
||||
if (!inventoryComponent)
|
||||
return;
|
||||
|
||||
std::unordered_map<LOT, int32_t> result = RollLootMatrix(player, matrixIndex);
|
||||
const auto result = ::RollLootMatrix(matrixIndex);
|
||||
|
||||
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);
|
||||
::DropLoot(player, source, result, minCoins, maxCoins);
|
||||
}
|
||||
|
||||
void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) {
|
||||
|
||||
@@ -6,20 +6,25 @@
|
||||
|
||||
class Entity;
|
||||
|
||||
namespace GameMessages {
|
||||
struct DropClientLoot;
|
||||
};
|
||||
|
||||
namespace Loot {
|
||||
struct Info {
|
||||
LWOOBJID id = 0;
|
||||
LOT lot = 0;
|
||||
uint32_t count = 0;
|
||||
int32_t count = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<LOT, int32_t> RollLootMatrix(Entity* player, uint32_t matrixIndex);
|
||||
std::unordered_map<LOT, int32_t> RollLootMatrix(uint32_t matrixIndex);
|
||||
using Return = std::map<LOT, int32_t>;
|
||||
|
||||
Loot::Return RollLootMatrix(Entity* player, uint32_t matrixIndex);
|
||||
void CacheMatrix(const uint32_t matrixIndex);
|
||||
void GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType = eLootSourceType::NONE);
|
||||
void GiveLoot(Entity* player, std::unordered_map<LOT, int32_t>& result, eLootSourceType lootSourceType = eLootSourceType::NONE);
|
||||
void GiveLoot(Entity* player, const Loot::Return& 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 DropLoot(Entity* player, const LWOOBJID source, std::unordered_map<LOT, int32_t>& result, uint32_t minCoins, uint32_t maxCoins);
|
||||
void DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam = false, bool forceFfa = false);
|
||||
void DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GeneralUtils.h"
|
||||
#include "Database.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "dServer.h"
|
||||
#include "Entity.h"
|
||||
#include "Character.h"
|
||||
@@ -28,6 +29,7 @@
|
||||
#include "ServiceType.h"
|
||||
#include "User.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "UserManager.h"
|
||||
|
||||
namespace {
|
||||
const std::string DefaultSender = "%[MAIL_SYSTEM_NOTIFICATION]";
|
||||
@@ -72,7 +74,10 @@ namespace Mail {
|
||||
void SendRequest::Handle() {
|
||||
SendResponse response;
|
||||
auto* character = player->GetCharacter();
|
||||
if (character && !(character->HasPermission(ePermissionMap::RestrictedMailAccess) || character->GetParentUser()->GetIsMuted())) {
|
||||
const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted();
|
||||
const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess);
|
||||
|
||||
if (character && !(restrictedMailAccess || restrictMailOnMute)) {
|
||||
mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), "");
|
||||
auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient);
|
||||
|
||||
@@ -83,7 +88,7 @@ namespace Mail {
|
||||
} else {
|
||||
uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee;
|
||||
uint32_t stackSize = 0;
|
||||
|
||||
|
||||
auto inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
Item* item = nullptr;
|
||||
|
||||
@@ -123,7 +128,7 @@ namespace Mail {
|
||||
|
||||
Database::Get()->InsertNewMail(mailInfo);
|
||||
response.status = eSendResponse::Success;
|
||||
character->SaveXMLToDatabase();
|
||||
character->SaveXMLToDatabase();
|
||||
} else {
|
||||
response.status = eSendResponse::AttachmentNotFound;
|
||||
}
|
||||
@@ -165,7 +170,7 @@ namespace Mail {
|
||||
void DataResponse::Serialize(RakNet::BitStream& bitStream) const {
|
||||
MailLUBitStream::Serialize(bitStream);
|
||||
bitStream.Write(this->throttled);
|
||||
|
||||
|
||||
bitStream.Write<uint16_t>(this->playerMail.size());
|
||||
bitStream.Write<uint16_t>(0); // packing
|
||||
for (const auto& mail : this->playerMail) {
|
||||
|
||||
@@ -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,6 +368,20 @@ 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;
|
||||
@@ -376,24 +390,16 @@ namespace DEVGMCommands {
|
||||
if (splitArgs[0].find("/") != std::string::npos) return;
|
||||
if (splitArgs[0].find("\\") != std::string::npos) return;
|
||||
|
||||
auto infile = Game::assetManager->GetFile(("macros/" + splitArgs[0] + ".scm").c_str());
|
||||
|
||||
if (!infile) {
|
||||
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()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?");
|
||||
return;
|
||||
}
|
||||
|
||||
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?");
|
||||
}
|
||||
HandleMacro(*entity, sysAddr, infile);
|
||||
HandleMacro(*entity, sysAddr, resServerInFile);
|
||||
}
|
||||
|
||||
void AddMission(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
@@ -1308,7 +1314,7 @@ namespace DEVGMCommands {
|
||||
|
||||
for (uint32_t i = 0; i < loops; i++) {
|
||||
while (true) {
|
||||
auto lootRoll = Loot::RollLootMatrix(lootMatrixIndex.value());
|
||||
const auto lootRoll = Loot::RollLootMatrix(nullptr, lootMatrixIndex.value());
|
||||
totalRuns += 1;
|
||||
bool doBreak = false;
|
||||
for (const auto& kv : lootRoll) {
|
||||
@@ -1473,52 +1479,62 @@ 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;
|
||||
|
||||
auto component = GeneralUtils::TryParse<eReplicaComponentType>(splitArgs[0]);
|
||||
if (!component) {
|
||||
component = eReplicaComponentType::INVALID;
|
||||
if (!closest) closest = PlayerManager::GetPlayer(splitArgs[0]);
|
||||
if (!closest) {
|
||||
auto component = GeneralUtils::TryParse<eReplicaComponentType>(splitArgs[0]);
|
||||
if (!component) {
|
||||
component = eReplicaComponentType::INVALID;
|
||||
|
||||
ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]);
|
||||
ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]);
|
||||
|
||||
isLDF = true;
|
||||
}
|
||||
|
||||
auto reference = entity->GetPosition();
|
||||
|
||||
auto closestDistance = 0.0f;
|
||||
|
||||
const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value());
|
||||
|
||||
for (auto* candidate : candidates) {
|
||||
if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) {
|
||||
continue;
|
||||
isLDF = true;
|
||||
}
|
||||
|
||||
if (isLDF && !candidate->HasVar(ldf)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!closest) {
|
||||
closest = candidate;
|
||||
|
||||
closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closest = candidate;
|
||||
|
||||
closestDistance = distance;
|
||||
auto reference = entity->GetPosition();
|
||||
|
||||
|
||||
const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value());
|
||||
|
||||
for (auto* candidate : candidates) {
|
||||
if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isLDF && !candidate->HasVar(ldf)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!closest) {
|
||||
closest = candidate;
|
||||
|
||||
closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closest = candidate;
|
||||
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
closestDistance = NiPoint3::Distance(entity->GetPosition(), closest->GetPosition());
|
||||
}
|
||||
|
||||
if (!closest) return;
|
||||
@@ -1684,7 +1700,7 @@ namespace DEVGMCommands {
|
||||
}
|
||||
|
||||
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
||||
|
||||
|
||||
// Prevent execute command recursion by checking if this is already an execute command
|
||||
for (const auto& arg : splitArgs) {
|
||||
if (arg == "execute" || arg == "exec") {
|
||||
@@ -1692,51 +1708,51 @@ namespace DEVGMCommands {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Context variables for execution
|
||||
Entity* execEntity = entity; // Entity to execute as
|
||||
NiPoint3 execPosition = entity->GetPosition(); // Position to execute from
|
||||
bool positionOverridden = false;
|
||||
std::string finalCommand;
|
||||
|
||||
|
||||
// Parse subcommands
|
||||
size_t i = 0;
|
||||
while (i < splitArgs.size()) {
|
||||
const std::string& subcommand = splitArgs[i];
|
||||
|
||||
|
||||
if (subcommand == "as") {
|
||||
if (i + 1 >= splitArgs.size()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'as' requires a player name");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const std::string& targetName = splitArgs[i + 1];
|
||||
auto* targetPlayer = PlayerManager::GetPlayer(targetName);
|
||||
if (!targetPlayer) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
execEntity = targetPlayer;
|
||||
i += 2;
|
||||
|
||||
|
||||
} else if (subcommand == "at") {
|
||||
if (i + 1 >= splitArgs.size()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'at' requires a player name");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const std::string& targetName = splitArgs[i + 1];
|
||||
auto* targetPlayer = PlayerManager::GetPlayer(targetName);
|
||||
if (!targetPlayer) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
execPosition = targetPlayer->GetPosition();
|
||||
positionOverridden = true;
|
||||
i += 2;
|
||||
|
||||
|
||||
} else if (subcommand == "positioned") {
|
||||
if (i + 3 >= splitArgs.size()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'positioned' requires x, y, z coordinates");
|
||||
@@ -1754,69 +1770,69 @@ namespace DEVGMCommands {
|
||||
|
||||
execPosition = NiPoint3(xOpt.value(), yOpt.value(), zOpt.value());
|
||||
positionOverridden = true;
|
||||
|
||||
|
||||
i += 4;
|
||||
|
||||
|
||||
} else if (subcommand == "run") {
|
||||
// Everything after "run" is the command to execute
|
||||
if (i + 1 >= splitArgs.size()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'run' requires a command");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Reconstruct the command from remaining args
|
||||
for (size_t j = i + 1; j < splitArgs.size(); ++j) {
|
||||
if (!finalCommand.empty()) finalCommand += " ";
|
||||
finalCommand += splitArgs[j];
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
} else {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: Unknown subcommand '" + GeneralUtils::ASCIIToUTF16(subcommand) + u"'");
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Valid subcommands: as, at, positioned, run");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (finalCommand.empty()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: No command specified to run. Use 'run <command>' at the end.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate that the command starts with a valid character
|
||||
if (finalCommand.empty() || finalCommand[0] == '/') {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Error: Command should not start with '/'. Just specify the command name.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Store original position if we need to restore it
|
||||
NiPoint3 originalPosition;
|
||||
bool needToRestore = false;
|
||||
|
||||
|
||||
if (positionOverridden && execEntity == entity) {
|
||||
// If we're executing as ourselves but from a different position,
|
||||
// temporarily move the entity
|
||||
originalPosition = entity->GetPosition();
|
||||
needToRestore = true;
|
||||
|
||||
|
||||
// Set the position temporarily for the command execution
|
||||
auto* controllable = entity->GetComponent<ControllablePhysicsComponent>();
|
||||
if (controllable) {
|
||||
controllable->SetPosition(execPosition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Provide feedback about what we're executing
|
||||
std::string execAsName = execEntity->GetCharacter() ? execEntity->GetCharacter()->GetName() : "Unknown";
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) +
|
||||
u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " +
|
||||
GeneralUtils::to_u16string(execPosition.y) + u", " +
|
||||
GeneralUtils::to_u16string(execPosition.z) + u">: /" +
|
||||
GeneralUtils::ASCIIToUTF16(finalCommand));
|
||||
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) +
|
||||
u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " +
|
||||
GeneralUtils::to_u16string(execPosition.y) + u", " +
|
||||
GeneralUtils::to_u16string(execPosition.z) + u">: /" +
|
||||
GeneralUtils::ASCIIToUTF16(finalCommand));
|
||||
|
||||
// Execute the command through the slash command handler
|
||||
SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16("/" + finalCommand), execEntity, sysAddr);
|
||||
|
||||
|
||||
// Restore original position if needed
|
||||
if (needToRestore) {
|
||||
auto* controllable = entity->GetComponent<ControllablePhysicsComponent>();
|
||||
|
||||
@@ -49,7 +49,7 @@ void ExplodingAsset::OnHit(Entity* self, Entity* attacker) {
|
||||
if (!self->GetBoolean(u"bIsHit")) {
|
||||
for (const auto objID : proximityComponent->GetProximityObjects("crateHitters")) {
|
||||
auto* const entity = Game::entityManager->GetEntity(objID);
|
||||
if (!entity) continue;
|
||||
if (!entity || entity->GetObjectID() != attacker->GetObjectID()) continue;
|
||||
|
||||
auto* const destroyable = entity->GetComponent<DestroyableComponent>();
|
||||
if (destroyable) destroyable->Smash(attacker->GetObjectID());
|
||||
|
||||
@@ -15,11 +15,6 @@ 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) {
|
||||
@@ -28,8 +23,19 @@ 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();
|
||||
|
||||
Loot::DropLoot(owner, self->GetObjectID(), drops, 0, 0);
|
||||
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);
|
||||
}
|
||||
|
||||
// Increment the current cycle
|
||||
|
||||
@@ -10,8 +10,21 @@ void AgPicnicBlanket::OnUse(Entity* self, Entity* user) {
|
||||
return;
|
||||
self->SetVar<bool>(u"active", true);
|
||||
|
||||
auto lootTable = std::unordered_map<LOT, int32_t>{ {935, 3} };
|
||||
Loot::DropLoot(user, self->GetObjectID(), lootTable, 0, 0);
|
||||
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);
|
||||
}
|
||||
|
||||
self->AddCallbackTimer(5.0f, [self]() {
|
||||
self->SetVar<bool>(u"active", false);
|
||||
|
||||
@@ -415,9 +415,7 @@ void SGCannon::SpawnNewModel(Entity* self) {
|
||||
}
|
||||
|
||||
if (lootMatrix != 0) {
|
||||
std::unordered_map<LOT, int32_t> toDrop = {};
|
||||
toDrop = Loot::RollLootMatrix(player, lootMatrix);
|
||||
|
||||
const auto toDrop = Loot::RollLootMatrix(player, lootMatrix);
|
||||
for (const auto [lot, count] : toDrop) {
|
||||
GameMessages::SetModelToBuild modelToBuild{};
|
||||
modelToBuild.modelLot = lot;
|
||||
|
||||
@@ -1166,32 +1166,36 @@ void HandlePacket(Packet* packet) {
|
||||
LOG("Couldn't find property ID for zone %i, clone %i", zoneId, cloneId);
|
||||
goto noBBB;
|
||||
}
|
||||
for (auto& bbbModel : Database::Get()->GetUgcModels(propertyId)) {
|
||||
|
||||
// Workaround for not having a UGC server to get model LXFML onto the client so it
|
||||
// can generate the physics and nif for the object.
|
||||
|
||||
auto bbbModels = Database::Get()->GetUgcModels(propertyId);
|
||||
if (bbbModels.empty()) {
|
||||
LOG("No BBB models found for property %llu", propertyId);
|
||||
goto noBBB;
|
||||
}
|
||||
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
|
||||
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
|
||||
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
|
||||
bitStream.Write<uint32_t>(bbbModels.size());
|
||||
for (auto& bbbModel : bbbModels) {
|
||||
LOG("Getting lxfml ugcID: %llu", bbbModel.id);
|
||||
|
||||
bbbModel.lxfmlData.seekg(0, std::ios::end);
|
||||
size_t lxfmlSize = bbbModel.lxfmlData.tellg();
|
||||
bbbModel.lxfmlData.seekg(0);
|
||||
|
||||
//Send message:
|
||||
// write data
|
||||
LWOOBJID blueprintID = bbbModel.id;
|
||||
|
||||
// Workaround for not having a UGC server to get model LXFML onto the client so it
|
||||
// can generate the physics and nif for the object.
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
|
||||
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
|
||||
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
|
||||
bitStream.Write<uint32_t>(1);
|
||||
bitStream.Write(blueprintID);
|
||||
|
||||
bitStream.Write<uint32_t>(lxfmlSize);
|
||||
|
||||
bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(bbbModel.lxfmlData.str().c_str()), lxfmlSize);
|
||||
|
||||
SystemAddress sysAddr = packet->systemAddress;
|
||||
SEND_PACKET;
|
||||
}
|
||||
SystemAddress sysAddr = packet->systemAddress;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
noBBB:
|
||||
|
||||
@@ -80,7 +80,7 @@ These commands are primarily for development and testing. The usage of many of t
|
||||
|getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8|
|
||||
|giveuscore|`/giveuscore <uscore>`|Gives uscore.|8|
|
||||
|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id.|8|
|
||||
|inspect|`/inspect <component> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8|
|
||||
|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8|
|
||||
|list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one.|8|
|
||||
|locrow|`/locrow`|Prints the your current position and rotation information to the console.|8|
|
||||
|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8|
|
||||
|
||||
@@ -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=1
|
||||
default_team_loot=0
|
||||
|
||||
# event gating for login response and luz gating
|
||||
event_1=Talk_Like_A_Pirate
|
||||
@@ -74,3 +74,9 @@ database_type=sqlite
|
||||
|
||||
# Skips the account creation check in master. Used for non-interactive setups.
|
||||
skip_account_creation=0
|
||||
# 0 or 1, restrict mail while account is muted
|
||||
mute_restrict_mail=1
|
||||
# 0 or 1, restrict trade while account is muted
|
||||
mute_restrict_trade=1
|
||||
# 0 or 1, automatically reject character and pet names submitted while muted
|
||||
mute_auto_reject_names=1
|
||||
|
||||
@@ -99,3 +99,9 @@ hardcore_uscore_excluded_enemies=
|
||||
|
||||
# Disables hardcore mode for specific worlds, if hardcore is enabled
|
||||
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
|
||||
|
||||
@@ -10,6 +10,7 @@ set(DCOMMONTEST_SOURCES
|
||||
"TestLUString.cpp"
|
||||
"TestLUWString.cpp"
|
||||
"dCommonDependencies.cpp"
|
||||
"LxfmlTests.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(dEnumsTests)
|
||||
@@ -32,6 +33,8 @@ target_link_libraries(dCommonTests ${COMMON_LIBRARIES} GTest::gtest_main)
|
||||
# Copy test files to testing directory
|
||||
add_subdirectory(TestBitStreams)
|
||||
file(COPY ${TESTBITSTREAMS} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_subdirectory(LxfmlTestFiles)
|
||||
file(COPY ${LXFMLTESTFILES} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# Discover the tests
|
||||
gtest_discover_tests(dCommonTests)
|
||||
|
||||
20
tests/dCommonTests/LxfmlTestFiles/CMakeLists.txt
Normal file
20
tests/dCommonTests/LxfmlTestFiles/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
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)
|
||||
132
tests/dCommonTests/LxfmlTestFiles/complex_grouping.lxfml
Normal file
132
tests/dCommonTests/LxfmlTestFiles/complex_grouping.lxfml
Normal file
@@ -0,0 +1,132 @@
|
||||
<?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>
|
||||
50
tests/dCommonTests/LxfmlTestFiles/deeply_nested.lxfml
Normal file
50
tests/dCommonTests/LxfmlTestFiles/deeply_nested.lxfml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="3001">
|
||||
<Part refID="0" designID="3001" materials="23">
|
||||
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
<RigidSystems>
|
||||
</RigidSystems>
|
||||
<GroupSystems>
|
||||
<GroupSystem>
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0">
|
||||
<Group partRefs="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</GroupSystem>
|
||||
</GroupSystems>
|
||||
</LXFML>
|
||||
11
tests/dCommonTests/LxfmlTestFiles/empty_transform.lxfml
Normal file
11
tests/dCommonTests/LxfmlTestFiles/empty_transform.lxfml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta></Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679">
|
||||
<Bone refID="0" transformation=""/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
</LXFML>
|
||||
48
tests/dCommonTests/LxfmlTestFiles/group_issue.lxfml
Normal file
48
tests/dCommonTests/LxfmlTestFiles/group_issue.lxfml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
20
tests/dCommonTests/LxfmlTestFiles/invalid_transform.lxfml
Normal file
20
tests/dCommonTests/LxfmlTestFiles/invalid_transform.lxfml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679" materials="23">
|
||||
<Bone refID="0" transformation="invalid,matrix,with,text,values,here,not,numbers,at,all,fails,parse"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="1" designID="41533">
|
||||
<Part refID="1" designID="41533" materials="23">
|
||||
<Bone refID="1" transformation="1,2,3"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
</LXFML>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta></Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679">
|
||||
<Bone refID="0" transformation="1,0,invalid,0,1,0,0,0,1,10,20,30"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
</LXFML>
|
||||
44
tests/dCommonTests/LxfmlTestFiles/mixed_valid_invalid.lxfml
Normal file
44
tests/dCommonTests/LxfmlTestFiles/mixed_valid_invalid.lxfml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679" materials="23">
|
||||
<Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,0,0,0"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="1" designID="41533">
|
||||
<Part refID="1" designID="41533" materials="23">
|
||||
<Bone refID="1" transformation="invalid,transform,here,bad,values,foo,bar,baz,qux,0,0,0"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="2" designID="74340">
|
||||
<Part refID="2" designID="3679" materials="23">
|
||||
<Bone refID="2" transformation="1,0,0,0,1,0,0,0,1,10,20,30"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="3" designID="41533">
|
||||
<Part refID="3" designID="41533" materials="23">
|
||||
<Bone refID="3" transformation="1,2,3"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
<RigidSystems>
|
||||
<RigidSystem>
|
||||
<Rigid boneRefs="0,2"/>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid boneRefs="1,3"/>
|
||||
</RigidSystem>
|
||||
</RigidSystems>
|
||||
<GroupSystems>
|
||||
<GroupSystem>
|
||||
<Group partRefs="0,2"/>
|
||||
<Group partRefs="1,3"/>
|
||||
</GroupSystem>
|
||||
</GroupSystems>
|
||||
</LXFML>
|
||||
4
tests/dCommonTests/LxfmlTestFiles/no_bricks.lxfml
Normal file
4
tests/dCommonTests/LxfmlTestFiles/no_bricks.lxfml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta></Meta>
|
||||
</LXFML>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta></Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679">
|
||||
<Bone refID="0" transformation="a,b,c,d,e,f,g,h,i,j,k,l"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
</LXFML>
|
||||
336
tests/dCommonTests/LxfmlTestFiles/test.lxfml
Normal file
336
tests/dCommonTests/LxfmlTestFiles/test.lxfml
Normal file
@@ -0,0 +1,336 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679" materials="23">
|
||||
<Bone refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.7959,4.83179,1.36732"/>
|
||||
</Part>
|
||||
<Part refID="1" designID="3680" materials="23">
|
||||
<Bone refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,10.8444,4.54236,1.49497"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="1" designID="41533">
|
||||
<Part refID="2" designID="41533" materials="23">
|
||||
<Bone refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="2" designID="74340">
|
||||
<Part refID="3" designID="3679" materials="23">
|
||||
<Bone refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.5689,3.28748,3.18812"/>
|
||||
</Part>
|
||||
<Part refID="4" designID="3680" materials="23">
|
||||
<Bone refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,11.6174,2.99808,3.31576"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="3" designID="41533">
|
||||
<Part refID="5" designID="41533" materials="23">
|
||||
<Bone refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,12.3419,1.74316,5.0089"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="4" designID="3614">
|
||||
<Part refID="6" designID="3614" materials="23">
|
||||
<Bone refID="6" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,13.1227,1.67822,5.36752"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="5" designID="3004">
|
||||
<Part refID="7" designID="3004" materials="23,0,0,0" decoration="0,0,0">
|
||||
<Bone refID="7" transformation="0,0,-1,0,1,0,1,0,0,22,0.320038,10.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="6" designID="3036">
|
||||
<Part refID="8" designID="3036" materials="23">
|
||||
<Bone refID="8" transformation="1,0,0,0,1,0,0,0,1,18,3.05176e-05,12.4"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="7" designID="3036">
|
||||
<Part refID="9" designID="3036" materials="23">
|
||||
<Bone refID="9" transformation="1,0,0,0,1,0,0,0,1,14.8,0.320038,13.2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="8" designID="3036">
|
||||
<Part refID="10" designID="3036" materials="23">
|
||||
<Bone refID="10" transformation="1,0,0,0,1,0,0,0,1,13.2,0.640045,11.6"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="9" designID="6106">
|
||||
<Part refID="11" designID="6106" materials="23">
|
||||
<Bone refID="11" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,0.960052,6.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="10" designID="6106">
|
||||
<Part refID="12" designID="6106" materials="23">
|
||||
<Bone refID="12" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,13.2,1.28006,6.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="11" designID="6106">
|
||||
<Part refID="13" designID="6106" materials="23">
|
||||
<Bone refID="13" transformation="1,0,0,0,1.0000001192092896,0,0,0,1,12.4,1.60007,6.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="12" designID="3730">
|
||||
<Part refID="14" designID="3730" materials="23">
|
||||
<Bone refID="14" transformation="-1,0,0,0,1,0,0,0,-1,13.2,1.92007,6.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="13" designID="73587">
|
||||
<Part refID="15" designID="4592" materials="23">
|
||||
<Bone refID="15" transformation="1,0,0,0,1,0,0,0,1,15.6,1.92007,6.8"/>
|
||||
</Part>
|
||||
<Part refID="16" designID="4593" materials="23">
|
||||
<Bone refID="16" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,15.6,2.34009,6.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="14" designID="3688">
|
||||
<Part refID="17" designID="3688" materials="23">
|
||||
<Bone refID="17" transformation="1,0,0,0,1,0,0,0,1,3.6,0.960022,15.6"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="15" designID="4085">
|
||||
<Part refID="18" designID="4085" materials="23">
|
||||
<Bone refID="18" transformation="1,0,0,0,1,0,0,0,1,5.2,0.960022,15.6"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="16" designID="3046">
|
||||
<Part refID="19" designID="3046" materials="23">
|
||||
<Bone refID="19" transformation="1,0,0,0,1,0,0,0,1,4.4,3.05176e-05,14.8"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="17" designID="73587">
|
||||
<Part refID="20" designID="4592" materials="23">
|
||||
<Bone refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-21.2,3.05176e-05,-9.2"/>
|
||||
</Part>
|
||||
<Part refID="21" designID="4593" materials="23">
|
||||
<Bone refID="21" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-21.2,0.420044,-9.2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="18" designID="74340">
|
||||
<Part refID="22" designID="3679" materials="23">
|
||||
<Bone refID="22" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
|
||||
</Part>
|
||||
<Part refID="23" designID="3680" materials="23">
|
||||
<Bone refID="23" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-15.8842,2.99805,-6.01737"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="19" designID="41533">
|
||||
<Part refID="24" designID="41533" materials="23">
|
||||
<Bone refID="24" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.3214133083820343,0.37305739521980286,-16.0119,3.28745,-5.96892"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="20" designID="41533">
|
||||
<Part refID="25" designID="41533" materials="23">
|
||||
<Bone refID="25" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-14.1911,1.74313,-6.74193"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="21" designID="3730">
|
||||
<Part refID="26" designID="3730" materials="23">
|
||||
<Bone refID="26" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-12.4,1.92004,-7.60001"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="22" designID="6106">
|
||||
<Part refID="27" designID="6106" materials="23">
|
||||
<Bone refID="27" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.60004,-6.80001"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="23" designID="6106">
|
||||
<Part refID="28" designID="6106" materials="23">
|
||||
<Bone refID="28" transformation="0,0,-1,0,1,0,1,0,0,-12.4,1.28003,-7.60001"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="24" designID="6106">
|
||||
<Part refID="29" designID="6106" materials="23">
|
||||
<Bone refID="29" transformation="0,0,-1,0,1,0,1,0,0,-12.4,0.960022,-6.80001"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="25" designID="3036">
|
||||
<Part refID="30" designID="3036" materials="23">
|
||||
<Bone refID="30" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-7.6,0.640015,-7.60001"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="26" designID="3036">
|
||||
<Part refID="31" designID="3036" materials="23">
|
||||
<Bone refID="31" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6,0.320007,-9.2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="27" designID="3036">
|
||||
<Part refID="32" designID="3036" materials="23">
|
||||
<Bone refID="32" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-6.8,0,-12.4"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="28" designID="3004">
|
||||
<Part refID="33" designID="3004" materials="23,0,0,0" decoration="0,0,0">
|
||||
<Bone refID="33" transformation="-1,0,0,0,0.99999994039535522,0,0,0,-1,-8.40001,0.320007,-16.4"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="29" designID="73587">
|
||||
<Part refID="34" designID="4592" materials="23">
|
||||
<Bone refID="34" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-12.4,1.92004,-10"/>
|
||||
</Part>
|
||||
<Part refID="35" designID="4593" materials="23">
|
||||
<Bone refID="35" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-12.4,2.34006,-10"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="30" designID="3614">
|
||||
<Part refID="36" designID="3614" materials="23">
|
||||
<Bone refID="36" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-13.8325,1.67819,-7.52268"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="31" designID="74340">
|
||||
<Part refID="37" designID="3679" materials="23">
|
||||
<Bone refID="37" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.8327,4.83176,-5.19592"/>
|
||||
</Part>
|
||||
<Part refID="38" designID="3680" materials="23">
|
||||
<Bone refID="38" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-17.705,4.54233,-5.24437"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="32" designID="3046">
|
||||
<Part refID="39" designID="3046" materials="23">
|
||||
<Bone refID="39" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-4.4,0,1.2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="33" designID="4085">
|
||||
<Part refID="40" designID="4085" materials="23">
|
||||
<Bone refID="40" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.6,0.959991,0.399994"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="34" designID="3688">
|
||||
<Part refID="41" designID="3688" materials="23">
|
||||
<Bone refID="41" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-3.60001,0.959991,2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
<Brick refID="35" designID="73587">
|
||||
<Part refID="42" designID="4592" materials="23">
|
||||
<Bone refID="42" transformation="1,0,0,0,1,0,0,0,1,7.60001,6.10352e-05,-9.2"/>
|
||||
</Part>
|
||||
<Part refID="43" designID="4593" materials="23">
|
||||
<Bone refID="43" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,7.6,0.420074,-9.2"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
<RigidSystems>
|
||||
<RigidSystem>
|
||||
<Rigid refID="0" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,5.9959182739257812,459.87173461914062,69.367317199707031" boneRefs="0"/>
|
||||
<Rigid refID="1" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.0443649291992187,459.58230590820312,69.494972229003906" boneRefs="1"/>
|
||||
<Rigid refID="2" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.7689208984375,458.32742309570312,71.188117980957031" boneRefs="2,3"/>
|
||||
<Rigid refID="3" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,6.8173675537109375,458.03802490234375,71.315757751464844" boneRefs="4"/>
|
||||
<Rigid refID="4" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,7.54193115234375,456.78311157226562,73.008895874023438" boneRefs="5"/>
|
||||
<Rigid refID="5" transformation="0.91537082195281982,0.28058713674545288,0.28873366117477417,-0.15140815079212189,0.90441381931304932,-0.39888754487037659,-0.37305742502212524,0.32141336798667908,0.87035715579986572,8.3226776123046875,456.71817016601562,73.367523193359375" boneRefs="6"/>
|
||||
<Rigid refID="6" transformation="0,0,-1,0,1,0,1,0,0,17.19999885559082,455.3599853515625,78.799995422363281" boneRefs="7,8,9,10,11,12,13,14,15"/>
|
||||
<Rigid refID="7" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,10.800004959106445,457.38003540039062,74.800003051757813" boneRefs="16"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="0" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
|
||||
<RigidRef rigidRef="1" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="1" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
<RigidRef rigidRef="2" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="2" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
|
||||
<RigidRef rigidRef="3" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
|
||||
<RigidRef rigidRef="3" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="5" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
|
||||
<RigidRef rigidRef="4" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
</Joint>
|
||||
<Joint type="ball">
|
||||
<RigidRef rigidRef="6" a="-1,0,0" z="0,1,0" t="4.799992561340332,1.7600365877151489,-9.1999912261962891"/>
|
||||
<RigidRef rigidRef="5" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="6" a="1,0,0" z="0,1,0" t="3.9999923706054687,2.0200366973876953,-6.3999929428100586"/>
|
||||
<RigidRef rigidRef="7" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid refID="8" transformation="1,0,0,0,1,0,0,0,1,-1.1999999284744263,455.99996948242187,83.599998474121094" boneRefs="17"/>
|
||||
<Rigid refID="9" transformation="1,0,0,0,1,0,0,0,1,0.40000009536743164,455.99996948242187,83.600006103515625" boneRefs="18,19"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="9" a="0,1,0" z="0,0,1" t="-0.80000007152557373,0,-0.8000030517578125"/>
|
||||
<RigidRef rigidRef="8" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid refID="10" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-26.000003814697266,455.03997802734375,58.799995422363281" boneRefs="20"/>
|
||||
<Rigid refID="11" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-26.000003814697266,455.45999145507812,58.799995422363281" boneRefs="21"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="10" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
|
||||
<RigidRef rigidRef="11" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid refID="12" transformation="0.28873360157012939,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140815079212189,0.87035715579986572,0.3214133083820343,0.37305739521980286,-20.811885833740234,458.327392578125,62.031078338623047" boneRefs="22,24"/>
|
||||
<Rigid refID="13" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-20.684246063232422,458.03799438476562,61.982631683349609" boneRefs="23"/>
|
||||
<Rigid refID="14" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.991107940673828,456.7830810546875,61.258068084716797" boneRefs="25"/>
|
||||
<Rigid refID="15" transformation="0,0,1,0,0.99999994039535522,0,-1,0,0,-17.200000762939453,456.95999145507812,60.399990081787109" boneRefs="26,27,28,29,30,31,32,33,34"/>
|
||||
<Rigid refID="16" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-18.632480621337891,456.7181396484375,60.477321624755859" boneRefs="36"/>
|
||||
<Rigid refID="17" transformation="0,-0.71323662996292114,-0.70092326402664185,0,0.70092320442199707,-0.71323668956756592,1,0,0,-17.200000762939453,457.3800048828125,57.999996185302734" boneRefs="35"/>
|
||||
<Rigid refID="18" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.632686614990234,459.8717041015625,62.804080963134766" boneRefs="37"/>
|
||||
<Rigid refID="19" transformation="0.28873363137245178,0.28058710694313049,-0.91537082195281982,-0.39888754487037659,0.90441375970840454,0.15140816569328308,0.87035715579986572,0.32141333818435669,0.37305739521980286,-22.505031585693359,459.582275390625,62.755634307861328" boneRefs="38"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="12" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
|
||||
<RigidRef rigidRef="13" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="12" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
|
||||
<RigidRef rigidRef="19" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="13" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0,1.9199999570846558,-0.80000007152557373"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="16" a="0,1,0" z="0,0,1" t="0,0.31999999284744263,0"/>
|
||||
<RigidRef rigidRef="14" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,0"/>
|
||||
</Joint>
|
||||
<Joint type="ball">
|
||||
<RigidRef rigidRef="15" a="0,0,-1" z="0,1,0" t="0.40000000596046448,0.15999999642372131,0.80000001192092896"/>
|
||||
<RigidRef rigidRef="16" a="0,0,1" z="0,-1,0" t="0,0.15999999642372131,0.80000001192092896"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="15" a="0,0,1" z="0,1,0" t="-2.3999977111816406,0.41999998688697815,0"/>
|
||||
<RigidRef rigidRef="17" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
|
||||
</Joint>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="18" a="0,-1,0" z="1,0,0" t="0.40000000596046448,-0.21002300083637238,-0.40000000596046448"/>
|
||||
<RigidRef rigidRef="19" a="0,-1,0" z="-1,0,0" t="0.40000000596046448,0.10999999940395355,-0.40000000596046448"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid refID="20" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-9.2000007629394531,455.03994750976562,69.199996948242188" boneRefs="39,40"/>
|
||||
<Rigid refID="21" transformation="0,0,-1,0,0.99999994039535522,0,1,0,0,-8.4000053405761719,455.99993896484375,70" boneRefs="41"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="20" a="0,1,0" z="0,0,1" t="0,0.95999997854232788,0"/>
|
||||
<RigidRef rigidRef="21" a="0,1,0" z="0,0,1" t="0.80000007152557373,0,-0.80000007152557373"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
<RigidSystem>
|
||||
<Rigid refID="22" transformation="1,0,0,0,1,0,0,0,1,2.8000054359436035,455.04000854492187,58.799999237060547" boneRefs="42"/>
|
||||
<Rigid refID="23" transformation="0.70092326402664185,-0.71323668956756592,0,0.71323668956756592,0.70092326402664185,0,0,0,1,2.8000044822692871,455.46002197265625,58.799999237060547" boneRefs="43"/>
|
||||
<Joint type="hinge">
|
||||
<RigidRef rigidRef="22" a="0,0,-1" z="0,1,0" t="0,0.41999998688697815,0"/>
|
||||
<RigidRef rigidRef="23" a="0,0,-1" z="-1,0,0" t="0,0,0"/>
|
||||
</Joint>
|
||||
</RigidSystem>
|
||||
</RigidSystems>
|
||||
<GroupSystems>
|
||||
<GroupSystem>
|
||||
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="15,16">
|
||||
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="19,18,17">
|
||||
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="11,12,13">
|
||||
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="10,9,7,8"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
<Group transformation="1,0,0,0,1,0,0,0,1,0,0,0" pivot="0,0,0" partRefs="25,34,35,20,21,31,37,38,30,24,39,22,23,32,41,26,28,27,36,40,29,33"/>
|
||||
</GroupSystem>
|
||||
</GroupSystems>
|
||||
</LXFML>
|
||||
11
tests/dCommonTests/LxfmlTestFiles/too_few_values.lxfml
Normal file
11
tests/dCommonTests/LxfmlTestFiles/too_few_values.lxfml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta></Meta>
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="74340">
|
||||
<Part refID="0" designID="3679">
|
||||
<Bone refID="0" transformation="1,0,0,0,1,0"/>
|
||||
</Part>
|
||||
</Brick>
|
||||
</Bricks>
|
||||
</LXFML>
|
||||
413
tests/dCommonTests/LxfmlTests.cpp
Normal file
413
tests/dCommonTests/LxfmlTests.cpp
Normal file
@@ -0,0 +1,413 @@
|
||||
#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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user