From aaf446fe6ea7c21a28da25010b1f74af1efcabe5 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:38:21 -0700 Subject: [PATCH] feat: Add Inventory Brick and Model groups (#1587) * Added feature grouping logic * Add saving of brick buckets * Add edge case check for max group count * Use vector for storing groups * Update InventoryComponent.cpp * Update InventoryComponent.h * Update InventoryComponent.h * fix string log format * Update GameMessages.cpp --- dCommon/dEnums/eInventoryType.h | 9 ++ dGame/dComponents/InventoryComponent.cpp | 124 +++++++++++++++++++++ dGame/dComponents/InventoryComponent.h | 43 ++++++- dGame/dGameMessages/GameMessageHandler.cpp | 7 ++ dGame/dGameMessages/GameMessages.cpp | 63 +++++++++++ dGame/dGameMessages/GameMessages.h | 3 + 6 files changed, 248 insertions(+), 1 deletion(-) diff --git a/dCommon/dEnums/eInventoryType.h b/dCommon/dEnums/eInventoryType.h index 12573aa4..1c6688b2 100644 --- a/dCommon/dEnums/eInventoryType.h +++ b/dCommon/dEnums/eInventoryType.h @@ -4,6 +4,9 @@ #define __EINVENTORYTYPE__H__ #include + +#include "magic_enum.hpp" + static const uint8_t NUMBER_OF_INVENTORIES = 17; /** * Represents the different types of inventories an entity may have @@ -56,4 +59,10 @@ public: }; }; +template <> +struct magic_enum::customize::enum_range { + static constexpr int min = 0; + static constexpr int max = 16; +}; + #endif //!__EINVENTORYTYPE__H__ diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 172877b0..d6883e17 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -37,6 +37,9 @@ #include "CDScriptComponentTable.h" #include "CDObjectSkillsTable.h" #include "CDSkillBehaviorTable.h" +#include "StringifiedEnum.h" + +#include InventoryComponent::InventoryComponent(Entity* parent) : Component(parent) { this->m_Dirty = true; @@ -492,6 +495,11 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) { return; } + auto* const groups = inventoryElement->FirstChildElement("grps"); + if (groups) { + LoadGroupXml(*groups); + } + m_Consumable = inventoryElement->IntAttribute("csl", LOT_NULL); auto* bag = bags->FirstChildElement(); @@ -630,6 +638,15 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) { bags->LinkEndChild(bag); } + auto* groups = inventoryElement->FirstChildElement("grps"); + if (groups) { + groups->DeleteChildren(); + } else { + groups = inventoryElement->InsertNewChildElement("grps"); + } + + UpdateGroupXml(*groups); + auto* items = inventoryElement->FirstChildElement("items"); if (items == nullptr) { @@ -1603,3 +1620,110 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) { m_Skills.insert_or_assign(slot, skillId); return true; } + +void InventoryComponent::UpdateGroup(const GroupUpdate& groupUpdate) { + if (groupUpdate.groupId.empty()) return; + + if (groupUpdate.inventory != eInventoryType::BRICKS && groupUpdate.inventory != eInventoryType::MODELS) { + LOG("Invalid inventory type for grouping %s", StringifiedEnum::ToString(groupUpdate.inventory).data()); + return; + } + + auto& groups = m_Groups[groupUpdate.inventory]; + auto groupItr = std::ranges::find_if(groups, [&groupUpdate](const Group& group) { + return group.groupId == groupUpdate.groupId; + }); + + if (groupUpdate.command != GroupUpdateCommand::ADD && groupItr == groups.end()) { + LOG("Group %s not found in inventory %s. Cannot process command.", groupUpdate.groupId.c_str(), StringifiedEnum::ToString(groupUpdate.inventory).data()); + return; + } + + if (groupUpdate.command == GroupUpdateCommand::ADD && groups.size() >= MaximumGroupCount) { + LOG("Cannot add group to inventory %s. Maximum group count reached.", StringifiedEnum::ToString(groupUpdate.inventory).data()); + return; + } + + switch (groupUpdate.command) { + case GroupUpdateCommand::ADD: { + auto& group = groups.emplace_back(); + group.groupId = groupUpdate.groupId; + group.groupName = groupUpdate.groupName; + break; + } + case GroupUpdateCommand::ADD_LOT: { + groupItr->lots.insert(groupUpdate.lot); + break; + } + case GroupUpdateCommand::REMOVE: { + groups.erase(groupItr); + break; + } + case GroupUpdateCommand::REMOVE_LOT: { + groupItr->lots.erase(groupUpdate.lot); + break; + } + case GroupUpdateCommand::MODIFY: { + groupItr->groupName = groupUpdate.groupName; + break; + } + default: { + LOG("Invalid group update command %i", groupUpdate.command); + break; + } + } +} + +void InventoryComponent::UpdateGroupXml(tinyxml2::XMLElement& groups) const { + for (const auto& [inventory, groupsData] : m_Groups) { + for (const auto& group : groupsData) { + auto* const groupElement = groups.InsertNewChildElement("grp"); + + groupElement->SetAttribute("id", group.groupId.c_str()); + groupElement->SetAttribute("n", group.groupName.c_str()); + groupElement->SetAttribute("t", static_cast(inventory)); + groupElement->SetAttribute("u", 0); + std::ostringstream lots; + bool first = true; + for (const auto lot : group.lots) { + if (!first) lots << ' '; + first = false; + + lots << lot; + } + groupElement->SetAttribute("l", lots.str().c_str()); + } + } +} + +void InventoryComponent::LoadGroupXml(const tinyxml2::XMLElement& groups) { + auto* groupElement = groups.FirstChildElement("grp"); + + while (groupElement) { + const char* groupId = nullptr; + const char* groupName = nullptr; + const char* lots = nullptr; + uint32_t inventory = eInventoryType::INVALID; + + groupElement->QueryStringAttribute("id", &groupId); + groupElement->QueryStringAttribute("n", &groupName); + groupElement->QueryStringAttribute("l", &lots); + groupElement->QueryAttribute("t", &inventory); + + if (!groupId || !groupName || !lots) { + LOG("Failed to load group from xml id %i name %i lots %i", + groupId == nullptr, groupName == nullptr, lots == nullptr); + } else { + auto& group = m_Groups[static_cast(inventory)].emplace_back(); + group.groupId = groupId; + group.groupName = groupName; + + for (const auto& lotStr : GeneralUtils::SplitString(lots, ' ')) { + auto lot = GeneralUtils::TryParse(lotStr); + if (lot) group.lots.insert(*lot); + } + } + + groupElement = groupElement->NextSiblingElement("grp"); + } +} diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index a1eb14d1..28158ab5 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -37,6 +37,35 @@ enum class eItemType : int32_t; */ class InventoryComponent final : public Component { public: + struct Group { + // Generated ID for the group. The ID is sent by the client and has the format user_group + Math.random() * UINT_MAX. + std::string groupId; + // Custom name assigned by the user. + std::string groupName; + // All the lots the user has in the group. + std::set lots; + }; + + enum class GroupUpdateCommand { + ADD, + ADD_LOT, + MODIFY, + REMOVE, + REMOVE_LOT, + }; + + // Based on the command, certain fields will be used or not used. + // for example, ADD_LOT wont use groupName, MODIFY wont use lots, etc. + struct GroupUpdate { + std::string groupId; + std::string groupName; + LOT lot; + eInventoryType inventory; + GroupUpdateCommand command; + }; + + static constexpr uint32_t MaximumGroupCount = 50; + static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::INVENTORY; InventoryComponent(Entity* parent); @@ -367,14 +396,23 @@ public: */ void UnequipScripts(Item* unequippedItem); - std::map GetSkills(){ return m_Skills; }; + std::map GetSkills() { return m_Skills; }; bool SetSkill(int slot, uint32_t skillId); bool SetSkill(BehaviorSlot slot, uint32_t skillId); + void UpdateGroup(const GroupUpdate& groupUpdate); + void RemoveGroup(const std::string& groupId); + ~InventoryComponent() override; private: + /** + * The key is the inventory the group belongs to, the value maps' key is the id for the group. + * This is only used for bricks and model inventories. + */ + std::map> m_Groups{ { eInventoryType::BRICKS, {} }, { eInventoryType::MODELS, {} } }; + /** * All the inventory this entity possesses */ @@ -477,6 +515,9 @@ private: * @param document the xml doc to load from */ void UpdatePetXml(tinyxml2::XMLDocument& document); + + void LoadGroupXml(const tinyxml2::XMLElement& groups); + void UpdateGroupXml(tinyxml2::XMLElement& groups) const; }; #endif diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index d2432e36..76fabce2 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -685,6 +685,13 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System case eGameMessageType::REQUEST_VENDOR_STATUS_UPDATE: GameMessages::SendVendorStatusUpdate(entity, sysAddr, true); break; + case eGameMessageType::UPDATE_INVENTORY_GROUP: + GameMessages::HandleUpdateInventoryGroup(inStream, entity, sysAddr); + break; + case eGameMessageType::UPDATE_INVENTORY_GROUP_CONTENTS: + GameMessages::HandleUpdateInventoryGroupContents(inStream, entity, sysAddr); + break; + default: LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 4bd667b4..2fba411b 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -6251,6 +6251,69 @@ void GameMessages::SendSlashCommandFeedbackText(Entity* entity, std::u16string t SEND_PACKET; } +void GameMessages::HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { + std::string action; + std::u16string groupName; + InventoryComponent::GroupUpdate groupUpdate; + bool locked{}; // All groups are locked by default + + uint32_t size{}; + if (!inStream.Read(size)) return; + action.resize(size); + if (!inStream.Read(action.data(), size)) return; + + if (!inStream.Read(size)) return; + groupUpdate.groupId.resize(size); + if (!inStream.Read(groupUpdate.groupId.data(), size)) return; + + if (!inStream.Read(size)) return; + groupName.resize(size); + if (!inStream.Read(reinterpret_cast(groupName.data()), size * 2)) return; + + if (!inStream.Read(groupUpdate.inventory)) return; + if (!inStream.Read(locked)) return; + + groupUpdate.groupName = GeneralUtils::UTF16ToWTF8(groupName); + + if (action == "ADD") groupUpdate.command = InventoryComponent::GroupUpdateCommand::ADD; + else if (action == "MODIFY") groupUpdate.command = InventoryComponent::GroupUpdateCommand::MODIFY; + else if (action == "REMOVE") groupUpdate.command = InventoryComponent::GroupUpdateCommand::REMOVE; + else { + LOG("Invalid action %s", action.c_str()); + return; + } + + auto* inventoryComponent = entity->GetComponent(); + if (inventoryComponent) inventoryComponent->UpdateGroup(groupUpdate); +} + +void GameMessages::HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { + std::string action; + InventoryComponent::GroupUpdate groupUpdate; + + uint32_t size{}; + if (!inStream.Read(size)) return; + action.resize(size); + if (!inStream.Read(action.data(), size)) return; + + if (action == "ADD") groupUpdate.command = InventoryComponent::GroupUpdateCommand::ADD_LOT; + else if (action == "REMOVE") groupUpdate.command = InventoryComponent::GroupUpdateCommand::REMOVE_LOT; + else { + LOG("Invalid action %s", action.c_str()); + return; + } + + if (!inStream.Read(size)) return; + groupUpdate.groupId.resize(size); + if (!inStream.Read(groupUpdate.groupId.data(), size)) return; + + if (!inStream.Read(groupUpdate.inventory)) return; + if (!inStream.Read(groupUpdate.lot)) return; + + auto* inventoryComponent = entity->GetComponent(); + if (inventoryComponent) inventoryComponent->UpdateGroup(groupUpdate); +} + void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID) { CBITSTREAM; CMSGHEADER; diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index e3c16471..090fcd4b 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -673,6 +673,9 @@ namespace GameMessages { void HandleCancelDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity); void SendSlashCommandFeedbackText(Entity* entity, std::u16string text); + + void HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); + void HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID); };