Skip to content

Commit

Permalink
aim: added optional hitbox-based aiming for bots (ref #579)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeefo committed Jun 2, 2024
1 parent 87cbd14 commit 8dece62
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 3 deletions.
2 changes: 1 addition & 1 deletion ext/linkage
Submodule linkage updated 2 files
+8 −0 .gitattributes
+46 −0 linkage/goldsrc.h
43 changes: 42 additions & 1 deletion inc/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ CR_DECLARE_SCOPED_ENUM (GameFlags,
HasBotVoice = cr::bit (11), // on that game version we can use chatter
AnniversaryHL25 = cr::bit (12), // half-life 25th anniversary engine
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
Expand All @@ -70,6 +71,18 @@ CR_DECLARE_SCOPED_ENUM (EntitySearchResult,
Break
)

// player body parts
CR_DECLARE_SCOPED_ENUM (PlayerPart,
Head = 1,
Chest,
Stomach,
LeftArm,
RightArm,
LeftLeg,
RightLeg,
Feet // custom!
)

// variable reg pair
struct ConVarReg {
cvar_t reg;
Expand Down Expand Up @@ -112,6 +125,29 @@ class EngineWrap final {
}
};

// 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)
class Game final : public Singleton <Game> {
public:
Expand Down Expand Up @@ -353,6 +389,11 @@ class Game final : public Singleton <Game> {
m_gameFlags |= type;
}

// clears game flag
void clearGameFlag (const int type) {
m_gameFlags &= ~type;
}

// gets the map type
bool mapIs (const int type) const {
return !!(m_mapFlags & type);
Expand Down
4 changes: 4 additions & 0 deletions inc/yapb.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ class Bot final {
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
Array <edict_t *> m_hostages {}; // pointer to used hostage entities

UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {};

Path *m_path {}; // pointer to the current path node
String m_chatBuffer {}; // space for strings (say text...)
Frustum::Planes m_viewFrustum {};
Expand Down Expand Up @@ -407,6 +409,8 @@ class Bot final {
bool isWeaponRestrictedAMX (int wid);
bool isInViewCone (const Vector &origin);
bool checkBodyParts (edict_t *target);
bool checkBodyPartsWithOffsets (edict_t *target);
bool checkBodyPartsWithHitboxes (edict_t *target);
bool seesEnemy (edict_t *player);
bool hasActiveGoal ();
bool advanceMovement ();
Expand Down
80 changes: 80 additions & 0 deletions src/combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_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_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 sv_gravity ("sv_gravity", nullptr, Var::GameRef);
Expand Down Expand Up @@ -137,6 +138,14 @@ bool Bot::checkBodyParts (edict_t *target) {
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 {};
const auto &eyes = getEyesPos ();

Expand Down Expand Up @@ -215,6 +224,77 @@ bool Bot::checkBodyParts (edict_t *target) {
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) {
auto isBehindSmokeClouds = [&] (const Vector &pos) {
if (cv_smoke_grenade_checks.as <int> () == 2) {
Expand Down
133 changes: 133 additions & 0 deletions src/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,11 @@ bool Game::loadCSBinary () {
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)) {
return false;
}
Expand Down Expand Up @@ -1404,3 +1409,131 @@ float LightMeasure::getLightLevel (const Vector &point) {
float LightMeasure::getSkyColor () {
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 = {};
}
}
5 changes: 4 additions & 1 deletion src/manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,9 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
// init async planner
m_planner = cr::makeUnique <AStarAlgo> (graph.length ());

// init player models parts enumerator
m_hitboxEnumerator = cr::makeUnique <PlayerHitboxEnumerator> ();

// bot is not kicked by rotation
m_kickedByRotation = false;

Expand All @@ -1241,7 +1244,6 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
newRound ();
}


void Bot::clearAmmoInfo () {
plat.bzero (&m_ammoInClip, sizeof (m_ammoInClip));
plat.bzero (&m_ammo, sizeof (m_ammo));
Expand Down Expand Up @@ -1545,6 +1547,7 @@ void Bot::newRound () {
m_followWaitTime = 0.0f;

m_hostages.clear ();
m_hitboxEnumerator->reset ();

m_approachingLadderTimer.invalidate ();
m_forgetLastVictimTimer.invalidate ();
Expand Down

0 comments on commit 8dece62

Please sign in to comment.