From d3f5c20323643aec4059722d2a919629b6b3a487 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 13:26:55 -0600 Subject: [PATCH] fix: a LUP exhibit's construction always carries the exhibit LWOLUPExhibitComponent::Deserialize (0x00c63790) reads a flag and the exhibit's LOT, and that is the only way the client learns which exhibit is shown. DLU wrote the block on a construction only while it was dirty, so a player who came into range after the first update saw no exhibit until the next one was picked, up to 20 seconds later. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/LUPExhibitComponent.cpp | 7 ++++-- .../ReplicaConstructionTests.cpp | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/LUPExhibitComponent.cpp b/dGame/dComponents/LUPExhibitComponent.cpp index 2680b0b2b..69315672a 100644 --- a/dGame/dComponents/LUPExhibitComponent.cpp +++ b/dGame/dComponents/LUPExhibitComponent.cpp @@ -16,8 +16,11 @@ void LUPExhibitComponent::NextLUPExhibit() { } void LUPExhibitComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { - outBitStream.Write(m_DirtyLUPExhibit); - if (m_DirtyLUPExhibit) { + // LWOLUPExhibitComponent::Deserialize (0x00c63790) only learns the exhibit from this block, so a client the object + // comes into range of gets it too + const bool writeExhibit = bIsInitialUpdate || m_DirtyLUPExhibit; + outBitStream.Write(writeExhibit); + if (writeExhibit) { outBitStream.Write(m_LUPExhibits[m_LUPExhibitIndex % m_LUPExhibits.size()]); if (!bIsInitialUpdate) m_DirtyLUPExhibit = false; } diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index b0e9b7970..2fc054ddb 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -19,6 +19,7 @@ #include "InventoryComponent.h" #include "ItemComponent.h" #include "LevelProgressionComponent.h" +#include "LUPExhibitComponent.h" #include "PlayerForcedMovementComponent.h" #include "Item.h" #include "ModelComponent.h" @@ -370,6 +371,27 @@ TEST_F(ReplicaConstructionTest, AConstructionKeepsPendingLevelAndForcedMovementC EXPECT_EQ(again.GetNumberOfBitsUsed(), 2u); } +// The client learns the exhibit's LOT only from the component's block (LWOLUPExhibitComponent::Deserialize: a flag +// and the LOT). DLU wrote it on a construction only while it was dirty, so a player who came into range after the +// first update saw no exhibit until the next one was picked. +TEST_F(ReplicaConstructionTest, LupExhibitConstructionAlwaysCarriesTheExhibit) { + Entity exhibit(1009, info); + auto* const component = exhibit.AddComponent(-1); + + RakNet::BitStream update; + component->Serialize(update, false); + RakNet::BitStream idle; + component->Serialize(idle, false); + EXPECT_EQ(idle.GetNumberOfBitsUsed(), 1u); + + RakNet::BitStream construction; + component->Serialize(construction, true); + RakNet::BitStream expected; + expected.Write1(); + expected.Write(11121); + ExpectSameBits(construction, expected); +} + // A guild change (docs/Guilds.md) is written once: the client reads guild id, a u8 length and 16-bit characters, and // redraws the name billboard every time it reads a name. TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) {