From 51981b7beee3ca8c348b7e15164fe9f1f3c51679 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 03:17:14 -0500 Subject: [PATCH] feat(property): resend the equipment on ResyncEquipment The property editor sends ResyncEquipment (1238, no payload) after the player picks up or sets down a model they carry: 59 live packets, every one right after PlaceModelResponse. Live answered with no game message, only replica serializations (packet 0x1b), i.e. the player's equipment sent again. DLU dropped it. The handler marks the inventory's equipped items dirty and serializes the player, so everyone gets the equipment again. That live re-sent the inventory part of the replica is inferred: the captures show a serialization but it was not decoded. Check in game: on your property, in build mode, pick up a model from the world and place it again a few times, then leave build mode: your hat, shirt, weapon etc. are all shown on you (and to a second player watching), none invisible. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/InventoryComponent.h | 5 ++++ dGame/dGameMessages/GameMessageHandler.cpp | 1 + dGame/dGameMessages/InventoryMessages.cpp | 7 ++++++ dGame/dGameMessages/InventoryMessages.h | 7 ++++++ .../ClientMessagesTests.cpp | 23 +++++++++++++++++++ 5 files changed, 43 insertions(+) diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 66305108c..202399065 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -301,6 +301,11 @@ public: */ void PopEquippedItems(); + /** + * Marks the equipped items to be sent again with the next serialization (ResyncEquipment) + */ + void ResyncEquipment() { m_Dirty = true; } + /** * Returns if the entity has an item equipped of the given lot * @param lot to lot to search for diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 9e4767855..9813c4b4a 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -98,6 +98,7 @@ namespace { { REQUEST_MOVE_ITEM_BETWEEN_INVENTORY_TYPES, []() { return std::make_unique(); } }, { PUSH_EQUIPPED_ITEMS_STATE, []() { return std::make_unique(); } }, { POP_EQUIPPED_ITEMS_STATE, []() { return std::make_unique(); } }, + { RESYNC_EQUIPMENT, []() { return std::make_unique(); } }, { CLIENT_ITEM_CONSUMED, []() { return std::make_unique(); } }, { USE_NON_EQUIPMENT_ITEM, []() { return std::make_unique(); } }, { SET_CONSUMABLE_ITEM, []() { return std::make_unique(); } }, diff --git a/dGame/dGameMessages/InventoryMessages.cpp b/dGame/dGameMessages/InventoryMessages.cpp index ea9e2f058..83fbf5693 100644 --- a/dGame/dGameMessages/InventoryMessages.cpp +++ b/dGame/dGameMessages/InventoryMessages.cpp @@ -422,6 +422,13 @@ namespace GameMessages { Game::entityManager->SerializeEntity(&entity); // so it updates on client side } + void ResyncEquipment::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* const inventoryComponent = entity.GetComponent(); + if (!inventoryComponent) return; + inventoryComponent->ResyncEquipment(); + Game::entityManager->SerializeEntity(&entity); + } + void UpdateInventoryGroup::Serialize(RakNet::BitStream& bitStream) const { BitStreamUtils::WriteLengthPrefixed(bitStream, action); BitStreamUtils::WriteLengthPrefixed(bitStream, groupID); diff --git a/dGame/dGameMessages/InventoryMessages.h b/dGame/dGameMessages/InventoryMessages.h index 136761278..ad6e7925d 100644 --- a/dGame/dGameMessages/InventoryMessages.h +++ b/dGame/dGameMessages/InventoryMessages.h @@ -288,6 +288,13 @@ namespace GameMessages { void Handle(Entity& entity, const SystemAddress& sysAddr) override; }; + // Client -> server. No payload. The property editor sends it after the player picks up or sets down a model + // they carry (live: right after PlaceModelResponse). Live answered with a serialization of the player, no message. + struct ResyncEquipment : public NetGameMsg { + ResyncEquipment() : NetGameMsg(MessageType::Game::RESYNC_EQUIPMENT) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + // Client -> server. Adds, modifies or removes an inventory group. struct UpdateInventoryGroup : public NetGameMsg { UpdateInventoryGroup() : NetGameMsg(MessageType::Game::UPDATE_INVENTORY_GROUP) {} diff --git a/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp index 0317725a0..9d332850b 100644 --- a/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp @@ -10,6 +10,8 @@ #include "SkillComponent.h" #include "SkillMessages.h" #include "Entity.h" +#include "InventoryComponent.h" +#include "InventoryMessages.h" #include "PlayerMessages.h" #include @@ -121,3 +123,24 @@ TEST_F(ClientMessagesTests, CasterDeadEndsTheDeadCastersSkill) { EXPECT_FALSE(skills->HasSkill(1)); EXPECT_TRUE(skills->HasSkill(2)); } + +// ResyncEquipment (1238): no payload (59 live packets). The next serialization carries the equipped items again. +TEST_F(ClientMessagesTests, ResyncEquipmentResendsTheEquipment) { + auto msg = FromLiveClientCapture(std::string(PLAYER_HEADER) + "d604"); + CDClientDatabase::Connect(":memory:"); // the inventory looks its component up + CDClientDatabase::ExecuteDML("CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);"); + auto* const inventory = player->AddComponent(-1); + RakNet::BitStream first; + inventory->Serialize(first, false); // clears the dirty flag it starts with + RakNet::BitStream clean; + inventory->Serialize(clean, false); + bool equipmentSent = true; + ASSERT_TRUE(clean.Read(equipmentSent)); + EXPECT_FALSE(equipmentSent); + + msg.Handle(*player, UNASSIGNED_SYSTEM_ADDRESS); + RakNet::BitStream resent; + inventory->Serialize(resent, false); + ASSERT_TRUE(resent.Read(equipmentSent)); + EXPECT_TRUE(equipmentSent); +}