Files
DarkflameServer/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp
Aaron Kimbrell c5c90f73a2 fix: write what the client reads for a chest, a choice build and a roller
The three components were made from the registry and wrote nothing, so
the client read the next component's bits as theirs.

- LWOChestComponent::Deserialize (0x00c22010), construction only: the
  player (u64) and the loot offer count (u32).
- LWOChoiceBuildComponent::Deserialize (0x00bf26d0): a flag, then the
  state (u32, 0 waiting, 1 building), the built object and the user
  (u64 each). Always in a construction.
- LWORollerComponent::Deserialize (0x00c15920), construction: the level's
  activity_camera_pos and activity_camera_lookat_pos (3 floats each) and
  the projectile skill (u32, the client starts at 1348); then the flag of
  the block about its user, which is never set.

LWOGameObject::Deserialize (0x00f99690) reads the components in the order
of the object's component list, which ObjectLoader2::DoObjectComponentLoad
(0x0105ebb0) fills in the order of FillComponentOrder (0x0101f8e0): the
roller after the shooting gallery and before the moving platform, the
choice build after the moving platform and before the LUP exhibit and the
model, the chest after the zone control and before render. They are
placed against the components their objects have.

The gate rush control (103) is left out: its two registry entries have no
object, and there is no component for it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:54:54 -06:00

666 lines
25 KiB
C++

#include "GameDependencies.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <functional>
#include <memory>
#include "BitStream.h"
#include "CDClientDatabase.h"
#include "Character.h"
#include "Entity.h"
#include "eReplicaComponentType.h"
#include "eReplicaPacketType.h"
#include "tinyxml2.h"
#include "AchievementVendorComponent.h"
#include "BaseCombatAIComponent.h"
#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"
#include "DonationVendorComponent.h"
#include "HavokVehiclePhysicsComponent.h"
#include "InventoryComponent.h"
#include "ItemComponent.h"
#include "LevelProgressionComponent.h"
#include "LUPExhibitComponent.h"
#include "MiniGameControlComponent.h"
#include "ModelComponent.h"
#include "ModuleAssemblyComponent.h"
#include "MovingPlatformComponent.h"
#include "PetComponent.h"
#include "PhantomPhysicsComponent.h"
#include "PlayerForcedMovementComponent.h"
#include "PossessableComponent.h"
#include "PossessorComponent.h"
#include "QuickBuildComponent.h"
#include "RacingControlComponent.h"
#include "RacingSoundTriggerComponent.h"
#include "RenderComponent.h"
#include "RigidbodyPhantomPhysicsComponent.h"
#include "ScriptComponent.h"
#include "ScriptedActivityComponent.h"
#include "ShootingGalleryComponent.h"
#include "SimplePhysicsComponent.h"
#include "SkillComponent.h"
#include "SoundTriggerComponent.h"
#include "SwitchComponent.h"
#include "VendorComponent.h"
/**
* Entity::WriteComponents writes the components in the client's order from a list of component types. Before, the
* order was spelled out component by component, with flags for where the destroyable went. That code is kept below,
* unchanged apart from looking components up by class, as the oracle: the bytes must not change.
*/
namespace {
template<typename T>
bool TryGet(const Entity& entity, T*& component) {
component = entity.GetComponent<T>();
return component != nullptr;
}
void OldWriteComponents(const Entity& entity, RakNet::BitStream& outBitStream, eReplicaPacketType packetType) {
/**
* This has to be done in a specific order.
*/
bool destroyableSerialized = false;
bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION;
PossessableComponent* possessableComponent;
if (TryGet(entity, possessableComponent)) {
possessableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ModuleAssemblyComponent* moduleAssemblyComponent;
if (TryGet(entity, moduleAssemblyComponent)) {
moduleAssemblyComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ControllablePhysicsComponent* controllablePhysicsComponent;
if (TryGet(entity, controllablePhysicsComponent)) {
controllablePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SimplePhysicsComponent* simplePhysicsComponent;
if (TryGet(entity, simplePhysicsComponent)) {
simplePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RigidbodyPhantomPhysicsComponent* rigidbodyPhantomPhysics;
if (TryGet(entity, rigidbodyPhantomPhysics)) {
rigidbodyPhantomPhysics->Serialize(outBitStream, bIsInitialUpdate);
}
HavokVehiclePhysicsComponent* havokVehiclePhysicsComponent;
if (TryGet(entity, havokVehiclePhysicsComponent)) {
havokVehiclePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
PhantomPhysicsComponent* phantomPhysicsComponent;
if (TryGet(entity, phantomPhysicsComponent)) {
phantomPhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SoundTriggerComponent* soundTriggerComponent;
if (TryGet(entity, soundTriggerComponent)) {
soundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RacingSoundTriggerComponent* racingSoundTriggerComponent;
if (TryGet(entity, racingSoundTriggerComponent)) {
racingSoundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BuffComponent* buffComponent;
if (TryGet(entity, buffComponent)) {
buffComponent->Serialize(outBitStream, bIsInitialUpdate);
DestroyableComponent* destroyableComponent;
if (TryGet(entity, destroyableComponent)) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
destroyableSerialized = true;
}
CollectibleComponent* collectibleComponent;
if (TryGet(entity, collectibleComponent)) {
DestroyableComponent* destroyableComponent;
if (TryGet(entity, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
destroyableSerialized = true;
collectibleComponent->Serialize(outBitStream, bIsInitialUpdate);
}
PetComponent* petComponent;
if (TryGet(entity, petComponent)) {
petComponent->Serialize(outBitStream, bIsInitialUpdate);
}
CharacterComponent* characterComponent;
if (TryGet(entity, characterComponent)) {
PossessorComponent* possessorComponent;
if (TryGet(entity, possessorComponent)) {
possessorComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
LevelProgressionComponent* levelProgressionComponent;
if (TryGet(entity, levelProgressionComponent)) {
levelProgressionComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
PlayerForcedMovementComponent* playerForcedMovementComponent;
if (TryGet(entity, playerForcedMovementComponent)) {
playerForcedMovementComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
characterComponent->Serialize(outBitStream, bIsInitialUpdate);
}
InventoryComponent* inventoryComponent;
if (TryGet(entity, inventoryComponent)) {
inventoryComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ScriptComponent* scriptComponent;
if (TryGet(entity, scriptComponent)) {
scriptComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SkillComponent* skillComponent;
if (TryGet(entity, skillComponent)) {
skillComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BaseCombatAIComponent* baseCombatAiComponent;
if (TryGet(entity, baseCombatAiComponent)) {
baseCombatAiComponent->Serialize(outBitStream, bIsInitialUpdate);
}
// The item comes after the skill and combat AI components, as the client reads them (ComponentOrderVector::Initialize
// 0x0101f8e0; live constructions of LOT 14535). It used to come before the inventory, which was harmless while
// it wrote a single 0 bit.
ItemComponent* itemComponent;
if (TryGet(entity, itemComponent)) {
itemComponent->Serialize(outBitStream, bIsInitialUpdate);
}
QuickBuildComponent* quickBuildComponent;
if (TryGet(entity, quickBuildComponent)) {
DestroyableComponent* destroyableComponent;
if (TryGet(entity, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
destroyableSerialized = true;
quickBuildComponent->Serialize(outBitStream, bIsInitialUpdate);
}
MovingPlatformComponent* movingPlatformComponent;
if (TryGet(entity, movingPlatformComponent)) {
movingPlatformComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SwitchComponent* switchComponent;
if (TryGet(entity, switchComponent)) {
switchComponent->Serialize(outBitStream, bIsInitialUpdate);
}
VendorComponent* vendorComponent;
if (TryGet(entity, vendorComponent)) {
vendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
DonationVendorComponent* donationVendorComponent;
if (TryGet(entity, donationVendorComponent)) {
donationVendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
AchievementVendorComponent* achievementVendorComponent;
if (TryGet(entity, achievementVendorComponent)) {
achievementVendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BouncerComponent* bouncerComponent;
if (TryGet(entity, bouncerComponent)) {
bouncerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ScriptedActivityComponent* scriptedActivityComponent;
if (TryGet(entity, scriptedActivityComponent)) {
scriptedActivityComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ShootingGalleryComponent* shootingGalleryComponent;
if (TryGet(entity, shootingGalleryComponent)) {
shootingGalleryComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RacingControlComponent* racingControlComponent;
if (TryGet(entity, racingControlComponent)) {
racingControlComponent->Serialize(outBitStream, bIsInitialUpdate);
}
LUPExhibitComponent* lupExhibitComponent;
if (TryGet(entity, lupExhibitComponent)) {
lupExhibitComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ModelComponent* modelComponent;
if (TryGet(entity, modelComponent)) {
modelComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RenderComponent* renderComponent;
if (TryGet(entity, renderComponent)) {
renderComponent->Serialize(outBitStream, bIsInitialUpdate);
}
if (modelComponent || !destroyableSerialized) {
DestroyableComponent* destroyableComponent;
if (TryGet(entity, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
destroyableSerialized = true;
}
}
MiniGameControlComponent* miniGameControlComponent;
if (TryGet(entity, miniGameControlComponent)) {
miniGameControlComponent->Serialize(outBitStream, bIsInitialUpdate);
}
// The BBB component (characters): no metadata source item
if (entity.GetLOT() == 1) outBitStream.Write0();
}
// Some components read the CDClient in their constructors; give them an empty one
void ConnectEmptyCDClient() {
if (CDClientDatabase::isConnected) return;
CDClientDatabase::Connect(":memory:");
for (const auto* table : {
"ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER)",
"Objects (id INTEGER, name TEXT, type TEXT, interactionDistance REAL)",
"BaseCombatAIComponent (id INTEGER, aggroRadius REAL, tetherSpeed REAL, pursuitSpeed REAL, softTetherRadius REAL, hardTetherRadius REAL, minRoundLength REAL, maxRoundLength REAL, combatRoundLength REAL)",
"ItemSets (setID INTEGER, itemIDs TEXT)",
"ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER)",
"SkillBehavior (skillID INTEGER, behaviorID INTEGER)",
"RenderComponent (id INTEGER)",
"BuffParameters (BuffID INTEGER)",
"PossessableComponent (id INTEGER, possessionType INTEGER, depossessOnHit INTEGER, skillSet INTEGER)",
}) {
CDClientDatabase::ExecuteDML(std::string("CREATE TABLE ") + table + ";");
}
}
std::vector<uint8_t> Bytes(RakNet::BitStream& stream) {
return { stream.GetData(), stream.GetData() + stream.GetNumberOfBytesUsed() };
}
}
class ReplicaComponentOrderTest : public GameDependenciesTest {
protected:
std::vector<std::unique_ptr<Character>> characters;
void SetUp() override {
SetUpDependencies();
ConnectEmptyCDClient();
}
void TearDown() override {
TearDownDependencies();
}
std::unique_ptr<Entity> MakePlayer() {
auto entity = std::make_unique<Entity>(1, info);
auto& character = characters.emplace_back(std::make_unique<Character>(1, nullptr));
tinyxml2::XMLDocument doc;
doc.LoadFile("./test_xml_data.xml");
tinyxml2::XMLPrinter printer{ 0, true, 0 };
doc.Print(&printer);
character->_setXmlData(printer.CStr());
character->_doQuickXMLDataParse();
entity->SetCharacter(character.get());
character->SetEntity(entity.get());
entity->AddComponent<ControllablePhysicsComponent>(-1);
entity->AddComponent<BuffComponent>(-1);
entity->AddComponent<DestroyableComponent>(-1)->SetMaxHealth(4.0f);
entity->AddComponent<PossessorComponent>(-1);
entity->AddComponent<LevelProgressionComponent>(-1);
entity->AddComponent<PlayerForcedMovementComponent>(-1);
entity->AddComponent<CharacterComponent>(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS);
entity->AddComponent<InventoryComponent>(-1);
entity->AddComponent<SkillComponent>(-1);
entity->AddComponent<RenderComponent>(-1);
return entity;
}
// Serializes an entity built by `build` with the old and the new code (each on its own copy, as serializing
// clears dirty flags) and expects the same bits, for the construction and a serialization.
void ExpectSameBytes(const std::function<void(Entity&)>& build) {
for (const auto packetType : { eReplicaPacketType::CONSTRUCTION, eReplicaPacketType::SERIALIZATION }) {
Entity oldEntity(15, info);
Entity newEntity(15, info);
// Same random numbers for both (a vehicle picks a random end behavior)
Game::randomEngine.seed(1);
build(oldEntity);
Game::randomEngine.seed(1);
build(newEntity);
RakNet::BitStream oldStream;
RakNet::BitStream newStream;
OldWriteComponents(oldEntity, oldStream, packetType);
newEntity.WriteComponents(newStream, packetType);
EXPECT_GT(newStream.GetNumberOfBitsUsed(), 1u);
EXPECT_EQ(oldStream.GetNumberOfBitsUsed(), newStream.GetNumberOfBitsUsed()) << "packet type " << static_cast<int>(packetType);
const auto oldBytes = Bytes(oldStream);
const auto newBytes = Bytes(newStream);
const auto mismatch = std::ranges::mismatch(oldBytes, newBytes);
EXPECT_EQ(mismatch.in1, oldBytes.end()) << "packet type " << static_cast<int>(packetType) << ", first different byte " << (mismatch.in1 - oldBytes.begin());
}
}
};
TEST(ReplicaComponentTypeTest, MatchesComponentsRegistry) {
// ComponentsRegistry.component_type values
EXPECT_EQ(static_cast<uint32_t>(eReplicaComponentType::DESTROYABLE), 7u);
EXPECT_EQ(static_cast<uint32_t>(eReplicaComponentType::BUFF), 98u);
EXPECT_EQ(DestroyableComponent::ComponentType, eReplicaComponentType::DESTROYABLE);
EXPECT_EQ(BuffComponent::ComponentType, eReplicaComponentType::BUFF);
}
TEST_F(ReplicaComponentOrderTest, Player) {
for (const auto packetType : { eReplicaPacketType::CONSTRUCTION, eReplicaPacketType::SERIALIZATION }) {
auto oldEntity = MakePlayer();
auto newEntity = MakePlayer();
RakNet::BitStream oldStream;
RakNet::BitStream newStream;
OldWriteComponents(*oldEntity, oldStream, packetType);
newEntity->WriteComponents(newStream, packetType);
EXPECT_EQ(oldStream.GetNumberOfBitsUsed(), newStream.GetNumberOfBitsUsed());
EXPECT_EQ(Bytes(oldStream), Bytes(newStream));
oldEntity->SetCharacter(nullptr);
newEntity->SetCharacter(nullptr);
}
}
TEST_F(ReplicaComponentOrderTest, Enemy) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<ControllablePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
auto* destroyable = entity.AddComponent<DestroyableComponent>(-1);
destroyable->SetMaxHealth(8.0f);
destroyable->SetHealth(8);
destroyable->AddFactionNoLookup(4);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<BaseCombatAIComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, SmashableWithDestructibleRow) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetIsSmashable(true);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, IsSmashableWithoutRegistryEntry) {
// Written after the render component
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<ScriptComponent>(-1, "", true);
auto* destroyable = entity.AddComponent<DestroyableComponent>(-1);
destroyable->SetIsSmashable(true);
destroyable->SetMaxHealth(1.0f);
destroyable->SetHealth(1);
entity.AddComponent<RenderComponent>(-1);
entity.AddComponent<MiniGameControlComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, QuickBuildWithoutDestroyable) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<QuickBuildComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, QuickBuildWithoutRegistryEntry) {
// Written right before the quick build, after the script
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetIsSmashable(true);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<QuickBuildComponent>(-1);
entity.AddComponent<MovingPlatformComponent>(-1, "");
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, QuickBuildWithRegistryEntry) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetMaxHealth(3.0f);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<QuickBuildComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, CollectibleWithoutRegistryEntry) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1);
entity.AddComponent<CollectibleComponent>(-1, 12);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, CollectibleWithRegistryEntry) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetIsSmashable(true);
entity.AddComponent<CollectibleComponent>(-1, 3);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, Npc) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<ControllablePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1);
entity.AddComponent<InventoryComponent>(-1);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<SwitchComponent>(-1);
entity.AddComponent<BouncerComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, Pet) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<ControllablePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1);
entity.AddComponent<PetComponent>(1);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, Vehicle) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<PossessableComponent>(-1);
entity.AddComponent<ModuleAssemblyComponent>(-1);
entity.AddComponent<HavokVehiclePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
});
}
TEST_F(ReplicaComponentOrderTest, Model) {
// A model's destroyable is written after the render component
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<ModelComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetIsSmashable(true);
});
}
TEST_F(ReplicaComponentOrderTest, EveryListedComponent) {
ExpectSameBytes([](Entity& entity) {
entity.AddComponent<PossessableComponent>(-1);
entity.AddComponent<ModuleAssemblyComponent>(-1);
entity.AddComponent<ControllablePhysicsComponent>(-1);
entity.AddComponent<SimplePhysicsComponent>(-1);
entity.AddComponent<RigidbodyPhantomPhysicsComponent>(-1);
entity.AddComponent<HavokVehiclePhysicsComponent>(-1);
entity.AddComponent<PhantomPhysicsComponent>(-1);
entity.AddComponent<SoundTriggerComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1);
entity.AddComponent<CollectibleComponent>(-1, 1);
entity.AddComponent<ItemComponent>(-1);
entity.AddComponent<InventoryComponent>(-1);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<QuickBuildComponent>(-1);
entity.AddComponent<SwitchComponent>(-1);
entity.AddComponent<BouncerComponent>(-1);
entity.AddComponent<ScriptedActivityComponent>(-1);
entity.AddComponent<LUPExhibitComponent>(-1);
entity.AddComponent<ModelComponent>(-1);
entity.AddComponent<RenderComponent>(-1);
entity.AddComponent<MiniGameControlComponent>(-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<PhantomPhysicsComponent>(-1);
auto* const chest = entity.AddComponent<ChestComponent>(0);
chest->SetPlayerID(0x1000000000000001LL);
chest->SetLootOfferCount(3);
auto* const render = entity.AddComponent<RenderComponent>(-1);
RakNet::BitStream actual;
entity.WriteComponents(actual, eReplicaPacketType::CONSTRUCTION);
Entity copy(15, info);
RakNet::BitStream expected;
copy.AddComponent<PhantomPhysicsComponent>(-1)->Serialize(expected, true);
expected.Write<LWOOBJID>(0x1000000000000001LL);
expected.Write<uint32_t>(3);
copy.AddComponent<RenderComponent>(-1)->Serialize(expected, true);
ExpectStartsWith(actual, expected);
static_cast<void>(physics);
static_cast<void>(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<SimplePhysicsComponent>(-1);
entity.AddComponent<BuffComponent>(-1);
entity.AddComponent<DestroyableComponent>(-1)->SetMaxHealth(1.0f);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<ChoiceBuildComponent>(1);
entity.AddComponent<RenderComponent>(-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<SimplePhysicsComponent>()->Serialize(expected, true);
copy.GetComponent<BuffComponent>()->Serialize(expected, true);
copy.GetComponent<DestroyableComponent>()->Serialize(expected, true);
copy.GetComponent<ScriptComponent>()->Serialize(expected, true);
expected.Write1();
expected.Write<uint32_t>(0); // waiting
expected.Write<LWOOBJID>(0); // nothing built
expected.Write<LWOOBJID>(0); // nobody using it
copy.GetComponent<RenderComponent>()->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<SimplePhysicsComponent>(-1);
entity.AddComponent<ScriptComponent>(-1, "", true);
entity.AddComponent<SkillComponent>(-1);
entity.AddComponent<RollerComponent>(13951);
entity.AddComponent<MovingPlatformComponent>(-1, "");
entity.AddComponent<RenderComponent>(-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<SimplePhysicsComponent>()->Serialize(expected, construction);
copy.GetComponent<ScriptComponent>()->Serialize(expected, construction);
copy.GetComponent<SkillComponent>()->Serialize(expected, construction);
copy.GetComponent<RollerComponent>()->Serialize(expected, construction);
copy.GetComponent<MovingPlatformComponent>()->Serialize(expected, construction);
copy.GetComponent<RenderComponent>()->Serialize(expected, construction);
ExpectStartsWith(actual, expected);
}
}