1
Fork 0
mirror of https://github.com/SapphireServer/Sapphire.git synced 2025-05-07 03:07:45 +00:00

wip: initial target select filter work

- todo: hook this up to EncounterTimeline
This commit is contained in:
Tahir 2024-06-19 03:21:51 +01:00
parent 011cf1b6d2
commit 8fb9846c0b
6 changed files with 501 additions and 33 deletions

View file

@ -0,0 +1,218 @@
#include "TargetHelper.h"
#include <algorithm>
#include <Actor/BNpc.h>
#include <Actor/Chara.h>
#include <Manager/RNGMgr.h>
#include <Util/UtilMath.h>
#include <Service.h>
namespace Sapphire::World::AI
{
bool InsideRadiusFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = Common::Util::distance( pSrc->getPos(), pTarget->getPos() ) <= m_distance;
return m_negate ? !ret : ret;
}
bool OutsideRadiusFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = Common::Util::distance( pSrc->getPos(), pTarget->getPos() ) >= m_distance;
return m_negate ? !ret : ret;
}
bool PlayerFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = pTarget->isPlayer();
return m_negate ? !ret : ret;
}
bool AllyFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = false;
if( pSrc->isPlayer() )
{
auto pBNpcTarget = pTarget->getAsBNpc();
if( pBNpcTarget && pBNpcTarget->getEnemyType() == 0 )
ret = true;
else if( pTarget->isPlayer() )
ret = true;
}
else if( pSrc->isBattleNpc() )
{
auto pBNpcTarget = pTarget->getAsBNpc();
if( pBNpcTarget && pBNpcTarget->getEnemyType() == 0 )
ret = true;
else if( pTarget->isPlayer() )
ret = true;
}
return m_negate ? !ret : ret;
}
bool OwnBattalionFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
return false;
}
bool TankFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = pTarget->getRole() == Common::Role::Tank;
return m_negate ? !ret : ret;
}
bool HealerFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = pTarget->getRole() == Common::Role::Healer;
return m_negate ? !ret : ret;
}
bool DpsFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
bool ret = true;
switch( pTarget->getRole() )
{
case Common::Role::Melee:
case Common::Role::RangedMagical:
case Common::Role::RangedPhysical:
ret = true;
break;
default:
ret = false;
break;
}
return m_negate ? !ret : ret;
}
bool HasStatusEffectFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
auto ret = pTarget->hasStatusEffect( m_statusId );
return m_negate ? !ret : ret;
}
bool TopAggroFilter ::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
auto pBNpc = pSrc->getAsBNpc();
bool ret = false;
if( pBNpc )
{
ret = pBNpc->hateListGetHighest() == pTarget;
}
return m_negate ? !ret : ret;
}
bool SecondAggroFilter ::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
auto pBNpc = pSrc->getAsBNpc();
bool ret = false;
if( pBNpc )
{
// todo: this is so dumb
auto hateList = pBNpc->getHateList();
std::vector sorted( hateList.begin(), hateList.end() );
std::sort( sorted.begin(), sorted.end(), []( Entity::HateListEntryPtr a, Entity::HateListEntryPtr b ) {
return a->m_hateAmount > b->m_hateAmount; } );
Entity::CharaPtr pChara = nullptr;
auto topIt = sorted.begin();
if( topIt != sorted.end() && ++topIt != sorted.end() )
pChara = topIt->get()->m_pChara;
ret = pChara == pTarget;
}
return m_negate ? !ret : ret;
}
void Snapshot::createSnapshot( Entity::CharaPtr pSrc, const std::set< Entity::GameObjectPtr >& inRange,
int count, bool fillWithRandom, const std::set< uint32_t >& exclude )
{
m_targets.clear();
auto& RNGMgr = Common::Service< World::Manager::RNGMgr >::ref();
for( const auto& pActor : inRange )
{
auto pChara = pActor->getAsChara();
if( pChara == nullptr )
continue;
if( exclude.find( pChara->getId() ) != exclude.end() ) continue;
bool matches = true;
for( const auto& filter : m_filters )
{
if( !filter->isConditionMet( pSrc, pChara ) )
{
matches = false;
break;
}
}
if( matches )
{
CharaEntry entry;
entry.m_entityId = pChara->getId();
entry.m_pos = pChara->getPos();
entry.m_rot = pChara->getRot();
m_targets.push_back( entry );
if( m_targets.size() == count ) break;
}
}
if( fillWithRandom && m_targets.size() < count )
{
std::vector< Entity::CharaPtr > remaining;
for( const auto& pActor : inRange )
{
auto pChara = pActor->getAsChara();
if( pChara == nullptr )
continue;
if( exclude.find( pChara->getId() ) == exclude.end() && std::find_if( m_targets.begin(), m_targets.end(),
[ &pChara ]( CharaEntry entry ) { return entry.m_entityId == pChara->getId(); } ) == m_targets.end() )
{
remaining.push_back( pChara );
}
}
while( m_targets.size() < count && !remaining.empty() )
{
// idk
std::shuffle( remaining.begin(), remaining.end(), *RNGMgr.getRNGEngine().get() );
auto pChara = remaining.back();
CharaEntry entry;
entry.m_entityId = pChara->getId();
entry.m_pos = pChara->getPos();
entry.m_rot = pChara->getRot();
m_targets.emplace_back( entry );
remaining.pop_back();
}
}
// sort by distance at the end always
auto srcPos = pSrc->getPos();
std::sort( m_targets.begin(), m_targets.end(),
[ srcPos ]( CharaEntry l, CharaEntry r )
{
return Common::Util::distance( srcPos, l.m_pos ) < Common::Util::distance( srcPos, r.m_pos );
}
);
}
const std::vector< Snapshot::CharaEntry >& Snapshot::getResults() const
{
return m_targets;
}
const std::vector< uint32_t > Snapshot::getTargetIds() const
{
std::vector< uint32_t > ret( m_targets.size() );
for( auto i = 0; i < m_targets.size(); ++i )
ret[ i ] = m_targets[ i ].m_entityId;
return ret;
}
};// namespace Sapphire::World::AI

232
src/world/AI/TargetHelper.h Normal file
View file

@ -0,0 +1,232 @@
#ifndef _TARGETHELPER_H
#define _TARGETHELPER_H
#include <memory>
#include <unordered_map>
#include <set>
#include <vector>
#include <ForwardsZone.h>
namespace Sapphire::World::AI
{
//
// Filters
//
class TargetSelectFilter :
public std::enable_shared_from_this< TargetSelectFilter >
{
public:
enum class Type
{
InsideRadius,
OutsideRadius,
Player,
Ally,
OwnBattalion,
Tank,
Healer,
Dps,
HasStatusEffect,
TopAggro,
SecondAggro,
AllianceA,
AllianceB,
AllianceC
};
protected:
Type m_type;
bool m_negate{ false };
public:
TargetSelectFilter( Type type, bool negate ) :
m_type( type ),
m_negate( negate )
{
}
virtual ~TargetSelectFilter()
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const
{
return false;
};
bool isNegate() const
{
return m_negate;
}
};
using TargetSelectFilterPtr = std::shared_ptr< TargetSelectFilter >;
class InsideRadiusFilter : public TargetSelectFilter
{
private:
float m_distance{ 0 };
public:
InsideRadiusFilter( float distance, bool negate ) :
TargetSelectFilter( Type::InsideRadius, negate ),
m_distance( distance )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class OutsideRadiusFilter : public TargetSelectFilter
{
private:
float m_distance{ 0 };
public:
OutsideRadiusFilter( float distance, bool negate ) :
TargetSelectFilter( Type::OutsideRadius, negate ),
m_distance( distance )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class PlayerFilter : public TargetSelectFilter
{
public:
PlayerFilter( bool negate ) :
TargetSelectFilter( Type::Player, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class AllyFilter : public TargetSelectFilter
{
public:
AllyFilter( bool negate ) :
TargetSelectFilter( Type::Ally, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class OwnBattalionFilter : public TargetSelectFilter
{
public:
OwnBattalionFilter( bool negate ) :
TargetSelectFilter( Type::OwnBattalion, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class TankFilter : public TargetSelectFilter
{
public:
TankFilter( bool negate ) :
TargetSelectFilter( Type::Tank, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class HealerFilter : public TargetSelectFilter
{
public:
HealerFilter( bool negate ) :
TargetSelectFilter( Type::Healer, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class DpsFilter : public TargetSelectFilter
{
public:
DpsFilter( bool negate ) :
TargetSelectFilter( Type::Dps, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class HasStatusEffectFilter : public TargetSelectFilter
{
private:
uint32_t m_statusId{ 0 };
public:
HasStatusEffectFilter( uint32_t statusId, bool negate ) :
TargetSelectFilter( Type::HasStatusEffect, negate ),
m_statusId( statusId )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class TopAggroFilter : public TargetSelectFilter
{
public:
TopAggroFilter( bool negate ) :
TargetSelectFilter( Type::TopAggro, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
class SecondAggroFilter : public TargetSelectFilter
{
public:
SecondAggroFilter( bool negate ) :
TargetSelectFilter( Type::SecondAggro, negate )
{
}
bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const;
};
//
// Helpers
//
class Snapshot :
public std::enable_shared_from_this< Snapshot >
{
struct CharaEntry
{
uint32_t m_entityId;
Common::FFXIVARR_POSITION3 m_pos;
float m_rot;
// todo: status effects?
};
private:
std::vector< TargetSelectFilterPtr > m_filters;
std::vector< CharaEntry > m_targets;
public:
Snapshot( const std::vector< TargetSelectFilterPtr > filters ) :
m_filters( filters )
{
}
void createSnapshot( Entity::CharaPtr pSrc, const std::set< Entity::GameObjectPtr >& inRange,
int count, bool fillWithRandom, const std::set< uint32_t >& exclude = {} );
// returns actors sorted by distance
const std::vector< CharaEntry >& getResults() const;
const std::vector< uint32_t > getTargetIds() const;
};
using SnapshotPtr = std::shared_ptr< Snapshot >;
}// namespace Sapphire::World::AI
#endif

View file

@ -442,6 +442,11 @@ void BNpc::sendPositionUpdate()
server().queueForPlayers( getInRangePlayerIds(), movePacket );
}
const std::set< std::shared_ptr< HateListEntry > >& BNpc::getHateList() const
{
return m_hateList;
}
void BNpc::hateListClear()
{
for( auto& listEntry : m_hateList )
@ -825,6 +830,11 @@ void BNpc::setFlag( uint32_t flag )
m_flags |= flag;
}
void BNpc::removeFlag( uint32_t flag )
{
m_flags &= ~flag;
}
void BNpc::clearFlags()
{
m_flags = 0;

View file

@ -18,6 +18,7 @@ namespace Sapphire::Entity
uint32_t m_hateAmount;
CharaPtr m_pChara;
};
using HateListEntryPtr = std::shared_ptr< HateListEntry >;
enum class BNpcState
{
@ -39,6 +40,7 @@ namespace Sapphire::Entity
NoDeaggro = 0x10,
Untargetable = 0x20,
AutoAttackDisabled = 0x40,
Invisible = 0x80,
Intermission = 0x77 // for transition phases to ensure boss only moves/acts when scripted
};
@ -106,6 +108,7 @@ namespace Sapphire::Entity
BNpcState getState() const;
void setState( BNpcState state );
const std::set< std::shared_ptr< HateListEntry > >& getHateList() const;
void hateListClear();
uint32_t hateListGetValue( const Sapphire::Entity::CharaPtr& pChara );
uint32_t hateListGetHighestValue();
@ -115,7 +118,7 @@ namespace Sapphire::Entity
void hateListUpdate( const CharaPtr& pChara, int32_t hateAmount );
void hateListRemove( const CharaPtr& pChara );
bool hateListHasActor( const CharaPtr& pChara );
void aggro( const CharaPtr& pChara );
void deaggro( const CharaPtr& pChara );
@ -143,6 +146,7 @@ namespace Sapphire::Entity
bool hasFlag( uint32_t flag ) const;
void setFlag( uint32_t flags );
void removeFlag( uint32_t flag );
void clearFlags();
void calculateStats() override;

View file

@ -69,6 +69,7 @@ namespace Sapphire
{
auto pBattleNpc = pTeri->getActiveBNpcByLayoutId( this->layoutId );
// todo: these should really use callbacks when the state transitions or we could miss this tick
switch( combatState )
{
case CombatStateType::Idle:
@ -114,14 +115,7 @@ namespace Sapphire
{
case TimepointDataType::Idle:
{
auto pIdleData = std::dynamic_pointer_cast< TimepointDataIdle, TimepointData >( getData() );
auto pBNpc = pTeri->getActiveBNpcByLayoutId( pIdleData->m_layoutId );
if( pBNpc )
{
// todo: idle
}
// just wait up the duration of this timepoint
}
break;
case TimepointDataType::CastAction:
@ -162,7 +156,7 @@ namespace Sapphire
else
{
// if we are at the pos, stop waiting
state.m_finished = true;
//state.m_finished = true;
}
pBNpc->setRot( pMoveToData->m_rot );
}
@ -557,7 +551,8 @@ namespace Sapphire
{ "spawnBNpc", TimepointDataType::SpawnBNpc },
{ "bNpcFlags", TimepointDataType::SetBNpcFlags },
{ "setEObjState", TimepointDataType::SetEObjState },
{ "setCondition", TimepointDataType::SetCondition }
{ "setCondition", TimepointDataType::SetCondition },
{ "snapshot", TimepointDataType::Snapshot }
};
const static std::unordered_map< std::string, TimepointOverrideFlags > overrideFlagMap =
@ -565,19 +560,6 @@ namespace Sapphire
{}
};
const static std::unordered_map< std::string, TargetSelectFilterFlags > targetFilterMap =
{
{ "self", TargetSelectFilterFlags::Self },
{ "tank", TargetSelectFilterFlags::Tank },
{ "healer", TargetSelectFilterFlags::Healer },
{ "dps", TargetSelectFilterFlags::Dps },
{ "melee", TargetSelectFilterFlags::Melee },
{ "ranged", TargetSelectFilterFlags::Ranged },
{ "furthest", TargetSelectFilterFlags::Furthest },
{ "aggro1", TargetSelectFilterFlags::Aggro1 },
{ "aggro2", TargetSelectFilterFlags::Aggro2 }
};
const static std::unordered_map< std::string, TimepointCallbackType > callbackTypeMap =
{
{ "onActionInit", TimepointCallbackType::OnActionInit },
@ -602,6 +584,12 @@ namespace Sapphire
{ "and", DirectorOpId::And }
};
const static std::unordered_map< std::string, Common::BNpcType > bnpcTypeMap =
{
{ "bnpc", Common::BNpcType::Enemy },
{ "ally", Common::BNpcType::Friendly } // todo: rename this
};
TimepointDataType tpType{ 0 };
auto typeStr = json.at( "type" ).get< std::string >();
@ -727,7 +715,7 @@ namespace Sapphire
auto& dataJ = json.at( "data" );
auto flags = dataJ.at( "val" ).get< uint32_t >();
auto opStr = dataJ.at( "opc" ).get< std::string >();
DirectorOpId op = directorOpMap.find( opStr )->second;
DirectorOpId op = directorOpMap.at( opStr );
auto pDirectorData = std::make_shared< TimepointDataDirector >( tpType, op );
pDirectorData->m_data.flags = flags;
@ -753,6 +741,9 @@ namespace Sapphire
auto hateSrcJ = dataJ.at( "hateSrc" );
auto actorRef = dataJ.at( "spawnActor" ).get< std::string >();
auto flags = dataJ.at( "flags" ).get< uint32_t >();
// todo: batallion
// auto battalion = dataJ.at( "batallion" ).get< uint32_t >();
auto bnpcType = bnpcTypeMap.at( dataJ.at( "type" ).get< std::string >() );
// todo: hateSrc
@ -762,7 +753,7 @@ namespace Sapphire
else
throw std::runtime_error( fmt::format( std::string( "EncounterTimeline::Timepoint::from_json: SpawnBNpc invalid actor ref: %s" ), actorRef ) );
m_pData = std::make_shared< TimepointDataSpawnBNpc >( layoutId, flags );
m_pData = std::make_shared< TimepointDataSpawnBNpc >( layoutId, flags, bnpcType );
}
break;
case TimepointDataType::SetBNpcFlags:

View file

@ -1,12 +1,12 @@
#include <fstream>
#include <memory>
#include <map>
#include <optional>
#include <stack>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include <queue>
#include <Common.h>
#include <Forwards.h>
@ -116,7 +116,9 @@ namespace Sapphire
SetEObjState,
SetBgm,
SetCondition
SetCondition,
Snapshot
};
enum class TimepointCallbackType : uint32_t
@ -193,7 +195,7 @@ namespace Sapphire
std::vector < TimepointCallbackFunc > m_callbacks;
};
using TimebackCallbackDataPtr = std::shared_ptr< TimepointCallbackData >;
using TimepointCallbacks = std::map< TimepointCallbackType, TimebackCallbackDataPtr >;
using TimepointCallbacks = std::unordered_map< TimepointCallbackType, TimebackCallbackDataPtr >;
struct TimepointData :
@ -341,12 +343,15 @@ namespace Sapphire
{
uint32_t m_layoutId{ 0xE0000000 };
uint32_t m_flags{ 0 };
uint32_t m_type{ 0 };
// todo: hate type, source
TimepointDataSpawnBNpc( uint32_t layoutId, uint32_t flags ) :
TimepointDataSpawnBNpc( uint32_t layoutId, uint32_t flags, uint32_t type ) :
TimepointData( TimepointDataType::SpawnBNpc ),
m_layoutId( layoutId ),
m_flags( flags)
m_flags( flags ),
m_type( type )
{
}
};
@ -615,13 +620,21 @@ namespace Sapphire
};
using PhaseConditionPtr = std::shared_ptr< PhaseCondition >;
// todo: bnpc parts
class TimelineBNpcPart
{
uint32_t m_hp{ 0 };
std::string m_name;
};
class TimelineActor
{
protected:
std::unordered_map< uint32_t, PhaseConditionPtr > m_phaseConditions;
std::unordered_map< uint32_t, ConditionState > m_conditionStates;
// PARENTNAME_SUBACTOR_1, ..., PARENTNAME_SUBACTOR_69
std::map< std::string, Entity::BNpcPtr > m_subActors;
std::unordered_map< std::string, Entity::BNpcPtr > m_subActors;
public:
uint32_t m_layoutId{ 0 };
uint32_t m_hp{ 0 };