From 8d4422fa5c814072f841e93a3734f89e77d5a55a Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 03:09:46 -0500 Subject: [PATCH] feat(character): keep the rocket build the client reports (SetLastCustomBuild) The client sends SetLastCustomBuild (890, a u32-length wide string; other references) with its rocket build, e.g. "1:14454;1:4714;1:4715;", when it assembles the rocket the player carries: after EquipInventory at a launchpad and on load when landing (202 live packets). It stores it as lastUsedRocketInfo (LWOCharacterComponent::SendMessage 0x00d34330) and reads it back from char@lcbp on load (0x00ca4910). Live saved lcbp in the same format on every character. DLU dropped the message and only set lcbp when the server equipped the rocket. The handler now keeps the client's build as the rocket config (saved as lcbp). It does not change whether the player lands on the next load: DLU uses an empty config to skip the landing after non-rocket transfers, and that still happens when the server clears it. Check in game: build a new rocket in the rocket builder, launch from a launchpad and land: the rocket shown on landing and the builder's default next time are the new build. Teleport to another world without a rocket (e.g. from a property): no landing animation. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/CharacterComponent.h | 7 +++++++ dGame/dGameMessages/GameMessageHandler.cpp | 1 + dGame/dGameMessages/PlayerMessages.cpp | 14 ++++++++++++++ dGame/dGameMessages/PlayerMessages.h | 12 ++++++++++++ .../dGameMessagesTests/ClientMessagesTests.cpp | 15 +++++++++++++++ 5 files changed, 49 insertions(+) diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index bc4be664b..05e5b7f4b 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -59,6 +59,13 @@ public: */ void SetLastRocketConfig(std::u16string config); + /** + * Keeps the rocket build the client reports (SetLastCustomBuild) as char@lcbp, without changing whether the + * player lands on the next load + */ + void SetLastCustomBuild(const std::u16string& tokenizedLOTList) { m_LastRocketConfig = tokenizedLOTList; } + bool GetIsLanding() const { return m_IsLanding; } + /** * Find a player's rocket * @param player the entity that triggered the event diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 79d18310c..b9be5e22e 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -194,6 +194,7 @@ namespace { { MODIFY_PLAYER_ZONE_STATISTIC, []() { return std::make_unique(); } }, { UPDATE_PLAYER_STATISTIC, []() { return std::make_unique(); } }, { SET_TOOLTIP_FLAG, []() { return std::make_unique(); } }, + { SET_LAST_CUSTOM_BUILD, []() { return std::make_unique(); } }, { REPORT_BUG, []() { return std::make_unique(); } }, { VERIFY_ACK, []() { return std::make_unique(); } }, diff --git a/dGame/dGameMessages/PlayerMessages.cpp b/dGame/dGameMessages/PlayerMessages.cpp index 02a43ecff..608071ce0 100644 --- a/dGame/dGameMessages/PlayerMessages.cpp +++ b/dGame/dGameMessages/PlayerMessages.cpp @@ -91,6 +91,20 @@ namespace GameMessages { if (characterComponent) characterComponent->SetTooltipFlag(iToolTip, bFlag); } + void SetLastCustomBuild::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, tokenizedLOTList); + } + + bool SetLastCustomBuild::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, tokenizedLOTList)); + return true; + } + + void SetLastCustomBuild::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* const characterComponent = entity.GetComponent(); + if (characterComponent) characterComponent->SetLastCustomBuild(tokenizedLOTList); + } + void ToggleGMInvis::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(bStateOut); } diff --git a/dGame/dGameMessages/PlayerMessages.h b/dGame/dGameMessages/PlayerMessages.h index 038564e98..b80bb5cb9 100644 --- a/dGame/dGameMessages/PlayerMessages.h +++ b/dGame/dGameMessages/PlayerMessages.h @@ -79,6 +79,18 @@ namespace GameMessages { int32_t iToolTip{}; }; + // Client -> server. The client's rocket build ("1:LOT;1:LOT;1:LOT;"), sent when it assembles the rocket the + // player carries (at the launchpad and when landing). The client keeps it as lastUsedRocketInfo and reads it + // back from char@lcbp on load. + struct SetLastCustomBuild : public NetGameMsg { + SetLastCustomBuild() : NetGameMsg(MessageType::Game::SET_LAST_CUSTOM_BUILD) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::u16string tokenizedLOTList{}; + }; + // Server -> client, broadcast. struct ToggleGMInvis : public NetGameMsg { ToggleGMInvis() : NetGameMsg(MessageType::Game::TOGGLE_GM_INVIS) {} diff --git a/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp index 1bb16e273..dd4017b86 100644 --- a/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/ClientMessagesTests.cpp @@ -67,3 +67,18 @@ TEST_F(ClientMessagesTests, SetTooltipFlagFollowsTheClient) { characterComponent->SetTooltipFlag(100, false); EXPECT_EQ(characterComponent->GetTooltipFlags(), 0u); } + +// SetLastCustomBuild (890): a u32-length wide string. 202 live packets, sent when the carried rocket is assembled. +TEST_F(ClientMessagesTests, SetLastCustomBuildMatchesLiveCapture) { + auto msg = FromLiveClientCapture(std::string(PLAYER_HEADER) + "7a03" + + "1600000031003a00310034003400350034003b0031003a0034003700310034003b0031003a0034003700310035003b00"); + EXPECT_EQ(msg.tokenizedLOTList, u"1:14454;1:4714;1:4715;"); + EXPECT_EQ(RoundTrip(msg).tokenizedLOTList, msg.tokenizedLOTList); + ExpectTruncatedFails(msg); + + // Kept as the rocket config (saved as char@lcbp) without turning on the landing + auto* const characterComponent = player->GetComponent(); + msg.Handle(*player, UNASSIGNED_SYSTEM_ADDRESS); + EXPECT_EQ(characterComponent->GetLastRocketConfig(), u"1:14454;1:4714;1:4715;"); + EXPECT_FALSE(characterComponent->GetIsLanding()); +}