fix(messages): ChangeObjectWorldState and UnEquipInventory as the client reads them; add UncastSkill

ChangeObjectWorldState's state is optional on the wire (a flag, then the
state when it isn't INWORLD): live sent 80 80 00 00 00 for ATTACHED. DLU
wrote the bare u32, so the client read the rocket's ATTACHED as INWORLD.

UnEquipInventory gets its optional replacementObjectID (the client always
sends the flag; the server-sent copies in the captures have it too), so it
can be sent server -> client.

New UncastSkill (1206, server -> client: the client ends its running
instance of the skill, LWOSkillComponent::msgUncastSkill 0x00bd86d0).

Tests: all three against live capture bytes.

Check in game: launch a rocket; the rocket is shown in the player's hands
during the launch animation (for the launcher and players nearby).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 02:34:46 -05:00
parent aed16a8c80
commit b845faee72
8 changed files with 58 additions and 9 deletions

View File

@@ -347,7 +347,12 @@ TEST_F(InventoryMessagesTests, InboundReadsLikeLegacy) {
unequip.bIgnoreCooldown = !flag;
unequip.bOutSuccess = flag;
unequip.itemToUnequip = id;
const auto legacyUnequip = ReadWithLegacy<LegacyGameMessages::LegacyEquip>(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);

View File

@@ -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<int32_t>(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<GameMessages::ChangeObjectWorldState>("5305000c000000002d21026701000010c7048080000000");
EXPECT_EQ(attached.target, 0x100000016702212dLL);
EXPECT_EQ(attached.newState, eObjectWorldState::ATTACHED);
const auto inventory = FromLiveCapture<GameMessages::ChangeObjectWorldState>("5305000c00000000b5915b6701000010c7048100000000");
EXPECT_EQ(inventory.newState, eObjectWorldState::INVENTORY);
const auto uncast = FromLiveCapture<GameMessages::UncastSkill>("5305000c000000001f147b5b01000010b6046a010000");
EXPECT_EQ(uncast.skillID, 362);
const auto unequip = FromLiveCapture<GameMessages::UnEquipInventory>("5305000c000000001f147b5b01000010e9004cb607ae4020000200");
EXPECT_FALSE(unequip.bEvenIfDead);
EXPECT_TRUE(unequip.bIgnoreCooldown);
EXPECT_FALSE(unequip.bOutSuccess);
EXPECT_EQ(unequip.replacementObjectID, LWOOBJID_EMPTY);
}