aim: added optional hitbox-based aiming for bots (ref #579)
This commit is contained in:
parent
87cbd144c2
commit
8dece62df6
6 changed files with 264 additions and 3 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit b13855d46f848daa43eee990081b97f3e83d87fd
|
Subproject commit bd5617f41c84fa1f4e5800c75ea286f4d3cba62e
|
||||||
43
inc/engine.h
43
inc/engine.h
|
|
@ -48,7 +48,8 @@ CR_DECLARE_SCOPED_ENUM (GameFlags,
|
||||||
HasBotVoice = cr::bit (11), // on that game version we can use chatter
|
HasBotVoice = cr::bit (11), // on that game version we can use chatter
|
||||||
AnniversaryHL25 = cr::bit (12), // half-life 25th anniversary engine
|
AnniversaryHL25 = cr::bit (12), // half-life 25th anniversary engine
|
||||||
Xash3DLegacy = cr::bit (13), // old xash3d-branch
|
Xash3DLegacy = cr::bit (13), // old xash3d-branch
|
||||||
ZombieMod = cr::bit (14) // zombie mod is active
|
ZombieMod = cr::bit (14), // zombie mod is active
|
||||||
|
HasStudioModels = cr::bit (15) // game supports studio models, so we can use hitbox-based aiming
|
||||||
)
|
)
|
||||||
|
|
||||||
// defines map type
|
// defines map type
|
||||||
|
|
@ -70,6 +71,18 @@ CR_DECLARE_SCOPED_ENUM (EntitySearchResult,
|
||||||
Break
|
Break
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// player body parts
|
||||||
|
CR_DECLARE_SCOPED_ENUM (PlayerPart,
|
||||||
|
Head = 1,
|
||||||
|
Chest,
|
||||||
|
Stomach,
|
||||||
|
LeftArm,
|
||||||
|
RightArm,
|
||||||
|
LeftLeg,
|
||||||
|
RightLeg,
|
||||||
|
Feet // custom!
|
||||||
|
)
|
||||||
|
|
||||||
// variable reg pair
|
// variable reg pair
|
||||||
struct ConVarReg {
|
struct ConVarReg {
|
||||||
cvar_t reg;
|
cvar_t reg;
|
||||||
|
|
@ -112,6 +125,29 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// player model part info enumerator
|
||||||
|
class PlayerHitboxEnumerator final {
|
||||||
|
public:
|
||||||
|
struct Info {
|
||||||
|
float updated {};
|
||||||
|
Vector head {};
|
||||||
|
Vector stomach {};
|
||||||
|
Vector feet {};
|
||||||
|
Vector right {};
|
||||||
|
Vector left {};
|
||||||
|
} m_parts[kGameMaxPlayers] {};
|
||||||
|
|
||||||
|
public:
|
||||||
|
// get's the enemy part based on bone info
|
||||||
|
Vector get (edict_t *ent, int part, float updateTimestamp);
|
||||||
|
|
||||||
|
// update bones positions for given player
|
||||||
|
void update (edict_t *ent);
|
||||||
|
|
||||||
|
// reset all the poisitons
|
||||||
|
void reset ();
|
||||||
|
};
|
||||||
|
|
||||||
// provides utility functions to not call original engine (less call-cost)
|
// provides utility functions to not call original engine (less call-cost)
|
||||||
class Game final : public Singleton <Game> {
|
class Game final : public Singleton <Game> {
|
||||||
public:
|
public:
|
||||||
|
|
@ -353,6 +389,11 @@ public:
|
||||||
m_gameFlags |= type;
|
m_gameFlags |= type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clears game flag
|
||||||
|
void clearGameFlag (const int type) {
|
||||||
|
m_gameFlags &= ~type;
|
||||||
|
}
|
||||||
|
|
||||||
// gets the map type
|
// gets the map type
|
||||||
bool mapIs (const int type) const {
|
bool mapIs (const int type) const {
|
||||||
return !!(m_mapFlags & type);
|
return !!(m_mapFlags & type);
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,8 @@ private:
|
||||||
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
|
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
|
||||||
Array <edict_t *> m_hostages {}; // pointer to used hostage entities
|
Array <edict_t *> m_hostages {}; // pointer to used hostage entities
|
||||||
|
|
||||||
|
UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {};
|
||||||
|
|
||||||
Path *m_path {}; // pointer to the current path node
|
Path *m_path {}; // pointer to the current path node
|
||||||
String m_chatBuffer {}; // space for strings (say text...)
|
String m_chatBuffer {}; // space for strings (say text...)
|
||||||
Frustum::Planes m_viewFrustum {};
|
Frustum::Planes m_viewFrustum {};
|
||||||
|
|
@ -407,6 +409,8 @@ private:
|
||||||
bool isWeaponRestrictedAMX (int wid);
|
bool isWeaponRestrictedAMX (int wid);
|
||||||
bool isInViewCone (const Vector &origin);
|
bool isInViewCone (const Vector &origin);
|
||||||
bool checkBodyParts (edict_t *target);
|
bool checkBodyParts (edict_t *target);
|
||||||
|
bool checkBodyPartsWithOffsets (edict_t *target);
|
||||||
|
bool checkBodyPartsWithHitboxes (edict_t *target);
|
||||||
bool seesEnemy (edict_t *player);
|
bool seesEnemy (edict_t *player);
|
||||||
bool hasActiveGoal ();
|
bool hasActiveGoal ();
|
||||||
bool advanceMovement ();
|
bool advanceMovement ();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ ConVar cv_check_enemy_rendering ("check_enemy_rendering", "0", "Enables or disab
|
||||||
ConVar cv_check_enemy_invincibility ("check_enemy_invincibility", "0", "Enables or disables checking enemy invincibility. Useful for some mods.");
|
ConVar cv_check_enemy_invincibility ("check_enemy_invincibility", "0", "Enables or disables checking enemy invincibility. Useful for some mods.");
|
||||||
ConVar cv_stab_close_enemies ("stab_close_enemies", "1", "Enables or disables bot ability to stab the enemy with knife if bot is in good condition.");
|
ConVar cv_stab_close_enemies ("stab_close_enemies", "1", "Enables or disables bot ability to stab the enemy with knife if bot is in good condition.");
|
||||||
ConVar cv_use_engine_pvs_check ("use_engine_pvs_check", "0", "Use engine to check potential visibility of an enemy.");
|
ConVar cv_use_engine_pvs_check ("use_engine_pvs_check", "0", "Use engine to check potential visibility of an enemy.");
|
||||||
|
ConVar cv_use_hitbox_enemy_targeting ("use_hitbox_enemy_targeting", "0", "Use hitbox-based enemy targeting, instead of offset based. Use with the yb_use_engine_pvs_check enabled to reduce CPU usage.");
|
||||||
|
|
||||||
ConVar mp_friendlyfire ("mp_friendlyfire", nullptr, Var::GameRef);
|
ConVar mp_friendlyfire ("mp_friendlyfire", nullptr, Var::GameRef);
|
||||||
ConVar sv_gravity ("sv_gravity", nullptr, Var::GameRef);
|
ConVar sv_gravity ("sv_gravity", nullptr, Var::GameRef);
|
||||||
|
|
@ -137,6 +138,14 @@ bool Bot::checkBodyParts (edict_t *target) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hitboxes requested ?
|
||||||
|
if (game.is (GameFlags::HasStudioModels) && cv_use_hitbox_enemy_targeting) {
|
||||||
|
return checkBodyPartsWithHitboxes (target);
|
||||||
|
}
|
||||||
|
return checkBodyPartsWithOffsets (target);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bot::checkBodyPartsWithOffsets (edict_t *target) {
|
||||||
TraceResult result {};
|
TraceResult result {};
|
||||||
const auto &eyes = getEyesPos ();
|
const auto &eyes = getEyesPos ();
|
||||||
|
|
||||||
|
|
@ -215,6 +224,77 @@ bool Bot::checkBodyParts (edict_t *target) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Bot::checkBodyPartsWithHitboxes (edict_t *target) {
|
||||||
|
const auto self = pev->pContainingEntity;
|
||||||
|
const auto refersh = m_frameInterval * 1.5f;
|
||||||
|
|
||||||
|
TraceResult result {};
|
||||||
|
const auto &eyes = getEyesPos ();
|
||||||
|
|
||||||
|
const auto hitsTarget = [&] () -> bool {
|
||||||
|
return result.flFraction >= 1.0f || result.pHit == target;
|
||||||
|
};
|
||||||
|
|
||||||
|
// creatures can't hurt behind anything
|
||||||
|
const auto ignoreFlags = m_isCreature ? TraceIgnore::None : TraceIgnore::Everything;
|
||||||
|
|
||||||
|
// get the stomach hitbox
|
||||||
|
m_enemyParts = Visibility::None;
|
||||||
|
game.testLine (eyes, m_hitboxEnumerator->get (target, PlayerPart::Stomach, refersh), ignoreFlags, self, &result);
|
||||||
|
|
||||||
|
if (hitsTarget ()) {
|
||||||
|
m_enemyParts |= Visibility::Body;
|
||||||
|
m_enemyOrigin = result.vecEndPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the stomach hitbox
|
||||||
|
m_enemyParts = Visibility::None;
|
||||||
|
game.testLine (eyes, m_hitboxEnumerator->get (target, PlayerPart::Head, refersh), ignoreFlags, self, &result);
|
||||||
|
|
||||||
|
if (hitsTarget ()) {
|
||||||
|
m_enemyParts |= Visibility::Head;
|
||||||
|
m_enemyOrigin = result.vecEndPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_enemyParts != Visibility::None) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the left hitbox
|
||||||
|
m_enemyParts = Visibility::None;
|
||||||
|
game.testLine (eyes, m_hitboxEnumerator->get (target, PlayerPart::LeftArm, refersh), ignoreFlags, self, &result);
|
||||||
|
|
||||||
|
if (hitsTarget ()) {
|
||||||
|
m_enemyParts |= Visibility::Other;
|
||||||
|
m_enemyOrigin = result.vecEndPos;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the right hitbox
|
||||||
|
m_enemyParts = Visibility::None;
|
||||||
|
game.testLine (eyes, m_hitboxEnumerator->get (target, PlayerPart::RightArm, refersh), ignoreFlags, self, &result);
|
||||||
|
|
||||||
|
if (hitsTarget ()) {
|
||||||
|
m_enemyParts |= Visibility::Other;
|
||||||
|
m_enemyOrigin = result.vecEndPos;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the feet spot
|
||||||
|
m_enemyParts = Visibility::None;
|
||||||
|
game.testLine (eyes, m_hitboxEnumerator->get (target, PlayerPart::Feet, refersh), ignoreFlags, self, &result);
|
||||||
|
|
||||||
|
if (hitsTarget ()) {
|
||||||
|
m_enemyParts |= Visibility::Other;
|
||||||
|
m_enemyOrigin = result.vecEndPos;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool Bot::seesEnemy (edict_t *player) {
|
bool Bot::seesEnemy (edict_t *player) {
|
||||||
auto isBehindSmokeClouds = [&] (const Vector &pos) {
|
auto isBehindSmokeClouds = [&] (const Vector &pos) {
|
||||||
if (cv_smoke_grenade_checks.as <int> () == 2) {
|
if (cv_smoke_grenade_checks.as <int> () == 2) {
|
||||||
|
|
|
||||||
133
src/engine.cpp
133
src/engine.cpp
|
|
@ -835,6 +835,11 @@ bool Game::loadCSBinary () {
|
||||||
m_gameFlags &= ~GameFlags::Modern;
|
m_gameFlags &= ~GameFlags::Modern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow to enable hitbox-based aiming on fresh games
|
||||||
|
if (is (GameFlags::Modern)) {
|
||||||
|
m_gameFlags |= GameFlags::HasStudioModels;
|
||||||
|
}
|
||||||
|
|
||||||
if (is (GameFlags::Metamod)) {
|
if (is (GameFlags::Metamod)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1404,3 +1409,131 @@ float LightMeasure::getLightLevel (const Vector &point) {
|
||||||
float LightMeasure::getSkyColor () {
|
float LightMeasure::getSkyColor () {
|
||||||
return static_cast <float> (Color (sv_skycolor_r.as <int> (), sv_skycolor_g.as <int> (), sv_skycolor_b.as <int> ()).avg ());
|
return static_cast <float> (Color (sv_skycolor_r.as <int> (), sv_skycolor_g.as <int> (), sv_skycolor_b.as <int> ()).avg ());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector PlayerHitboxEnumerator::get (edict_t *ent, int part, float updateTimestamp) {
|
||||||
|
auto parts = &m_parts[game.indexOfEntity (ent) % kGameMaxPlayers];
|
||||||
|
|
||||||
|
if (game.time () > updateTimestamp) {
|
||||||
|
update (ent);
|
||||||
|
parts->updated = game.time () + updateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (part) {
|
||||||
|
default:
|
||||||
|
case PlayerPart::Head:
|
||||||
|
return parts->head;
|
||||||
|
|
||||||
|
case PlayerPart::Stomach:
|
||||||
|
return parts->stomach;
|
||||||
|
|
||||||
|
case PlayerPart::LeftArm:
|
||||||
|
return parts->left;
|
||||||
|
|
||||||
|
case PlayerPart::RightArm:
|
||||||
|
return parts->right;
|
||||||
|
|
||||||
|
case PlayerPart::Feet:
|
||||||
|
return parts->feet;
|
||||||
|
|
||||||
|
case PlayerPart::RightLeg:
|
||||||
|
return { parts->right.x, parts->right.y, parts->feet.z };
|
||||||
|
|
||||||
|
case PlayerPart::LeftLeg:
|
||||||
|
return { parts->left.x, parts->left.y, parts->feet.z };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerHitboxEnumerator::update (edict_t *ent) {
|
||||||
|
constexpr auto kInvalidHitbox = -1;
|
||||||
|
|
||||||
|
if (!util.isAlive (ent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// get info about player
|
||||||
|
auto parts = &m_parts[game.indexOfEntity (ent) % kGameMaxPlayers];
|
||||||
|
|
||||||
|
// set the feet without bones
|
||||||
|
parts->feet = ent->v.origin;
|
||||||
|
|
||||||
|
constexpr auto kStandFeet = 34.0f;
|
||||||
|
constexpr auto kCrouchFeet = 14.0f;
|
||||||
|
|
||||||
|
// legs position isn't calculated to reduce cpu usage, just use some universal feet spot
|
||||||
|
if (ent->v.flags & FL_DUCKING) {
|
||||||
|
parts->feet.z = ent->v.origin.z - kCrouchFeet;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parts->feet.z = ent->v.origin.z - kStandFeet;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto getHitbox = [&kInvalidHitbox] (studiohdr_t *hdr, mstudiobbox_t *bb, int part) {
|
||||||
|
int hitbox = kInvalidHitbox;
|
||||||
|
|
||||||
|
for (auto i = 0; i < hdr->numhitboxes; ++i) {
|
||||||
|
const auto set = &bb[i];
|
||||||
|
|
||||||
|
if (set->group != part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hitbox = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return hitbox;
|
||||||
|
};
|
||||||
|
auto model = engfuncs.pfnGetModelPtr (ent);
|
||||||
|
auto studiohdr = reinterpret_cast <studiohdr_t *> (model);
|
||||||
|
|
||||||
|
// this can be null ?
|
||||||
|
if (model && studiohdr) {
|
||||||
|
auto bboxset = reinterpret_cast <mstudiobbox_t *> (reinterpret_cast <uint8_t *> (studiohdr) + studiohdr->hitboxindex);
|
||||||
|
|
||||||
|
// get the head
|
||||||
|
auto hitbox = getHitbox (studiohdr, bboxset, PlayerPart::Head);
|
||||||
|
|
||||||
|
if (hitbox != kInvalidHitbox) {
|
||||||
|
engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->head, nullptr);
|
||||||
|
|
||||||
|
parts->head.z += bboxset[hitbox].bbmax.z;
|
||||||
|
parts->head = { ent->v.origin.x, ent->v.origin.y, parts->head.z };
|
||||||
|
}
|
||||||
|
hitbox = kInvalidHitbox;
|
||||||
|
|
||||||
|
// get the body (stomach)
|
||||||
|
hitbox = getHitbox (studiohdr, bboxset, PlayerPart::Stomach);
|
||||||
|
|
||||||
|
if (hitbox != kInfiniteDistance) {
|
||||||
|
engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->stomach, nullptr);
|
||||||
|
}
|
||||||
|
hitbox = kInvalidHitbox;
|
||||||
|
|
||||||
|
// get the left (arm)
|
||||||
|
hitbox = getHitbox (studiohdr, bboxset, PlayerPart::LeftArm);
|
||||||
|
|
||||||
|
if (hitbox != kInfiniteDistance) {
|
||||||
|
engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->left, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the right (arm)
|
||||||
|
hitbox = getHitbox (studiohdr, bboxset, PlayerPart::RightArm);
|
||||||
|
|
||||||
|
if (hitbox != kInfiniteDistance) {
|
||||||
|
engfuncs.pfnGetBonePosition (ent, bboxset[hitbox].bone, parts->right, nullptr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
game.clearGameFlag (GameFlags::HasStudioModels); // yes, only a single fail will disable this
|
||||||
|
}
|
||||||
|
|
||||||
|
parts->head = ent->v.origin + ent->v.view_ofs;
|
||||||
|
parts->stomach = ent->v.origin;
|
||||||
|
|
||||||
|
parts->left = parts->head;
|
||||||
|
parts->right = parts->head;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayerHitboxEnumerator::reset () {
|
||||||
|
for (auto &part : m_parts) {
|
||||||
|
part = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1229,6 +1229,9 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
|
||||||
// init async planner
|
// init async planner
|
||||||
m_planner = cr::makeUnique <AStarAlgo> (graph.length ());
|
m_planner = cr::makeUnique <AStarAlgo> (graph.length ());
|
||||||
|
|
||||||
|
// init player models parts enumerator
|
||||||
|
m_hitboxEnumerator = cr::makeUnique <PlayerHitboxEnumerator> ();
|
||||||
|
|
||||||
// bot is not kicked by rotation
|
// bot is not kicked by rotation
|
||||||
m_kickedByRotation = false;
|
m_kickedByRotation = false;
|
||||||
|
|
||||||
|
|
@ -1241,7 +1244,6 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
|
||||||
newRound ();
|
newRound ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Bot::clearAmmoInfo () {
|
void Bot::clearAmmoInfo () {
|
||||||
plat.bzero (&m_ammoInClip, sizeof (m_ammoInClip));
|
plat.bzero (&m_ammoInClip, sizeof (m_ammoInClip));
|
||||||
plat.bzero (&m_ammo, sizeof (m_ammo));
|
plat.bzero (&m_ammo, sizeof (m_ammo));
|
||||||
|
|
@ -1545,6 +1547,7 @@ void Bot::newRound () {
|
||||||
m_followWaitTime = 0.0f;
|
m_followWaitTime = 0.0f;
|
||||||
|
|
||||||
m_hostages.clear ();
|
m_hostages.clear ();
|
||||||
|
m_hitboxEnumerator->reset ();
|
||||||
|
|
||||||
m_approachingLadderTimer.invalidate ();
|
m_approachingLadderTimer.invalidate ();
|
||||||
m_forgetLastVictimTimer.invalidate ();
|
m_forgetLastVictimTimer.invalidate ();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue