fix: constructions keep pending health, quick build and equipment changes

The destroyable's health and threat blocks, the quick build's state and
the inventory's equipped items are each read behind a flag bit
(LWODestroyableComponent::Deserialize 0x00d2bbd0 and the others). DLU
cleared their dirty flags when it wrote a construction, which goes only
to the client the object came into range of, so a change waiting for
the next update never reached the clients already watching: a health
bar, a quick build's state, a player's new gear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 13:27:39 -06:00
parent d3f5c20323
commit e786d2b1c5
5 changed files with 33 additions and 4 deletions

View File

@@ -162,7 +162,7 @@ void QuickBuildComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIni
outBitStream.Write(m_ActivatorPosition);
outBitStream.Write(m_RepositionPlayer);
}
m_StateDirty = false;
if (!bIsInitialUpdate) m_StateDirty = false;
}
}

View File

@@ -259,13 +259,14 @@ void DestroyableComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIn
if (m_ExplodeFactor != 1.0f) outBitStream.Write(m_ExplodeFactor);
}
}
m_DirtyHealth = false;
// A construction goes to one client; the players already watching still need a waiting change
if (!bIsInitialUpdate) m_DirtyHealth = false;
}
outBitStream.Write(m_DirtyThreatList || bIsInitialUpdate);
if (m_DirtyThreatList || bIsInitialUpdate) {
outBitStream.Write(m_HasThreats);
m_DirtyThreatList = false;
if (!bIsInitialUpdate) m_DirtyThreatList = false;
}
}

View File

@@ -1048,7 +1048,8 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
outBitStream.Write(item.bound);
}
m_Dirty = false;
// A construction goes to one client; the players already watching still need a waiting equipment change
if (!bIsInitialUpdate) m_Dirty = false;
} else {
outBitStream.Write(false);
}

View File

@@ -622,6 +622,28 @@ TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) {
player.SetCharacter(nullptr);
}
// A construction goes to the one client the object came into range of; it must not use up the health change the
// players already watching are waiting for (LWODestroyableComponent::Deserialize reads it behind a flag bit).
TEST_F(DestroyableTest, AConstructionKeepsThePendingHealthChange) {
destroyableComponent->SetHealth(3);
RakNet::BitStream construction;
destroyableComponent->Serialize(construction, true);
RakNet::BitStream update;
destroyableComponent->Serialize(update, false);
bool hasHealth{};
uint32_t health{};
ASSERT_TRUE(update.Read(hasHealth));
ASSERT_TRUE(hasHealth);
ASSERT_TRUE(update.Read(health));
EXPECT_EQ(health, 3u);
// Written once
RakNet::BitStream again;
destroyableComponent->Serialize(again, false);
EXPECT_EQ(again.GetNumberOfBitsUsed(), 2u);
}
// LWOCharacterComponent::SendMessage, IsValidPVPTarget: two players with PVP on are enemies, a player is never its own
TEST_F(DestroyableTest, APlayerWithPvpIsNotItsOwnEnemy) {
auto playerInfo = info;

View File

@@ -700,6 +700,11 @@ TEST_F(InventoryConstructionTest, NpcItemsLikeLive) {
coleExpected.Write<uint32_t>(0);
ExpectSameBits(coleConstruction, coleExpected);
// The equipment put on before the construction still goes to the clients already watching, once
RakNet::BitStream pending;
coleInventory->Serialize(pending, false);
EXPECT_GT(pending.GetNumberOfBitsUsed(), 2u);
// A serialization with nothing changed writes neither list
RakNet::BitStream serialization;
coleInventory->Serialize(serialization, false);