feat(destroyable): enemy preconditions from the level, as the client

An object's EnemyPrecondition (level config) decides who it is an enemy of:
whoever asks and doesn't meet the preconditions, whatever the factions
(LWODestroyableComponent::msgIsEnemy 0x00c9e930; ',' ';' ':' join groups
that must all pass, '|' alternatives). Forbidden Valley Siege's objects use
it today; any level object can.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 09:31:35 -05:00
parent 4b84677cba
commit 0253fea006
3 changed files with 38 additions and 0 deletions

View File

@@ -28,6 +28,7 @@
#include "BaseCombatAIComponent.h"
#include "TeamManager.h"
#include "BuffComponent.h"
#include "Preconditions.h"
#include "SkillComponent.h"
#include "Item.h"
#include "Amf3.h"
@@ -163,6 +164,9 @@ void DestroyableComponent::LoadDataFromTemplate() {
// The level config (LWODestroyableComponent::LoadConfigData 0x00c44cb0 reads it for every destroyable): is_smashable,
// set_faction / override_faction and the smashable loot matrix
void DestroyableComponent::LoadConfigData() {
const auto enemyPrecondition = m_Parent->GetVarAsString(u"EnemyPrecondition");
if (!enemyPrecondition.empty()) m_EnemyPrecondition = std::make_unique<PreconditionExpression>(enemyPrecondition);
const bool isSmashable = m_Parent->GetVarAs<int32_t>(u"is_smashable") != 0;
const bool isCharacter = m_Parent->GetCharacter() != nullptr;
if (!isCharacter) {
@@ -536,6 +540,10 @@ bool DestroyableComponent::IsEnemy(const Entity* other) const {
return false;
}
const auto* otherDestroyableComponent = other->GetComponent<DestroyableComponent>();
// An object with enemy preconditions is an enemy unless this one meets them; factions don't count then
if (otherDestroyableComponent && otherDestroyableComponent->GetEnemyPrecondition()) {
return !otherDestroyableComponent->GetEnemyPrecondition()->Check(m_Parent);
}
if (otherDestroyableComponent != nullptr) {
for (const auto enemyFaction : m_EnemyFactionIDs) {
for (const auto otherFaction : otherDestroyableComponent->GetFactionIDs()) {

View File

@@ -2,6 +2,7 @@
#define DESTROYABLECOMPONENT_H
#include "RakNetTypes.h"
#include <memory>
#include <optional>
#include <vector>
#include "tinyxml2.h"
@@ -10,6 +11,8 @@
#include "eReplicaComponentType.h"
#include "Implementation.h"
class PreconditionExpression;
struct ObjectLoadState;
namespace GameMessages {
@@ -471,6 +474,10 @@ public:
*/
static void Resurrect(Entity& entity);
// The level's EnemyPrecondition: an object asking whether this one is its enemy must meet it, or it is one
// (LWODestroyableComponent::msgIsEnemy 0x00c9e930); its factions don't count then
const PreconditionExpression* GetEnemyPrecondition() const { return m_EnemyPrecondition.get(); }
// Whether a resurrection has started and not finished (the client's isResurrecting)
bool GetIsResurrecting() const { return m_IsResurrecting; }
@@ -640,6 +647,9 @@ private:
// A resurrection has started (Resurrect) and its respawn animation hasn't finished
bool m_IsResurrecting = false;
// The level config's EnemyPrecondition, when it has one
std::unique_ptr<PreconditionExpression> m_EnemyPrecondition;
/**
* Whether this entity is smashed. Unused, here for serialization
*/

View File

@@ -731,3 +731,23 @@ TEST_F(DestroyableTest, RequestResurrectOnlyWhenDead) {
request.Handle(*baseEntity, UNASSIGNED_SYSTEM_ADDRESS);
EXPECT_TRUE(destroyableComponent->GetIsResurrecting());
}
// LWODestroyableComponent::msgIsEnemy: a target with enemy preconditions is an enemy of whoever doesn't meet them,
// whatever its factions
TEST_F(DestroyableTest, EnemyPreconditionDecidesEnemies) {
if (!CDClientDatabase::isConnected) CDClientDatabase::Connect(":memory:");
CDClientDatabase::ExecuteDML("CREATE TABLE IF NOT EXISTS Preconditions (id INTEGER, type INTEGER, targetLOT TEXT, targetCount INTEGER);");
CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (90201, 2, '6194', 1);"); // has an item the asker lacks
CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (90202, 2, '', 1);"); // always met
Entity target(0x0102030405060709LL, info);
auto* targetDestroyable = target.AddComponent<DestroyableComponent>(-1);
target.SetVar<std::string>(u"EnemyPrecondition", "90201");
targetDestroyable->LoadConfigData();
EXPECT_TRUE(destroyableComponent->IsEnemy(&target));
target.SetVar<std::string>(u"EnemyPrecondition", "90202");
targetDestroyable->LoadConfigData();
EXPECT_FALSE(destroyableComponent->IsEnemy(&target));
}