fix: breakable headache (ref #660)

fix: bots firing rates on short distances (ref #658)
This commit is contained in:
jeefo 2025-01-14 14:17:53 +03:00
commit c07d02c14e
No known key found for this signature in database
GPG key ID: D696786B81B667C8
9 changed files with 157 additions and 53 deletions

View file

@ -307,7 +307,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
game.testHull (pos, { next.x, next.y, next.z + 19.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr);
// we're can't reach next point
if (!cr::fequal (tr.flFraction, 1.0f) && !util.isShootableBreakable (tr.pHit)) {
if (!cr::fequal (tr.flFraction, 1.0f) && !util.isBreakableEntity (tr.pHit)) {
return;
}

View file

@ -153,6 +153,10 @@ void Bot::avoidGrenades () {
}
void Bot::checkBreakable (edict_t *touch) {
if (!game.hasBreakables ()) {
return;
}
if (game.isNullEntity (touch)) {
m_breakableEntity = lookupBreakable ();
}
@ -199,7 +203,7 @@ void Bot::checkBreakablesAround () {
continue;
}
if (!util.isShootableBreakable (breakable)) {
if (!util.isBreakableEntity (breakable)) {
continue;
}
@ -246,26 +250,69 @@ void Bot::checkBreakablesAround () {
edict_t *Bot::lookupBreakable () {
// this function checks if bot is blocked by a shoot able breakable in his moving direction
TraceResult tr {};
game.testLine (pev->origin, pev->origin + (m_destOrigin - pev->origin).normalize_apx () * 72.0f, TraceIgnore::None, ent (), &tr);
auto doLookup = [&] (const Vector &start, const Vector &end, const float dist) -> edict_t * {
TraceResult tr {};
game.testLine (start, start + (end - start).normalize_apx () * dist, TraceIgnore::None, ent (), &tr);
if (!cr::fequal (tr.flFraction, 1.0f)) {
auto ent = tr.pHit;
if (!cr::fequal (tr.flFraction, 1.0f)) {
auto hit = tr.pHit;
// check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap!
if (util.isShootableBreakable (ent)) {
m_breakableOrigin = game.getEntityOrigin (ent);
return ent;
// check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap!
if (util.isBreakableEntity (hit)) {
m_breakableOrigin = tr.vecEndPos;
m_breakableEntity = hit;
return hit;
}
}
}
game.testLine (getEyesPos (), getEyesPos () + (m_destOrigin - getEyesPos ()).normalize_apx () * 72.0f, TraceIgnore::None, ent (), &tr);
return nullptr;
};
if (!cr::fequal (tr.flFraction, 1.0f)) {
auto ent = tr.pHit;
// got recipe from KWo
for (auto i = 0; i < 5; ++i) {
if (i == 1 && game.isNullEntity (m_breakableEntity)) {
continue;
}
Vector end = m_pathOrigin;
Vector start = getEyesPos ();
if (util.isShootableBreakable (ent)) {
m_breakableOrigin = game.getEntityOrigin (ent);
return ent;
if (graph.exists (m_currentNodeIndex)) {
end = graph[m_currentNodeIndex].origin;
}
switch (i) {
case 0:
if (graph.exists (m_previousNodes[0])) {
start = graph[m_previousNodes[0]].origin;
}
else {
continue;
}
break;
case 1:
start = getEyesPos ();
end = game.getEntityOrigin (m_breakableEntity);
break;
case 2:
start = pev->origin;
end = m_destOrigin;
break;
case 3:
start = getEyesPos ();
end = m_destOrigin;
break;
case 4:
start = getEyesPos ();
break;
}
auto hit = doLookup (start, end, usesKnife () ? 32.0f : rg (72.0f, 256.0f));
if (!game.isNullEntity (hit)) {
return hit;
}
}
m_breakableEntity = nullptr;

View file

@ -1147,6 +1147,7 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
}
}
}
const float timeDelta = game.time () - m_frameInterval;
// need to care for burst fire?
if (distance < kSprayDistance || m_blindTime > game.time () || usesKnife ()) {
@ -1164,7 +1165,7 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
}
else {
// if automatic weapon press attack
if (tab[choosen].primaryFireHold && getAmmo (tab[index].id) > tab[index].minPrimaryAmmo) {
if (tab[choosen].primaryFireHold && getAmmoInClip () > tab[index].minPrimaryAmmo) {
pev->button |= IN_ATTACK;
}
@ -1177,13 +1178,13 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
}
if (pev->button & IN_ATTACK) {
m_shootTime = game.time ();
m_shootTime = timeDelta;
}
}
else {
// don't attack with knife over long distance
if (id == Weapon::Knife) {
m_shootTime = game.time ();
m_shootTime = timeDelta;
return;
}
@ -1192,8 +1193,8 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
}
if (tab[choosen].primaryFireHold) {
m_shootTime = game.time ();
m_zoomCheckTime = game.time ();
m_shootTime = timeDelta;
m_zoomCheckTime = timeDelta;
pev->button |= IN_ATTACK; // use primary attack
}
@ -1207,8 +1208,8 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
const int offset = cr::abs <int> (m_difficulty * 25 / 20 - 5);
m_shootTime = game.time () + 0.1f + rg (kMinFireDelay[offset], kMaxFireDelay[offset]);
m_zoomCheckTime = game.time ();
m_shootTime = timeDelta + 0.1f + rg (kMinFireDelay[offset], kMaxFireDelay[offset]);
m_zoomCheckTime = timeDelta;
}
}
}

View file

@ -62,6 +62,7 @@ void Game::levelInitialize (edict_t *entities, int max) {
// clear all breakables before initialization
m_breakables.clear ();
m_checkedBreakables.clear ();
// initialize all config files
conf.loadConfigs ();
@ -154,7 +155,10 @@ void Game::levelInitialize (edict_t *entities, int max) {
else if (classname.startsWith ("func_button")) {
m_mapFlags |= MapFlags::HasButtons;
}
else if (util.isShootableBreakable (ent)) {
else if (util.isBreakableEntity (ent, true)) {
// add breakable for material check
m_checkedBreakables[indexOfEntity (ent)] = ent->v.impulse <= 0;
m_breakables.push (ent);
}
}
@ -1229,6 +1233,10 @@ edict_t *Game::createFakeClient (StringRef name) {
return ent;
}
void Game::markBreakableAsInvalid (edict_t *ent) {
m_checkedBreakables[indexOfEntity (ent)] = false;
}
void LightMeasure::initializeLightstyles () {
// this function initializes lighting information...

View file

@ -153,10 +153,13 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
// the two entities both have velocities, for example two players colliding, this function
// is called twice, once for each entity moving.
if (!game.isNullEntity (pentTouched) && pentOther != game.getStartEntity ()) {
if (game.hasBreakables ()
&& !game.isNullEntity (pentTouched)
&& pentOther != game.getStartEntity ()) {
auto bot = bots[pentTouched];
if (bot && util.isShootableBreakable (pentOther)) {
if (bot && util.isBreakableEntity (pentOther)) {
bot->checkBreakable (pentOther);
}
}
@ -413,6 +416,14 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
if (game.is (GameFlags::HasFakePings) && !game.is (GameFlags::Metamod)) {
table->pfnUpdateClientData = [] (const struct edict_s *player, int sendweapons, struct clientdata_s *cd) {
// this function is a synchronization tool that is used periodically by the engine to tell
// the game DLL to send player info over the network to one of its clients when it suspects
// that this client is desynchronizing. Early bots were using it to ask the game DLL for the
// weapon list of players (by setting sendweapons to TRUE), but most of the time having a
// look around the ent->v.weapons bitmask is enough, since that's the place commonly used for
// MODs to store weapon information. If it can't be read from there, catching a few network
// messages (like in DMC) do the job better than this function anyway.
dllapi.pfnUpdateClientData (player, sendweapons, cd);
// do a post-processing with non-metamod
@ -439,6 +450,28 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
}
dllapi.pfnPM_Move (pm, server);
};
table->pfnKeyValue = [] (edict_t *ent, KeyValueData *kvd) {
// this function is called when the game requests a pointer to some entity's keyvalue data.
// The keyvalue data is held in each entity's infobuffer (basically a char buffer where each
// game DLL can put the stuff it wants) under - as it says - the form of a key/value pair. A
// common example of key/value pair is the "model", "(name of player model here)" one which
// is often used for client DLLs to display player characters with the right model (else they
// would all have the dull "models/player.mdl" one). The entity for which the keyvalue data
// pointer is requested is pentKeyvalue, the pointer to the keyvalue data structure pkvd.
if (game.isNullEntity (ent) && strcmp (ent->v.classname.chars (), "func_breakable") == 0) {
if (kvd && kvd->szKeyName && strcmp (kvd->szKeyName, "material") == 0) {
if (atoi (kvd->szValue) == 7) {
game.markBreakableAsInvalid (ent);
}
}
}
if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_IGNORED);
}
dllapi.pfnKeyValue (ent, kvd);
};
return HLTrue;
}
@ -499,6 +532,14 @@ CR_LINKAGE_C int GetEntityAPI_Post (gamefuncs_t *table, int) {
if (game.is (GameFlags::HasFakePings)) {
table->pfnUpdateClientData = [] (const struct edict_s *player, int, struct clientdata_s *) {
// this function is a synchronization tool that is used periodically by the engine to tell
// the game DLL to send player info over the network to one of its clients when it suspects
// that this client is desynchronizing. Early bots were using it to ask the game DLL for the
// weapon list of players (by setting sendweapons to TRUE), but most of the time having a
// look around the ent->v.weapons bitmask is enough, since that's the place commonly used for
// MODs to store weapon information. If it can't be read from there, catching a few network
// messages (like in DMC) do the job better than this function anyway.
//
// do a post-processing with non-metamod
auto ent = const_cast <edict_t *> (reinterpret_cast <const edict_t *> (player));

View file

@ -242,21 +242,19 @@ bool BotSupport::isHostageEntity (edict_t *ent) {
return classHash == kHostageEntity || classHash == kMonsterScientist;
}
bool BotSupport::isShootableBreakable (edict_t *ent) {
if (game.isNullEntity (ent) || ent == game.getStartEntity ()) {
return false;
bool BotSupport::isBreakableEntity (edict_t *ent, bool initialSeed) {
if (!initialSeed) {
if (!game.hasBreakables ()) {
return false;
}
}
// todo: move the breakables list into own array, and refresh them every round, since next thing is very expensive
#if 0
StringRef material = engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "material");
if (material == "7") {
if (game.isNullEntity (ent) || ent == game.getStartEntity () || (!initialSeed && !game.isBreakableValid (ent))) {
return false;
}
#endif
const auto limit = cv_breakable_health_limit.as <float> ();
// not shootable
// not shoot-able
if (ent->v.health >= limit) {
return false;
}
@ -271,14 +269,12 @@ bool BotSupport::isShootableBreakable (edict_t *ent) {
return ent->v.movetype == MOVETYPE_PUSH || ent->v.movetype == MOVETYPE_PUSHSTEP;
}
}
return false;
}
bool BotSupport::isFakeClient (edict_t *ent) {
if (bots[ent] != nullptr || (!game.isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT))) {
return true;
}
return false;
return bots[ent] != nullptr || (!game.isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT));
}
void BotSupport::checkWelcome () {

View file

@ -1460,8 +1460,9 @@ void Bot::escapeFromBomb_ () {
}
void Bot::shootBreakable_ () {
// breakable destroyed?
if (!util.isShootableBreakable (m_breakableEntity)) {
if (!util.isBreakableEntity (m_breakableEntity)) {
completeTask ();
return;
}
@ -1469,30 +1470,30 @@ void Bot::shootBreakable_ () {
m_checkTerrain = false;
m_moveToGoal = false;
m_navTimeset = game.time ();
m_lookAtSafe = m_breakableOrigin;
// is bot facing the breakable?
if (util.getConeDeviation (ent (), m_breakableOrigin) >= 0.90f) {
if (util.getConeDeviation (ent (), m_lookAtSafe) >= 0.95f) {
m_aimFlags |= AimFlags::Override;
m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f;
m_aimFlags |= AimFlags::Override;
if (usesKnife ()) {
selectBestWeapon ();
}
m_wantsToFire = true;
m_shootTime = game.time ();
if (!hasAnyAmmoInClip ()
&& usesKnife ()
&& pev->origin.distanceSq (m_lookAtSafe) > cr::sqrf (72.0f)) {
completeTask ();
}
}
else {
m_checkTerrain = true;
m_moveToGoal = true;
m_breakableOrigin.clear ();
m_breakableEntity = nullptr;
completeTask ();
}
}