mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
598 lines
16 KiB
C++
598 lines
16 KiB
C++
#include "Item.h"
|
|
#include "EconomyLedger.h"
|
|
|
|
#include <sstream>
|
|
|
|
#include "ObjectIDManager.h"
|
|
#include "GeneralUtils.h"
|
|
#include "GameMessages.h"
|
|
#include "Entity.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "EntityManager.h"
|
|
#include "RenderComponent.h"
|
|
#include "PossessableComponent.h"
|
|
#include "CharacterComponent.h"
|
|
#include "eItemType.h"
|
|
#include "AssetManager.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Loot.h"
|
|
#include "eObjectBits.h"
|
|
#include "eReplicaComponentType.h"
|
|
#include "eUseItemResponse.h"
|
|
#include "dZoneManager.h"
|
|
#include "ChatPackets.h"
|
|
#include "MissionComponent.h"
|
|
#include "eMissionTaskType.h"
|
|
|
|
#include "CDBrickIDTableTable.h"
|
|
#include "CDObjectSkillsTable.h"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDPackageComponentTable.h"
|
|
#include <ranges>
|
|
|
|
namespace {
|
|
const std::map<std::string, std::string> ExtraSettingAbbreviations = {
|
|
{ "assemblyPartLOTs", "ma" },
|
|
{ "blueprintID", "b" },
|
|
{ "userModelID", "ui" },
|
|
{ "userModelName", "un" },
|
|
{ "userModelDesc", "ud" },
|
|
{ "userModelHasBhvr", "ub" },
|
|
{ "userModelBehaviors", "ubh" },
|
|
{ "userModelBehaviorSourceID", "ubs" },
|
|
{ "userModelPhysicsType", "up" },
|
|
{ "userModelMod", "um" },
|
|
{ "userModelOpt", "uo" },
|
|
{ "reforgedLOT", "rl" },
|
|
};
|
|
}
|
|
|
|
Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_t slot, const uint32_t count, const bool bound, const LwoNameValue& config, const LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType) {
|
|
if (!Inventory::IsValidItem(lot)) {
|
|
return;
|
|
}
|
|
|
|
this->id = id;
|
|
this->lot = lot;
|
|
this->inventory = inventory;
|
|
this->slot = slot;
|
|
this->count = count;
|
|
this->bound = bound;
|
|
this->config = config;
|
|
this->parent = parent;
|
|
this->info = &Inventory::FindItemComponent(lot);
|
|
this->preconditions = new PreconditionExpression(this->info->reqPrecondition);
|
|
this->subKey = subKey;
|
|
|
|
inventory->AddManagedItem(this);
|
|
}
|
|
|
|
Item::Item(
|
|
const LOT lot,
|
|
Inventory* inventory,
|
|
const uint32_t slot,
|
|
const uint32_t count,
|
|
const LwoNameValue& config,
|
|
const LWOOBJID parent,
|
|
bool showFlyingLoot,
|
|
bool isModMoveAndEquip,
|
|
LWOOBJID subKey,
|
|
bool bound,
|
|
eLootSourceType lootSourceType) {
|
|
if (!Inventory::IsValidItem(lot)) {
|
|
return;
|
|
}
|
|
|
|
if (isModMoveAndEquip) {
|
|
showFlyingLoot = false;
|
|
}
|
|
|
|
this->lot = lot;
|
|
this->inventory = inventory;
|
|
this->slot = slot;
|
|
this->count = count;
|
|
this->config = config;
|
|
this->parent = parent;
|
|
this->id = LWOOBJID_EMPTY;
|
|
this->info = &Inventory::FindItemComponent(lot);
|
|
this->bound = info->isBOP || bound;
|
|
this->preconditions = new PreconditionExpression(this->info->reqPrecondition);
|
|
this->subKey = subKey;
|
|
|
|
auto* const inventoryComponent = inventory->GetComponent();
|
|
GenerateID();
|
|
|
|
inventory->AddManagedItem(this);
|
|
|
|
auto* entity = inventoryComponent->GetParent();
|
|
if (parent == LWOOBJID_EMPTY) EconomyLedger::RecordItems(entity, lot, this->count, static_cast<uint32_t>(lootSourceType));
|
|
GameMessages::SendAddItemToInventoryClientSync(entity, entity->GetSystemAddress(), this, id, showFlyingLoot, static_cast<int>(this->count), subKey, lootSourceType);
|
|
|
|
if (isModMoveAndEquip) {
|
|
Equip();
|
|
|
|
LOG("Move and equipped (%i) from (%i)", this->lot, this->inventory->GetType());
|
|
|
|
Game::entityManager->SerializeEntity(inventoryComponent->GetParent());
|
|
}
|
|
}
|
|
|
|
LWOOBJID Item::GetId() const {
|
|
return id;
|
|
}
|
|
|
|
LOT Item::GetLot() const {
|
|
return lot;
|
|
}
|
|
|
|
uint32_t Item::GetCount() const {
|
|
return count;
|
|
}
|
|
|
|
uint32_t Item::GetSlot() const {
|
|
return slot;
|
|
}
|
|
|
|
const LwoNameValue& Item::GetConfig() const {
|
|
return config;
|
|
}
|
|
|
|
LwoNameValue& Item::GetConfig() {
|
|
return config;
|
|
}
|
|
|
|
const CDItemComponent& Item::GetInfo() const {
|
|
return *info;
|
|
}
|
|
|
|
bool Item::GetBound() const {
|
|
return bound;
|
|
}
|
|
|
|
Inventory* Item::GetInventory() const {
|
|
return inventory;
|
|
}
|
|
|
|
LWOOBJID Item::GetParent() const {
|
|
return parent;
|
|
}
|
|
|
|
LWOOBJID Item::GetSubKey() const {
|
|
return subKey;
|
|
}
|
|
|
|
PreconditionExpression* Item::GetPreconditionExpression() const {
|
|
return preconditions;
|
|
}
|
|
|
|
void Item::SetCount(const uint32_t value, const bool silent, const bool disassemble, const bool showFlyingLoot, eLootSourceType lootSourceType) {
|
|
if (value == count) {
|
|
return;
|
|
}
|
|
|
|
const auto delta = std::abs(static_cast<int32_t>(value) - static_cast<int32_t>(count));
|
|
|
|
const auto type = static_cast<eItemType>(info->itemType);
|
|
|
|
if (disassemble) {
|
|
if (value < count) {
|
|
for (auto i = 0; i < delta; ++i) {
|
|
Disassemble();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!silent) {
|
|
auto* entity = inventory->GetComponent()->GetParent();
|
|
|
|
if (value > count) {
|
|
GameMessages::SendAddItemToInventoryClientSync(entity, entity->GetSystemAddress(), this, id, showFlyingLoot, delta, LWOOBJID_EMPTY, lootSourceType);
|
|
} else {
|
|
GameMessages::SendRemoveItemFromInventory(entity, entity->GetSystemAddress(), id, lot, inventory->GetType(), delta, value);
|
|
}
|
|
}
|
|
|
|
// Proxies (sub-items of a set) come and go with their parent and are not items of their own
|
|
if (parent == LWOOBJID_EMPTY) {
|
|
EconomyLedger::RecordItems(inventory->GetComponent()->GetParent(), lot, static_cast<int64_t>(value) - static_cast<int64_t>(count), static_cast<uint32_t>(lootSourceType));
|
|
}
|
|
|
|
count = value;
|
|
|
|
if (count == 0) {
|
|
RemoveFromInventory();
|
|
}
|
|
}
|
|
|
|
void Item::SetSlot(const uint32_t value) {
|
|
if (slot == value) {
|
|
return;
|
|
}
|
|
|
|
for (const auto& pair : inventory->GetItems()) {
|
|
auto* item = pair.second;
|
|
|
|
if (item->slot == value) {
|
|
item->slot = slot;
|
|
}
|
|
}
|
|
|
|
slot = value;
|
|
}
|
|
|
|
void Item::SetBound(const bool value) {
|
|
bound = value;
|
|
}
|
|
|
|
void Item::SetSubKey(LWOOBJID value) {
|
|
subKey = value;
|
|
}
|
|
|
|
void Item::SetInventory(Inventory* value) {
|
|
inventory->RemoveManagedItem(this);
|
|
|
|
inventory = value;
|
|
|
|
inventory->AddManagedItem(this);
|
|
}
|
|
|
|
void Item::Equip(const bool skipChecks) {
|
|
if (IsEquipped()) {
|
|
return;
|
|
}
|
|
|
|
inventory->GetComponent()->EquipItem(this, skipChecks);
|
|
}
|
|
|
|
void Item::UnEquip() {
|
|
if (!IsEquipped()) {
|
|
return;
|
|
}
|
|
|
|
inventory->GetComponent()->UnEquipItem(this);
|
|
}
|
|
|
|
bool Item::IsEquipped() const {
|
|
auto* component = inventory->GetComponent();
|
|
|
|
for (const auto& pair : component->GetEquippedItems()) {
|
|
const auto item = pair.second;
|
|
|
|
if (item.id == id) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Item::Consume() {
|
|
auto* const skillsTable = CDClientManager::GetTable<CDObjectSkillsTable>();
|
|
|
|
const auto skills = skillsTable->Query([this](const CDObjectSkills& entry) {
|
|
return entry.objectTemplate == static_cast<uint32_t>(lot);
|
|
});
|
|
|
|
auto success = false;
|
|
|
|
for (auto& skill : skills) {
|
|
if (skill.castOnType == 3) // Consumable type
|
|
{
|
|
success = true;
|
|
}
|
|
}
|
|
|
|
LOG_DEBUG("Consumed LOT (%i) itemID (%llu). Success=(%d)", lot, id, success);
|
|
|
|
GameMessages::SendUseItemResult(inventory->GetComponent()->GetParent(), lot, success);
|
|
|
|
const auto myLot = this->lot;
|
|
if (success && inventory->GetComponent()->RemoveItem(lot, 1, eInventoryType::ALL)) {
|
|
// Save this because if this is the last item in the inventory
|
|
// we may delete ourself (lol)
|
|
auto* missionComponent = inventory->GetComponent()->GetParent()->GetComponent<MissionComponent>();
|
|
if (missionComponent) missionComponent->Progress(eMissionTaskType::GATHER, myLot, LWOOBJID_EMPTY, "", -1);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void Item::UseNonEquip(Item* item) {
|
|
LOT thisLot = this->GetLot();
|
|
if (!GetInventory()) {
|
|
LOG_DEBUG("item %i has no inventory??", this->GetLot());
|
|
return;
|
|
}
|
|
|
|
auto* playerInventoryComponent = GetInventory()->GetComponent();
|
|
if (!playerInventoryComponent) {
|
|
LOG_DEBUG("no inventory component attached to item id %llu lot %i", this->GetId(), this->GetLot());
|
|
return;
|
|
}
|
|
|
|
auto* playerEntity = playerInventoryComponent->GetParent();
|
|
if (!playerEntity) {
|
|
LOG_DEBUG("no player entity attached to inventory? item id is %llu", this->GetId());
|
|
return;
|
|
}
|
|
|
|
const auto type = static_cast<eItemType>(info->itemType);
|
|
if (type == eItemType::MOUNT) {
|
|
if (Game::zoneManager->GetMountsAllowed()) {
|
|
playerInventoryComponent->HandlePossession(this);
|
|
} else {
|
|
ChatPackets::SendSystemMessage(playerEntity->GetSystemAddress(), u"Mounts are not allowed in this zone");
|
|
}
|
|
} else if (type == eItemType::PET_INVENTORY_ITEM && subKey != LWOOBJID_EMPTY) {
|
|
if (Game::zoneManager->GetPetsAllowed()) {
|
|
const auto& databasePet = playerInventoryComponent->GetDatabasePet(subKey);
|
|
if (databasePet.lot != LOT_NULL) {
|
|
playerInventoryComponent->SpawnPet(this);
|
|
}
|
|
} else {
|
|
ChatPackets::SendSystemMessage(playerEntity->GetSystemAddress(), u"Pets are not allowed in this zone");
|
|
}
|
|
// This precondition response is taken care of in SpawnPet().
|
|
} else {
|
|
bool success = false;
|
|
auto inventory = item->GetInventory();
|
|
if (inventory && inventory->GetType() == eInventoryType::ITEMS) {
|
|
auto* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
const auto packageComponentId = compRegistryTable->GetByIDAndType(lot, eReplicaComponentType::PACKAGE);
|
|
|
|
if (packageComponentId == 0) return;
|
|
|
|
auto* packCompTable = CDClientManager::GetTable<CDPackageComponentTable>();
|
|
auto packages = packCompTable->Query([=](const CDPackageComponent entry) {return entry.id == static_cast<uint32_t>(packageComponentId); });
|
|
|
|
auto success = !packages.empty();
|
|
if (success) {
|
|
if (this->GetPreconditionExpression()->Check(playerInventoryComponent->GetParent())) {
|
|
auto* entityParent = playerInventoryComponent->GetParent();
|
|
// Roll the loot for all the packages then see if it all fits. If it fits, give it to the player, otherwise don't.
|
|
Loot::Return rolledLoot{};
|
|
for (auto& pack : packages) {
|
|
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);
|
|
if (existingLoot == rolledLoot.end()) {
|
|
rolledLoot.insert(loot);
|
|
} else {
|
|
existingLoot->second += loot.second;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (playerInventoryComponent->HasSpaceForLoot(rolledLoot)) {
|
|
Loot::GiveLoot(playerInventoryComponent->GetParent(), rolledLoot, eLootSourceType::CONSUMPTION);
|
|
item->SetCount(item->GetCount() - 1);
|
|
} else {
|
|
success = false;
|
|
}
|
|
} else {
|
|
GameMessages::SendUseItemRequirementsResponse(
|
|
playerInventoryComponent->GetParent()->GetObjectID(),
|
|
playerInventoryComponent->GetParent()->GetSystemAddress(),
|
|
eUseItemResponse::FailedPrecondition
|
|
);
|
|
success = false;
|
|
}
|
|
}
|
|
}
|
|
LOG_DEBUG("Player %llu %s used item %i", playerEntity->GetObjectID(), success ? "successfully" : "unsuccessfully", thisLot);
|
|
GameMessages::SendUseItemResult(playerInventoryComponent->GetParent(), thisLot, success);
|
|
}
|
|
}
|
|
|
|
void Item::Disassemble(const eInventoryType inventoryType) {
|
|
for (const auto& data : config.values | std::views::values) {
|
|
if (data->GetKey() == u"assemblyPartLOTs") {
|
|
auto modStr = data->GetValueAsString();
|
|
|
|
// This shouldn't be null but always check your pointers.
|
|
if (GetInventory()) {
|
|
auto inventoryComponent = GetInventory()->GetComponent();
|
|
if (inventoryComponent) {
|
|
auto entity = inventoryComponent->GetParent();
|
|
if (entity) entity->SetVar<std::string>(u"currentModifiedBuild", modStr);
|
|
}
|
|
}
|
|
|
|
std::vector<LOT> modArray;
|
|
|
|
std::stringstream ssData(modStr);
|
|
|
|
std::string token;
|
|
|
|
const auto deliminator = '+';
|
|
|
|
while (std::getline(ssData, token, deliminator)) {
|
|
if (token.size() <= 2) continue; // invalid token, must have size of at least 3.
|
|
const auto modLot = GeneralUtils::TryParse(token.substr(2, token.size() - 1), LOT_NULL);
|
|
|
|
modArray.push_back(modLot);
|
|
}
|
|
|
|
for (const auto mod : modArray) {
|
|
inventory->GetComponent()->AddItem(mod, 1, eLootSourceType::DELETION, inventoryType);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Item::DisassembleModel(uint32_t numToDismantle) {
|
|
auto* table = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
|
|
const auto componentId = table->GetByIDAndType(GetLot(), eReplicaComponentType::RENDER);
|
|
|
|
auto query = CDClientDatabase::CreatePreppedStmt("SELECT render_asset, LXFMLFolder FROM RenderComponent WHERE id = ?;");
|
|
query.bind(1, static_cast<int>(componentId));
|
|
|
|
auto result = query.execQuery();
|
|
|
|
if (result.eof() || result.fieldIsNull("render_asset")) {
|
|
return;
|
|
}
|
|
|
|
std::string renderAsset = std::string(result.getStringField("render_asset"));
|
|
|
|
// normalize path slashes
|
|
for (auto& c : renderAsset) {
|
|
if (c == '\\') c = '/';
|
|
}
|
|
|
|
std::string lxfmlFolderName = std::string(result.getStringField("LXFMLFolder"));
|
|
if (!lxfmlFolderName.empty()) lxfmlFolderName.insert(0, "/");
|
|
|
|
std::vector<std::string> renderAssetSplit = GeneralUtils::SplitString(renderAsset, '/');
|
|
if (renderAssetSplit.empty()) return;
|
|
|
|
const auto renderAssetSplitSplit = GeneralUtils::SplitString(renderAssetSplit.back(), '.');
|
|
if (renderAssetSplitSplit.empty()) return;
|
|
|
|
std::string lxfmlPath = "BrickModels" + lxfmlFolderName + "/" + renderAssetSplitSplit[0] + ".lxfml";
|
|
auto file = Game::assetManager->GetFile(lxfmlPath.c_str());
|
|
|
|
if (!file) {
|
|
LOG("Failed to load %s to disassemble model into bricks, check that this file exists", lxfmlPath.c_str());
|
|
return;
|
|
}
|
|
|
|
std::stringstream data;
|
|
data << file.rdbuf();
|
|
|
|
uint32_t fileSize;
|
|
file.seekg(0, std::ios::end);
|
|
fileSize = static_cast<uint32_t>(file.tellg());
|
|
file.seekg(0, std::ios::beg);
|
|
|
|
if (fileSize == 0) return;
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
if (doc.Parse(data.str().c_str(), data.str().size()) != tinyxml2::XML_SUCCESS) {
|
|
return;
|
|
}
|
|
|
|
auto* lxfml = doc.FirstChildElement("LXFML");
|
|
if (!lxfml) return;
|
|
auto* bricks = lxfml->FirstChildElement("Bricks");
|
|
std::string searchTerm = "Brick";
|
|
|
|
if (!bricks) {
|
|
searchTerm = "Part";
|
|
auto* scene = lxfml->FirstChildElement("Scene");
|
|
if (!scene) return;
|
|
|
|
auto* model = scene->FirstChildElement("Model");
|
|
if (!model) return;
|
|
|
|
bricks = model->FirstChildElement("Group");
|
|
if (!bricks) return;
|
|
}
|
|
|
|
auto* currentBrick = bricks->FirstChildElement(searchTerm.c_str());
|
|
|
|
// First iteration gets the count
|
|
std::map<int32_t, int32_t> parts;
|
|
while (currentBrick) {
|
|
const char* const designID = currentBrick->Attribute("designID");
|
|
if (designID) {
|
|
const auto designId = GeneralUtils::TryParse<uint32_t>(designID);
|
|
if (!designId) {
|
|
LOG("Failed to parse designID %s", designID);
|
|
continue;
|
|
}
|
|
parts[designId.value()]++;
|
|
}
|
|
|
|
currentBrick = currentBrick->NextSiblingElement(searchTerm.c_str());
|
|
}
|
|
|
|
auto* brickIDTable = CDClientManager::GetTable<CDBrickIDTableTable>();
|
|
|
|
// Second iteration actually distributes the bricks
|
|
for (const auto& [part, count] : parts) {
|
|
const auto partLocal = part;
|
|
const auto brickID = brickIDTable->Query([&](const CDBrickIDTable& entry) {
|
|
return entry.LEGOBrickID == partLocal;
|
|
});
|
|
|
|
if (brickID.empty()) continue;
|
|
|
|
GetInventory()->GetComponent()->AddItem(brickID[0].NDObjectID, count * numToDismantle, eLootSourceType::DELETION);
|
|
}
|
|
}
|
|
|
|
void Item::RemoveFromInventory() {
|
|
UnEquip();
|
|
|
|
count = 0;
|
|
|
|
inventory->RemoveManagedItem(this);
|
|
|
|
delete this;
|
|
}
|
|
|
|
Item::~Item() {
|
|
delete preconditions;
|
|
}
|
|
|
|
void Item::SaveConfigXml(tinyxml2::XMLElement& i) const {
|
|
tinyxml2::XMLElement* x = nullptr;
|
|
|
|
for (const auto& config : config.values | std::views::values) {
|
|
const auto& key = GeneralUtils::UTF16ToWTF8(config->GetKey());
|
|
const auto saveKey = ExtraSettingAbbreviations.find(key);
|
|
if (saveKey == ExtraSettingAbbreviations.end()) {
|
|
continue;
|
|
}
|
|
|
|
if (!x) {
|
|
x = i.InsertNewChildElement("x");
|
|
}
|
|
|
|
const auto dataToSave = config->GetString(false);
|
|
x->SetAttribute(saveKey->second.c_str(), dataToSave.c_str());
|
|
}
|
|
}
|
|
|
|
void Item::LoadConfigXml(const tinyxml2::XMLElement& i) {
|
|
const auto* x = i.FirstChildElement("x");
|
|
if (!x) return;
|
|
|
|
for (const auto& [fullName, abbreviation] : ExtraSettingAbbreviations) {
|
|
const auto* data = x->Attribute(abbreviation.c_str());
|
|
if (!data) continue;
|
|
|
|
config.ParseInsert(fullName + "=" + data);
|
|
}
|
|
}
|
|
|
|
LWOOBJID Item::GenerateID() {
|
|
auto* const inventoryComponent = inventory->GetComponent();
|
|
const bool isPlayer = inventoryComponent->GetParent()->IsPlayer();
|
|
LWOOBJID id{};
|
|
|
|
// Only players and non-proxy items get persistent IDs (since they are the only ones that will persist between worlds)
|
|
if (isPlayer && parent == LWOOBJID_EMPTY) {
|
|
id = ObjectIDManager::GetPersistentID();
|
|
} else {
|
|
id = ObjectIDManager::GenerateObjectID();
|
|
GeneralUtils::SetBit(id, eObjectBits::SPAWNED);
|
|
GeneralUtils::SetBit(id, eObjectBits::CLIENT);
|
|
}
|
|
|
|
LOG("Parent %llu lot %u Generated id %u:%llu", parent, GetLot(), static_cast<uint32_t>(id), id);
|
|
const auto type = static_cast<eItemType>(info->itemType);
|
|
|
|
if (type == eItemType::MOUNT) {
|
|
GeneralUtils::SetBit(id, eObjectBits::CLIENT);
|
|
}
|
|
|
|
this->id = id;
|
|
return id;
|
|
}
|