From ca7c02cf0cd6d77640ee33618eab4d177f9d552d Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 04:51:45 -0500 Subject: [PATCH] fix(character): always write the GM, activity and social blocks on construction Live wrote all three optional blocks of the character component in every player construction (9,726 LOT 1 constructions: gm_pvp_info with is_gm false and gm_level 0 for civilians, current_activity Some(None) on 9,620, social_info always Some with guild 0 and an empty name). DLU wrote each one only after something set its dirty flag, so civilians got none of them. Serializations are unchanged (still only when dirty). Writing the social block every time exposed two bugs in it: the guild ID was never initialized (now 0), and the guild name was written with sizeof(wchar_t) (4 on Linux) bits per character, reading past the end of the UTF-16 string; it is now written as 16-bit characters. Test: ReplicaConstructionTest.CharacterConstructionAlwaysWritesGmActivityAndSocialBlocks. Check in game: log in as a civilian and as a GM, with a second player watching; both see each other normally (name tags, GM tag/level for the GM, no PvP flag), and starting a quickbuild or pet taming still shows the activity to the other player. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/CharacterComponent.cpp | 22 +++++---- dGame/dComponents/CharacterComponent.h | 2 +- .../ReplicaConstructionTests.cpp | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index 6d7eb5fe9..363a115e4 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -228,8 +228,11 @@ void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInit } } - outBitStream.Write(m_DirtyGMInfo); - if (m_DirtyGMInfo) { + // Live wrote the GM, current-activity and social blocks on every player construction (civilians included), + // and afterwards only when they changed. + const bool writeGMInfo = bIsInitialUpdate || m_DirtyGMInfo; + outBitStream.Write(writeGMInfo); + if (writeGMInfo) { outBitStream.Write(m_PvpEnabled); outBitStream.Write(m_IsGM); outBitStream.Write(m_GMLevel); @@ -237,15 +240,16 @@ void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInit outBitStream.Write(m_EditorLevel); } - outBitStream.Write(m_DirtyCurrentActivity); - if (m_DirtyCurrentActivity) outBitStream.Write(m_CurrentActivity); + const bool writeCurrentActivity = bIsInitialUpdate || m_DirtyCurrentActivity; + outBitStream.Write(writeCurrentActivity); + if (writeCurrentActivity) outBitStream.Write(m_CurrentActivity); - outBitStream.Write(m_DirtySocialInfo); - if (m_DirtySocialInfo) { + const bool writeSocialInfo = bIsInitialUpdate || m_DirtySocialInfo; + outBitStream.Write(writeSocialInfo); + if (writeSocialInfo) { outBitStream.Write(m_GuildID); - outBitStream.Write(m_GuildName.size()); - if (!m_GuildName.empty()) - outBitStream.WriteBits(reinterpret_cast(m_GuildName.c_str()), static_cast(m_GuildName.size()) * sizeof(wchar_t) * 8); + outBitStream.Write(m_GuildName.size()); + for (const char16_t character : m_GuildName) outBitStream.Write(character); outBitStream.Write(m_IsLEGOClubMember); outBitStream.Write(m_CountryCode); diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index 78afb19ad..86a7613f7 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -452,7 +452,7 @@ private: /** * The guild this character is in */ - LWOOBJID m_GuildID; + LWOOBJID m_GuildID = LWOOBJID_EMPTY; /** * The name of the guild this character is in diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 74a66e9df..6e114a640 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -84,3 +84,48 @@ TEST_F(ReplicaConstructionTest, TimeSinceCreatedOnServerIsTheObjectsAge) { EXPECT_LT(later, 60000u); EXPECT_GE(entity.GetTimeSinceCreatedMs(), later); } + +// A civilian's character component: live always wrote the GM, current-activity and social blocks on construction +// (9,726 LOT 1 constructions: is_gm false, gm_level 0, current_activity Some(None), social_info always Some with guild 0 +// and an empty guild name). DLU wrote them only after something dirtied them, so a civilian got none of them. +TEST_F(ReplicaConstructionTest, CharacterConstructionAlwaysWritesGmActivityAndSocialBlocks) { + User user(UNASSIGNED_SYSTEM_ADDRESS, "tester", "key"); + Character character(1, &user); + info.lot = 1; + Entity player(1152921506064087003, info); + player.SetCharacter(&character); + character.SetEntity(&player); + auto* const component = player.AddComponent(-1, &character, UNASSIGNED_SYSTEM_ADDRESS); + + RakNet::BitStream construction; + component->Serialize(construction, true); + + // Everything before the GM block: 4 absent claim codes, 10 u32 appearance fields, 4 u64s (account, last logout, + // prop mod display time, u-score), the free-trial bit, 27 u64 statistics and the 2-bit transition state. + constexpr uint32_t beforeGm = 4 + 10 * 32 + 4 * 64 + 1 + 27 * 64 + 2; + RakNet::BitStream tail; + Tail(construction, beforeGm, tail); + + RakNet::BitStream expected; + expected.Write1(); // gm_pvp_info Some + expected.Write0(); // pvp_enabled + expected.Write0(); // is_gm + expected.Write(0); // gm_level + expected.Write0(); // editor_enabled + expected.Write(0); // editor_level + expected.Write1(); // current_activity Some + expected.Write(0); // GameActivity::None + expected.Write1(); // social_info Some + expected.Write(0); // guild_id + expected.Write(0); // guild_name "" + expected.Write1(); // is_lego_club_member (DLU treats everyone as a member) + expected.Write(0); // country code + ExpectSameBits(tail, expected); + + // A serialization with nothing changed still writes none of them + RakNet::BitStream serialization; + component->Serialize(serialization, false); + EXPECT_EQ(serialization.GetNumberOfBitsUsed(), 3u); + + player.SetCharacter(nullptr); +}