diff --git a/dGame/dGameMessages/InventoryMessages.cpp b/dGame/dGameMessages/InventoryMessages.cpp index e3bd8cf61..ea9e2f058 100644 --- a/dGame/dGameMessages/InventoryMessages.cpp +++ b/dGame/dGameMessages/InventoryMessages.cpp @@ -168,6 +168,7 @@ namespace GameMessages { bitStream.Write(bIgnoreCooldown); bitStream.Write(bOutSuccess); bitStream.Write(itemToUnequip); + BitStreamUtils::WriteOptional(bitStream, replacementObjectID, LWOOBJID_EMPTY); } bool UnEquipInventory::Deserialize(RakNet::BitStream& bitStream) { @@ -175,6 +176,7 @@ namespace GameMessages { VALIDATE_READ(bitStream.Read(bIgnoreCooldown)); VALIDATE_READ(bitStream.Read(bOutSuccess)); VALIDATE_READ(bitStream.Read(itemToUnequip)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, replacementObjectID, LWOOBJID_EMPTY)); return true; } diff --git a/dGame/dGameMessages/InventoryMessages.h b/dGame/dGameMessages/InventoryMessages.h index a76e8867e..136761278 100644 --- a/dGame/dGameMessages/InventoryMessages.h +++ b/dGame/dGameMessages/InventoryMessages.h @@ -93,8 +93,8 @@ namespace GameMessages { LWOOBJID itemToEquip{}; }; - // Client -> server. The client also sends an optional replacementObjectID after itemToUnequip, which DLU - // has never read (and still does not). + // Both directions: the client asks to unequip; the server tells the client an item was unequipped when it took + // it away (live: bIgnoreCooldown set, after RemoveItemFromInventory). struct UnEquipInventory : public NetGameMsg { UnEquipInventory() : NetGameMsg(MessageType::Game::UN_EQUIP_INVENTORY) {} void Serialize(RakNet::BitStream& bitStream) const override; @@ -105,6 +105,7 @@ namespace GameMessages { bool bIgnoreCooldown{}; bool bOutSuccess{}; LWOOBJID itemToUnequip{}; + LWOOBJID replacementObjectID{ LWOOBJID_EMPTY }; // optional }; // Client -> server. diff --git a/dGame/dGameMessages/SkillMessages.cpp b/dGame/dGameMessages/SkillMessages.cpp index eba5413d5..b2a9b61c3 100644 --- a/dGame/dGameMessages/SkillMessages.cpp +++ b/dGame/dGameMessages/SkillMessages.cpp @@ -48,6 +48,15 @@ namespace GameMessages { return true; } + void UncastSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(skillID); + } + + bool UncastSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(skillID)); + return true; + } + void SelectSkill::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(bFromSkillSet); bitStream.Write(skillID); diff --git a/dGame/dGameMessages/SkillMessages.h b/dGame/dGameMessages/SkillMessages.h index c6cd5f14b..0be118386 100644 --- a/dGame/dGameMessages/SkillMessages.h +++ b/dGame/dGameMessages/SkillMessages.h @@ -39,6 +39,16 @@ namespace GameMessages { TSkillID skillID{}; }; + // Server -> client, to the player only. The client's SkillComponent ends its running instance of the skill + // (LWOSkillComponent::msgUncastSkill, 0x00bd86d0): live sent it for an unequipped item's equip skills. + struct UncastSkill : public NetGameMsg { + UncastSkill() : NetGameMsg(MessageType::Game::UNCAST_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t skillID{}; + }; + // Client -> server. The player picked a skill; DLU only uses it to fix invisible items on first load. struct SelectSkill : public NetGameMsg { SelectSkill() : NetGameMsg(MessageType::Game::SELECT_SKILL) {} diff --git a/dGame/dGameMessages/ZoneMessages.cpp b/dGame/dGameMessages/ZoneMessages.cpp index 6d45a2572..36c0f8884 100644 --- a/dGame/dGameMessages/ZoneMessages.cpp +++ b/dGame/dGameMessages/ZoneMessages.cpp @@ -238,11 +238,12 @@ namespace GameMessages { } void ChangeObjectWorldState::Serialize(RakNet::BitStream& bitStream) const { - bitStream.Write(newState); + BitStreamUtils::WriteOptional(bitStream, newState, eObjectWorldState::INWORLD); } bool ChangeObjectWorldState::Deserialize(RakNet::BitStream& bitStream) { - return bitStream.Read(newState); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, newState, eObjectWorldState::INWORLD)); + return true; } void LocalizedAnnouncementServerToSingleClient::Serialize(RakNet::BitStream& bitStream) const { diff --git a/dGame/dGameMessages/ZoneMessages.h b/dGame/dGameMessages/ZoneMessages.h index 0597e1ff7..ccf1c8306 100644 --- a/dGame/dGameMessages/ZoneMessages.h +++ b/dGame/dGameMessages/ZoneMessages.h @@ -82,13 +82,14 @@ namespace GameMessages { void Handle(Entity& entity, const SystemAddress& sysAddr) override; }; - // Server -> client, to one client (UNASSIGNED broadcasts). + // Server -> client, to one client (UNASSIGNED broadcasts). newState is optional (a flag, then the state when it + // isn't INWORLD), as live sent it (e.g. 80 80 00 00 00 for ATTACHED). struct ChangeObjectWorldState : public NetGameMsg { ChangeObjectWorldState() : NetGameMsg(MessageType::Game::CHANGE_OBJECT_WORLD_STATE) {} void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; - eObjectWorldState newState{}; + eObjectWorldState newState{ eObjectWorldState::INWORLD }; // optional }; // Server -> client, to one client. The client looks both strings up in its locale (falling back to the text diff --git a/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp index 8f490b852..4781393fe 100644 --- a/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp @@ -347,7 +347,12 @@ TEST_F(InventoryMessagesTests, InboundReadsLikeLegacy) { unequip.bIgnoreCooldown = !flag; unequip.bOutSuccess = flag; unequip.itemToUnequip = id; - const auto legacyUnequip = ReadWithLegacy(unequip, LegacyGameMessages::ReadUnequipItem); + // The legacy read stopped before the optional replacementObjectID (its flag bit is left over) + RakNet::BitStream unequipWire; + unequip.Serialize(unequipWire); + RakNet::BitStream legacyUnequipStream(unequipWire.GetData(), unequipWire.GetNumberOfBytesUsed(), false); + const auto legacyUnequip = LegacyGameMessages::ReadUnequipItem(legacyUnequipStream); + EXPECT_EQ(legacyUnequipStream.GetReadOffset() + 1, unequipWire.GetNumberOfBitsUsed()); EXPECT_EQ(RoundTrip(unequip).itemToUnequip, legacyUnequip.objectID); ExpectTruncatedFails(unequip); diff --git a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp index 58e110b11..389ddb991 100644 --- a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp @@ -6,6 +6,8 @@ #include "ObjectMessages.h" #include "PlayerMessages.h" #include "QuickBuildMessages.h" +#include "InventoryMessages.h" +#include "SkillMessages.h" #include "StatisticID.h" #include "ZoneMessages.h" @@ -352,8 +354,7 @@ TEST_F(RemainingMessagesTests, ZoneMessagesMatchLegacy) { GameMessages::ChangeObjectWorldState msg; msg.target = target; msg.newState = state; - ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendChangeObjectWorldState(target, state, address); }, msg); - RoundTrip(msg); + EXPECT_EQ(RoundTrip(msg).newState, state); } } // Name-value text: a null terminator follows non-empty text and is not counted. @@ -743,3 +744,22 @@ TEST_F(RemainingMessagesTests, UpdatePlayerStatisticMatchesLiveCapture) { EXPECT_EQ(meters.updateID, static_cast(StatisticID::MetersTraveled)); EXPECT_EQ(meters.updateValue, 29); } + +// Packets from 2011/2012 live captures, sent when items were equipped and unequipped. DLU wrote ChangeObjectWorldState's +// state without the flag the client reads first, so the client read ATTACHED as INWORLD. +TEST_F(RemainingMessagesTests, EquipMessagesMatchLiveCapture) { + const auto attached = FromLiveCapture("5305000c000000002d21026701000010c7048080000000"); + EXPECT_EQ(attached.target, 0x100000016702212dLL); + EXPECT_EQ(attached.newState, eObjectWorldState::ATTACHED); + const auto inventory = FromLiveCapture("5305000c00000000b5915b6701000010c7048100000000"); + EXPECT_EQ(inventory.newState, eObjectWorldState::INVENTORY); + + const auto uncast = FromLiveCapture("5305000c000000001f147b5b01000010b6046a010000"); + EXPECT_EQ(uncast.skillID, 362); + + const auto unequip = FromLiveCapture("5305000c000000001f147b5b01000010e9004cb607ae4020000200"); + EXPECT_FALSE(unequip.bEvenIfDead); + EXPECT_TRUE(unequip.bIgnoreCooldown); + EXPECT_FALSE(unequip.bOutSuccess); + EXPECT_EQ(unequip.replacementObjectID, LWOOBJID_EMPTY); +}