Files
DarkflameServer/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp
Aaron Kimbrell 08053a1b01 feat(combat): dead enemies are stunned right after Die
Live sent SetStunned to every dead object with a combat AI in the
packet right after its Die: 7,098 of 7,105 such deaths (7,424 decoded
stuns, all the same: no originator, push, can't attack, move or turn,
ignore immunity). Objects without combat AI (smashables, 9,393 deaths)
never got one. DLU sent none, so a dying enemy could keep turning or
start an attack while its death plays.

Check: kill enemies mid-attack and while they chase you; they stop
moving, turning and attacking the moment they die, and the death and
loot still play normally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:50:02 -05:00

709 lines
32 KiB
C++

#include "GameDependencies.h"
#include <gtest/gtest.h>
#include "BitStream.h"
#include "DestroyableComponent.h"
#include "Entity.h"
#include "eReplicaComponentType.h"
#include "eStateChangeType.h"
#include "Character.h"
#include "CharacterComponent.h"
#include "BaseCombatAIComponent.h"
#include "CDClientDatabase.h"
#include "CombatMessages.h"
#include "dGameMessagesTests/GameMessageTestUtils.h"
#include "PacketTestUtils.h"
#include "dCommonVars.h"
#include <algorithm>
#include <cstring>
#include <optional>
namespace {
// The first game message with this ID among captured packets, read into T.
template<typename T>
std::optional<T> FindGameMessage(const std::vector<CapturedPacket>& packets, const MessageType::Game id) {
for (const auto& packet : packets) {
RakNet::BitStream bitStream(const_cast<unsigned char*>(packet.bytes.data()), packet.bytes.size(), false);
bitStream.IgnoreBytes(8); // CLIENT GAME_MSG header
LWOOBJID target{};
MessageType::Game messageID{};
if (!bitStream.Read(target) || !bitStream.Read(messageID) || messageID != id) continue;
T msg;
if (!msg.Deserialize(bitStream)) continue;
msg.target = target;
return msg;
}
return std::nullopt;
}
}
class DestroyableTest : public GameDependenciesTest {
protected:
Entity* baseEntity;
DestroyableComponent* destroyableComponent;
RakNet::BitStream bitStream;
uint32_t flags = 0;
void SetUp() override {
SetUpDependencies();
baseEntity = new Entity(15, GameDependenciesTest::info);
destroyableComponent = baseEntity->AddComponent<DestroyableComponent>(-1);
// Initialize some values to be not default
destroyableComponent->SetMaxHealth(12345.0f);
destroyableComponent->SetHealth(23);
destroyableComponent->SetMaxArmor(14.0f);
destroyableComponent->SetArmor(7);
destroyableComponent->SetMaxImagination(14000.0f);
destroyableComponent->SetImagination(6000);
destroyableComponent->SetIsSmashable(true);
destroyableComponent->SetExplodeFactor(1.1f);
destroyableComponent->AddFactionNoLookup(-1);
destroyableComponent->AddFactionNoLookup(6);
}
void TearDown() override {
delete baseEntity;
TearDownDependencies();
}
};
TEST_F(DestroyableTest, PlacementNewAddComponentTest) {
ASSERT_NE(destroyableComponent, nullptr);
ASSERT_EQ(destroyableComponent->GetArmor(), 7);
baseEntity->AddComponent<DestroyableComponent>(-1);
ASSERT_NE(baseEntity->GetComponent<DestroyableComponent>(), nullptr);
ASSERT_EQ(destroyableComponent->GetArmor(), 0);
}
/**
* Test Construction of a DestroyableComponent
*/
TEST_F(DestroyableTest, DestroyableComponentSerializeConstructionTest) {
destroyableComponent->Serialize(bitStream, true);
// Assert that the full number of bits are present
ASSERT_EQ(bitStream.GetNumberOfUnreadBits(), 748);
{
// Now read in the full serialized construction BitStream
bool optionStatusImmunityInfo{};
uint32_t ImmuneToBasicAttackCount{};
uint32_t ImmuneToDamageOverTimeCount{};
uint32_t ImmuneToKnockbackCount{};
uint32_t ImmuneToInterruptCount{};
uint32_t ImmuneToSpeedCount{};
uint32_t ImmuneToImaginationGainCount{};
uint32_t ImmuneToImaginationLossCount{};
uint32_t ImmuneToQuickbuildInterruptCount{};
uint32_t ImmuneToPullToPointCount{};
bool optionStatsInfo{};
uint32_t currentHealth{};
float maxHealth{};
uint32_t currentArmor{};
float maxArmor{};
uint32_t currentImagination{};
float maxImagination{};
uint32_t damageAbsorptionPoints{};
bool hasImmunity{};
bool isGmImmune{};
bool isShielded{};
float actualMaxHealth{};
float actualMaxArmor{};
float actualMaxImagination{};
uint32_t factionsSize{};
std::vector<int32_t> factions{};
bool isSmashable{};
bool isDead{};
bool isSmashed{};
bool isModuleAssembly{};
bool optionExplodeFactor{};
float explodeFactor{};
bool optionIsOnThreatList{};
bool isThreatened{};
bitStream.Read(optionStatusImmunityInfo);
bitStream.Read(ImmuneToBasicAttackCount);
bitStream.Read(ImmuneToDamageOverTimeCount);
bitStream.Read(ImmuneToKnockbackCount);
bitStream.Read(ImmuneToInterruptCount);
bitStream.Read(ImmuneToSpeedCount);
bitStream.Read(ImmuneToImaginationGainCount);
bitStream.Read(ImmuneToImaginationLossCount);
bitStream.Read(ImmuneToQuickbuildInterruptCount);
bitStream.Read(ImmuneToPullToPointCount);
bitStream.Read(optionStatsInfo);
bitStream.Read(currentHealth);
bitStream.Read(maxHealth);
bitStream.Read(currentArmor);
bitStream.Read(maxArmor);
bitStream.Read(currentImagination);
bitStream.Read(maxImagination);
bitStream.Read(damageAbsorptionPoints);
bitStream.Read(hasImmunity);
bitStream.Read(isGmImmune);
bitStream.Read(isShielded);
bitStream.Read(actualMaxHealth);
bitStream.Read(actualMaxArmor);
bitStream.Read(actualMaxImagination);
bitStream.Read(factionsSize);
for (uint32_t i = 0; i < factionsSize; i++) {
int32_t factionID{};
bitStream.Read(factionID);
factions.push_back(factionID);
}
bitStream.Read(isSmashable); // This is an option later and also a flag at this spot
bitStream.Read(isDead);
bitStream.Read(isSmashed);
// if IsSmashable is true, read the next bits.
bitStream.Read(isModuleAssembly);
bitStream.Read(optionExplodeFactor);
bitStream.Read(explodeFactor);
bitStream.Read(optionIsOnThreatList);
bitStream.Read(isThreatened);
EXPECT_EQ(optionStatusImmunityInfo, true);
EXPECT_EQ(ImmuneToBasicAttackCount, 0);
EXPECT_EQ(ImmuneToDamageOverTimeCount, 0);
EXPECT_EQ(ImmuneToKnockbackCount, 0);
EXPECT_EQ(ImmuneToInterruptCount, 0);
EXPECT_EQ(ImmuneToSpeedCount, 0);
EXPECT_EQ(ImmuneToImaginationGainCount, 0);
EXPECT_EQ(ImmuneToImaginationLossCount, 0);
EXPECT_EQ(ImmuneToQuickbuildInterruptCount, 0);
EXPECT_EQ(ImmuneToPullToPointCount, 0);
EXPECT_EQ(optionStatsInfo, true);
EXPECT_EQ(currentHealth, 23);
EXPECT_EQ(maxHealth, 12345.0f);
EXPECT_EQ(currentArmor, 7);
EXPECT_EQ(maxArmor, 14.0f);
EXPECT_EQ(currentImagination, 6000);
EXPECT_EQ(maxImagination, 14000.0f);
EXPECT_EQ(damageAbsorptionPoints, 0.0f);
EXPECT_EQ(hasImmunity, false);
EXPECT_EQ(isGmImmune, false);
EXPECT_EQ(isShielded, false);
EXPECT_EQ(actualMaxHealth, 12345.0f);
EXPECT_EQ(actualMaxArmor, 14.0f);
EXPECT_EQ(actualMaxImagination, 14000.0f);
EXPECT_EQ(factionsSize, 2);
EXPECT_NE(std::find(factions.begin(), factions.end(), -1), factions.end());
EXPECT_NE(std::find(factions.begin(), factions.end(), 6), factions.end());
EXPECT_EQ(isSmashable, true);
EXPECT_EQ(isDead, false);
EXPECT_EQ(isSmashed, false);
EXPECT_EQ(isSmashable, true); // For the sake of readability with the struct viewers, we will test this twice since its used as an option here, but as a bool above.
EXPECT_EQ(isModuleAssembly, false);
EXPECT_EQ(optionExplodeFactor, true);
EXPECT_EQ(explodeFactor, 1.1f);
EXPECT_EQ(optionIsOnThreatList, true);
EXPECT_EQ(isThreatened, false);
}
bitStream.Reset();
}
/**
* Test serialization of a DestroyableComponent
*/
TEST_F(DestroyableTest, DestroyableComponentSerializeTest) {
bitStream.Reset();
// Initialize some values to be not default so we can test a full serialization
destroyableComponent->SetMaxHealth(1233.0f);
// Now we test a serialization for correctness.
destroyableComponent->Serialize(bitStream, false);
ASSERT_EQ(bitStream.GetNumberOfUnreadBits(), 422);
{
// Now read in the full serialized BitStream
bool optionStatsInfo{};
uint32_t currentHealth{};
float maxHealth{};
uint32_t currentArmor{};
float maxArmor{};
uint32_t currentImagination{};
float maxImagination{};
uint32_t damageAbsorptionPoints{};
bool hasImmunity{};
bool isGmImmune{};
bool isShielded{};
float actualMaxHealth{};
float actualMaxArmor{};
float actualMaxImagination{};
uint32_t factionsSize{};
std::vector<int32_t> factions{};
bool isSmashable{};
bool optionIsOnThreatList{};
bitStream.Read(optionStatsInfo);
bitStream.Read(currentHealth);
bitStream.Read(maxHealth);
bitStream.Read(currentArmor);
bitStream.Read(maxArmor);
bitStream.Read(currentImagination);
bitStream.Read(maxImagination);
bitStream.Read(damageAbsorptionPoints);
bitStream.Read(hasImmunity);
bitStream.Read(isGmImmune);
bitStream.Read(isShielded);
bitStream.Read(actualMaxHealth);
bitStream.Read(actualMaxArmor);
bitStream.Read(actualMaxImagination);
bitStream.Read(factionsSize);
for (uint32_t i = 0; i < factionsSize; i++) {
int32_t factionID{};
bitStream.Read(factionID);
factions.push_back(factionID);
}
bitStream.Read(isSmashable);
bitStream.Read(optionIsOnThreatList);
EXPECT_EQ(optionStatsInfo, true);
EXPECT_EQ(currentHealth, 23);
EXPECT_EQ(maxHealth, 1233.0f);
EXPECT_EQ(currentArmor, 7);
EXPECT_EQ(maxArmor, 14.0f);
EXPECT_EQ(currentImagination, 6000);
EXPECT_EQ(maxImagination, 14000.0f);
EXPECT_EQ(damageAbsorptionPoints, 0.0f);
EXPECT_EQ(hasImmunity, false);
EXPECT_EQ(isGmImmune, false);
EXPECT_EQ(isShielded, false);
EXPECT_EQ(actualMaxHealth, 1233.0f);
EXPECT_EQ(actualMaxArmor, 14.0f);
EXPECT_EQ(actualMaxImagination, 14000.0f);
EXPECT_EQ(factionsSize, 2);
EXPECT_NE(std::find(factions.begin(), factions.end(), -1), factions.end());
EXPECT_NE(std::find(factions.begin(), factions.end(), 6), factions.end());
EXPECT_EQ(isSmashable, true);
EXPECT_EQ(optionIsOnThreatList, false); // Always zero for now on serialization
}
}
/**
* Test the Damage method of DestroyableComponent
*/
TEST_F(DestroyableTest, DestroyableComponentDamageTest) {
// Do some actions
destroyableComponent->SetMaxHealth(100.0f);
destroyableComponent->SetHealth(100);
destroyableComponent->SetMaxArmor(0.0f);
destroyableComponent->Damage(10, LWOOBJID_EMPTY);
// Check that we take damage
ASSERT_EQ(destroyableComponent->GetHealth(), 90);
// Check that if we have armor, we take the correct amount of damage
destroyableComponent->SetMaxArmor(10.0f);
destroyableComponent->SetArmor(5);
destroyableComponent->Damage(10, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 85);
// Check that if we have damage absorption we take the correct damage
destroyableComponent->SetDamageToAbsorb(10);
destroyableComponent->Damage(9, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 85);
ASSERT_EQ(destroyableComponent->GetDamageToAbsorb(), 1);
destroyableComponent->Damage(6, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 80);
// Check that we take the correct reduced damage if we take reduced damage
destroyableComponent->SetDamageReduction(2);
destroyableComponent->Damage(7, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 75);
destroyableComponent->Damage(2, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 74);
ASSERT_EQ(destroyableComponent->GetDamageReduction(), 2);
destroyableComponent->SetDamageReduction(0);
// Check that blocking works
destroyableComponent->SetAttacksToBlock(1);
destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 74);
destroyableComponent->Damage(4, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 70);
// Check that immunity works
destroyableComponent->SetIsImmune(true);
destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 70);
ASSERT_TRUE(destroyableComponent->IsImmune());
destroyableComponent->SetIsImmune(false);
destroyableComponent->SetIsGMImmune(true);
destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 70);
ASSERT_TRUE(destroyableComponent->IsImmune());
destroyableComponent->SetIsGMImmune(false);
// Check knockback immunity
destroyableComponent->SetIsShielded(true);
ASSERT_TRUE(destroyableComponent->IsKnockbackImmune());
// Finally deal enough damage to kill the Entity
destroyableComponent->Damage(71, LWOOBJID_EMPTY);
ASSERT_EQ(destroyableComponent->GetHealth(), 0);
// Now lets heal some stats
destroyableComponent->Heal(15);
ASSERT_EQ(destroyableComponent->GetHealth(), 15);
destroyableComponent->Heal(15000);
ASSERT_EQ(destroyableComponent->GetHealth(), 100);
destroyableComponent->Repair(10);
ASSERT_EQ(destroyableComponent->GetArmor(), 10);
destroyableComponent->Repair(15000);
ASSERT_EQ(destroyableComponent->GetArmor(), 10);
destroyableComponent->SetMaxImagination(100.0f);
destroyableComponent->SetImagination(0);
destroyableComponent->Imagine(99);
ASSERT_EQ(destroyableComponent->GetImagination(), 99);
destroyableComponent->Imagine(4);
ASSERT_EQ(destroyableComponent->GetImagination(), 100);
}
TEST_F(DestroyableTest, DestroyableComponentFactionTest) {
ASSERT_TRUE(destroyableComponent->HasFaction(-1));
ASSERT_TRUE(destroyableComponent->HasFaction(6));
}
TEST_F(DestroyableTest, DestroyableComponentValiditiyTest) {
auto* enemyEntity = new Entity(19, info);
enemyEntity->AddComponent<DestroyableComponent>(-1)->AddFactionNoLookup(16);
destroyableComponent->AddEnemyFaction(16);
EXPECT_TRUE(destroyableComponent->IsEnemy(enemyEntity));
EXPECT_FALSE(destroyableComponent->IsFriend(enemyEntity));
delete enemyEntity;
}
TEST_F(DestroyableTest, DestroyableComponentImmunityTest) {
// assert to show that they are empty
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// set them all to true (count 1) and check
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true, true, true, true, true, true, true, true, true);
ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_TRUE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_TRUE(destroyableComponent->GetImmuneToKnockback());
ASSERT_TRUE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_TRUE(destroyableComponent->GetImmuneToSpeed());
ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_TRUE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_TRUE(destroyableComponent->GetImmuneToPullToPoint());
// remove them to check that they get removed properly
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, true, true, true, true, true, true, true, true);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// should not crash to remove them again
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, true, true, true, true, true, true, true, true);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// just do one
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true);
ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// now stack it to 2 on basic attack
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true);
ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// remove one and still shoudl be true
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true);
ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// go back to 0
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
// check individual ones now
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true, false, false, false, false, false, false, false, false);
ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, false, false, false, false, false, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, true, false, false, false, false, false, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_TRUE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, true, false, false, false, false, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, true, false, false, false, false, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_TRUE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, true, false, false, false, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, true, false, false, false, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_TRUE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, true, false, false, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, true, false, false, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_TRUE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, true, false, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, true, false, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, true, false, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, true, false, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, true, false, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, false, true, false);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_TRUE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, false, true, false);
destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, false, false, true);
ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack());
ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime());
ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback());
ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt());
ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain());
ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss());
ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt());
ASSERT_TRUE(destroyableComponent->GetImmuneToPullToPoint());
destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, false, false, true);
}
/**
* Test the Damage cooldown timer of DestroyableComponent
*/
TEST_F(DestroyableTest, DestroyableComponentDamageCooldownTest) {
// Test the damage immune timer state (anything above 0.0f)
destroyableComponent->SetDamageCooldownTimer(1.0f);
EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 1.0f);
ASSERT_TRUE(destroyableComponent->IsCooldownImmune());
// Test that the Update() function correctly decrements the damage cooldown timer
destroyableComponent->Update(0.5f);
EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.5f);
ASSERT_TRUE(destroyableComponent->IsCooldownImmune());
// Test the non damage immune timer state (anything below or equal to 0.0f)
destroyableComponent->SetDamageCooldownTimer(0.0f);
EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.0f);
ASSERT_FALSE(destroyableComponent->IsCooldownImmune());
}
// Live Die never sets client_death (19,428 live Die, 452 of them players).
TEST_F(DestroyableTest, DieDoesNotClaimAClientDeath) {
destroyableComponent->SetMaxArmor(0.0f);
const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); });
const auto die = FindGameMessage<GameMessages::Die>(sent, MessageType::Game::DIE);
ASSERT_TRUE(die.has_value());
EXPECT_EQ(die->target, baseEntity->GetObjectID());
EXPECT_FALSE(die->bClientDeath);
EXPECT_TRUE(die->bSpawnLoot);
}
TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) {
auto playerInfo = info;
playerInfo.lot = 1;
Entity player(0x1000000000000001LL, playerInfo);
Character character(1, nullptr);
player.SetCharacter(&character);
character.SetEntity(&player);
SystemAddress address;
address.binaryAddress = 0x0100007f;
address.port = 2003;
player.AddComponent<CharacterComponent>(-1, &character, address);
auto* destroyable = player.AddComponent<DestroyableComponent>(-1);
destroyable->SetMaxHealth(4.0f);
destroyable->SetHealth(4);
ASSERT_TRUE(player.IsPlayer());
const auto sent = PacketTestUtils::Capture([&] { destroyable->Smash(LWOOBJID_EMPTY); });
const auto die = FindGameMessage<GameMessages::Die>(sent, MessageType::Game::DIE);
ASSERT_TRUE(die.has_value());
EXPECT_EQ(die->target, player.GetObjectID());
EXPECT_FALSE(die->bClientDeath);
player.SetCharacter(nullptr);
}
// BasicAttack's dir_angle_* are degrees; live's Die carries them as these exact floats.
TEST_F(DestroyableTest, DeathDirectionDegreesMatchLiveBits) {
const auto bits = [](float value) { uint32_t out; std::memcpy(&out, &value, sizeof(out)); return out; };
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(-25.0f)), 0xbedf66f3u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(25.0f)), 0x3edf66f3u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(20.0f)), 0x3eb2b8c3u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(-20.0f)), 0xbeb2b8c3u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(75.0f)), 0x3fa78d36u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(80.0f)), 0x3fb2b8c3u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(15.0f)), 0x3e860a92u);
EXPECT_EQ(bits(DeathDirection::DegreesToRadians(45.0f)), 0x3f490fdbu);
EXPECT_EQ(DeathDirection::DegreesToRadians(0.0f), 0.0f);
}
// The killing blow's direction goes into Die. Bytes: a live Die from a -25 degree, force 12 BasicAttack with the
// object IDs replaced.
TEST_F(DestroyableTest, KillingBlowDirectionIsSentInDie) {
Entity target(0x0102030405060708LL, info);
auto* destroyable = target.AddComponent<DestroyableComponent>(-1);
destroyable->SetMaxHealth(4.0f);
destroyable->SetHealth(4);
const DeathDirection direction{ DeathDirection::DegreesToRadians(-25.0f), 0.0f, 12.0f };
const LWOOBJID killer = 0x1000000000000001LL;
auto sent = PacketTestUtils::Capture([&] { destroyable->Damage(1, killer, 0, true, direction); });
EXPECT_FALSE(FindGameMessage<GameMessages::Die>(sent, MessageType::Game::DIE).has_value()); // not a killing blow
sent = PacketTestUtils::Capture([&] { destroyable->Damage(3, killer, 0, true, direction); });
bool found = false;
for (const auto& packet : sent) {
if (packet.bytes.size() < 18 || packet.bytes[16] != 0x25 || packet.bytes[17] != 0x00) continue;
found = true;
EXPECT_PACKET_EQ(PacketTestUtils::FromHex(
"53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 25 00 40 00 00 00 3c d9 b7 ef 80 00 00 00 00 00 10 10 "
"40 20 00 00 00 00 00 02 10 10 00 00 00 00 00 01 00", 404), PacketTestUtils::FromCapture(packet));
}
EXPECT_TRUE(found);
}
// Deaths that are not a BasicAttack killing blow carry no direction.
TEST_F(DestroyableTest, SmashWithoutAttackHasNoDirection) {
const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Smash(0x1000000000000001LL); });
const auto die = FindGameMessage<GameMessages::Die>(sent, MessageType::Game::DIE);
ASSERT_TRUE(die.has_value());
EXPECT_EQ(die->directionRelative_AngleXZ, 0.0f);
EXPECT_EQ(die->directionRelative_AngleY, 0.0f);
EXPECT_EQ(die->directionRelative_Force, 0.0f);
}
// Live stunned every dead enemy in the packet right after its Die; bytes are a live SetStunned with the ID replaced.
TEST_F(DestroyableTest, DeadEnemyIsStunnedAfterDie) {
if (!CDClientDatabase::isConnected) {
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)",
"ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER)",
"SkillBehavior (skillID INTEGER, behaviorID INTEGER)",
}) {
CDClientDatabase::ExecuteDML(std::string("CREATE TABLE ") + table + ";");
}
}
Entity enemy(0x0102030405060708LL, info);
enemy.AddComponent<BaseCombatAIComponent>(-1);
auto* destroyable = enemy.AddComponent<DestroyableComponent>(-1);
destroyable->SetMaxHealth(4.0f);
destroyable->SetHealth(4);
const auto sent = PacketTestUtils::Capture([&] { destroyable->Smash(LWOOBJID_EMPTY); });
const auto ids = GameMessageTestUtils::SentGameMessageIds(sent);
const auto die = std::find(ids.begin(), ids.end(), MessageType::Game::DIE);
ASSERT_NE(die, ids.end());
ASSERT_NE(die + 1, ids.end());
EXPECT_EQ(*(die + 1), MessageType::Game::SET_STUNNED);
const auto stuns = GameMessageTestUtils::SentGameMessages<GameMessages::SetStunned>(sent);
ASSERT_EQ(stuns.size(), 1u);
EXPECT_PACKET_EQ(PacketTestUtils::FromHex("53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 c6 00 00 00 00 00 40 50 80", 193), GameMessageTestUtils::StructPacket(stuns[0]));
}
TEST_F(DestroyableTest, DeadSmashableIsNotStunned) {
const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Smash(LWOOBJID_EMPTY); });
EXPECT_TRUE(GameMessageTestUtils::SentGameMessages<GameMessages::SetStunned>(sent).empty());
}