From 5ac39dca8b346efdd215d44e33270aca911a6323 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 14:50:33 -0600 Subject: [PATCH] fix: an activity's construction always carries its players LWOActivityComponent::Deserialize (0x00d002b0) reads a flag, the player count and each player with its 10 values; a construction is the only way a client coming into range learns who is in the activity. The block was written only while it was waiting for the next update. Live finish line constructions (LOT 4967) had the flag and the count in 40 of 40, one of them with a player in it. Co-Authored-By: Claude Opus 5.5 --- .../Activity/ActivityComponent.cpp | 7 +++- .../ReplicaConstructionTests.cpp | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/Activity/ActivityComponent.cpp b/dGame/dComponents/Activity/ActivityComponent.cpp index 8279d201a..276a74243 100644 --- a/dGame/dComponents/Activity/ActivityComponent.cpp +++ b/dGame/dComponents/Activity/ActivityComponent.cpp @@ -79,8 +79,11 @@ void ActivityComponent::LoadActivityData(const int32_t activityId) { } void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { - outBitStream.Write(m_DirtyActivityInfo); - if (m_DirtyActivityInfo) { + // A construction always carries the players: it is the only way a client coming into range learns who is in the + // activity (LWOActivityComponent::Deserialize 0x00d002b0), and live wrote the block, empty or not, on every one + const bool writeActivityInfo = m_DirtyActivityInfo || bIsInitialUpdate; + outBitStream.Write(writeActivityInfo); + if (writeActivityInfo) { outBitStream.Write(m_ActivityPlayers.size()); if (!m_ActivityPlayers.empty()) { for (const auto& [playerID, values] : m_ActivityPlayers) { diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 7a4d87703..8d082274f 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -21,6 +21,7 @@ #include "LevelProgressionComponent.h" #include "LUPExhibitComponent.h" #include "PlayerForcedMovementComponent.h" +#include "ScriptedActivityComponent.h" #include "Item.h" #include "ModelComponent.h" #include "MovingPlatformComponent.h" @@ -439,6 +440,47 @@ TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) { player.SetCharacter(nullptr); } +// A client that comes into range of an activity is told its players, whether or not they changed since the last +// update. Live finish line constructions (LOT 4967): flag 1 and the count in 40 of 40, one of them with a player. +TEST_F(ReplicaConstructionTest, AnActivityConstructionAlwaysCarriesItsPlayers) { + ConnectRegistry(); + Entity activity(1010, info); + auto* const component = activity.AddComponent(-1); + const LWOOBJID player = 1152921506064087006; + component->SetActivityValue(player, 1, 42.0f); + + RakNet::BitStream update; + component->Serialize(update, false); + RakNet::BitStream idle; + component->Serialize(idle, false); + EXPECT_EQ(Bits(idle), "0"); // nothing changed since + + RakNet::BitStream construction; + component->Serialize(construction, true); + EXPECT_EQ(Bits(construction), Bits(update)); + EXPECT_TRUE(construction.ReadBit()); + uint32_t count{}; + ASSERT_TRUE(construction.Read(count)); + EXPECT_EQ(count, 1u); + LWOOBJID id{}; + ASSERT_TRUE(construction.Read(id)); + EXPECT_EQ(id, player); + for (uint32_t i = 0; i < 10; i++) { + float value{}; + ASSERT_TRUE(construction.Read(value)); + EXPECT_EQ(value, i == 1 ? 42.0f : 0.0f); + } + EXPECT_EQ(construction.GetNumberOfUnreadBits(), 0u); + + // Without players: the flag and a count of 0 + component->RemoveActivityPlayerData(player); + RakNet::BitStream cleared; + component->Serialize(cleared, false); + RakNet::BitStream empty; + component->Serialize(empty, true); + EXPECT_EQ(Bits(empty), "1" + std::string(32, '0')); +} + // Run speed modifiers stack as in LWOControllablePhysComponent::msgAddRunspeedModifier (0x00be0af0) and // msgRemoveRunSpeedModifier (0x00be0720): the lowest slow wins, otherwise the highest boost; 500 and anything over 1000 // count for nothing