mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
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>
709 lines
32 KiB
C++
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());
|
|
}
|