diff --git a/ext/linkage b/ext/linkage index b13855d..bd5617f 160000 --- a/ext/linkage +++ b/ext/linkage @@ -1 +1 @@ -Subproject commit b13855d46f848daa43eee990081b97f3e83d87fd +Subproject commit bd5617f41c84fa1f4e5800c75ea286f4d3cba62e diff --git a/inc/engine.h b/inc/engine.h index ab9698a..ff80462 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -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 @@ -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; @@ -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) class Game final : public Singleton { public: @@ -353,6 +389,11 @@ public: 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); diff --git a/inc/yapb.h b/inc/yapb.h index 2b14284..1968172 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -355,6 +355,8 @@ private: Array m_ignoredBreakable {}; // list of ignored breakables Array m_hostages {}; // pointer to used hostage entities + UniquePtr m_hitboxEnumerator {}; + Path *m_path {}; // pointer to the current path node String m_chatBuffer {}; // space for strings (say text...) Frustum::Planes m_viewFrustum {}; @@ -407,6 +409,8 @@ private: 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 (); diff --git a/src/combat.cpp b/src/combat.cpp index 455b251..74a76d4 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -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); @@ -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 (); @@ -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 () == 2) { diff --git a/src/engine.cpp b/src/engine.cpp index 64b7afd..98e819a 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -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; } @@ -1404,3 +1409,131 @@ float LightMeasure::getLightLevel (const Vector &point) { float LightMeasure::getSkyColor () { return static_cast (Color (sv_skycolor_r.as (), sv_skycolor_g.as (), sv_skycolor_b.as ()).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 (model); + + // this can be null ? + if (model && studiohdr) { + auto bboxset = reinterpret_cast (reinterpret_cast (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 = {}; + } +} diff --git a/src/manager.cpp b/src/manager.cpp index 68fb7e3..27575d3 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1229,6 +1229,9 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { // init async planner m_planner = cr::makeUnique (graph.length ()); + // init player models parts enumerator + m_hitboxEnumerator = cr::makeUnique (); + // bot is not kicked by rotation m_kickedByRotation = false; @@ -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)); @@ -1545,6 +1547,7 @@ void Bot::newRound () { m_followWaitTime = 0.0f; m_hostages.clear (); + m_hitboxEnumerator->reset (); m_approachingLadderTimer.invalidate (); m_forgetLastVictimTimer.invalidate ();