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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 14:50:33 -06:00
parent 44b35adbd6
commit 5ac39dca8b
2 changed files with 47 additions and 2 deletions

View File

@@ -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<uint32_t>(m_ActivityPlayers.size());
if (!m_ActivityPlayers.empty()) {
for (const auto& [playerID, values] : m_ActivityPlayers) {

View File

@@ -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<ScriptedActivityComponent>(-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