diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 633ec41e0..8c30bd591 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -760,6 +760,14 @@ namespace { * The order the client reads components in (it builds an object's component list in this order in * ObjectLoader2::DoObjectComponentLoad). CHARACTER is preceded by POSSESSOR, LEVEL_PROGRESSION and * PLAYER_FORCED_MOVEMENT, which WriteComponents writes with it. + * + * LWOGameObject::Deserialize (0x00f99690) calls each component's Deserialize in the order of the object's component + * list, which DoObjectComponentLoad (0x0105ebb0) fills in the order of ObjectLoader2::FillComponentOrder + * (0x0101f8e0). From that list: ROLLER (105) comes after SHOOTING_GALLERY (19) and before SCRIPTED_ACTIVITY (39) and + * MOVING_PLATFORM (25); CHOICE_BUILD (52) after MOVING_PLATFORM and before LUP_EXHIBIT (75) and MODEL (42); CHEST (22) + * after MINI_GAME_CONTROL (50) and before RENDER (2). They are placed here against the components their objects + * have (roller: physics, script, skill, inventory, moving platform, render; choice build: physics, destroyable, + * script, render; chest: phantom physics, render). */ constexpr auto SERIALIZATION_ORDER = [] { using enum eReplicaComponentType; @@ -767,8 +775,8 @@ namespace { POSSESSABLE, MODULE_ASSEMBLY, CONTROLLABLE_PHYSICS, SIMPLE_PHYSICS, RIGID_BODY_PHANTOM_PHYSICS, HAVOK_VEHICLE_PHYSICS, PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, PLAYER_FORCED_MOVEMENT, CHARACTER, INVENTORY, - SCRIPT, SKILL, BASE_COMBAT_AI, ITEM, QUICK_BUILD, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, - BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT, MODEL, RENDER, MINI_GAME_CONTROL, BBB, + SCRIPT, SKILL, BASE_COMBAT_AI, ITEM, QUICK_BUILD, ROLLER, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, + BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, CHOICE_BUILD, LUP_EXHIBIT, MODEL, CHEST, RENDER, MINI_GAME_CONTROL, BBB, }; }(); diff --git a/dGame/dComponents/CMakeLists.txt b/dGame/dComponents/CMakeLists.txt index 18d82a797..08a2464c6 100644 --- a/dGame/dComponents/CMakeLists.txt +++ b/dGame/dComponents/CMakeLists.txt @@ -33,6 +33,7 @@ set(DGAME_DCOMPONENTS_SOURCES "ProximityMonitorComponent.cpp" "Activity/RacingControlComponent.cpp" "RailActivatorComponent.cpp" + "RollerComponent.cpp" "Activity/QuickBuildComponent.cpp" "RenderComponent.cpp" "Physics/RigidbodyPhantomPhysicsComponent.cpp" diff --git a/dGame/dComponents/ChestComponent.h b/dGame/dComponents/ChestComponent.h index 391904202..7e74cc2a9 100644 --- a/dGame/dComponents/ChestComponent.h +++ b/dGame/dComponents/ChestComponent.h @@ -1,17 +1,40 @@ #ifndef CHESTCOMPONENT_H #define CHESTCOMPONENT_H +#include "BitStream.h" #include "Component.h" +#include "dCommonVars.h" #include "eReplicaComponentType.h" /** - * Chest, as the client's LWOChestComponent makes it from the registry. Nothing of it is sent or done on the server yet. + * Chest, as the client's LWOChestComponent makes it from the registry (LOT 2196). Its construction carries the player + * the chest is for and the number of loot offers in it; nothing else of it is done on the server yet. */ class ChestComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::CHEST; ChestComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {} + + // LWOChestComponent::Deserialize (0x00c22010) reads on construction only: the player (u64) and the loot offer count + // (u32), which it keeps when the chest is the local player's. Nothing in an update. + void Serialize(RakNet::BitStream& outBitStream, const bool isConstruction) override { + if (!isConstruction) return; + outBitStream.Write(m_PlayerID); + outBitStream.Write(m_LootOfferCount); + } + + LWOOBJID GetPlayerID() const { return m_PlayerID; } + void SetPlayerID(const LWOOBJID value) { m_PlayerID = value; } + uint32_t GetLootOfferCount() const { return m_LootOfferCount; } + void SetLootOfferCount(const uint32_t value) { m_LootOfferCount = value; } + +private: + // The player the chest belongs to; none (the client's own start value) until something gives it one + LWOOBJID m_PlayerID = LWOOBJID_EMPTY; + + // The loot offers in the chest + uint32_t m_LootOfferCount = 0; }; #endif // CHESTCOMPONENT_H diff --git a/dGame/dComponents/ChoiceBuildComponent.h b/dGame/dComponents/ChoiceBuildComponent.h index 74c2b61f3..363450653 100644 --- a/dGame/dComponents/ChoiceBuildComponent.h +++ b/dGame/dComponents/ChoiceBuildComponent.h @@ -1,17 +1,61 @@ #ifndef CHOICEBUILDCOMPONENT_H #define CHOICEBUILDCOMPONENT_H +#include "BitStream.h" #include "Component.h" +#include "dCommonVars.h" #include "eReplicaComponentType.h" /** - * Choice build, as the client's LWOChoiceBuildComponent makes it from the registry. Nothing of it is sent or done on the server yet. + * Choice build, as the client's LWOChoiceBuildComponent makes it from the registry (LOTs 5668, 6414, 6462, 7002, 16205). + * Its state, the object built and the player using it are sent; nothing else of it is done on the server yet. */ class ChoiceBuildComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::CHOICE_BUILD; + // The client's states (its constructor starts at Waiting) + enum class eState : uint32_t { + Waiting = 0, + Building = 1, + }; + ChoiceBuildComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {} + + // LWOChoiceBuildComponent::Deserialize (0x00bf26d0) reads, in constructions and updates alike: a flag, then the + // state (u32), the built object (u64) and the user (u64). A construction always carries them. + void Serialize(RakNet::BitStream& outBitStream, const bool isConstruction) override { + outBitStream.Write(isConstruction || m_Dirty); + if (isConstruction || m_Dirty) { + outBitStream.Write(m_State); + outBitStream.Write(m_ChildObject); + outBitStream.Write(m_UserID); + } + if (!isConstruction) m_Dirty = false; + } + + eState GetState() const { return m_State; } + LWOOBJID GetChildObject() const { return m_ChildObject; } + LWOOBJID GetUserID() const { return m_UserID; } + + // Changes what the clients are told with the next update (the caller serializes the object) + void SetState(const eState state, const LWOOBJID childObject, const LWOOBJID userID) { + m_State = state; + m_ChildObject = childObject; + m_UserID = userID; + m_Dirty = true; + } + +private: + eState m_State = eState::Waiting; + + // The object the choice build made, none while waiting + LWOOBJID m_ChildObject = LWOOBJID_EMPTY; + + // The player using the choice build, none while waiting + LWOOBJID m_UserID = LWOOBJID_EMPTY; + + bool m_Dirty = false; }; #endif // CHOICEBUILDCOMPONENT_H diff --git a/dGame/dComponents/RollerComponent.cpp b/dGame/dComponents/RollerComponent.cpp new file mode 100644 index 000000000..0b6185732 --- /dev/null +++ b/dGame/dComponents/RollerComponent.cpp @@ -0,0 +1,17 @@ +#include "RollerComponent.h" + +#include "Entity.h" +#include "GeneralUtils.h" + +namespace { + // A level point: the three numbers with character 31 between them + NiPoint3 LevelPoint(const Entity& entity, const std::u16string& key) { + const auto parts = GeneralUtils::SplitString(entity.GetVarAsString(key), '\x1f'); + return GeneralUtils::TryParse(parts).value_or(NiPoint3Constant::ZERO); + } +} + +void RollerComponent::LoadConfigData() { + m_CameraPosition = LevelPoint(*m_Parent, u"activity_camera_pos"); + m_CameraLookAt = LevelPoint(*m_Parent, u"activity_camera_lookat_pos"); +} diff --git a/dGame/dComponents/RollerComponent.h b/dGame/dComponents/RollerComponent.h index 0cd64106b..9d2f6df9c 100644 --- a/dGame/dComponents/RollerComponent.h +++ b/dGame/dComponents/RollerComponent.h @@ -1,17 +1,54 @@ #ifndef ROLLERCOMPONENT_H #define ROLLERCOMPONENT_H +#include "BitStream.h" #include "Component.h" +#include "NiPoint3.h" +#include "dCommonVars.h" #include "eReplicaComponentType.h" /** - * Roller, as the client's LWORollerComponent makes it from the registry. Nothing of it is sent or done on the server yet. + * Roller (a launcher a player fires from: LOTs 2714, 13951), as the client's LWORollerComponent makes it from the + * registry. Its construction carries the activity camera and the projectile skill; nobody uses one on the server + * yet, so the block about its user is never sent. */ class RollerComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::ROLLER; + // The projectile skill the client's constructor starts with (LWORollerComponent::Initialize 0x00cc7e30) + static constexpr uint32_t DEFAULT_PROJECTILE_SKILL = 1348; + RollerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {} + + // The level's activity_camera_pos and activity_camera_lookat_pos (LWORollerComponent::LoadConfigData 0x00c31820); + // both stay at the origin without the key, as in the client + void LoadConfigData() override; + + // LWORollerComponent::Deserialize (0x00c15920) reads on construction: the camera position and what it looks at + // (3 floats each) and the projectile skill (u32). Then, in constructions and updates alike, a flag in front of the + // block about the player using it (3 x u64, 3 floats, a float, 3 floats, the player u64, a float): never set here. + void Serialize(RakNet::BitStream& outBitStream, const bool isConstruction) override { + if (isConstruction) { + outBitStream.Write(m_CameraPosition.x); + outBitStream.Write(m_CameraPosition.y); + outBitStream.Write(m_CameraPosition.z); + outBitStream.Write(m_CameraLookAt.x); + outBitStream.Write(m_CameraLookAt.y); + outBitStream.Write(m_CameraLookAt.z); + outBitStream.Write(m_ProjectileSkill); + } + outBitStream.Write0(); + } + + const NiPoint3& GetCameraPosition() const { return m_CameraPosition; } + const NiPoint3& GetCameraLookAt() const { return m_CameraLookAt; } + uint32_t GetProjectileSkill() const { return m_ProjectileSkill; } + +private: + NiPoint3 m_CameraPosition = NiPoint3Constant::ZERO; + NiPoint3 m_CameraLookAt = NiPoint3Constant::ZERO; + uint32_t m_ProjectileSkill = DEFAULT_PROJECTILE_SKILL; }; #endif // ROLLERCOMPONENT_H diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 95c57b4e6..fb9cd3edd 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -6,6 +6,7 @@ set(DCOMPONENTS_TESTS "DestroyableLevelConfigTests.cpp" "ProximityMonitorComponentTests.cpp" "RailActivatorComponentTests.cpp" + "ConstructionOnlyComponentTests.cpp" "PropertyVendorComponentTests.cpp" "VendorBuybackTests.cpp" "RacingWrongWayTests.cpp" diff --git a/tests/dGameTests/dComponentsTests/ConstructionOnlyComponentTests.cpp b/tests/dGameTests/dComponentsTests/ConstructionOnlyComponentTests.cpp new file mode 100644 index 000000000..ef58f8714 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/ConstructionOnlyComponentTests.cpp @@ -0,0 +1,113 @@ +// What the client reads for a chest, a choice build and a roller: LWOChestComponent::Deserialize (0x00c22010), +// LWOChoiceBuildComponent::Deserialize (0x00bf26d0) and LWORollerComponent::Deserialize (0x00c15920). Where they are +// read among an object's components is in ReplicaComponentOrderTests. +#include "GameDependencies.h" + +#include "BitStream.h" +#include "ChestComponent.h" +#include "ChoiceBuildComponent.h" +#include "Entity.h" +#include "RollerComponent.h" + +#include +#include + +class ConstructionOnlyComponentTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } + + template + static std::vector Bytes(Component& component, const bool construction, uint32_t* const bits = nullptr) { + RakNet::BitStream stream; + component.Serialize(stream, construction); + if (bits) *bits = stream.GetNumberOfBitsUsed(); + return { stream.GetData(), stream.GetData() + stream.GetNumberOfBytesUsed() }; + } +}; + +// Construction: the player (u64) and the loot offer count (u32); nothing in an update +TEST_F(ConstructionOnlyComponentTests, Chest) { + Entity entity(15, info); + auto* const chest = entity.AddComponent(0); + uint32_t bits{}; + EXPECT_EQ(Bytes(*chest, true, &bits), (std::vector{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); + EXPECT_EQ(bits, 96u); + + chest->SetPlayerID(0x1000000000000001LL); + chest->SetLootOfferCount(3); + EXPECT_EQ(Bytes(*chest, true, &bits), (std::vector{ 0x01, 0, 0, 0, 0, 0, 0, 0x10, 0x03, 0, 0, 0 })); + EXPECT_EQ(bits, 96u); + + EXPECT_TRUE(Bytes(*chest, false, &bits).empty()); + EXPECT_EQ(bits, 0u); +} + +// A flag, then the state (u32), the built object (u64) and the user (u64): always in a construction, in an update +// after a change only +TEST_F(ConstructionOnlyComponentTests, ChoiceBuild) { + Entity entity(15, info); + auto* const choiceBuild = entity.AddComponent(1); + uint32_t bits{}; + // 1 flag bit, then 160 zero bits: waiting, nothing built, nobody using it (the client's own start values) + std::vector waiting(21, 0); + waiting[0] = 0x80; + EXPECT_EQ(Bytes(*choiceBuild, true, &bits), waiting); + EXPECT_EQ(bits, 161u); + + EXPECT_EQ(Bytes(*choiceBuild, false, &bits), (std::vector{ 0x00 })); + EXPECT_EQ(bits, 1u); + + choiceBuild->SetState(ChoiceBuildComponent::eState::Building, 0x0000400000000002LL, 0x1000000000000001LL); + // The same fields shifted by the flag bit: state 1, object 0x0000400000000002, user 0x1000000000000001 + RakNet::BitStream expected; + expected.Write1(); + expected.Write(1); + expected.Write(0x0000400000000002LL); + expected.Write(0x1000000000000001LL); + const std::vector building{ expected.GetData(), expected.GetData() + expected.GetNumberOfBytesUsed() }; + EXPECT_EQ(building[0], 0x80); // the flag, then the low byte of the state (1) one bit on + EXPECT_EQ(building[1], 0x80); + EXPECT_EQ(Bytes(*choiceBuild, false, &bits), building); + EXPECT_EQ(bits, 161u); + // Sent once + EXPECT_EQ(Bytes(*choiceBuild, false, &bits), (std::vector{ 0x00 })); + // A later construction carries the state as it is + EXPECT_EQ(Bytes(*choiceBuild, true, &bits), building); +} + +// Construction: camera position and look-at (3 floats each), the projectile skill (u32, the client starts at 1348), +// then the flag of the user block, which is never set. An update is that flag alone. +TEST_F(ConstructionOnlyComponentTests, Roller) { + Entity entity(15, info); + auto* const roller = entity.AddComponent(13951); + uint32_t bits{}; + std::vector expected(29, 0); + expected[24] = 0x44; // 1348 = 0x544 + expected[25] = 0x05; + EXPECT_EQ(Bytes(*roller, true, &bits), expected); + EXPECT_EQ(bits, 225u); + + EXPECT_EQ(Bytes(*roller, false, &bits), (std::vector{ 0x00 })); + EXPECT_EQ(bits, 1u); +} + +// The level's activity_camera_pos / activity_camera_lookat_pos: three numbers with character 31 between them +TEST_F(ConstructionOnlyComponentTests, RollerCameraComesFromTheLevel) { + Entity entity(15, info); + entity.SetVar(u"activity_camera_pos", u"-327.861\x1f" u"256.91\x1f" u".64822"); + entity.SetVar(u"activity_camera_lookat_pos", u"1\x1f" u"2\x1f" u"-0.5"); + auto* const roller = entity.AddComponent(13951); // loads the level's keys + EXPECT_EQ(roller->GetCameraPosition(), NiPoint3(-327.861f, 256.91f, 0.64822f)); + EXPECT_EQ(roller->GetCameraLookAt(), NiPoint3(1.0f, 2.0f, -0.5f)); + + RakNet::BitStream expected; + for (const float value : { -327.861f, 256.91f, 0.64822f, 1.0f, 2.0f, -0.5f }) expected.Write(value); + expected.Write(1348); + expected.Write0(); + uint32_t bits{}; + EXPECT_EQ(Bytes(*roller, true, &bits), (std::vector{ expected.GetData(), expected.GetData() + expected.GetNumberOfBytesUsed() })); + EXPECT_EQ(bits, 225u); + // 1.0f little endian, the first look-at number + EXPECT_EQ(Bytes(*roller, true)[12 + 3], 0x3f); +} diff --git a/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp index caa34c288..5b62663e5 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp @@ -18,6 +18,9 @@ #include "BouncerComponent.h" #include "BuffComponent.h" #include "CharacterComponent.h" +#include "ChestComponent.h" +#include "ChoiceBuildComponent.h" +#include "RollerComponent.h" #include "CollectibleComponent.h" #include "ControllablePhysicsComponent.h" #include "DestroyableComponent.h" @@ -561,3 +564,102 @@ TEST_F(ReplicaComponentOrderTest, EveryListedComponent) { entity.AddComponent(-1); }); } + +namespace { + // The bits of `expected` are the first bits of `actual` + void ExpectStartsWith(RakNet::BitStream& actual, RakNet::BitStream& expected) { + ASSERT_GE(actual.GetNumberOfBitsUsed(), expected.GetNumberOfBitsUsed()); + actual.ResetReadPointer(); + expected.ResetReadPointer(); + for (uint32_t i = 0; i < expected.GetNumberOfBitsUsed(); i++) { + ASSERT_EQ(actual.ReadBit(), expected.ReadBit()) << "bit " << i; + } + } +} + +// LWOGameObject::Deserialize (0x00f99690) reads the components in the order ObjectLoader2::FillComponentOrder +// (0x0101f8e0) lists them: a chest (22) after its phantom physics (40) and before its render component (2). +// LOT 2196: phantom physics, chest, render. +TEST_F(ReplicaComponentOrderTest, ChestIsWrittenBeforeRender) { + Entity entity(15, info); + auto* const physics = entity.AddComponent(-1); + auto* const chest = entity.AddComponent(0); + chest->SetPlayerID(0x1000000000000001LL); + chest->SetLootOfferCount(3); + auto* const render = entity.AddComponent(-1); + + RakNet::BitStream actual; + entity.WriteComponents(actual, eReplicaPacketType::CONSTRUCTION); + + Entity copy(15, info); + RakNet::BitStream expected; + copy.AddComponent(-1)->Serialize(expected, true); + expected.Write(0x1000000000000001LL); + expected.Write(3); + copy.AddComponent(-1)->Serialize(expected, true); + ExpectStartsWith(actual, expected); + static_cast(physics); + static_cast(render); +} + +// A choice build (52) is read after physics, destroyable and script and before render. LOT 6414: simple physics, +// destroyable, choice build, render; LOT 5668 has a script in place of the destroyable. +TEST_F(ReplicaComponentOrderTest, ChoiceBuildIsWrittenAfterScriptAndBeforeRender) { + const auto build = [](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1)->SetMaxHealth(1.0f); + entity.AddComponent(-1, "", true); + entity.AddComponent(1); + entity.AddComponent(-1); + }; + Entity entity(15, info); + build(entity); + RakNet::BitStream actual; + entity.WriteComponents(actual, eReplicaPacketType::CONSTRUCTION); + + Entity copy(15, info); + build(copy); + RakNet::BitStream expected; + copy.GetComponent()->Serialize(expected, true); + copy.GetComponent()->Serialize(expected, true); + copy.GetComponent()->Serialize(expected, true); + copy.GetComponent()->Serialize(expected, true); + expected.Write1(); + expected.Write(0); // waiting + expected.Write(0); // nothing built + expected.Write(0); // nobody using it + copy.GetComponent()->Serialize(expected, true); + ExpectStartsWith(actual, expected); +} + +// A roller (105) is read after script, skill and inventory and before the moving platform (25) and render. +// LOT 2714: simple physics, roller, moving platform, render; LOT 13951 adds script, skill and inventory. +TEST_F(ReplicaComponentOrderTest, RollerIsWrittenBeforeTheMovingPlatform) { + const auto build = [](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(13951); + entity.AddComponent(-1, ""); + entity.AddComponent(-1); + }; + for (const auto packetType : { eReplicaPacketType::CONSTRUCTION, eReplicaPacketType::SERIALIZATION }) { + const bool construction = packetType == eReplicaPacketType::CONSTRUCTION; + Entity entity(15, info); + build(entity); + RakNet::BitStream actual; + entity.WriteComponents(actual, packetType); + + Entity copy(15, info); + build(copy); + RakNet::BitStream expected; + copy.GetComponent()->Serialize(expected, construction); + copy.GetComponent()->Serialize(expected, construction); + copy.GetComponent()->Serialize(expected, construction); + copy.GetComponent()->Serialize(expected, construction); + copy.GetComponent()->Serialize(expected, construction); + copy.GetComponent()->Serialize(expected, construction); + ExpectStartsWith(actual, expected); + } +}