fix: give the destroyable and buff components their real component types

eReplicaComponentType had the destroyable component's registry type (7) named
BUFF, the real buff component (98) as BUFF_REAL, and a made-up DESTROYABLE =
1000 that DestroyableComponent was stored under. Now DESTROYABLE = 7 and
BUFF = 98, as in ComponentsRegistry and the client.

Undone with it:
- Destructible stats came from whichever of the "buff" (7), quick build and
  collectible registry ids was set, so the few objects without a type 7 entry
  read a DestructibleComponent row with an unrelated id (the NJ dragon relics
  16482-16485 via their collectible id, 125 quick build LOTs when placed with
  is_smashable). The type 7 entry is used now; objects without one keep the
  defaults (is_smashable objects: 1 health, smashable, factions -1 and 6;
  collectibles: an empty destroyable). The client does the same
  (LWODestroyableComponent::AllocateComponents / DoObjectComponentLoad).
- DestroyableComponent::Reinitialize, an unused copy of that pick order.
- WriteComponents' destroyableSerialized flags: the components are written from
  a list in the client's order, and where the destroyable goes (its own place
  after the buff, right before a quick build that has no registry entry for it,
  or after the render component) is one function.
- The dashboard's registry 7 -> DESTROYABLE mapping; the destroyable type also
  has a name there now (1000 was outside magic_enum's range).

Component types are not stored or sent as enum numbers anywhere besides the
CDClient's own values, which now match. migrations/cdserver/4 is unrelated (it
restores LOT 12916's registry rows that migration 0 overwrote) and stays.

Verified: dGameTests ReplicaComponentOrderTests serialize players, enemies,
smashables, quick builds and collectibles with and without a registry entry,
NPCs, pets, vehicles, models and an entity with every listed component with
the new code and a frozen copy of the old WriteComponents, and expect the same
bits for construction and serialization (a deliberately wrong destroyable place
fails them). Full ctest passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 00:38:57 -05:00
parent 931277e76c
commit caebff4dca
10 changed files with 622 additions and 282 deletions

View File

@@ -11,7 +11,7 @@ enum class eReplicaComponentType : uint32_t {
CHARACTER,
SCRIPT,
BOUNCER,
BUFF, // buff is really 98, this is DESTROYABLE
DESTROYABLE,
GHOST,
SKILL,
SPAWNER,
@@ -102,7 +102,7 @@ enum class eReplicaComponentType : uint32_t {
USER_CONTROL,
IGNORE_LIST,
MULTI_ZONE_ENTRANCE,
BUFF_REAL, // the real buff component, should just be name BUFF
BUFF,
INTERACTION_MANAGER,
DONATION_VENDOR,
COMBAT_MEDIATOR,
@@ -121,7 +121,6 @@ enum class eReplicaComponentType : uint32_t {
BUILD_BORDER,
UNKNOWN_115,
CULLING_PLANE,
DESTROYABLE = 1000 // Actually 7
};
#endif //!__EREPLICACOMPONENTTYPE__H__

View File

@@ -199,7 +199,7 @@ namespace CDClientSchema {
inline const Table* ComponentTable(const Schema& schema, eReplicaComponentType type) {
using enum eReplicaComponentType;
static const std::map<eReplicaComponentType, std::string_view> DIFFERENT{
{ BUFF, "DestructibleComponent" }, // the enum's BUFF (7) is the destroyable component
{ DESTROYABLE, "DestructibleComponent" },
{ CONTROLLABLE_PHYSICS, "PhysicsComponent" }, { SIMPLE_PHYSICS, "PhysicsComponent" }, { PHANTOM_PHYSICS, "PhysicsComponent" },
{ RIGID_BODY_PHANTOM_PHYSICS, "PhysicsComponent" }, { HAVOK_VEHICLE_PHYSICS, "VehiclePhysics" },
{ QUICK_BUILD, "RebuildComponent" }, { MISSION_OFFER, "MissionNPCComponent" }, { ROCKET_LAUNCH, "RocketLaunchpadControlComponent" },

View File

@@ -131,16 +131,11 @@ namespace WorldScene {
eReplicaComponentType::SCRIPTED_ACTIVITY, eReplicaComponentType::DESTROYABLE
};
// ComponentsRegistry stores the destroyable component as 7 (the enum's BUFF; see eReplicaComponentType)
inline eReplicaComponentType FromRegistry(uint32_t componentType) {
return componentType == 7 ? eReplicaComponentType::DESTROYABLE : static_cast<eReplicaComponentType>(componentType);
}
// Index into KINDS of an object with these ComponentsRegistry component types, or KINDS.size() for "other"
inline size_t Classify(const std::vector<uint32_t>& registryComponents) {
size_t best = KINDS.size();
for (const auto component : registryComponents) {
const auto type = FromRegistry(component);
const auto type = static_cast<eReplicaComponentType>(component);
for (size_t i = 0; i < best; i++) {
if (KINDS[i] == type) { best = i; break; }
}

View File

@@ -10,6 +10,7 @@
#include "CDDestructibleComponentTable.h"
#include "CDClientDatabase.h"
#include <sstream>
#include <array>
#include "dServer.h"
#include "GameMessages.h"
#include "EffectsMessages.h"
@@ -383,11 +384,6 @@ void Entity::Initialize() {
AddComponent<RacingSoundTriggerComponent>(racingSoundTriggerID);
}
const auto buffComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::BUFF);
if (buffComponentID > 0) {
AddComponent<BuffComponent>(buffComponentID);
}
const auto collectibleComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::COLLECTIBLE);
if (collectibleComponentID > 0) {
@@ -395,18 +391,18 @@ void Entity::Initialize() {
}
/**
* Multiple components require the destructible component.
* The destroyable component. The client makes one (with a buff component in front of it) for a DESTROYABLE
* registry entry, whose id is the DestructibleComponent row. Collectibles and smashable (is_smashable) objects
* without that entry get one too, with no row.
*/
const auto quickBuildComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::QUICK_BUILD);
int32_t componentID = -1;
if (collectibleComponentID > 0) componentID = collectibleComponentID;
if (quickBuildComponentID > 0) componentID = quickBuildComponentID;
if (buffComponentID > 0) componentID = buffComponentID;
const auto destroyableComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::DESTROYABLE);
if (destroyableComponentID > 0) {
AddComponent<BuffComponent>(destroyableComponentID);
}
bool isSmashable = GetVarAs<int32_t>(u"is_smashable") != 0;
if (buffComponentID > 0 || collectibleComponentID > 0 || isSmashable) {
if (destroyableComponentID > 0 || collectibleComponentID > 0 || isSmashable) {
const int32_t componentID = destroyableComponentID > 0 ? destroyableComponentID : -1;
DestroyableComponent* comp = AddComponent<DestroyableComponent>(componentID);
auto* const destCompTable = CDClientManager::GetTable<CDDestructibleComponentTable>();
std::vector<CDDestructibleComponent> destCompData = destCompTable->Query([componentID](const CDDestructibleComponent& entry) { return (entry.id == componentID); });
@@ -451,7 +447,8 @@ void Entity::Initialize() {
comp->SetMaxCoins(currencyValues[0].maxvalue);
}
}
} else {
} else if (isSmashable) {
// A smashable object without a DestructibleComponent row
comp->SetHealth(1);
comp->SetArmor(0);
@@ -599,6 +596,7 @@ void Entity::Initialize() {
AddComponent<BaseCombatAIComponent>(combatAiID);
}
const auto quickBuildComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::QUICK_BUILD);
if (quickBuildComponentID > 0) {
auto* const quickBuildComponent = AddComponent<QuickBuildComponent>(quickBuildComponentID);
@@ -1064,222 +1062,56 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke
}
}
void Entity::WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const {
namespace {
/**
* This has to be done in a specific order.
* 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.
*/
constexpr auto SERIALIZATION_ORDER = [] {
using enum eReplicaComponentType;
return std::array{
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, CHARACTER, ITEM, INVENTORY,
SCRIPT, SKILL, BASE_COMBAT_AI, QUICK_BUILD, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR,
BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT, MODEL, RENDER, MINI_GAME_CONTROL,
};
}();
bool destroyableSerialized = false;
bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION;
PossessableComponent* possessableComponent;
if (TryGetComponent(eReplicaComponentType::POSSESSABLE, possessableComponent)) {
possessableComponent->Serialize(outBitStream, bIsInitialUpdate);
// The component in SERIALIZATION_ORDER the destroyable is written in front of (DESTROYABLE: at its own place)
eReplicaComponentType DestroyableSerializationSlot(const Entity& entity) {
using enum eReplicaComponentType;
// A DESTROYABLE registry entry puts the destroyable (with the buff in front of it) at its own place. Collectibles
// and quick builds without one get theirs right before themselves, which for a collectible is the same place.
if (entity.HasComponent(BUFF) || entity.HasComponent(COLLECTIBLE)) return DESTROYABLE;
if (entity.HasComponent(QUICK_BUILD)) return QUICK_BUILD;
// Anything else that has one (is_smashable objects, models) writes it after the others
return MINI_GAME_CONTROL;
}
}
ModuleAssemblyComponent* moduleAssemblyComponent;
if (TryGetComponent(eReplicaComponentType::MODULE_ASSEMBLY, moduleAssemblyComponent)) {
moduleAssemblyComponent->Serialize(outBitStream, bIsInitialUpdate);
}
void Entity::WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const {
const bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION;
auto* const destroyableComponent = GetComponent<DestroyableComponent>();
const auto destroyableSlot = DestroyableSerializationSlot(*this);
ControllablePhysicsComponent* controllablePhysicsComponent;
if (TryGetComponent(eReplicaComponentType::CONTROLLABLE_PHYSICS, controllablePhysicsComponent)) {
controllablePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
using enum eReplicaComponentType;
for (const auto type : SERIALIZATION_ORDER) {
if (type == destroyableSlot && destroyableComponent) destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
if (type == DESTROYABLE) continue;
SimplePhysicsComponent* simplePhysicsComponent;
if (TryGetComponent(eReplicaComponentType::SIMPLE_PHYSICS, simplePhysicsComponent)) {
simplePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
auto* const component = GetComponent(type);
if (!component) continue;
RigidbodyPhantomPhysicsComponent* rigidbodyPhantomPhysics;
if (TryGetComponent(eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS, rigidbodyPhantomPhysics)) {
rigidbodyPhantomPhysics->Serialize(outBitStream, bIsInitialUpdate);
}
HavokVehiclePhysicsComponent* havokVehiclePhysicsComponent;
if (TryGetComponent(eReplicaComponentType::HAVOK_VEHICLE_PHYSICS, havokVehiclePhysicsComponent)) {
havokVehiclePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
PhantomPhysicsComponent* phantomPhysicsComponent;
if (TryGetComponent(eReplicaComponentType::PHANTOM_PHYSICS, phantomPhysicsComponent)) {
phantomPhysicsComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SoundTriggerComponent* soundTriggerComponent;
if (TryGetComponent(eReplicaComponentType::SOUND_TRIGGER, soundTriggerComponent)) {
soundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RacingSoundTriggerComponent* racingSoundTriggerComponent;
if (TryGetComponent(eReplicaComponentType::RACING_SOUND_TRIGGER, racingSoundTriggerComponent)) {
racingSoundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BuffComponent* buffComponent;
if (TryGetComponent(eReplicaComponentType::BUFF, buffComponent)) {
buffComponent->Serialize(outBitStream, bIsInitialUpdate);
DestroyableComponent* destroyableComponent;
if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent)) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
if (type == CHARACTER) {
// Written with the character component; every character has them, a 0 bit stands in should one be missing
for (const auto characterPart : { POSSESSOR, LEVEL_PROGRESSION, PLAYER_FORCED_MOVEMENT }) {
auto* const part = GetComponent(characterPart);
if (part) part->Serialize(outBitStream, bIsInitialUpdate);
else outBitStream.Write0();
}
}
destroyableSerialized = true;
}
CollectibleComponent* collectibleComponent;
if (TryGetComponent(eReplicaComponentType::COLLECTIBLE, collectibleComponent)) {
DestroyableComponent* destroyableComponent;
if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
destroyableSerialized = true;
collectibleComponent->Serialize(outBitStream, bIsInitialUpdate);
}
PetComponent* petComponent;
if (TryGetComponent(eReplicaComponentType::PET, petComponent)) {
petComponent->Serialize(outBitStream, bIsInitialUpdate);
}
CharacterComponent* characterComponent;
if (TryGetComponent(eReplicaComponentType::CHARACTER, characterComponent)) {
PossessorComponent* possessorComponent;
if (TryGetComponent(eReplicaComponentType::POSSESSOR, possessorComponent)) {
possessorComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
LevelProgressionComponent* levelProgressionComponent;
if (TryGetComponent(eReplicaComponentType::LEVEL_PROGRESSION, levelProgressionComponent)) {
levelProgressionComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
PlayerForcedMovementComponent* playerForcedMovementComponent;
if (TryGetComponent(eReplicaComponentType::PLAYER_FORCED_MOVEMENT, playerForcedMovementComponent)) {
playerForcedMovementComponent->Serialize(outBitStream, bIsInitialUpdate);
} else {
// Should never happen, but just to be safe
outBitStream.Write0();
}
characterComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ItemComponent* itemComponent;
if (TryGetComponent(eReplicaComponentType::ITEM, itemComponent)) {
itemComponent->Serialize(outBitStream, bIsInitialUpdate);
}
InventoryComponent* inventoryComponent;
if (TryGetComponent(eReplicaComponentType::INVENTORY, inventoryComponent)) {
inventoryComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ScriptComponent* scriptComponent;
if (TryGetComponent(eReplicaComponentType::SCRIPT, scriptComponent)) {
scriptComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SkillComponent* skillComponent;
if (TryGetComponent(eReplicaComponentType::SKILL, skillComponent)) {
skillComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BaseCombatAIComponent* baseCombatAiComponent;
if (TryGetComponent(eReplicaComponentType::BASE_COMBAT_AI, baseCombatAiComponent)) {
baseCombatAiComponent->Serialize(outBitStream, bIsInitialUpdate);
}
QuickBuildComponent* quickBuildComponent;
if (TryGetComponent(eReplicaComponentType::QUICK_BUILD, quickBuildComponent)) {
DestroyableComponent* destroyableComponent;
if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
}
destroyableSerialized = true;
quickBuildComponent->Serialize(outBitStream, bIsInitialUpdate);
}
MovingPlatformComponent* movingPlatformComponent;
if (TryGetComponent(eReplicaComponentType::MOVING_PLATFORM, movingPlatformComponent)) {
movingPlatformComponent->Serialize(outBitStream, bIsInitialUpdate);
}
SwitchComponent* switchComponent;
if (TryGetComponent(eReplicaComponentType::SWITCH, switchComponent)) {
switchComponent->Serialize(outBitStream, bIsInitialUpdate);
}
VendorComponent* vendorComponent;
if (TryGetComponent(eReplicaComponentType::VENDOR, vendorComponent)) {
vendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
DonationVendorComponent* donationVendorComponent;
if (TryGetComponent(eReplicaComponentType::DONATION_VENDOR, donationVendorComponent)) {
donationVendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
AchievementVendorComponent* achievementVendorComponent;
if (TryGetComponent(eReplicaComponentType::ACHIEVEMENT_VENDOR, achievementVendorComponent)) {
achievementVendorComponent->Serialize(outBitStream, bIsInitialUpdate);
}
BouncerComponent* bouncerComponent;
if (TryGetComponent(eReplicaComponentType::BOUNCER, bouncerComponent)) {
bouncerComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ScriptedActivityComponent* scriptedActivityComponent;
if (TryGetComponent(eReplicaComponentType::SCRIPTED_ACTIVITY, scriptedActivityComponent)) {
scriptedActivityComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ShootingGalleryComponent* shootingGalleryComponent;
if (TryGetComponent(eReplicaComponentType::SHOOTING_GALLERY, shootingGalleryComponent)) {
shootingGalleryComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RacingControlComponent* racingControlComponent;
if (TryGetComponent(eReplicaComponentType::RACING_CONTROL, racingControlComponent)) {
racingControlComponent->Serialize(outBitStream, bIsInitialUpdate);
}
LUPExhibitComponent* lupExhibitComponent;
if (TryGetComponent(eReplicaComponentType::LUP_EXHIBIT, lupExhibitComponent)) {
lupExhibitComponent->Serialize(outBitStream, bIsInitialUpdate);
}
ModelComponent* modelComponent;
if (TryGetComponent(eReplicaComponentType::MODEL, modelComponent)) {
modelComponent->Serialize(outBitStream, bIsInitialUpdate);
}
RenderComponent* renderComponent;
if (TryGetComponent(eReplicaComponentType::RENDER, renderComponent)) {
renderComponent->Serialize(outBitStream, bIsInitialUpdate);
}
if (modelComponent || !destroyableSerialized) {
DestroyableComponent* destroyableComponent;
if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) {
destroyableComponent->Serialize(outBitStream, bIsInitialUpdate);
destroyableSerialized = true;
}
}
MiniGameControlComponent* miniGameControlComponent;
if (TryGetComponent(eReplicaComponentType::MINI_GAME_CONTROL, miniGameControlComponent)) {
miniGameControlComponent->Serialize(outBitStream, bIsInitialUpdate);
component->Serialize(outBitStream, bIsInitialUpdate);
}
// BBB Component, unused currently

View File

@@ -97,48 +97,6 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone
DestroyableComponent::~DestroyableComponent() {
}
void DestroyableComponent::Reinitialize(LOT templateID) {
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
int32_t buffComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::BUFF);
int32_t collectibleComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::COLLECTIBLE);
int32_t quickBuildComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::QUICK_BUILD);
int32_t componentID = 0;
if (collectibleComponentID > 0) componentID = collectibleComponentID;
if (quickBuildComponentID > 0) componentID = quickBuildComponentID;
if (buffComponentID > 0) componentID = buffComponentID;
CDDestructibleComponentTable* destCompTable = CDClientManager::GetTable<CDDestructibleComponentTable>();
std::vector<CDDestructibleComponent> destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); });
if (componentID > 0) {
std::vector<CDDestructibleComponent> destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); });
if (destCompData.size() > 0) {
SetHealth(destCompData[0].life);
SetImagination(destCompData[0].imagination);
SetArmor(destCompData[0].armor);
SetMaxHealth(destCompData[0].life);
SetMaxImagination(destCompData[0].imagination);
SetMaxArmor(destCompData[0].armor);
SetIsSmashable(destCompData[0].isSmashable);
}
} else {
SetHealth(1);
SetImagination(0);
SetArmor(0);
SetMaxHealth(1);
SetMaxImagination(0);
SetMaxArmor(0);
SetIsSmashable(true);
}
}
void DestroyableComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
if (bIsInitialUpdate) {
outBitStream.Write1(); // always write these on construction

View File

@@ -36,12 +36,6 @@ public:
void LoadFromXml(const tinyxml2::XMLDocument& doc) override;
void UpdateXml(tinyxml2::XMLDocument& doc) override;
/**
* Initializes the component using a different LOT
* @param templateID the ID to use for initialization
*/
void Reinitialize(LOT templateID);
/**
* Sets the health of this entity. Makes sure this is serialized on the next tick and if this is a character its
* stats will also update.

View File

@@ -25,7 +25,7 @@ file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
# Add the executable. Remember to add all tests above this!
add_executable(dGameTests ${DGAMETEST_SOURCES})
target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer")
target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer" "${PROJECT_SOURCE_DIR}/dScripts")
if(APPLE)
add_custom_target(dGameTestsLink

View File

@@ -3,6 +3,7 @@ set(DCOMPONENTS_TESTS
"PetComponentTests.cpp"
"SimplePhysicsComponentTests.cpp"
"SavingTests.cpp"
"ReplicaComponentOrderTests.cpp"
)
# Get the folder name and prepend it to the files above

View File

@@ -0,0 +1,561 @@
#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 "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);
}
ItemComponent* itemComponent;
if (TryGet(entity, itemComponent)) {
itemComponent->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);
}
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);
}
// BBB Component, unused currently
// Need to to write0 so that is serialized correctly
// TODO: Implement BBB Component
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)",
"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);
});
}

View File

@@ -74,7 +74,7 @@ TEST(CDClientSchemaTest, ComponentTables) {
};
EXPECT_EQ(name(eReplicaComponentType::VENDOR), "VendorComponent");
EXPECT_EQ(name(eReplicaComponentType::RENDER), "RenderComponent");
EXPECT_EQ(name(eReplicaComponentType::BUFF), "DestructibleComponent");
EXPECT_EQ(name(eReplicaComponentType::DESTROYABLE), "DestructibleComponent");
EXPECT_EQ(name(eReplicaComponentType::SIMPLE_PHYSICS), "PhysicsComponent");
EXPECT_EQ(name(eReplicaComponentType::PET), "-");
}