backport: nodes flooder (analyzer) from cs-ebot
analyze: allow to disable goal marking analyze: add cvars descriptions and bounds nav: added optional post path smoothing for astar algorithm nav: now bots will use Dijkstra algo instead of floyd-warshall if memory usage too high (controlled via yb_path_floyd_memory_limit cvar) (fixes #434) nav: vistable are now calculated every frame to prevent game-freeze during loading the game (fixes #434) graph: pracrice reworked to hash table so memory footprint is as low as possible (at cost 5-10% performance loss on practice) (fixes #434) control: bots commands now is case-insensitive bot: major refactoring of bot's code nav: issue warnings about path fail only with debug practice: check for visibility when updating danger index analyzer: suspend any analyzing on change level control: add kickall_ct/kickall_t nav: increase blocked distance in stuck check
This commit is contained in:
parent
bb2e93a539
commit
e7712a551a
31 changed files with 3114 additions and 1722 deletions
359
src/analyze.cpp
Normal file
359
src/analyze.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_graph_analyze_auto_start ("yb_graph_analyze_auto_start", "1", "Autostart analyzer if all other cases are failed.");
|
||||
ConVar cv_graph_analyze_auto_save ("yb_graph_analyze_auto_save", "1", "Auto save results of analyzation to graph file. And re-add bots.");
|
||||
ConVar cv_graph_analyze_distance ("yb_graph_analyze_distance", "64", "The minimum distance to keep nodes from each other.", true, 42.0f, 128.0f);
|
||||
ConVar cv_graph_analyze_max_jump_height ("yb_graph_analyze_max_jump_height", "44", "Max jump height to test if next node will be unreachable.", true, 44.0f, 64.0f);
|
||||
ConVar cv_graph_analyze_fps ("yb_graph_analyze_fps", "30.0", "The FPS at which analyzer process is running. This keeps game from freezing during analyzing.", false, 25.0f, 99.0f);
|
||||
ConVar cv_graph_analyze_clean_paths_on_finish ("yb_graph_analyze_clean_paths_on_finish", "1", "Specifies if analyzer should clean the unnecessary paths upon finishing.");
|
||||
ConVar cv_graph_analyze_optimize_nodes_on_finish ("yb_graph_analyze_optimize_nodes_on_finish", "1", "Specifies if analyzer should merge some near-placed nodes with much of connections together.");
|
||||
ConVar cv_graph_analyze_mark_goals_on_finish ("yb_graph_analyze_mark_goals_on_finish", "1", "Specifies if analyzer should mark nodes as map goals automatically upon finish.");
|
||||
|
||||
void GraphAnalyze::start () {
|
||||
// start analyzer in few seconds after level initialized
|
||||
if (cv_graph_analyze_auto_start.bool_ ()) {
|
||||
m_updateInterval = game.time () + 3.0f;
|
||||
m_basicsCreated = false;
|
||||
|
||||
// set as we're analyzing
|
||||
m_isAnalyzing = true;
|
||||
|
||||
// silence all graph messages
|
||||
graph.setMessageSilence (true);
|
||||
|
||||
// set all nodes as not expanded
|
||||
for (auto &expanded : m_expandedNodes) {
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
// set all nodes as not optimized
|
||||
for (auto &optimized : m_optimizedNodes) {
|
||||
optimized = false;
|
||||
}
|
||||
ctrl.msg ("Starting map analyzation.");
|
||||
}
|
||||
else {
|
||||
m_updateInterval = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::update () {
|
||||
if (cr::fzero (m_updateInterval) || !m_isAnalyzing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_updateInterval >= game.time ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add basic nodes
|
||||
if (!m_basicsCreated) {
|
||||
graph.addBasic ();
|
||||
m_basicsCreated = true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < graph.length (); ++i) {
|
||||
if (m_updateInterval >= game.time ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph.exists (i)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_expandedNodes[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_expandedNodes[i] = true;
|
||||
setUpdateInterval ();
|
||||
|
||||
auto pos = graph[i].origin;
|
||||
auto range = cv_graph_analyze_distance.float_ ();
|
||||
|
||||
for (int dir = 1; dir < kMaxNodeLinks; ++dir) {
|
||||
switch (dir) {
|
||||
case 1:
|
||||
flood (pos, { pos.x + range, pos.y, pos.z }, range);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
flood (pos, { pos.x - range, pos.y, pos.z }, range);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
flood (pos, { pos.x, pos.y + range, pos.z }, range);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
flood (pos, { pos.x, pos.y - range, pos.z }, range);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
flood (pos, { pos.x + range, pos.y, pos.z + 128.0f }, range);
|
||||
break;
|
||||
|
||||
case 6:
|
||||
flood (pos, { pos.x - range, pos.y, pos.z + 128.0f }, range);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
flood (pos, { pos.x, pos.y + range, pos.z + 128.0f }, range);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
flood (pos, { pos.x, pos.y - range, pos.z + 128.0f }, range);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finish generation if no updates occurred recently
|
||||
if (m_updateInterval + 2.0f < game.time ()) {
|
||||
finish ();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::suspend () {
|
||||
m_updateInterval = 0.0f;
|
||||
m_isAnalyzing = false;
|
||||
m_isAnalyzed = false;
|
||||
}
|
||||
|
||||
void GraphAnalyze::finish () {
|
||||
// run optimization on finish
|
||||
optimize ();
|
||||
|
||||
// mark goal nodes
|
||||
markGoals ();
|
||||
|
||||
m_isAnalyzed = true;
|
||||
m_isAnalyzing = false;
|
||||
m_updateInterval = 0.0f;
|
||||
|
||||
// un-silence all graph messages
|
||||
graph.setMessageSilence (false);
|
||||
|
||||
ctrl.msg ("Complete map analyzation.");
|
||||
|
||||
// autosave bots graph
|
||||
if (cv_graph_analyze_auto_save.bool_ ()) {
|
||||
if (!graph.saveGraphData ()) {
|
||||
ctrl.msg ("Can't save analyzed graph. Internal error.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph.loadGraphData ()) {
|
||||
ctrl.msg ("Can't load analyzed graph. Internal error.");
|
||||
return;
|
||||
}
|
||||
vistab.startRebuild ();
|
||||
cv_quota.revert ();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::optimize () {
|
||||
if (graph.length () == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cv_graph_analyze_optimize_nodes_on_finish.bool_ ()) {
|
||||
return;
|
||||
}
|
||||
cleanup ();
|
||||
|
||||
// clear the uselss connections
|
||||
if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) {
|
||||
for (auto i = 0; i < graph.length (); ++i) {
|
||||
graph.clearConnections (i);
|
||||
}
|
||||
}
|
||||
|
||||
auto smooth = [] (const Array <int> &nodes) {
|
||||
Vector result;
|
||||
|
||||
for (const auto &node : nodes) {
|
||||
result += graph[node].origin;
|
||||
}
|
||||
result /= kMaxNodeLinks;
|
||||
result.z = graph[nodes.first ()].origin.z;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// set all nodes as not optimized
|
||||
for (auto &optimized : m_optimizedNodes) {
|
||||
optimized = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < graph.length (); ++i) {
|
||||
if (m_optimizedNodes[i]) {
|
||||
continue;
|
||||
}
|
||||
const auto &path = graph[i];
|
||||
Array <int> indexes;
|
||||
|
||||
for (const auto &link : path.links) {
|
||||
if (graph.exists (link.index) && !m_optimizedNodes[link.index] && cr::fequal (path.origin.z, graph[link.index].origin.z)) {
|
||||
indexes.emplace (link.index);
|
||||
}
|
||||
}
|
||||
|
||||
// we're have max out node links
|
||||
if (indexes.length () >= kMaxNodeLinks) {
|
||||
const Vector &pos = smooth (indexes);
|
||||
|
||||
for (const auto &index : indexes) {
|
||||
graph.erase (index);
|
||||
}
|
||||
graph.add (NodeAddFlag::Normal, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::cleanup () {
|
||||
int connections = 0; // clean bad paths
|
||||
|
||||
for (auto i = 0; i < graph.length (); ++i) {
|
||||
connections = 0;
|
||||
|
||||
for (const auto &link : graph[i].links) {
|
||||
if (link.index != kInvalidNodeIndex) {
|
||||
if (link.index > graph.length ()) {
|
||||
graph.erase (i);
|
||||
}
|
||||
++connections;
|
||||
}
|
||||
}
|
||||
|
||||
// no connections
|
||||
if (!connections) {
|
||||
graph.erase (i);
|
||||
}
|
||||
|
||||
// path number differs
|
||||
if (graph[i].number != i) {
|
||||
graph.erase (i);
|
||||
}
|
||||
|
||||
for (const auto &link : graph[i].links) {
|
||||
if (link.index != kInvalidNodeIndex) {
|
||||
if (link.index >= graph.length () || link.index < -kInvalidNodeIndex) {
|
||||
graph.erase (i);
|
||||
}
|
||||
else if (link.index == i) {
|
||||
graph.erase (i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!graph.isConnected (i)) {
|
||||
graph.erase (i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
|
||||
range *= 0.75f;
|
||||
|
||||
TraceResult tr;
|
||||
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) && !game.isShootableBreakable (tr.pHit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we're have something in around, skip
|
||||
if (graph.exists (graph.getForAnalyzer (tr.vecEndPos, range))) {
|
||||
return;
|
||||
}
|
||||
game.testHull (tr.vecEndPos, { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z - 999.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr);
|
||||
|
||||
// ground is away for a break
|
||||
if (cr::fequal (tr.flFraction, 1.0f)) {
|
||||
return;
|
||||
}
|
||||
Vector nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f };
|
||||
|
||||
const int endIndex = graph.getForAnalyzer (nextPos, range);
|
||||
const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f);
|
||||
|
||||
if (graph.exists (endIndex) || !graph.exists (targetIndex)) {
|
||||
return;
|
||||
}
|
||||
auto targetPos = graph[targetIndex].origin;
|
||||
|
||||
// re-check there's nothing nearby, and add something we're want
|
||||
if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) {
|
||||
m_isCrouch = false;
|
||||
game.testLine (nextPos, { nextPos.x, nextPos.y, nextPos.z + 36.0f }, TraceIgnore::Monsters, nullptr, &tr);
|
||||
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
m_isCrouch = true;
|
||||
}
|
||||
auto testPos = m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 18.0f } : nextPos;
|
||||
|
||||
if ((graph.isNodeReacheable (targetPos, testPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheableWithJump (targetPos, testPos))) {
|
||||
graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::setUpdateInterval () {
|
||||
const auto frametime (globals->frametime);
|
||||
|
||||
if ((cv_graph_analyze_fps.float_ () + frametime) <= 1.0f / frametime) {
|
||||
m_updateInterval = game.time () + frametime * 0.06f;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphAnalyze::markGoals () {
|
||||
if (!cv_graph_analyze_mark_goals_on_finish.bool_ ()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto updateNodeFlags = [] (int type, const char *entity) {
|
||||
game.searchEntities ("classname", entity, [&] (edict_t *ent) {
|
||||
for (auto &path : graph) {
|
||||
const auto &absOrigin = path.origin + Vector (1.0f, 1.0f, 1.0f);
|
||||
|
||||
if (ent->v.absmin.x > absOrigin.x || ent->v.absmin.y > absOrigin.y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ent->v.absmax.x < absOrigin.x || ent->v.absmax.y < absOrigin.y) {
|
||||
continue;
|
||||
}
|
||||
path.flags |= type;
|
||||
}
|
||||
return EntitySearchResult::Continue;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (game.mapIs (MapFlags::Demolition)) {
|
||||
updateNodeFlags (NodeFlag::Goal, "func_bomb_target"); // bombspot zone
|
||||
updateNodeFlags (NodeFlag::Goal, "info_bomb_target"); // bombspot zone (same as above)
|
||||
}
|
||||
else if (game.mapIs (MapFlags::HostageRescue)) {
|
||||
updateNodeFlags (NodeFlag::Rescue, "func_hostage_rescue"); // hostage rescue zone
|
||||
updateNodeFlags (NodeFlag::Rescue, "info_hostage_rescue"); // hostage rescue zone (same as above)
|
||||
updateNodeFlags (NodeFlag::Rescue, "info_player_start"); // then add ct spawnpoints
|
||||
|
||||
updateNodeFlags (NodeFlag::Goal, "hostage_entity"); // hostage entities
|
||||
updateNodeFlags (NodeFlag::Goal, "monster_scientist"); // hostage entities (same as above)
|
||||
}
|
||||
else if (game.mapIs (MapFlags::Assassination)) {
|
||||
updateNodeFlags (NodeFlag::Goal, "func_vip_safetyzone"); // vip rescue (safety) zone
|
||||
updateNodeFlags (NodeFlag::Goal, "func_escapezone"); // terrorist escape zone
|
||||
}
|
||||
}
|
||||
184
src/botlib.cpp
184
src/botlib.cpp
|
|
@ -167,7 +167,7 @@ void Bot::avoidGrenades () {
|
|||
float distance = pent->v.origin.distanceSq (pev->origin);
|
||||
float distanceMoved = pev->origin.distance (pent->v.origin + pent->v.velocity * m_frameInterval);
|
||||
|
||||
if (distanceMoved < distance && distance < cr::square (500.0f)) {
|
||||
if (distanceMoved < distance && distance < cr::sqrf (500.0f)) {
|
||||
const auto &dirToPoint = (pev->origin - pent->v.origin).normalize2d ();
|
||||
const auto &rightSide = pev->v_angle.right ().normalize2d ();
|
||||
|
||||
|
|
@ -242,12 +242,12 @@ void Bot::checkBreakablesAround () {
|
|||
const auto lengthToObstacle = origin.distanceSq (pev->origin);
|
||||
|
||||
// too far, skip it
|
||||
if (lengthToObstacle > cr::square (radius)) {
|
||||
if (lengthToObstacle > cr::sqrf (radius)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// too close, skip it
|
||||
if (lengthToObstacle < cr::square (100.0f)) {
|
||||
if (lengthToObstacle < cr::sqrf (100.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +333,7 @@ void Bot::updatePickups () {
|
|||
}
|
||||
|
||||
const auto &intresting = bots.getIntrestingEntities ();
|
||||
const float radius = cr::square (cv_object_pickup_radius.float_ ());
|
||||
const float radius = cr::sqrf (cv_object_pickup_radius.float_ ());
|
||||
|
||||
if (!game.isNullEntity (m_pickupItem)) {
|
||||
bool itemExists = false;
|
||||
|
|
@ -573,7 +573,7 @@ void Bot::updatePickups () {
|
|||
if (allowPickup) {
|
||||
for (auto &client : util.getClients ()) {
|
||||
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
|
||||
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) {
|
||||
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
|
||||
allowPickup = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -620,7 +620,7 @@ void Bot::updatePickups () {
|
|||
}
|
||||
}
|
||||
|
||||
if (pev->origin.distanceSq (origin) > cr::square (60.0f)) {
|
||||
if (pev->origin.distanceSq (origin) > cr::sqrf (60.0f)) {
|
||||
|
||||
if (!graph.isNodeReacheable (pev->origin, origin)) {
|
||||
allowPickup = false;
|
||||
|
|
@ -731,7 +731,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
|
|||
if (tr.flFraction < 1.0f) {
|
||||
float length = tr.vecEndPos.distanceSq (src);
|
||||
|
||||
if (length > cr::square (1024.0f)) {
|
||||
if (length > cr::sqrf (1024.0f)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -750,7 +750,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
|
|||
if (link.index == kInvalidNodeIndex) {
|
||||
continue;
|
||||
}
|
||||
auto distance = static_cast <float> (graph.getPathDist (link.index, enemyIndex));
|
||||
auto distance = static_cast <float> (planner.dist (link.index, enemyIndex));
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
|
|
@ -762,7 +762,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
|
|||
return graph[lookAtWaypoint].origin;
|
||||
}
|
||||
}
|
||||
auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
|
||||
if (graph.exists (dangerIndex)) {
|
||||
return graph[dangerIndex].origin;
|
||||
|
|
@ -1110,9 +1110,12 @@ bool Bot::canReplaceWeapon () {
|
|||
return isWeaponRestricted (m_currentWeapon);
|
||||
}
|
||||
|
||||
int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
|
||||
int Bot::pickBestWeapon (Array <int> &vec, int moneySave) {
|
||||
// this function picks best available weapon from random choice with money save
|
||||
|
||||
if (vec.length () < 2) {
|
||||
return vec.first ();
|
||||
}
|
||||
bool needMoreRandomWeapon = (m_personality == Personality::Careful) || (rg.chance (25) && m_personality == Personality::Normal);
|
||||
|
||||
if (needMoreRandomWeapon) {
|
||||
|
|
@ -1121,14 +1124,11 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
|
|||
if (buyFactor < 1.0f) {
|
||||
buyFactor = 1.0f;
|
||||
}
|
||||
|
||||
// swap array values
|
||||
for (int *begin = vec, *end = vec + count - 1; begin < end; ++begin, --end) {
|
||||
cr::swap (*end, *begin);
|
||||
}
|
||||
return vec[static_cast <int> (static_cast <float> (count - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)];
|
||||
}
|
||||
vec.reverse ();
|
||||
|
||||
return vec[static_cast <int> (static_cast <float> (vec.length <int32_t> () - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)];
|
||||
}
|
||||
int chance = 95;
|
||||
|
||||
// high skilled bots almost always prefer best weapon
|
||||
|
|
@ -1140,17 +1140,17 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
|
|||
chance = 75;
|
||||
}
|
||||
}
|
||||
auto &info = conf.getWeapons ();
|
||||
const auto &tab = conf.getWeapons ();
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto &weapon = info[vec[i]];
|
||||
for (const auto &w : vec) {
|
||||
const auto &weapon = tab[w];
|
||||
|
||||
// if wea have enough money for weapon buy it
|
||||
if (weapon.price + moneySave < m_moneyAmount + rg.get (50, 200) && rg.chance (chance)) {
|
||||
return vec[i];
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return vec[rg.get (0, count - 1)];
|
||||
return vec.random ();
|
||||
}
|
||||
|
||||
void Bot::buyStuff () {
|
||||
|
|
@ -1163,8 +1163,8 @@ void Bot::buyStuff () {
|
|||
m_nextBuyTime += rg.get (0.3f, 0.5f);
|
||||
}
|
||||
|
||||
int count = 0, weaponCount = 0;
|
||||
int choices[kNumWeapons] {};
|
||||
int count = 0;
|
||||
Array <int32_t> choices;
|
||||
|
||||
// select the priority tab for this personality
|
||||
const int *pref = conf.getWeaponPrefs (m_personality) + kNumWeapons;
|
||||
|
|
@ -1312,23 +1312,14 @@ void Bot::buyStuff () {
|
|||
}
|
||||
|
||||
if (selectedWeapon->price <= (m_moneyAmount - moneySave)) {
|
||||
choices[weaponCount++] = *pref;
|
||||
choices.emplace (*pref);
|
||||
}
|
||||
|
||||
} while (count < kNumWeapons && weaponCount < 4);
|
||||
} while (count < kNumWeapons && choices.length () < 4);
|
||||
|
||||
// found a desired weapon?
|
||||
if (weaponCount > 0) {
|
||||
int chosenWeapon;
|
||||
|
||||
// choose randomly from the best ones...
|
||||
if (weaponCount > 1) {
|
||||
chosenWeapon = pickBestWeapon (choices, weaponCount, moneySave);
|
||||
}
|
||||
else {
|
||||
chosenWeapon = choices[weaponCount - 1];
|
||||
}
|
||||
selectedWeapon = &tab[chosenWeapon];
|
||||
if (!choices.empty ()) {
|
||||
selectedWeapon = &tab[pickBestWeapon (choices, moneySave)];
|
||||
}
|
||||
else {
|
||||
selectedWeapon = nullptr;
|
||||
|
|
@ -1406,23 +1397,14 @@ void Bot::buyStuff () {
|
|||
}
|
||||
|
||||
if (selectedWeapon->price <= (m_moneyAmount - rg.get (100, 200))) {
|
||||
choices[weaponCount++] = *pref;
|
||||
choices.emplace (*pref);
|
||||
}
|
||||
|
||||
} while (count < kNumWeapons && weaponCount < 4);
|
||||
} while (count < kNumWeapons && choices.length () < 4);
|
||||
|
||||
// found a desired weapon?
|
||||
if (weaponCount > 0) {
|
||||
int chosenWeapon;
|
||||
|
||||
// choose randomly from the best ones...
|
||||
if (weaponCount > 1) {
|
||||
chosenWeapon = pickBestWeapon (choices, weaponCount, rg.get (100, 200));
|
||||
}
|
||||
else {
|
||||
chosenWeapon = choices[weaponCount - 1];
|
||||
}
|
||||
selectedWeapon = &tab[chosenWeapon];
|
||||
if (!choices.empty ()) {
|
||||
selectedWeapon = &tab[pickBestWeapon (choices, rg.get (100, 200))];
|
||||
}
|
||||
else {
|
||||
selectedWeapon = nullptr;
|
||||
|
|
@ -1708,14 +1690,17 @@ void Bot::setConditions () {
|
|||
}
|
||||
|
||||
if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && m_seeEnemyTime + 0.5f < game.time ()) {
|
||||
m_lastPredictIndex = kInvalidNodeIndex;
|
||||
|
||||
auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin);
|
||||
|
||||
if (distanceToLastEnemySq < cr::square (1600.0f)) {
|
||||
if (distanceToLastEnemySq < cr::sqrf (1600.0f)) {
|
||||
auto pathLength = 0;
|
||||
auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength);
|
||||
|
||||
if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::square (384.0f)) {
|
||||
if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::sqrf (384.0f)) {
|
||||
m_aimFlags |= AimFlags::PredictPath;
|
||||
m_lastPredictIndex = nodeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2050,7 +2035,7 @@ bool Bot::isEnemyThreat () {
|
|||
}
|
||||
|
||||
// if enemy is near or facing us directly
|
||||
if (m_enemy->v.origin.distanceSq (pev->origin) < cr::square (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) {
|
||||
if (m_enemy->v.origin.distanceSq (pev->origin) < cr::sqrf (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -2071,13 +2056,13 @@ bool Bot::reactOnEnemy () {
|
|||
}
|
||||
int enemyIndex = graph.getNearest (m_enemy->v.origin);
|
||||
|
||||
auto lineDist = m_enemy->v.origin.distance (pev->origin);
|
||||
auto pathDist = static_cast <float> (graph.getPathDist (ownIndex, enemyIndex));
|
||||
auto lineDist = m_enemy->v.origin.distance2d (pev->origin);
|
||||
auto pathDist = static_cast <float> (planner.dist (ownIndex, enemyIndex));
|
||||
|
||||
if (pathDist - lineDist > 112.0f || isOnLadder ()) {
|
||||
if (pathDist - lineDist > (planner.hasRealPathDistance () ? 112.0f : 32.0f) || isOnLadder ()) {
|
||||
m_isEnemyReachable = false;
|
||||
}
|
||||
else {
|
||||
else if (vistab.visible (ownIndex, enemyIndex)) {
|
||||
m_isEnemyReachable = true;
|
||||
}
|
||||
m_enemyReachableTimer = game.time () + 1.0f;
|
||||
|
|
@ -2647,19 +2632,19 @@ void Bot::updateAimDir () {
|
|||
else if (flags & AimFlags::PredictPath) {
|
||||
bool changePredictedEnemy = true;
|
||||
|
||||
if (m_timeNextTracking > game.time () && m_trackingEdict == m_lastEnemy) {
|
||||
if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) {
|
||||
changePredictedEnemy = false;
|
||||
}
|
||||
|
||||
if (changePredictedEnemy) {
|
||||
auto pathLength = 0;
|
||||
auto aimNode = findAimingNode (m_lastEnemyOrigin, pathLength);
|
||||
auto aimNode = graph.exists (m_lastPredictIndex) ? m_lastPredictIndex : findAimingNode (m_lastEnemyOrigin, pathLength);
|
||||
|
||||
if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) {
|
||||
m_lookAt = graph[aimNode].origin;
|
||||
m_lookAtSafe = m_lookAt;
|
||||
|
||||
m_timeNextTracking = game.time () + 0.5f;
|
||||
m_timeNextTracking = game.time () + 0.75f;
|
||||
m_trackingEdict = m_lastEnemy;
|
||||
}
|
||||
else {
|
||||
|
|
@ -2678,10 +2663,10 @@ void Bot::updateAimDir () {
|
|||
else if (flags & AimFlags::Nav) {
|
||||
m_lookAt = m_destOrigin;
|
||||
|
||||
if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::square (512.0f)) {
|
||||
if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::sqrf (512.0f)) {
|
||||
auto nextPathIndex = m_pathWalk.next ();
|
||||
|
||||
if (graph.isVisible (m_currentNodeIndex, nextPathIndex)) {
|
||||
if (vistab.visible (m_currentNodeIndex, nextPathIndex)) {
|
||||
m_lookAt = graph[nextPathIndex].origin + pev->view_ofs;
|
||||
}
|
||||
else {
|
||||
|
|
@ -2696,10 +2681,10 @@ void Bot::updateAimDir () {
|
|||
}
|
||||
|
||||
if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) {
|
||||
auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
|
||||
if (graph.exists (dangerIndex) && graph.isVisible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) {
|
||||
if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::square (160.0f)) {
|
||||
if (graph.exists (dangerIndex) && vistab.visible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) {
|
||||
if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (160.0f)) {
|
||||
m_lookAt = m_destOrigin;
|
||||
}
|
||||
else {
|
||||
|
|
@ -2730,7 +2715,7 @@ void Bot::checkDarkness () {
|
|||
}
|
||||
|
||||
// do not check every frame
|
||||
if (m_checkDarkTime + 5.0f > game.time ()) {
|
||||
if (m_checkDarkTime + 5.0f > game.time () || cr::fzero (m_path->light)) {
|
||||
return;
|
||||
}
|
||||
auto skyColor = illum.getSkyColor ();
|
||||
|
|
@ -2798,7 +2783,7 @@ void Bot::frame () {
|
|||
if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) {
|
||||
const Vector &bombPosition = graph.getBombOrigin ();
|
||||
|
||||
if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::square (1540.0f) && !isBombDefusing (bombPosition)) {
|
||||
if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::sqrf (1540.0f) && !isBombDefusing (bombPosition)) {
|
||||
m_itemIgnore = nullptr;
|
||||
m_itemCheckTime = game.time ();
|
||||
|
||||
|
|
@ -2823,7 +2808,7 @@ void Bot::frame () {
|
|||
}
|
||||
|
||||
// clear enemy far away
|
||||
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::square (2048.0)) {
|
||||
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::sqrf (2048.0)) {
|
||||
m_lastEnemy = nullptr;
|
||||
m_lastEnemyOrigin = nullptr;
|
||||
}
|
||||
|
|
@ -3128,6 +3113,7 @@ void Bot::normal_ () {
|
|||
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
|
||||
pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal;
|
||||
}
|
||||
ensureCurrentNodeIndex ();
|
||||
|
||||
// do pathfinding if it's not the current waypoint
|
||||
if (destIndex != m_currentNodeIndex) {
|
||||
|
|
@ -3347,6 +3333,8 @@ void Bot::seekCover_ () {
|
|||
m_prevGoalIndex = destIndex;
|
||||
getTask ()->data = destIndex;
|
||||
|
||||
ensureCurrentNodeIndex ();
|
||||
|
||||
if (destIndex != m_currentNodeIndex) {
|
||||
findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
|
||||
}
|
||||
|
|
@ -3432,6 +3420,7 @@ void Bot::blind_ () {
|
|||
}
|
||||
else if (!hasActiveGoal ()) {
|
||||
clearSearchNodes ();
|
||||
ensureCurrentNodeIndex ();
|
||||
|
||||
m_prevGoalIndex = m_blindNodeIndex;
|
||||
getTask ()->data = m_blindNodeIndex;
|
||||
|
|
@ -3652,6 +3641,7 @@ void Bot::moveToPos_ () {
|
|||
m_prevGoalIndex = destIndex;
|
||||
getTask ()->data = destIndex;
|
||||
|
||||
ensureCurrentNodeIndex ();
|
||||
findPath (m_currentNodeIndex, destIndex, m_pathType);
|
||||
}
|
||||
else {
|
||||
|
|
@ -3918,7 +3908,7 @@ void Bot::followUser_ () {
|
|||
m_reloadState = Reload::Primary;
|
||||
}
|
||||
|
||||
if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::square (130.0f)) {
|
||||
if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::sqrf (130.0f)) {
|
||||
m_followWaitTime = 0.0f;
|
||||
}
|
||||
else {
|
||||
|
|
@ -3998,7 +3988,7 @@ void Bot::throwExplosive_ () {
|
|||
|
||||
ignoreCollision ();
|
||||
|
||||
if (pev->origin.distanceSq (dest) < cr::square (450.0f)) {
|
||||
if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) {
|
||||
// heck, I don't wanna blow up myself
|
||||
m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f;
|
||||
|
||||
|
|
@ -4065,7 +4055,7 @@ void Bot::throwFlashbang_ () {
|
|||
|
||||
ignoreCollision ();
|
||||
|
||||
if (pev->origin.distanceSq (dest) < cr::square (450.0f)) {
|
||||
if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) {
|
||||
m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself
|
||||
|
||||
selectBestWeapon ();
|
||||
|
|
@ -4255,7 +4245,7 @@ void Bot::escapeFromBomb_ () {
|
|||
completeTask (); // we're done
|
||||
|
||||
// press duck button if we still have some enemies
|
||||
if (numEnemiesNear (pev->origin, 2048.0f)) {
|
||||
if (m_numEnemiesLeft > 0) {
|
||||
m_campButtons = IN_DUCK;
|
||||
}
|
||||
|
||||
|
|
@ -4267,37 +4257,39 @@ void Bot::escapeFromBomb_ () {
|
|||
else if (!hasActiveGoal ()) {
|
||||
clearSearchNodes ();
|
||||
|
||||
int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong;
|
||||
int bestIndex = kInvalidNodeIndex;
|
||||
|
||||
float safeRadius = rg.get (1513.0f, 2048.0f);
|
||||
float closestDistance = kInfiniteDistance;
|
||||
|
||||
for (const auto &path : graph) {
|
||||
if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) {
|
||||
continue;
|
||||
}
|
||||
int pathDistance = graph.getPathDist (m_currentNodeIndex, path.number);
|
||||
float distance = pev->origin.distance (path.origin);
|
||||
|
||||
if (minPathDistance > pathDistance) {
|
||||
minPathDistance = pathDistance;
|
||||
lastSelectedGoal = path.number;
|
||||
if (closestDistance > distance) {
|
||||
closestDistance = distance;
|
||||
bestIndex = path.number;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSelectedGoal < 0) {
|
||||
lastSelectedGoal = graph.getFarest (pev->origin, safeRadius);
|
||||
if (bestIndex < 0) {
|
||||
bestIndex = graph.getFarest (pev->origin, safeRadius);
|
||||
}
|
||||
|
||||
// still no luck?
|
||||
if (lastSelectedGoal < 0) {
|
||||
if (bestIndex < 0) {
|
||||
completeTask (); // we're done
|
||||
|
||||
// we have no destination point, so just sit down and camp
|
||||
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true);
|
||||
return;
|
||||
}
|
||||
m_prevGoalIndex = lastSelectedGoal;
|
||||
getTask ()->data = lastSelectedGoal;
|
||||
m_prevGoalIndex = bestIndex;
|
||||
getTask ()->data = bestIndex;
|
||||
|
||||
findPath (m_currentNodeIndex, lastSelectedGoal, FindPath::Fast);
|
||||
findPath (m_currentNodeIndex, bestIndex, FindPath::Fast);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4511,7 +4503,7 @@ void Bot::pickupItem_ () {
|
|||
// check if hostage is with a human teammate (hack)
|
||||
for (auto &client : util.getClients ()) {
|
||||
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
|
||||
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) {
|
||||
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
|
||||
return EntitySearchResult::Continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -4760,7 +4752,7 @@ void Bot::logic () {
|
|||
|
||||
// calculate 2 direction vectors, 1 without the up/down component
|
||||
const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval);
|
||||
const Vector &dirNormal = dirOld.normalize2d ();
|
||||
const Vector &dirNormal = dirOld.normalize2d_apx ();
|
||||
|
||||
m_moveAngles = dirOld.angles ();
|
||||
m_moveAngles.clampAngles ();
|
||||
|
|
@ -4968,7 +4960,7 @@ bool Bot::hasHostage () {
|
|||
if (!game.isNullEntity (hostage)) {
|
||||
|
||||
// don't care about dead hostages
|
||||
if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::square (600.0f)) {
|
||||
if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::sqrf (600.0f)) {
|
||||
hostage = nullptr;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -5114,10 +5106,13 @@ void Bot::updatePracticeValue (int damage) {
|
|||
}
|
||||
auto health = static_cast <int> (m_healthValue);
|
||||
|
||||
// max goal value
|
||||
constexpr int maxGoalValue = PracticeLimit::Goal;
|
||||
|
||||
// only rate goal waypoint if bot died because of the damage
|
||||
// FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons
|
||||
if (health - damage <= 0) {
|
||||
graph.setDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (graph.getDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -kMaxPracticeGoalValue, kMaxPracticeGoalValue));
|
||||
practice.setValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (practice.getValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -maxGoalValue, maxGoalValue));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5134,6 +5129,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
|
|||
if (attackerTeam == victimTeam) {
|
||||
return;
|
||||
}
|
||||
constexpr int maxDamageValue = PracticeLimit::Damage;
|
||||
|
||||
// if these are bots also remember damage to rank destination of the bot
|
||||
m_goalValue -= static_cast <float> (damage);
|
||||
|
|
@ -5155,18 +5151,18 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
|
|||
|
||||
if (m_healthValue > 20.0f) {
|
||||
if (victimTeam == Team::Terrorist || victimTeam == Team::CT) {
|
||||
graph.setDangerDamage (victimIndex, victimIndex, victimIndex, cr::clamp (graph.getDangerDamage (victimTeam, victimIndex, victimIndex), 0, kMaxPracticeDamageValue));
|
||||
practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, maxDamageValue));
|
||||
}
|
||||
}
|
||||
auto updateDamage = util.isFakeClient (attacker) ? 10 : 7;
|
||||
|
||||
// store away the damage done
|
||||
int damageValue = cr::clamp (graph.getDangerDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxPracticeDamageValue);
|
||||
int damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, maxDamageValue);
|
||||
|
||||
if (damageValue > graph.getHighestDamageForTeam (m_team)) {
|
||||
graph.setHighestDamageForTeam (m_team, damageValue);
|
||||
}
|
||||
graph.setDangerDamage (m_team, victimIndex, attackerIndex, damageValue);
|
||||
practice.setDamage (m_team, victimIndex, attackerIndex, damageValue);
|
||||
}
|
||||
|
||||
void Bot::pushChatMessage (int type, bool isTeamSay) {
|
||||
|
|
@ -5434,10 +5430,10 @@ bool Bot::isOutOfBombTimer () {
|
|||
if (timeLeft > 13.0f) {
|
||||
return false;
|
||||
}
|
||||
const Vector &bombOrigin = graph.getBombOrigin ();
|
||||
const auto &bombOrigin = graph.getBombOrigin ();
|
||||
|
||||
// for terrorist, if timer is lower than 13 seconds, return true
|
||||
if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::square (964.0f)) {
|
||||
if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) {
|
||||
return true;
|
||||
}
|
||||
bool hasTeammatesWithDefuserKit = false;
|
||||
|
|
@ -5445,7 +5441,7 @@ bool Bot::isOutOfBombTimer () {
|
|||
// check if our teammates has defusal kit
|
||||
for (const auto &bot : bots) {
|
||||
// search players with defuse kit
|
||||
if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::square (512.0f)) {
|
||||
if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::sqrf (512.0f)) {
|
||||
hasTeammatesWithDefuserKit = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -5474,7 +5470,7 @@ void Bot::updateHearing () {
|
|||
|
||||
// loop through all enemy clients to check for hearable stuff
|
||||
for (int i = 0; i < game.maxClients (); ++i) {
|
||||
const Client &client = util.getClient (i);
|
||||
const auto &client = util.getClient (i);
|
||||
|
||||
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () || client.team == m_team || client.noise.last < game.time ()) {
|
||||
continue;
|
||||
|
|
@ -5615,7 +5611,7 @@ bool Bot::isBombDefusing (const Vector &bombOrigin) {
|
|||
return false;
|
||||
}
|
||||
bool defusingInProgress = false;
|
||||
constexpr auto distanceToBomb = cr::square (165.0f);
|
||||
constexpr auto distanceToBomb = cr::sqrf (165.0f);
|
||||
|
||||
for (const auto &client : util.getClients ()) {
|
||||
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive)) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ int Bot::numFriendsNear (const Vector &origin, float radius) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (client.origin.distanceSq (origin) < cr::square (radius)) {
|
||||
if (client.origin.distanceSq (origin) < cr::sqrf (radius)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ int Bot::numEnemiesNear (const Vector &origin, float radius) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (client.origin.distanceSq (origin) < cr::square (radius)) {
|
||||
if (client.origin.distanceSq (origin) < cr::sqrf (radius)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -231,7 +231,7 @@ bool Bot::lookupEnemies () {
|
|||
return false;
|
||||
}
|
||||
edict_t *player, *newEnemy = nullptr;
|
||||
float nearestDistance = cr::square (m_viewDistance);
|
||||
float nearestDistance = cr::sqrf (m_viewDistance);
|
||||
|
||||
// clear suspected flag
|
||||
if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) {
|
||||
|
|
@ -300,7 +300,7 @@ bool Bot::lookupEnemies () {
|
|||
|
||||
// extra skill player can see thru smoke... if beeing attacked
|
||||
if ((player->v.button & (IN_ATTACK | IN_ATTACK2)) && m_viewDistance < m_maxViewDistance && m_difficulty == Difficulty::Expert) {
|
||||
nearestDistance = cr::square (m_maxViewDistance);
|
||||
nearestDistance = cr::sqrf (m_maxViewDistance);
|
||||
}
|
||||
|
||||
// see if bot can see the player...
|
||||
|
|
@ -645,7 +645,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) {
|
|||
game.testLine (dest, source, TraceIgnore::Monsters, ent (), &tr);
|
||||
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
if (tr.vecEndPos.distanceSq (dest) > cr::square (800.0f)) {
|
||||
if (tr.vecEndPos.distanceSq (dest) > cr::sqrf (800.0f)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -655,7 +655,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) {
|
|||
obstacleDistance = tr.vecEndPos.distanceSq (source);
|
||||
}
|
||||
}
|
||||
const float distance = cr::square (75.0f);
|
||||
const float distance = cr::sqrf (75.0f);
|
||||
|
||||
if (obstacleDistance > 0.0f) {
|
||||
while (power > 0) {
|
||||
|
|
@ -679,7 +679,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) {
|
|||
}
|
||||
|
||||
const Vector &source = getEyesPos ();
|
||||
const Vector &direction = (dest - source).normalize (); // 1 unit long
|
||||
const Vector &direction = (dest - source).normalize_apx (); // 1 unit long
|
||||
|
||||
int thikness = 0;
|
||||
int numHits = 0;
|
||||
|
|
@ -703,7 +703,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) {
|
|||
}
|
||||
|
||||
if (numHits < 3 && thikness < 98) {
|
||||
if (dest.distanceSq (point) < cr::square (112.0f)) {
|
||||
if (dest.distanceSq (point) < cr::sqrf (112.0f)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -724,7 +724,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) {
|
|||
TraceResult tr {};
|
||||
|
||||
Vector source = getEyesPos ();
|
||||
const auto &dir = (dest - source).normalize () * 8.0f;
|
||||
const auto &dir = (dest - source).normalize_apx () * 8.0f;
|
||||
|
||||
for (;;) {
|
||||
game.testLine (source, dest, TraceIgnore::Monsters, ent (), &tr);
|
||||
|
|
@ -773,8 +773,8 @@ bool Bot::needToPauseFiring (float distance) {
|
|||
else if (distance < kDoubleSprayDistance) {
|
||||
offset = 2.75f;
|
||||
}
|
||||
const float xPunch = cr::square (cr::deg2rad (pev->punchangle.x));
|
||||
const float yPunch = cr::square (cr::deg2rad (pev->punchangle.y));
|
||||
const float xPunch = cr::sqrf (cr::deg2rad (pev->punchangle.x));
|
||||
const float yPunch = cr::sqrf (cr::deg2rad (pev->punchangle.y));
|
||||
|
||||
const float interval = m_frameInterval;
|
||||
const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f;
|
||||
|
|
@ -1277,7 +1277,7 @@ void Bot::attackMovement () {
|
|||
else if ((distance > kDoubleSprayDistance && hasPrimaryWeapon ()) && (m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) {
|
||||
int enemyNearestIndex = graph.getNearest (m_enemy->v.origin);
|
||||
|
||||
if (graph.isVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (enemyNearestIndex, m_currentNodeIndex)) {
|
||||
if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) {
|
||||
m_duckTime = game.time () + m_frameInterval * 2.0f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1657,7 +1657,7 @@ bool Bot::isGroupOfEnemies (const Vector &location, int numEnemies, float radius
|
|||
continue;
|
||||
}
|
||||
|
||||
if (client.ent->v.origin.distanceSq (location) < cr::square (radius)) {
|
||||
if (client.ent->v.origin.distanceSq (location) < cr::sqrf (radius)) {
|
||||
// don't target our teammates...
|
||||
if (client.team == m_team) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -58,14 +58,21 @@ int BotControl::cmdKickBot () {
|
|||
}
|
||||
|
||||
int BotControl::cmdKickBots () {
|
||||
enum args { alias = 1, instant };
|
||||
enum args { alias = 1, instant, team };
|
||||
|
||||
// check if we're need to remove bots instantly
|
||||
auto kickInstant = strValue (instant) == "instant";
|
||||
|
||||
// kick the bots
|
||||
bots.kickEveryone (kickInstant);
|
||||
|
||||
// if team is specified, kick from specified tram
|
||||
if (strValue (alias).endsWith ("_ct") || intValue (team) == 2 || strValue (team) == "ct") {
|
||||
bots.kickFromTeam (Team::CT, true);
|
||||
}
|
||||
else if (strValue (alias).endsWith ("_t") || intValue (team) == 1 || strValue (team) == "t") {
|
||||
bots.kickFromTeam (Team::Terrorist, true);
|
||||
}
|
||||
else {
|
||||
bots.kickEveryone (kickInstant);
|
||||
}
|
||||
return BotCommandResult::Handled;
|
||||
}
|
||||
|
||||
|
|
@ -529,7 +536,7 @@ int BotControl::cmdNodeErase () {
|
|||
|
||||
// prevent accidents when graph are deleted unintentionally
|
||||
if (strValue (iamsure) == "iamsure") {
|
||||
graph.eraseFromDisk ();
|
||||
bstor.unlinkFromDisk ();
|
||||
}
|
||||
else {
|
||||
msg ("Please, append \"iamsure\" as parameter to get graph erased from the disk.");
|
||||
|
|
@ -761,10 +768,16 @@ int BotControl::cmdNodeReleaseEditor () {
|
|||
int BotControl::cmdNodeUpload () {
|
||||
enum args { graph_cmd = 1, cmd };
|
||||
|
||||
// do not allow to upload analyzed graphs
|
||||
if (graph.isAnalyzed ()) {
|
||||
msg ("Sorry, unable to upload graph that was generated automatically.");
|
||||
return BotCommandResult::Handled;
|
||||
}
|
||||
|
||||
// do not allow to upload bad graph
|
||||
if (!graph.checkNodes (false)) {
|
||||
msg ("Sorry, unable to upload graph file that contains errors. Please type \"wp check\" to verify graph consistency.");
|
||||
return BotCommandResult::BadFormat;
|
||||
return BotCommandResult::Handled;
|
||||
}
|
||||
|
||||
msg ("\n");
|
||||
|
|
@ -776,7 +789,7 @@ int BotControl::cmdNodeUpload () {
|
|||
String uploadUrl = cv_graph_url_upload.str ();
|
||||
|
||||
// try to upload the file
|
||||
if (http.uploadFile (uploadUrl, util.buildPath (BotFile::Graph))) {
|
||||
if (http.uploadFile (uploadUrl, bstor.buildPath (BotFile::Graph))) {
|
||||
msg ("Graph file was successfully validated and uploaded to the YaPB Graph DB (%s).", product.download);
|
||||
msg ("It will be available for download for all YaPB users in a few minutes.");
|
||||
msg ("\n");
|
||||
|
|
@ -1717,7 +1730,7 @@ bool BotControl::executeCommands () {
|
|||
if (m_args.empty ()) {
|
||||
return false;
|
||||
}
|
||||
const auto &prefix = m_args[0];
|
||||
const auto &prefix = m_args.first ();
|
||||
|
||||
// no handling if not for us
|
||||
if (prefix != product.cmdPri && prefix != product.cmdSec) {
|
||||
|
|
@ -2055,7 +2068,7 @@ BotControl::BotControl () {
|
|||
|
||||
m_cmds.emplace ("add/addbot/add_ct/addbot_ct/add_t/addbot_t/addhs/addhs_t/addhs_ct", "add [difficulty] [personality] [team] [model] [name]", "Adding specific bot into the game.", &BotControl::cmdAddBot);
|
||||
m_cmds.emplace ("kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t", "kick [team]", "Kicks off the random bot from the game.", &BotControl::cmdKickBot);
|
||||
m_cmds.emplace ("removebots/kickbots/kickall", "removebots [instant]", "Kicks all the bots from the game.", &BotControl::cmdKickBots);
|
||||
m_cmds.emplace ("removebots/kickbots/kickall/kickall_ct/kickall_t", "removebots [instant] [team]", "Kicks all the bots from the game.", &BotControl::cmdKickBots);
|
||||
m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots);
|
||||
m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill);
|
||||
m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote);
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@ void Game::precache () {
|
|||
|
||||
m_engineWrap.precacheSound ("weapons/xbow_hit1.wav"); // waypoint add
|
||||
m_engineWrap.precacheSound ("weapons/mine_activate.wav"); // waypoint delete
|
||||
m_engineWrap.precacheSound ("common/wpn_hudoff.wav"); // path add/delete start
|
||||
m_engineWrap.precacheSound ("common/wpn_hudon.wav"); // path add/delete done
|
||||
m_engineWrap.precacheSound ("common/wpn_moveselect.wav"); // path add/delete cancel
|
||||
m_engineWrap.precacheSound ("common/wpn_denyselect.wav"); // path add/delete error
|
||||
|
||||
m_mapFlags = 0; // reset map type as worldspawn is the first entity spawned
|
||||
|
||||
|
|
@ -68,27 +65,27 @@ void Game::levelInitialize (edict_t *entities, int max) {
|
|||
// update worldmodel
|
||||
illum.resetWorldModel ();
|
||||
|
||||
// do level initialization stuff here...
|
||||
graph.loadGraphData ();
|
||||
|
||||
// execute main config
|
||||
conf.loadMainConfig ();
|
||||
|
||||
// load map-specific config
|
||||
conf.loadMapSpecificConfig ();
|
||||
|
||||
// do level initialization stuff here...
|
||||
graph.loadGraphData ();
|
||||
|
||||
// initialize quota management
|
||||
bots.initQuota ();
|
||||
|
||||
// rebuild vistable if needed
|
||||
graph.rebuildVisibility ();
|
||||
|
||||
// install the sendto hook to fake queries
|
||||
util.installSendTo ();
|
||||
|
||||
// flush any print queue
|
||||
ctrl.resetFlushTimestamp ();
|
||||
|
||||
// suspend any analyzer tasks
|
||||
analyzer.suspend ();
|
||||
|
||||
// go thru the all entities on map, and do whatever we're want
|
||||
for (int i = 0; i < max; ++i) {
|
||||
auto ent = entities + i;
|
||||
|
|
@ -413,11 +410,11 @@ void Game::playSound (edict_t *ent, const char *sound) {
|
|||
}
|
||||
|
||||
void Game::setPlayerStartDrawModels () {
|
||||
HashMap <String, String> models;
|
||||
|
||||
models["info_player_start"] = "models/player/urban/urban.mdl";
|
||||
models["info_player_deathmatch"] = "models/player/terror/terror.mdl";
|
||||
models["info_vip_start"] = "models/player/vip/vip.mdl";
|
||||
static HashMap <String, String> models {
|
||||
{"info_player_start", "models/player/urban/urban.mdl"},
|
||||
{"info_player_deathmatch", "models/player/terror/terror.mdl"},
|
||||
{"info_vip_start", "models/player/vip/vip.mdl"}
|
||||
};
|
||||
|
||||
models.foreach ([&] (const String &key, const String &val) {
|
||||
game.searchEntities ("classname", key, [&] (edict_t *ent) {
|
||||
|
|
@ -620,6 +617,7 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo
|
|||
|
||||
reg.reg.name = const_cast <char *> (name);
|
||||
reg.reg.string = const_cast <char *> (value);
|
||||
reg.name = name;
|
||||
reg.missing = missingAction;
|
||||
reg.init = value;
|
||||
reg.info = info;
|
||||
|
|
@ -654,6 +652,20 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo
|
|||
m_cvars.push (cr::move (reg));
|
||||
}
|
||||
|
||||
void ConVar::revert () {
|
||||
if (!ptr) {
|
||||
return;
|
||||
}
|
||||
const auto &cvars = game.getCvars ();
|
||||
|
||||
for (const auto &var : cvars) {
|
||||
if (var.name == ptr->name) {
|
||||
set (var.initial);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Game::checkCvarsBounds () {
|
||||
for (const auto &var : m_cvars) {
|
||||
|
||||
|
|
@ -669,14 +681,14 @@ void Game::checkCvarsBounds () {
|
|||
continue;
|
||||
}
|
||||
auto value = var.self->float_ ();
|
||||
auto str = var.self->str ();
|
||||
auto str = String (var.self->str ());
|
||||
|
||||
// check the bounds and set default if out of bounds
|
||||
if (value > var.max || value < var.min || (!strings.isEmpty (str) && isalpha (*str))) {
|
||||
if (value > var.max || value < var.min || (!str.empty () && isalpha (str[0]))) {
|
||||
var.self->set (var.initial);
|
||||
|
||||
// notify about that
|
||||
ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.reg.name, var.min, var.max, str, var.initial);
|
||||
ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.name, var.min, var.max, str, var.initial);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -741,19 +753,7 @@ bool Game::loadCSBinary () {
|
|||
if (!modname) {
|
||||
return false;
|
||||
}
|
||||
StringArray libs;
|
||||
|
||||
if (plat.win) {
|
||||
libs.push ("mp.dll");
|
||||
libs.push ("cs.dll");
|
||||
}
|
||||
else if (plat.nix) {
|
||||
libs.push ("cs.so");
|
||||
libs.push ("cs_i386.so");
|
||||
}
|
||||
else if (plat.osx) {
|
||||
libs.push ("cs.dylib");
|
||||
}
|
||||
StringArray libs { "mp", "cs", "cs_i386" };
|
||||
|
||||
auto libCheck = [&] (StringRef mod, StringRef dll) {
|
||||
// try to load gamedll
|
||||
|
|
@ -771,7 +771,7 @@ bool Game::loadCSBinary () {
|
|||
|
||||
// search the libraries inside game dlls directory
|
||||
for (const auto &lib : libs) {
|
||||
auto path = strings.format ("%s/dlls/%s", modname, lib);
|
||||
auto path = strings.format ("%s/dlls/%s.%s", modname, lib, DLL_SUFFIX);
|
||||
|
||||
// if we can't read file, skip it
|
||||
if (!File::exists (path)) {
|
||||
|
|
@ -834,7 +834,7 @@ bool Game::loadCSBinary () {
|
|||
bool Game::postload () {
|
||||
|
||||
// register logger
|
||||
logger.initialize (util.buildPath (BotFile::LogFile), [] (const char *msg) {
|
||||
logger.initialize (bstor.buildPath (BotFile::LogFile), [] (const char *msg) {
|
||||
game.print (msg);
|
||||
});
|
||||
|
||||
|
|
|
|||
1056
src/graph.cpp
1056
src/graph.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -332,8 +332,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
|
|||
// any case, when the new map will be booting, ServerActivate() will be called, so we'll do
|
||||
// the loading of new bots and the new BSP data parsing there.
|
||||
|
||||
// save collected experience on shutdown
|
||||
graph.savePractice ();
|
||||
// save collected practice on shutdown
|
||||
practice.save ();
|
||||
|
||||
// destroy global killer entity
|
||||
bots.destroyKillerEntity ();
|
||||
|
|
@ -384,9 +384,15 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
|
|||
graph.frame ();
|
||||
}
|
||||
|
||||
// update analyzer if needed
|
||||
analyzer.update ();
|
||||
|
||||
// run stuff periodically
|
||||
game.slowFrame ();
|
||||
|
||||
// rebuild vistable if needed
|
||||
vistab.rebuild ();
|
||||
|
||||
if (bots.hasBotsOnline ()) {
|
||||
// keep track of grenades on map
|
||||
bots.updateActiveGrenade ();
|
||||
|
|
@ -903,8 +909,9 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) {
|
|||
}
|
||||
bots.kickEveryone (true); // kick all bots off this server
|
||||
|
||||
// save collected experience on shutdown
|
||||
graph.savePractice ();
|
||||
// save collected practice on shutdown
|
||||
practice.save ();
|
||||
|
||||
util.disableSendTo ();
|
||||
|
||||
// make sure all stuff cleared
|
||||
|
|
|
|||
|
|
@ -1251,7 +1251,6 @@ void Bot::newRound () {
|
|||
|
||||
// delete all allocated path nodes
|
||||
clearSearchNodes ();
|
||||
clearRoute ();
|
||||
|
||||
m_pathOrigin = nullptr;
|
||||
m_destOrigin = nullptr;
|
||||
|
|
@ -1688,13 +1687,18 @@ void BotManager::notifyBombDefuse () {
|
|||
if (!isBombPlanted ()) {
|
||||
return;
|
||||
}
|
||||
auto bombPos = graph.getBombOrigin ();
|
||||
|
||||
for (const auto &bot : bots) {
|
||||
if (bot->m_team == Team::Terrorist && bot->m_notKilled && bot->getCurrentTaskId () != Task::MoveToPosition) {
|
||||
bot->clearSearchNodes ();
|
||||
auto task = bot->getCurrentTaskId ();
|
||||
|
||||
bot->m_position = graph.getBombOrigin ();
|
||||
bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
||||
if (bot->m_notKilled && task != Task::MoveToPosition && task != Task::DefuseBomb && task != Task::EscapeFromBomb) {
|
||||
if (bot->m_team == Team::CT || bot->pev->origin.distanceSq (bombPos) < cr::sqrf (384.0f)) {
|
||||
bot->clearSearchNodes ();
|
||||
|
||||
bot->m_position = bombPos;
|
||||
bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1891,7 +1895,7 @@ void BotManager::initRound () {
|
|||
m_botsCanPause = false;
|
||||
|
||||
resetFilters ();
|
||||
graph.updateGlobalPractice (); // update experience data on round start
|
||||
practice.update (); // update practice data on round start
|
||||
|
||||
// calculate the round mid/end in world time
|
||||
m_timeRoundStart = game.time () + mp_freezetime.float_ ();
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ void MessageDispatcher::netMsgBarTime () {
|
|||
m_bot->m_hasProgressBar = true; // the progress bar on a hud
|
||||
|
||||
// notify bots about defusing has started
|
||||
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_bot->m_team == Team::CT) {
|
||||
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
|
||||
bots.notifyBombDefuse ();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
623
src/navigate.cpp
623
src/navigate.cpp
|
|
@ -7,10 +7,6 @@
|
|||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "4", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
|
||||
ConVar cv_path_danger_factor_min ("yb_path_danger_factor_min", "200", "Lower bound of danger factor that used to add additional danger to path based on practice.", true, 100.0f, 2400.0f);
|
||||
ConVar cv_path_danger_factor_max ("yb_path_danger_factor_max", "400", "Upper bound of danger factor that used to add additional danger to path based on practice.", true, 200.0f, 4800.0f);
|
||||
|
||||
int Bot::findBestGoal () {
|
||||
auto pushToHistroy = [&] (int32_t goal) -> int32_t {
|
||||
m_nodeHistory.push (goal);
|
||||
|
|
@ -233,7 +229,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) {
|
|||
for (auto &point : graph.m_goalPoints) {
|
||||
float distance = graph[point].origin.distanceSq (pev->origin);
|
||||
|
||||
if (distance > cr::square (1024.0f)) {
|
||||
if (distance > cr::sqrf (1024.0f)) {
|
||||
continue;
|
||||
}
|
||||
if (distance < minDist) {
|
||||
|
|
@ -299,7 +295,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) {
|
|||
break;
|
||||
}
|
||||
|
||||
if (graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i]) < graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) {
|
||||
if (practice.getValue (m_team, m_currentNodeIndex, goalChoices[i]) < practice.getValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) {
|
||||
cr::swap (goalChoices[i + 1], goalChoices[i]);
|
||||
sorting = true;
|
||||
}
|
||||
|
|
@ -368,7 +364,7 @@ void Bot::ignoreCollision () {
|
|||
|
||||
void Bot::doPlayerAvoidance (const Vector &normal) {
|
||||
m_hindrance = nullptr;
|
||||
float distance = cr::square (348.0f);
|
||||
float distance = cr::sqrf (348.0f);
|
||||
|
||||
if (getCurrentTaskId () == Task::Attack || isOnLadder ()) {
|
||||
return;
|
||||
|
|
@ -400,7 +396,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
|
|||
}
|
||||
auto nearest = client.ent->v.origin.distanceSq (pev->origin);
|
||||
|
||||
if (nearest < cr::square (pev->maxspeed) && nearest < distance) {
|
||||
if (nearest < cr::sqrf (pev->maxspeed) && nearest < distance) {
|
||||
m_hindrance = client.ent;
|
||||
distance = nearest;
|
||||
}
|
||||
|
|
@ -425,19 +421,19 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
|
|||
auto nextFrameDistance = pev->origin.distanceSq (m_hindrance->v.origin + m_hindrance->v.velocity * interval);
|
||||
|
||||
// is player that near now or in future that we need to steer away?
|
||||
if (movedDistance <= cr::square (48.0f) || (distance <= cr::square (56.0f) && nextFrameDistance < distance)) {
|
||||
auto dir = (pev->origin - m_hindrance->v.origin).normalize2d ();
|
||||
if (movedDistance <= cr::sqrf (48.0f) || (distance <= cr::sqrf (56.0f) && nextFrameDistance < distance)) {
|
||||
auto dir = (pev->origin - m_hindrance->v.origin).normalize2d_apx ();
|
||||
|
||||
// to start strafing, we have to first figure out if the target is on the left side or right side
|
||||
if ((dir | right.normalize2d ()) > 0.0f) {
|
||||
if ((dir | right.normalize2d_apx ()) > 0.0f) {
|
||||
setStrafeSpeed (normal, pev->maxspeed);
|
||||
}
|
||||
else {
|
||||
setStrafeSpeed (normal, -pev->maxspeed);
|
||||
}
|
||||
|
||||
if (distance < cr::square (76.0f)) {
|
||||
if ((dir | forward.normalize2d ()) < 0.0f) {
|
||||
if (distance < cr::sqrf (76.0f)) {
|
||||
if ((dir | forward.normalize2d_apx ()) < 0.0f) {
|
||||
m_moveSpeed = -pev->maxspeed;
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +491,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
|
|||
}
|
||||
|
||||
|
||||
// bot is stuc, but not yet decided what to do?
|
||||
// bot is stuck, but not yet decided what to do?
|
||||
if (m_collisionState == CollisionState::Undecided) {
|
||||
uint32_t bits = 0;
|
||||
|
||||
|
|
@ -541,19 +537,20 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
|
|||
else {
|
||||
dirLeft = true;
|
||||
}
|
||||
const Vector &testDir = m_moveSpeed > 0.0f ? forward : -forward;
|
||||
const auto &testDir = m_moveSpeed > 0.0f ? forward : -forward;
|
||||
constexpr float blockDistance = 56.0f;
|
||||
|
||||
// now check which side is blocked
|
||||
src = pev->origin + right * 32.0f;
|
||||
dst = src + testDir * 32.0f;
|
||||
src = pev->origin + right * blockDistance;
|
||||
dst = src + testDir * blockDistance;
|
||||
|
||||
game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr);
|
||||
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
blockedRight = true;
|
||||
}
|
||||
src = pev->origin - right * 32.0f;
|
||||
dst = src + testDir * 32.0f;
|
||||
src = pev->origin - right * blockDistance;
|
||||
dst = src + testDir * blockDistance;
|
||||
|
||||
game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr);
|
||||
|
||||
|
|
@ -1057,7 +1054,7 @@ bool Bot::updateNavigation () {
|
|||
}
|
||||
|
||||
// needs precise placement - check if we get past the point
|
||||
if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::square (nodeDistance)) {
|
||||
if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::sqrf (nodeDistance)) {
|
||||
desiredDistance = nodeDistance + 1.0f;
|
||||
}
|
||||
|
||||
|
|
@ -1078,15 +1075,16 @@ bool Bot::updateNavigation () {
|
|||
// did we reach a destination node?
|
||||
if (getTask ()->data == m_currentNodeIndex) {
|
||||
if (m_chosenGoalIndex != kInvalidNodeIndex) {
|
||||
constexpr int maxGoalValue = PracticeLimit::Goal;
|
||||
|
||||
// add goal values
|
||||
int goalValue = graph.getDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex);
|
||||
int goalValue = practice.getValue (m_team, m_chosenGoalIndex, m_currentNodeIndex);
|
||||
int addedValue = static_cast <int> (m_healthValue * 0.5f + m_goalValue * 0.5f);
|
||||
|
||||
goalValue = cr::clamp (goalValue + addedValue, -kMaxPracticeGoalValue, kMaxPracticeGoalValue);
|
||||
goalValue = cr::clamp (goalValue + addedValue, -maxGoalValue, maxGoalValue);
|
||||
|
||||
// update the practice for team
|
||||
graph.setDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue);
|
||||
practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1193,7 +1191,7 @@ bool Bot::updateLiftHandling () {
|
|||
m_destOrigin = m_liftTravelPos;
|
||||
|
||||
// check if we enough to destination
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
|
||||
// need to wait our following teammate ?
|
||||
|
|
@ -1247,7 +1245,7 @@ bool Bot::updateLiftHandling () {
|
|||
if (needWaitForTeammate) {
|
||||
m_destOrigin = m_liftTravelPos;
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1278,7 +1276,7 @@ bool Bot::updateLiftHandling () {
|
|||
m_liftState = LiftState::TravelingBy;
|
||||
m_liftUsageTime = game.time () + 14.0f;
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1288,7 +1286,7 @@ bool Bot::updateLiftHandling () {
|
|||
if (m_liftState == LiftState::TravelingBy) {
|
||||
m_destOrigin = Vector (m_liftTravelPos.x, m_liftTravelPos.y, pev->origin.z);
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1305,7 +1303,7 @@ bool Bot::updateLiftHandling () {
|
|||
m_destOrigin = pev->origin;
|
||||
}
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1338,7 +1336,7 @@ bool Bot::updateLiftHandling () {
|
|||
m_destOrigin = button->v.origin;
|
||||
}
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1369,7 +1367,7 @@ bool Bot::updateLiftHandling () {
|
|||
}
|
||||
}
|
||||
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) {
|
||||
if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
|
||||
wait ();
|
||||
}
|
||||
}
|
||||
|
|
@ -1433,344 +1431,15 @@ bool Bot::updateLiftStates () {
|
|||
return true;
|
||||
}
|
||||
|
||||
void Bot::findShortestPath (int srcIndex, int destIndex) {
|
||||
// this function finds the shortest path from source index to destination index
|
||||
|
||||
if (!graph.exists (srcIndex)) {
|
||||
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
|
||||
return;
|
||||
}
|
||||
else if (!graph.exists (destIndex)) {
|
||||
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
|
||||
return;
|
||||
}
|
||||
clearSearchNodes ();
|
||||
|
||||
m_chosenGoalIndex = srcIndex;
|
||||
m_goalValue = 0.0f;
|
||||
|
||||
m_pathWalk.add (srcIndex);
|
||||
|
||||
while (srcIndex != destIndex) {
|
||||
srcIndex = (graph.m_matrix.data () + (srcIndex * graph.length ()) + destIndex)->index;
|
||||
|
||||
if (srcIndex < 0) {
|
||||
m_prevGoalIndex = kInvalidNodeIndex;
|
||||
getTask ()->data = kInvalidNodeIndex;
|
||||
|
||||
return;
|
||||
}
|
||||
m_pathWalk.add (srcIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) {
|
||||
// this function finds a path from srcIndex to destIndex
|
||||
|
||||
auto dangerFactor = [&] () -> float {
|
||||
return rg.get (cv_path_danger_factor_min.float_ (), cv_path_danger_factor_max.float_ ()) * 2.0f / cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f);
|
||||
};
|
||||
|
||||
// least kills and number of nodes to goal for a team
|
||||
auto gfunctionKillsDist = [&dangerFactor] (int team, int currentIndex, int parentIndex) -> float {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
auto cost = static_cast <float> (graph.getDangerDamage (team, currentIndex, currentIndex) + graph.getHighestDamageForTeam (team));
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (auto &neighbour : current.links) {
|
||||
if (neighbour.index != kInvalidNodeIndex) {
|
||||
cost += static_cast <float> (graph.getDangerDamage (team, neighbour.index, neighbour.index));
|
||||
}
|
||||
}
|
||||
|
||||
if (current.flags & NodeFlag::Crouch) {
|
||||
cost *= 1.5f;
|
||||
}
|
||||
return cost + dangerFactor ();
|
||||
};
|
||||
|
||||
// least kills and number of nodes to goal for a team
|
||||
auto gfunctionKillsDistCTWithHostage = [&gfunctionKillsDist] (int team, int currentIndex, int parentIndex) -> float {
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return 65355.0f;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionKillsDist (team, currentIndex, parentIndex);
|
||||
};
|
||||
|
||||
// least kills to goal for a team
|
||||
auto gfunctionKills = [&dangerFactor] (int team, int currentIndex, int) -> float {
|
||||
auto cost = static_cast <float> (graph.getDangerDamage (team, currentIndex, currentIndex));
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (auto &neighbour : current.links) {
|
||||
if (neighbour.index != kInvalidNodeIndex) {
|
||||
cost += static_cast <float> (graph.getDangerDamage (team, neighbour.index, neighbour.index));
|
||||
}
|
||||
}
|
||||
|
||||
if (current.flags & NodeFlag::Crouch) {
|
||||
cost *= 1.5f;
|
||||
}
|
||||
return cost + dangerFactor () + 1.0f;
|
||||
};
|
||||
|
||||
// least kills to goal for a team
|
||||
auto gfunctionKillsCTWithHostage = [&gfunctionKills] (int team, int currentIndex, int parentIndex) -> float {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return 65355.0f;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionKills (team, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionKills (team, currentIndex, parentIndex);
|
||||
};
|
||||
|
||||
auto gfunctionPathDist = [] (int, int currentIndex, int parentIndex) -> float {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
const auto &parent = graph[parentIndex];
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (const auto &link : parent.links) {
|
||||
if (link.index == currentIndex) {
|
||||
const auto distance = static_cast <float> (link.distance);
|
||||
|
||||
// we don't like ladder or crouch point
|
||||
if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return distance * 1.5f;
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
return 65355.0f;
|
||||
};
|
||||
|
||||
auto gfunctionPathDistWithHostage = [&gfunctionPathDist] (int, int currentIndex, int parentIndex) -> float {
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return 65355.0f;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex);
|
||||
};
|
||||
|
||||
// square distance heuristic
|
||||
auto hfunctionPathDist = [] (int index, int, int goalIndex) -> float {
|
||||
const auto &start = graph[index];
|
||||
const auto &goal = graph[goalIndex];
|
||||
|
||||
float x = cr::abs (start.origin.x - goal.origin.x);
|
||||
float y = cr::abs (start.origin.y - goal.origin.y);
|
||||
float z = cr::abs (start.origin.z - goal.origin.z);
|
||||
|
||||
switch (cv_path_heuristic_mode.int_ ()) {
|
||||
case 0:
|
||||
default:
|
||||
return cr::max (cr::max (x, y), z); // chebyshev distance
|
||||
|
||||
case 1:
|
||||
return x + y + z; // manhattan distance
|
||||
|
||||
case 2:
|
||||
return 0.0f; // no heuristic
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
// euclidean based distance
|
||||
float euclidean = cr::powf (cr::powf (x, 2.0f) + cr::powf (y, 2.0f) + cr::powf (z, 2.0f), 0.5f);
|
||||
|
||||
if (cv_path_heuristic_mode.int_ () == 4) {
|
||||
return 1000.0f * (cr::ceilf (euclidean) - euclidean);
|
||||
}
|
||||
return euclidean;
|
||||
}
|
||||
};
|
||||
|
||||
// square distance heuristic with hostages
|
||||
auto hfunctionPathDistWithHostage = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
|
||||
if (graph[index].flags & NodeFlag::NoHostage) {
|
||||
return 65355.0f;
|
||||
}
|
||||
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex);
|
||||
};
|
||||
|
||||
// none heuristic
|
||||
auto hfunctionNone = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
|
||||
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f);
|
||||
};
|
||||
|
||||
if (!graph.exists (srcIndex)) {
|
||||
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
|
||||
return;
|
||||
}
|
||||
else if (!graph.exists (destIndex)) {
|
||||
|
||||
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// astar needs some nodes to work with
|
||||
if (graph.length () < kMaxNodeLinks) {
|
||||
logger.error ("A* Search for bot \"%s\" has failed due to too small number of nodes (%d). Seems to be graph is broken.", pev->netname.chars (), graph.length ());
|
||||
return;
|
||||
}
|
||||
|
||||
// holders for heuristic functions
|
||||
static Lambda <float (int, int, int)> gcalc, hcalc;
|
||||
|
||||
// get correct calculation for heuristic
|
||||
if (pathType == FindPath::Optimal) {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
hcalc = hfunctionPathDistWithHostage;
|
||||
gcalc = gfunctionKillsDistCTWithHostage;
|
||||
}
|
||||
else {
|
||||
hcalc = hfunctionPathDist;
|
||||
gcalc = gfunctionKillsDist;
|
||||
}
|
||||
}
|
||||
else if (pathType == FindPath::Safe) {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
hcalc = hfunctionNone;
|
||||
gcalc = gfunctionKillsCTWithHostage;
|
||||
}
|
||||
else {
|
||||
hcalc = hfunctionNone;
|
||||
gcalc = gfunctionKills;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
hcalc = hfunctionPathDistWithHostage;
|
||||
gcalc = gfunctionPathDistWithHostage;
|
||||
}
|
||||
else {
|
||||
hcalc = hfunctionPathDist;
|
||||
gcalc = gfunctionPathDist;
|
||||
}
|
||||
}
|
||||
clearSearchNodes ();
|
||||
|
||||
m_chosenGoalIndex = srcIndex;
|
||||
m_goalValue = 0.0f;
|
||||
|
||||
clearRoute ();
|
||||
auto srcRoute = &m_routes[srcIndex];
|
||||
|
||||
// put start node into open list
|
||||
srcRoute->g = gcalc (m_team, srcIndex, kInvalidNodeIndex);
|
||||
srcRoute->f = srcRoute->g + hcalc (srcIndex, kInvalidNodeIndex, destIndex);
|
||||
srcRoute->state = RouteState::Open;
|
||||
|
||||
m_routeQue.clear ();
|
||||
m_routeQue.emplace (srcIndex, srcRoute->g);
|
||||
|
||||
while (!m_routeQue.empty ()) {
|
||||
// remove the first node from the open list
|
||||
int currentIndex = m_routeQue.pop ().first;
|
||||
|
||||
// safes us from bad graph...
|
||||
if (m_routeQue.length () >= graph.getMaxRouteLength () - 1) {
|
||||
logger.error ("A* Search for bot \"%s\" has tried to build path with at least %d nodes. Seems to be graph is broken.", pev->netname.chars (), m_routeQue.length ());
|
||||
|
||||
kick (); // kick the bot off...
|
||||
return;
|
||||
}
|
||||
|
||||
// is the current node the goal node?
|
||||
if (currentIndex == destIndex) {
|
||||
|
||||
// build the complete path
|
||||
do {
|
||||
m_pathWalk.add (currentIndex);
|
||||
currentIndex = m_routes[currentIndex].parent;
|
||||
|
||||
} while (currentIndex != kInvalidNodeIndex);
|
||||
|
||||
// reverse path for path follower
|
||||
m_pathWalk.reverse ();
|
||||
|
||||
return;
|
||||
}
|
||||
auto curRoute = &m_routes[currentIndex];
|
||||
|
||||
if (curRoute->state != RouteState::Open) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// put current node into CLOSED list
|
||||
curRoute->state = RouteState::Closed;
|
||||
|
||||
// now expand the current node
|
||||
for (const auto &child : graph[currentIndex].links) {
|
||||
if (child.index == kInvalidNodeIndex) {
|
||||
continue;
|
||||
}
|
||||
auto childRoute = &m_routes[child.index];
|
||||
|
||||
// calculate the F value as F = G + H
|
||||
const float g = curRoute->g + gcalc (m_team, child.index, currentIndex);
|
||||
const float h = hcalc (child.index, kInvalidNodeIndex, destIndex);
|
||||
const float f = g + h;
|
||||
|
||||
if (childRoute->state == RouteState::New || childRoute->f > f) {
|
||||
// put the current child into open list
|
||||
childRoute->parent = currentIndex;
|
||||
childRoute->state = RouteState::Open;
|
||||
|
||||
childRoute->g = g;
|
||||
childRoute->f = f;
|
||||
|
||||
m_routeQue.emplace (child.index, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ());
|
||||
|
||||
// fallback to shortest path
|
||||
findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead
|
||||
}
|
||||
|
||||
void Bot::clearSearchNodes () {
|
||||
m_pathWalk.clear ();
|
||||
m_chosenGoalIndex = kInvalidNodeIndex;
|
||||
}
|
||||
|
||||
void Bot::clearRoute () {
|
||||
m_routes.resize (static_cast <size_t> (graph.length ()));
|
||||
|
||||
for (const auto &path : graph) {
|
||||
auto route = &m_routes[path.number];
|
||||
|
||||
route->g = route->f = 0.0f;
|
||||
route->parent = kInvalidNodeIndex;
|
||||
route->state = RouteState::New;
|
||||
}
|
||||
m_routes.clear ();
|
||||
}
|
||||
|
||||
int Bot::findAimingNode (const Vector &to, int &pathLength) {
|
||||
// return the most distant node which is seen from the bot to the target and is within count
|
||||
|
||||
if (m_currentNodeIndex == kInvalidNodeIndex) {
|
||||
changeNodeIndex (findNearestNode ());
|
||||
}
|
||||
ensureCurrentNodeIndex ();
|
||||
|
||||
int destIndex = graph.getNearest (to);
|
||||
int bestIndex = m_currentNodeIndex;
|
||||
|
|
@ -1779,19 +1448,15 @@ int Bot::findAimingNode (const Vector &to, int &pathLength) {
|
|||
return kInvalidNodeIndex;
|
||||
}
|
||||
|
||||
while (destIndex != m_currentNodeIndex) {
|
||||
destIndex = (graph.m_matrix.data () + (destIndex * graph.length ()) + m_currentNodeIndex)->index;
|
||||
|
||||
if (destIndex < 0) {
|
||||
break;
|
||||
}
|
||||
planner.find (destIndex, m_currentNodeIndex, [&] (int index) {
|
||||
++pathLength;
|
||||
|
||||
if (graph.isVisible (m_currentNodeIndex, destIndex)) {
|
||||
bestIndex = destIndex;
|
||||
break;
|
||||
if (vistab.visible (m_currentNodeIndex, index)) {
|
||||
bestIndex = index;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (bestIndex == m_currentNodeIndex) {
|
||||
return kInvalidNodeIndex;
|
||||
|
|
@ -1920,7 +1585,7 @@ float Bot::getEstimatedNodeReachTime () {
|
|||
float distance = graph[m_previousNodes[0]].origin.distanceSq (graph[m_currentNodeIndex].origin);
|
||||
|
||||
// caclulate estimated time
|
||||
estimatedTime = 5.0f * (distance / cr::square (m_moveSpeed + 1.0f));
|
||||
estimatedTime = 5.0f * (distance / cr::sqrf (m_moveSpeed + 1.0f));
|
||||
|
||||
bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK);
|
||||
|
||||
|
|
@ -1942,21 +1607,23 @@ void Bot::findValidNode () {
|
|||
findNextBestNode ();
|
||||
}
|
||||
else if (m_navTimeset + getEstimatedNodeReachTime () < game.time ()) {
|
||||
constexpr int maxDamageValue = PracticeLimit::Damage;
|
||||
|
||||
// increase danager for both teams
|
||||
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
|
||||
int damageValue = graph.getDangerDamage (team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
damageValue = cr::clamp (damageValue + 100, 0, kMaxPracticeDamageValue);
|
||||
int damageValue = practice.getDamage (team, m_currentNodeIndex, m_currentNodeIndex);
|
||||
damageValue = cr::clamp (damageValue + 100, 0, maxDamageValue);
|
||||
|
||||
// affect nearby connected with victim nodes
|
||||
for (auto &neighbour : m_path->links) {
|
||||
if (graph.exists (neighbour.index)) {
|
||||
int neighbourValue = graph.getDangerDamage (team, neighbour.index, neighbour.index);
|
||||
neighbourValue = cr::clamp (neighbourValue + 100, 0, kMaxPracticeDamageValue);
|
||||
int neighbourValue = practice.getDamage (team, neighbour.index, neighbour.index);
|
||||
neighbourValue = cr::clamp (neighbourValue + 100, 0, maxDamageValue);
|
||||
|
||||
graph.setDangerDamage (m_team, neighbour.index, neighbour.index, neighbourValue);
|
||||
practice.setDamage (m_team, neighbour.index, neighbour.index, neighbourValue);
|
||||
}
|
||||
}
|
||||
graph.setDangerDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue);
|
||||
practice.setDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue);
|
||||
}
|
||||
clearSearchNodes ();
|
||||
findNextBestNode ();
|
||||
|
|
@ -1990,7 +1657,7 @@ int Bot::findNearestNode () {
|
|||
constexpr float kMaxDistance = 1024.0f;
|
||||
|
||||
int index = kInvalidNodeIndex;
|
||||
float minimumDistance = cr::square (kMaxDistance);
|
||||
float minimumDistance = cr::sqrf (kMaxDistance);
|
||||
|
||||
const auto &origin = pev->origin + pev->velocity * m_frameInterval;
|
||||
const auto &bucket = graph.getNodesInBucket (origin);
|
||||
|
|
@ -2005,7 +1672,7 @@ int Bot::findNearestNode () {
|
|||
|
||||
if (distance < minimumDistance) {
|
||||
// if bot doing navigation, make sure node really visible and reacheable
|
||||
if (graph.isVisible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) {
|
||||
if (vistab.visible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) {
|
||||
index = path.number;
|
||||
minimumDistance = distance;
|
||||
}
|
||||
|
|
@ -2014,7 +1681,7 @@ int Bot::findNearestNode () {
|
|||
|
||||
// try to search ANYTHING that can be reachaed
|
||||
if (index == kInvalidNodeIndex) {
|
||||
minimumDistance = cr::square (kMaxDistance);
|
||||
minimumDistance = cr::sqrf (kMaxDistance);
|
||||
const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin);
|
||||
|
||||
for (const auto &i : nearestNodes) {
|
||||
|
|
@ -2059,7 +1726,7 @@ int Bot::findBombNode () {
|
|||
const auto &audible = isBombAudible ();
|
||||
|
||||
// take the nearest to bomb nodes instead of goal if close enough
|
||||
if (pev->origin.distanceSq (bomb) < cr::square (96.0f)) {
|
||||
if (pev->origin.distanceSq (bomb) < cr::sqrf (96.0f)) {
|
||||
int node = graph.getNearest (bomb, 420.0f);
|
||||
|
||||
m_bombSearchOverridden = true;
|
||||
|
|
@ -2105,9 +1772,7 @@ int Bot::findDefendNode (const Vector &origin) {
|
|||
// this function tries to find a good position which has a line of sight to a position,
|
||||
// provides enough cover point, and is far away from the defending position
|
||||
|
||||
if (m_currentNodeIndex == kInvalidNodeIndex) {
|
||||
changeNodeIndex (findNearestNode ());
|
||||
}
|
||||
ensureCurrentNodeIndex ();
|
||||
TraceResult tr {};
|
||||
|
||||
int nodeIndex[kMaxNodeLinks] {};
|
||||
|
|
@ -2132,12 +1797,12 @@ int Bot::findDefendNode (const Vector &origin) {
|
|||
// find the best node now
|
||||
for (const auto &path : graph) {
|
||||
// exclude ladder & current nodes
|
||||
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !graph.isVisible (path.number, posIndex)) {
|
||||
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !vistab.visible (path.number, posIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// use the 'real' pathfinding distances
|
||||
int distance = graph.getPathDist (srcIndex, path.number);
|
||||
int distance = planner.dist (srcIndex, path.number);
|
||||
|
||||
// skip wayponts too far
|
||||
if (distance > kMaxDistance) {
|
||||
|
|
@ -2192,11 +1857,11 @@ int Bot::findDefendNode (const Vector &origin) {
|
|||
// use statistic if we have them
|
||||
for (int i = 0; i < kMaxNodeLinks; ++i) {
|
||||
if (nodeIndex[i] != kInvalidNodeIndex) {
|
||||
int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]);
|
||||
practice = (practice * 100) / graph.getHighestDamageForTeam (m_team);
|
||||
int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
|
||||
practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
|
||||
|
||||
minDistance[i] = (practice * 100) / 8192;
|
||||
minDistance[i] += practice;
|
||||
minDistance[i] = (practiceDamage * 100) / 8192;
|
||||
minDistance[i] += practiceDamage;
|
||||
}
|
||||
}
|
||||
bool sorting = false;
|
||||
|
|
@ -2220,7 +1885,7 @@ int Bot::findDefendNode (const Vector &origin) {
|
|||
IntArray found;
|
||||
|
||||
for (const auto &path : graph) {
|
||||
if (origin.distanceSq (path.origin) < cr::square (static_cast <float> (kMaxDistance)) && graph.isVisible (path.number, posIndex) && !isOccupiedNode (path.number)) {
|
||||
if (origin.distanceSq (path.origin) < cr::sqrf (static_cast <float> (kMaxDistance)) && vistab.visible (path.number, posIndex) && !isOccupiedNode (path.number)) {
|
||||
found.push (path.number);
|
||||
}
|
||||
}
|
||||
|
|
@ -2284,13 +1949,13 @@ int Bot::findCoverNode (float maxDistance) {
|
|||
// find the best node now
|
||||
for (const auto &path : graph) {
|
||||
// exclude ladder, current node and nodes seen by the enemy
|
||||
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || graph.isVisible (enemyIndex, path.number)) {
|
||||
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || vistab.visible (enemyIndex, path.number)) {
|
||||
continue;
|
||||
}
|
||||
bool neighbourVisible = false; // now check neighbour nodes for visibility
|
||||
|
||||
for (auto &enemy : enemies) {
|
||||
if (graph.isVisible (enemy, path.number)) {
|
||||
if (vistab.visible (enemy, path.number)) {
|
||||
neighbourVisible = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -2302,8 +1967,8 @@ int Bot::findCoverNode (float maxDistance) {
|
|||
}
|
||||
|
||||
// use the 'real' pathfinding distances
|
||||
int distance = graph.getPathDist (srcIndex, path.number);
|
||||
int enemyDistance = graph.getPathDist (enemyIndex, path.number);
|
||||
int distance = planner.dist (srcIndex, path.number);
|
||||
int enemyDistance = planner.dist (enemyIndex, path.number);
|
||||
|
||||
if (distance >= enemyDistance) {
|
||||
continue;
|
||||
|
|
@ -2346,11 +2011,11 @@ int Bot::findCoverNode (float maxDistance) {
|
|||
// use statistic if we have them
|
||||
for (int i = 0; i < kMaxNodeLinks; ++i) {
|
||||
if (nodeIndex[i] != kInvalidNodeIndex) {
|
||||
int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]);
|
||||
practice = (practice * 100) / graph.getHighestDamageForTeam (m_team);
|
||||
int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
|
||||
practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
|
||||
|
||||
minDistance[i] = (practice * 100) / 8192;
|
||||
minDistance[i] += practice;
|
||||
minDistance[i] = (practiceDamage * 100) / 8192;
|
||||
minDistance[i] += practiceDamage;
|
||||
}
|
||||
}
|
||||
bool sorting = false;
|
||||
|
|
@ -2396,23 +2061,36 @@ bool Bot::selectBestNextNode () {
|
|||
assert (!m_pathWalk.empty ());
|
||||
assert (m_pathWalk.hasNext ());
|
||||
|
||||
if (!isOccupiedNode (m_pathWalk.first ())) {
|
||||
auto nextNodeIndex = m_pathWalk.next ();
|
||||
auto currentNodeIndex = m_pathWalk.first ();
|
||||
auto prevNodeIndex = m_currentNodeIndex;
|
||||
|
||||
if (!isOccupiedNode (currentNodeIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &link : m_path->links) {
|
||||
if (graph.exists (link.index) && m_pathWalk.first () != link.index && graph.isConnected (link.index, m_pathWalk.next ()) && graph.isConnected (m_currentNodeIndex, link.index)) {
|
||||
// check the links
|
||||
for (const auto &link : graph[prevNodeIndex].links) {
|
||||
|
||||
// don't use ladder nodes as alternative
|
||||
if (graph[link.index].flags & (NodeFlag::Ladder | PathFlag::Jump)) {
|
||||
continue;
|
||||
}
|
||||
// skip invalid links, or links that points to itself
|
||||
if (!graph.exists (link.index) || currentNodeIndex == link.index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (graph[link.index].origin.z <= graph[m_currentNodeIndex].origin.z + 10.0f && !isOccupiedNode (link.index)) {
|
||||
m_pathWalk.first () = link.index;
|
||||
|
||||
return true;
|
||||
}
|
||||
// skip itn't connected links
|
||||
if (!graph.isConnected (link.index, nextNodeIndex) || !graph.isConnected (link.index, prevNodeIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't use ladder nodes as alternative
|
||||
if (graph[link.index].flags & (NodeFlag::Ladder | NodeFlag::Camp | PathFlag::Jump)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if not occupied, just set advance
|
||||
if (!isOccupiedNode (link.index)) {
|
||||
m_pathWalk.first () = link.index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
@ -2462,7 +2140,7 @@ bool Bot::advanceMovement () {
|
|||
m_campButtons = 0;
|
||||
|
||||
const int nextIndex = m_pathWalk.next ();
|
||||
auto kills = static_cast <float> (graph.getDangerDamage (m_team, nextIndex, nextIndex));
|
||||
auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex));
|
||||
|
||||
// if damage done higher than one
|
||||
if (kills > 1.0f && bots.getRoundMidTime () > game.time ()) {
|
||||
|
|
@ -3010,7 +2688,7 @@ bool Bot::isDeadlyMove (const Vector &to) {
|
|||
// this function eturns if given location would hurt Bot with falling damage
|
||||
|
||||
TraceResult tr {};
|
||||
const auto &direction = (to - pev->origin).normalize (); // 1 unit long
|
||||
const auto &direction = (to - pev->origin).normalize_apx (); // 1 unit long
|
||||
|
||||
Vector check = to, down = to;
|
||||
down.z -= 1000.0f; // straight down 1000 units
|
||||
|
|
@ -3022,13 +2700,13 @@ bool Bot::isDeadlyMove (const Vector &to) {
|
|||
}
|
||||
|
||||
float lastHeight = tr.flFraction * 1000.0f; // height from ground
|
||||
float distance = to.distance (check); // distance from goal
|
||||
float distance = to.distanceSq (check); // distance from goal
|
||||
|
||||
if (distance <= 30.0f && lastHeight > 150.0f) {
|
||||
if (distance <= cr::sqrf (30.0f) && lastHeight > 150.0f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (distance > 30.0f) {
|
||||
while (distance > cr::sqrf (30.0f)) {
|
||||
check = check - direction * 30.0f; // move 10 units closer to the goal...
|
||||
|
||||
down = check;
|
||||
|
|
@ -3047,7 +2725,7 @@ bool Bot::isDeadlyMove (const Vector &to) {
|
|||
return true;
|
||||
}
|
||||
lastHeight = height;
|
||||
distance = to.distance (check); // distance from goal
|
||||
distance = to.distanceSq (check); // distance from goal
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3116,9 +2794,7 @@ void Bot::changeYaw (float speed) {
|
|||
int Bot::getRandomCampDir () {
|
||||
// find a good node to look at when camping
|
||||
|
||||
if (m_currentNodeIndex == kInvalidNodeIndex) {
|
||||
changeNodeIndex (findNearestNode ());
|
||||
}
|
||||
ensureCurrentNodeIndex ();
|
||||
constexpr int kMaxNodesToSearch = 5;
|
||||
|
||||
int count = 0, indices[kMaxNodesToSearch] {};
|
||||
|
|
@ -3126,7 +2802,7 @@ int Bot::getRandomCampDir () {
|
|||
uint16_t visibility[kMaxNodesToSearch] {};
|
||||
|
||||
for (const auto &path : graph) {
|
||||
if (m_currentNodeIndex == path.number || !graph.isVisible (m_currentNodeIndex, path.number)) {
|
||||
if (m_currentNodeIndex == path.number || !vistab.visible (m_currentNodeIndex, path.number)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -3196,7 +2872,7 @@ void Bot::updateLookAngles () {
|
|||
float stiffness = 200.0f;
|
||||
float damping = 25.0f;
|
||||
|
||||
if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_isEnemyReachable) && m_difficulty > Difficulty::Normal) {
|
||||
if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) {
|
||||
if (m_difficulty == Difficulty::Expert) {
|
||||
accelerate += 600.0f;
|
||||
}
|
||||
|
|
@ -3355,7 +3031,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
|
|||
}
|
||||
|
||||
// do not check clients far away from us
|
||||
if (pev->origin.distanceSq (client.origin) > cr::square (320.0f)) {
|
||||
if (pev->origin.distanceSq (client.origin) > cr::sqrf (320.0f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -3364,7 +3040,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
|
|||
}
|
||||
auto length = client.origin.distanceSq (graph[index].origin);
|
||||
|
||||
if (length < cr::clamp (cr::square (graph[index].radius) * 2.0f, cr::square (40.0f), cr::square (90.0f))) {
|
||||
if (length < cr::clamp (cr::sqrf (graph[index].radius) * 2.0f, cr::sqrf (40.0f), cr::sqrf (90.0f))) {
|
||||
return true;
|
||||
}
|
||||
auto bot = bots[client.ent];
|
||||
|
|
@ -3417,7 +3093,7 @@ bool Bot::isReachableNode (int index) {
|
|||
const Vector &dst = graph[index].origin;
|
||||
|
||||
// is the destination close enough?
|
||||
if (dst.distanceSq (src) >= cr::square (600.0f)) {
|
||||
if (dst.distanceSq (src) >= cr::sqrf (600.0f)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -3473,3 +3149,100 @@ bool Bot::isBannedNode (int index) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Bot::findShortestPath (int srcIndex, int destIndex) {
|
||||
// this function finds the shortest path from source index to destination index
|
||||
|
||||
clearSearchNodes ();
|
||||
|
||||
m_chosenGoalIndex = srcIndex;
|
||||
m_goalValue = 0.0f;
|
||||
|
||||
bool success = planner.find (srcIndex, destIndex, [&] (int index) {
|
||||
m_pathWalk.add (index);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
m_prevGoalIndex = kInvalidNodeIndex;
|
||||
getTask ()->data = kInvalidNodeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) {
|
||||
// this function finds a path from srcIndex to destIndex
|
||||
|
||||
if (!graph.exists (srcIndex)) {
|
||||
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
|
||||
return;
|
||||
}
|
||||
else if (!graph.exists (destIndex)) {
|
||||
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
|
||||
return;
|
||||
}
|
||||
auto pathPlanner = planner.getAStar ();
|
||||
|
||||
// get correct calculation for heuristic
|
||||
if (pathType == FindPath::Optimal) {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
|
||||
pathPlanner->setG (Heuristic::gfunctionKillsDistCTWithHostage);
|
||||
}
|
||||
else {
|
||||
pathPlanner->setH (Heuristic::hfunctionPathDist);
|
||||
pathPlanner->setG (Heuristic::gfunctionKillsDist);
|
||||
}
|
||||
}
|
||||
else if (pathType == FindPath::Safe) {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
pathPlanner->setH (Heuristic::hfunctionNone);
|
||||
pathPlanner->setG (Heuristic::gfunctionKillsCTWithHostage);
|
||||
}
|
||||
else {
|
||||
pathPlanner->setH (Heuristic::hfunctionNone);
|
||||
pathPlanner->setG (Heuristic::gfunctionKills);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
|
||||
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
|
||||
pathPlanner->setG (Heuristic::gfunctionPathDistWithHostage);
|
||||
}
|
||||
else {
|
||||
pathPlanner->setH (Heuristic::hfunctionPathDist);
|
||||
pathPlanner->setG (Heuristic::gfunctionPathDist);
|
||||
}
|
||||
}
|
||||
clearSearchNodes ();
|
||||
|
||||
m_chosenGoalIndex = srcIndex;
|
||||
m_goalValue = 0.0f;
|
||||
|
||||
auto result = pathPlanner->find (m_team, srcIndex, destIndex, [&] (int index) {
|
||||
m_pathWalk.add (index);
|
||||
return true;
|
||||
});
|
||||
|
||||
// view the results
|
||||
switch (result) {
|
||||
case AStarResult::Success:
|
||||
m_pathWalk.reverse (); // reverse path for path follower
|
||||
break;
|
||||
|
||||
case AStarResult::InternalError:
|
||||
// bot should not roam when this occurs
|
||||
kick (); // kick the bot off...
|
||||
|
||||
logger.error ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ());
|
||||
break;
|
||||
|
||||
case AStarResult::Failed:
|
||||
// fallback to shortest path
|
||||
findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead
|
||||
|
||||
if (cv_debug.bool_ ()) {
|
||||
logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
531
src/planner.cpp
Normal file
531
src/planner.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "3", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
|
||||
ConVar cv_path_floyd_memory_limit ("yb_path_floyd_memory_limit", "3", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 12.0f);
|
||||
ConVar cv_path_dijkstra_simple_distance ("yb_path_dijkstra_simple_distance", "1", "Use simple distance path calculation instead of running full Dijkstra path cycle. Used only when Floyd matrices unavailable due to memory limit.");
|
||||
ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "1", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles.");
|
||||
|
||||
float Heuristic::gfunctionKillsDist (int team, int currentIndex, int parentIndex) {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
auto cost = static_cast <float> (practice.getDamage (team, currentIndex, currentIndex) + practice.getHighestDamageForTeam (team));
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (const auto &neighbour : current.links) {
|
||||
if (neighbour.index != kInvalidNodeIndex) {
|
||||
cost += static_cast <float> (practice.getDamage (team, neighbour.index, neighbour.index));
|
||||
}
|
||||
}
|
||||
|
||||
if (current.flags & NodeFlag::Crouch) {
|
||||
cost *= 1.5f;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
float Heuristic::gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex) {
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return kInfiniteHeuristic;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionKillsDist (team, currentIndex, parentIndex);
|
||||
}
|
||||
|
||||
float Heuristic::gfunctionKills (int team, int currentIndex, int) {
|
||||
auto cost = static_cast <float> (practice.getDamage (team, currentIndex, currentIndex));
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (const auto &neighbour : current.links) {
|
||||
if (neighbour.index != kInvalidNodeIndex) {
|
||||
cost += static_cast <float> (practice.getDamage (team, neighbour.index, neighbour.index));
|
||||
}
|
||||
}
|
||||
|
||||
if (current.flags & NodeFlag::Crouch) {
|
||||
cost *= 1.5f;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
auto Heuristic::gfunctionKillsCTWithHostage (int team, int currentIndex, int parentIndex) -> float {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return kInfiniteHeuristic;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionKills (team, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionKills (team, currentIndex, parentIndex);
|
||||
}
|
||||
|
||||
float Heuristic::gfunctionPathDist (int, int currentIndex, int parentIndex) {
|
||||
if (parentIndex == kInvalidNodeIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const auto &parent = graph[parentIndex];
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
for (const auto &link : parent.links) {
|
||||
if (link.index == currentIndex) {
|
||||
const auto distance = static_cast <float> (link.distance);
|
||||
|
||||
// we don't like ladder or crouch point
|
||||
if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return distance * 1.5f;
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
return kInfiniteHeuristic;
|
||||
}
|
||||
|
||||
float Heuristic::gfunctionPathDistWithHostage (int, int currentIndex, int parentIndex) {
|
||||
const auto ¤t = graph[currentIndex];
|
||||
|
||||
if (current.flags & NodeFlag::NoHostage) {
|
||||
return kInfiniteHeuristic;
|
||||
}
|
||||
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
|
||||
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f;
|
||||
}
|
||||
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex);
|
||||
}
|
||||
|
||||
float Heuristic::hfunctionPathDist (int index, int, int goalIndex) {
|
||||
const auto &start = graph[index];
|
||||
const auto &goal = graph[goalIndex];
|
||||
|
||||
const float x = start.origin.x - goal.origin.x;
|
||||
const float y = start.origin.y - goal.origin.y;
|
||||
const float z = start.origin.z - goal.origin.z;
|
||||
|
||||
switch (cv_path_heuristic_mode.int_ ()) {
|
||||
case 0:
|
||||
return cr::max (cr::max (cr::abs (x), cr::abs (y)), cr::abs (z)); // chebyshev distance
|
||||
|
||||
case 1:
|
||||
return cr::abs (x) + cr::abs (y) + cr::abs (z); // manhattan distance
|
||||
|
||||
case 2:
|
||||
return 0.0f; // no heuristic
|
||||
|
||||
case 3: {
|
||||
const float dx = cr::abs (x);
|
||||
const float dy = cr::abs (y);
|
||||
const float dz = cr::abs (z);
|
||||
|
||||
const float dmin = cr::min (cr::min (dx, dy), dz);
|
||||
const float dmax = cr::max (cr::max (dx, dy), dz);
|
||||
const float dmid = dx + dy + dz - dmin - dmax;
|
||||
|
||||
const float d1 = 1.0f;
|
||||
const float d2 = cr::sqrtf (2.0f);
|
||||
const float d3 = cr::sqrtf (3.0f);
|
||||
|
||||
return (d3 - d2) * dmin + (d2 - d1) * dmid + d1 * dmax; // diagonal distance
|
||||
}
|
||||
|
||||
default:
|
||||
case 4:
|
||||
return 10.0f * cr::sqrtf (cr::sqrf (x) + cr::sqrf (y) + cr::sqrf (z)); // euclidean distance
|
||||
}
|
||||
}
|
||||
|
||||
float Heuristic::hfunctionPathDistWithHostage (int index, int, int goalIndex) {
|
||||
if (graph[index].flags & NodeFlag::NoHostage) {
|
||||
return kInfiniteHeuristic;
|
||||
}
|
||||
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex);
|
||||
}
|
||||
|
||||
float Heuristic::hfunctionNone (int index, int, int goalIndex) {
|
||||
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f);
|
||||
}
|
||||
|
||||
void AStarAlgo::clearRoute () {
|
||||
m_routes.resize (static_cast <size_t> (m_length));
|
||||
|
||||
for (const auto &path : graph) {
|
||||
auto route = &m_routes[path.number];
|
||||
|
||||
route->g = route->f = 0.0f;
|
||||
route->parent = kInvalidNodeIndex;
|
||||
route->state = RouteState::New;
|
||||
}
|
||||
m_routes.clear ();
|
||||
}
|
||||
|
||||
bool AStarAlgo::cantSkipNode (const int a, const int b) {
|
||||
const auto &ag = graph[a];
|
||||
const auto &bg = graph[b];
|
||||
|
||||
const bool notVisible = !vistab.visible (ag.number, bg.number);
|
||||
|
||||
if (notVisible) {
|
||||
return true;
|
||||
}
|
||||
const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f;
|
||||
|
||||
if (tooHigh) {
|
||||
return true;
|
||||
}
|
||||
const bool tooNarrow = (ag.flags | bg.flags) & NodeFlag::Narrow;
|
||||
|
||||
if (tooNarrow) {
|
||||
return true;
|
||||
}
|
||||
const bool tooFar = ag.origin.distanceSq (bg.origin) > cr::sqrf (300.0f);
|
||||
|
||||
if (tooFar) {
|
||||
return true;
|
||||
}
|
||||
bool hasJumps = false;
|
||||
|
||||
for (int i = 0; i < kMaxNodeLinks; ++i) {
|
||||
if ((ag.links[i].flags | bg.links[i].flags) & PathFlag::Jump) {
|
||||
hasJumps = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasJumps;
|
||||
}
|
||||
|
||||
void AStarAlgo::postSmooth (NodeAdderFn onAddedNode) {
|
||||
int index = 0;
|
||||
m_smoothedPath.emplace (m_constructedPath.first ());
|
||||
|
||||
for (size_t i = 1; i < m_constructedPath.length () - 1; ++i) {
|
||||
if (cantSkipNode (m_smoothedPath[index], m_constructedPath[i + 1])) {
|
||||
++index;
|
||||
m_smoothedPath.emplace (m_constructedPath[i]);
|
||||
}
|
||||
}
|
||||
m_smoothedPath.emplace (m_constructedPath.last ());
|
||||
|
||||
for (const auto &spn : m_smoothedPath) {
|
||||
onAddedNode (spn);
|
||||
}
|
||||
m_constructedPath.clear ();
|
||||
m_smoothedPath.clear ();
|
||||
}
|
||||
|
||||
AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode) {
|
||||
if (m_length < kMaxNodeLinks) {
|
||||
return AStarResult::InternalError; // astar needs some nodes to work with
|
||||
}
|
||||
|
||||
clearRoute ();
|
||||
auto srcRoute = &m_routes[srcIndex];
|
||||
|
||||
// put start node into open list
|
||||
srcRoute->g = m_gcalc (botTeam, srcIndex, kInvalidNodeIndex);
|
||||
srcRoute->f = srcRoute->g + m_hcalc (srcIndex, kInvalidNodeIndex, destIndex);
|
||||
srcRoute->state = RouteState::Open;
|
||||
|
||||
m_routeQue.clear ();
|
||||
m_routeQue.emplace (srcIndex, srcRoute->g);
|
||||
|
||||
bool postSmoothPath = cv_path_astar_post_smooth.bool_ ();
|
||||
|
||||
while (!m_routeQue.empty ()) {
|
||||
// remove the first node from the open list
|
||||
int currentIndex = m_routeQue.pop ().index;
|
||||
|
||||
// safes us from bad graph...
|
||||
if (m_routeQue.length () >= getMaxLength () - 1) {
|
||||
return AStarResult::InternalError;
|
||||
}
|
||||
|
||||
// is the current node the goal node?
|
||||
if (currentIndex == destIndex) {
|
||||
// build the complete path
|
||||
do {
|
||||
if (postSmoothPath) {
|
||||
m_constructedPath.emplace (currentIndex);
|
||||
}
|
||||
else {
|
||||
onAddedNode (currentIndex);
|
||||
}
|
||||
currentIndex = m_routes[currentIndex].parent;
|
||||
} while (currentIndex != kInvalidNodeIndex);
|
||||
|
||||
// do a post-smooth if requested
|
||||
if (postSmoothPath) {
|
||||
postSmooth (onAddedNode);
|
||||
}
|
||||
return AStarResult::Success;
|
||||
}
|
||||
auto curRoute = &m_routes[currentIndex];
|
||||
|
||||
if (curRoute->state != RouteState::Open) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// put current node into CLOSED list
|
||||
curRoute->state = RouteState::Closed;
|
||||
|
||||
// now expand the current node
|
||||
for (const auto &child : graph[currentIndex].links) {
|
||||
if (child.index == kInvalidNodeIndex) {
|
||||
continue;
|
||||
}
|
||||
auto childRoute = &m_routes[child.index];
|
||||
|
||||
// calculate the F value as F = G + H
|
||||
const float g = curRoute->g + m_gcalc (botTeam, child.index, currentIndex);
|
||||
const float h = m_hcalc (child.index, kInvalidNodeIndex, destIndex);
|
||||
const float f = g + h;
|
||||
|
||||
if (childRoute->state == RouteState::New || childRoute->f > f) {
|
||||
// put the current child into open list
|
||||
childRoute->parent = currentIndex;
|
||||
childRoute->state = RouteState::Open;
|
||||
|
||||
childRoute->g = g;
|
||||
childRoute->f = f;
|
||||
|
||||
m_routeQue.emplace (child.index, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
return AStarResult::Failed;
|
||||
}
|
||||
|
||||
void FloydWarshallAlgo::rebuild () {
|
||||
m_length = graph.length ();
|
||||
m_matrix.resize (cr::sqrf (m_length));
|
||||
|
||||
auto matrix = m_matrix.data ();
|
||||
|
||||
// re-initialize matrix every load
|
||||
for (int i = 0; i < m_length; ++i) {
|
||||
for (int j = 0; j < m_length; ++j) {
|
||||
*(matrix + (i * m_length) + j) = { kInvalidNodeIndex, SHRT_MAX };
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_length; ++i) {
|
||||
for (const auto &link : graph[i].links) {
|
||||
if (!graph.exists (link.index)) {
|
||||
continue;
|
||||
}
|
||||
*(matrix + (i * m_length) + link.index) = { link.index, link.distance };
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_length; ++i) {
|
||||
(matrix + (i * m_length) + i)->dist = 0;
|
||||
}
|
||||
|
||||
for (int k = 0; k < m_length; ++k) {
|
||||
for (int i = 0; i < m_length; ++i) {
|
||||
for (int j = 0; j < m_length; ++j) {
|
||||
int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist;
|
||||
|
||||
if (distance < (matrix + (i * m_length) + j)->dist) {
|
||||
*(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
save (); // save path matrix to file for faster access
|
||||
}
|
||||
|
||||
bool FloydWarshallAlgo::load () {
|
||||
m_length = graph.length ();
|
||||
|
||||
if (!m_length) {
|
||||
return false;
|
||||
}
|
||||
bool dataLoaded = bstor.load <Matrix> (m_matrix);
|
||||
|
||||
// do not rebuild if loaded
|
||||
if (dataLoaded) {
|
||||
return true;
|
||||
}
|
||||
rebuild (); // rebuilds matrix
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FloydWarshallAlgo::save () {
|
||||
if (!m_length) {
|
||||
return;
|
||||
}
|
||||
bstor.save <Matrix> (m_matrix);
|
||||
}
|
||||
|
||||
bool FloydWarshallAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
|
||||
onAddedNode (srcIndex);
|
||||
|
||||
while (srcIndex != destIndex) {
|
||||
srcIndex = (m_matrix.data () + (srcIndex * m_length) + destIndex)->index;
|
||||
|
||||
if (srcIndex < 0) {
|
||||
// only fill path distance on full path
|
||||
if (pathDistance != nullptr) {
|
||||
*pathDistance = dist (srcIndex, destIndex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!onAddedNode (srcIndex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DijkstraAlgo::init (const int length) {
|
||||
m_length = length;
|
||||
|
||||
m_distance.resize (length);
|
||||
m_parent.resize (length);
|
||||
}
|
||||
|
||||
void DijkstraAlgo::resetState () {
|
||||
m_queue.clear ();
|
||||
|
||||
m_parent.fill (kInvalidNodeIndex);
|
||||
m_distance.fill (kInfiniteDistanceLong);
|
||||
}
|
||||
|
||||
bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
|
||||
resetState ();
|
||||
|
||||
m_queue.emplace (0, srcIndex);
|
||||
m_distance[srcIndex] = 0;
|
||||
|
||||
while (!m_queue.empty ()) {
|
||||
auto p = cr::move (m_queue.pop ());
|
||||
auto current = p.second;
|
||||
|
||||
// finished search
|
||||
if (current == destIndex) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_distance[current] != p.first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto &link : graph[current].links) {
|
||||
if (link.index != kInvalidNodeIndex) {
|
||||
auto dlink = m_distance[current] + link.distance;
|
||||
|
||||
if (dlink < m_distance[link.index]) {
|
||||
m_distance[link.index] = dlink;
|
||||
m_parent[link.index] = current;
|
||||
|
||||
m_queue.emplace (m_distance[link.index], link.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static SmallArray <int> pathInReverse;
|
||||
pathInReverse.clear ();
|
||||
|
||||
for (int i = destIndex; i != kInvalidNodeIndex; i = m_parent[i]) {
|
||||
pathInReverse.emplace (i);
|
||||
}
|
||||
pathInReverse.reverse ();
|
||||
|
||||
for (const auto &node : pathInReverse) {
|
||||
if (!onAddedNode (node)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// always fill path distance if we're need to
|
||||
if (pathDistance != nullptr) {
|
||||
*pathDistance = m_distance[destIndex];
|
||||
}
|
||||
return m_distance[destIndex] < kInfiniteDistanceLong;
|
||||
}
|
||||
|
||||
int DijkstraAlgo::dist (int srcIndex, int destIndex) {
|
||||
int pathDistance = 0;
|
||||
|
||||
find (srcIndex, destIndex, [&] (int) {
|
||||
return true;
|
||||
}, &pathDistance);
|
||||
|
||||
return pathDistance;
|
||||
}
|
||||
|
||||
PathPlanner::PathPlanner () {
|
||||
m_dijkstra = cr::makeUnique <DijkstraAlgo> ();
|
||||
m_floyd = cr::makeUnique <FloydWarshallAlgo> ();
|
||||
m_astar = cr::makeUnique <AStarAlgo> ();
|
||||
}
|
||||
|
||||
void PathPlanner::init () {
|
||||
const int length = graph.length ();
|
||||
|
||||
const float limitInMb = cv_path_floyd_memory_limit.float_ ();
|
||||
const float memoryUse = static_cast <float> (sizeof (FloydWarshallAlgo::Matrix) * cr::sqrf (length) / 1024 / 1024);
|
||||
|
||||
// if we're have too much memory for floyd matrices, planner will use dijkstra or uniform planner for other than pathfinding needs
|
||||
if (memoryUse > limitInMb) {
|
||||
m_memoryLimitHit = true;
|
||||
}
|
||||
m_dijkstra->init (length);
|
||||
m_astar->init (length);
|
||||
|
||||
// load (re-create) floyds, if we're not hitting memory limits
|
||||
if (!m_memoryLimitHit) {
|
||||
m_floyd->load ();
|
||||
}
|
||||
}
|
||||
|
||||
bool PathPlanner::hasRealPathDistance () const {
|
||||
return !m_memoryLimitHit || !cv_path_dijkstra_simple_distance.bool_ ();
|
||||
}
|
||||
|
||||
bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
|
||||
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// limit hit, use dijkstra
|
||||
if (m_memoryLimitHit) {
|
||||
return m_dijkstra->find (srcIndex, destIndex, onAddedNode, pathDistance);
|
||||
}
|
||||
return m_floyd->find (srcIndex, destIndex, onAddedNode, pathDistance);;
|
||||
}
|
||||
|
||||
int PathPlanner::dist (int srcIndex, int destIndex) {
|
||||
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
|
||||
return kInfiniteDistanceLong;
|
||||
}
|
||||
|
||||
if (srcIndex == destIndex) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// limit hit, use dijkstra
|
||||
if (m_memoryLimitHit) {
|
||||
if (cv_path_dijkstra_simple_distance.bool_ ()) {
|
||||
return static_cast <int> (graph[srcIndex].origin.distance2d (graph[destIndex].origin));
|
||||
}
|
||||
return m_dijkstra->dist (srcIndex, destIndex);
|
||||
}
|
||||
return m_floyd->dist (srcIndex, destIndex);
|
||||
}
|
||||
157
src/practice.cpp
Normal file
157
src/practice.cpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
int32_t BotPractice::getIndex (int32_t team, int32_t start, int32_t goal) {
|
||||
if (!exists (team, start, goal)) {
|
||||
return kInvalidNodeIndex;
|
||||
}
|
||||
return m_data[{start, goal, team}].index;
|
||||
}
|
||||
|
||||
void BotPractice::setIndex (int32_t team, int32_t start, int32_t goal, int32_t value) {
|
||||
if (team != Team::Terrorist && team != Team::CT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reliability check
|
||||
if (!graph.exists (start) || !graph.exists (goal) || !graph.exists (value)) {
|
||||
return;
|
||||
}
|
||||
m_data[{start, goal, team}].index = static_cast <int16_t> (value);
|
||||
}
|
||||
|
||||
int32_t BotPractice::getValue (int32_t team, int32_t start, int32_t goal) {
|
||||
if (!exists (team, start, goal)) {
|
||||
return 0;
|
||||
}
|
||||
return m_data[{start, goal, team}].value;
|
||||
}
|
||||
|
||||
void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t value) {
|
||||
if (team != Team::Terrorist && team != Team::CT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reliability check
|
||||
if (!graph.exists (start) || !graph.exists (goal)) {
|
||||
return;
|
||||
}
|
||||
m_data[{start, goal, team}].value = static_cast <int16_t> (value);
|
||||
}
|
||||
|
||||
int32_t BotPractice::getDamage (int32_t team, int32_t start, int32_t goal) {
|
||||
if (!exists (team, start, goal)) {
|
||||
return 0;
|
||||
}
|
||||
return m_data[{start, goal, team}].damage;
|
||||
}
|
||||
|
||||
void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t value) {
|
||||
if (team != Team::Terrorist && team != Team::CT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reliability check
|
||||
if (!graph.exists (start) || !graph.exists (goal)) {
|
||||
return;
|
||||
}
|
||||
m_data[{start, goal, team}].damage = static_cast <int16_t> (value);
|
||||
}
|
||||
|
||||
void BotPractice::update () {
|
||||
// this function called after each end of the round to update knowledge about most dangerous nodes for each team.
|
||||
|
||||
auto graphLength = graph.length ();
|
||||
|
||||
// no nodes, no practice used or nodes edited or being edited?
|
||||
if (!graphLength || graph.hasChanged () || !vistab.isReady ()) {
|
||||
return; // no action
|
||||
}
|
||||
auto adjustValues = false;
|
||||
|
||||
// get the most dangerous node for this position for both teams
|
||||
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
|
||||
auto bestIndex = kInvalidNodeIndex; // best index to store
|
||||
|
||||
for (int i = 0; i < graphLength; ++i) {
|
||||
auto maxDamage = 0;
|
||||
bestIndex = kInvalidNodeIndex;
|
||||
|
||||
for (int j = 0; j < graphLength; ++j) {
|
||||
if (i == j || !vistab.visible (i, j) || !exists (team, i, j)) {
|
||||
continue;
|
||||
}
|
||||
auto actDamage = getDamage (team, i, j);
|
||||
|
||||
if (actDamage > maxDamage) {
|
||||
maxDamage = actDamage;
|
||||
bestIndex = j;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDamage > PracticeLimit::Damage) {
|
||||
adjustValues = true;
|
||||
}
|
||||
setIndex (team, i, i, bestIndex);
|
||||
}
|
||||
}
|
||||
constexpr auto kFullDamageVal = static_cast <int32_t> (PracticeLimit::Damage);
|
||||
constexpr auto kHalfDamageVal = static_cast <int32_t> (PracticeLimit::Damage / 2);
|
||||
|
||||
// adjust values if overflow is about to happen
|
||||
if (adjustValues) {
|
||||
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
|
||||
for (int i = 0; i < graphLength; ++i) {
|
||||
for (int j = 0; j < graphLength; ++j) {
|
||||
if (i == j || !exists (team, i, j)) {
|
||||
continue;
|
||||
}
|
||||
setDamage (team, i, j, cr::clamp (getDamage (team, i, j) - kHalfDamageVal, 0, kFullDamageVal));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
|
||||
m_teamHighestDamage[team] = cr::clamp (m_teamHighestDamage[team] - kHalfDamageVal, 1, kFullDamageVal);
|
||||
}
|
||||
}
|
||||
|
||||
void BotPractice::save () {
|
||||
if (!graph.length () || graph.hasChanged ()) {
|
||||
return; // no action
|
||||
}
|
||||
SmallArray <DangerSaveRestore> data;
|
||||
|
||||
// copy hash-map data to our vector
|
||||
m_data.foreach ([&data] (const DangerStorage &ds, const PracticeData &pd) {
|
||||
data.emplace (ds, pd);
|
||||
});
|
||||
bstor.save <DangerSaveRestore> (data);
|
||||
}
|
||||
|
||||
void BotPractice::load () {
|
||||
if (!graph.length ()) {
|
||||
return; // no action
|
||||
}
|
||||
SmallArray <DangerSaveRestore> data;
|
||||
m_data.clear ();
|
||||
|
||||
bool dataLoaded = bstor.load <DangerSaveRestore> (data);
|
||||
|
||||
// copy back to hash table
|
||||
if (dataLoaded) {
|
||||
for (const auto &dsr : data) {
|
||||
if (dsr.data.damage > 0 || dsr.data.index != kInvalidNodeIndex || dsr.data.value > 0) {
|
||||
m_data.insert (dsr.danger, dsr.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
394
src/storage.cpp
Normal file
394
src/storage.cpp
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
#if defined (BOT_STORAGE_EXPLICIT_INSTANTIATIONS)
|
||||
|
||||
template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *exten, int32_t *outOptions) {
|
||||
auto type = guessType <U> ();
|
||||
String filename = buildPath (storageToBotFile (type.option), true);
|
||||
|
||||
extern ConVar cv_debug, cv_graph_url;
|
||||
|
||||
// graphs can be downloaded...
|
||||
auto isGraph = !!(type.option & StorageOption::Graph);
|
||||
auto isDebug = cv_debug.bool_ ();
|
||||
|
||||
MemFile file (filename); // open the file
|
||||
data.clear ();
|
||||
|
||||
// resize data to fit the stuff
|
||||
auto resizeData = [&] (const size_t length) {
|
||||
data.resize (length); // for non-graph data the graph should be already loaded
|
||||
data.shrink (); // free up memory to minimum
|
||||
|
||||
// ensure we're have enough memory to decompress the data
|
||||
data.ensure (length + ULZ::Excess);
|
||||
};
|
||||
|
||||
// if graph & attempted to load multiple times, bail out, we're failed
|
||||
if (isGraph && ++m_retries > 2) {
|
||||
resetRetries ();
|
||||
|
||||
return error (isGraph, isDebug, file, "Unable to load %s (filename: '%s'). Download process has failed as well. No nodes has been found.", type.name, filename);
|
||||
}
|
||||
|
||||
// downloader for graph
|
||||
auto download = [&] () -> bool {
|
||||
if (!graph.canDownload ()) {
|
||||
return false;
|
||||
}
|
||||
auto downloadAddress = cv_graph_url.str ();
|
||||
|
||||
auto toDownload = buildPath (storageToBotFile (type.option), false);
|
||||
auto fromDownload = strings.format ("http://%s/graph/%s.graph", downloadAddress, game.getMapName ());
|
||||
|
||||
// try to download
|
||||
if (http.downloadFile (fromDownload, toDownload)) {
|
||||
ctrl.msg ("%s file '%s' successfully downloaded. Processing...", type.name, filename);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
ctrl.msg ("Can't download '%s' from '%s' to '%s'... (%d).", filename, fromDownload, toDownload, http.getLastStatusCode ());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// tries to reload or open pwf file
|
||||
auto tryReload = [&] () -> bool {
|
||||
file.close ();
|
||||
|
||||
if (!isGraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (download ()) {
|
||||
return load (data, exten, outOptions);
|
||||
}
|
||||
|
||||
if (graph.convertOldFormat ()) {
|
||||
return load (data, exten, outOptions);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// no open no fun
|
||||
if (!file) {
|
||||
if (tryReload ()) {
|
||||
return true;
|
||||
}
|
||||
return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename);
|
||||
}
|
||||
|
||||
// read the header
|
||||
StorageHeader hdr {};
|
||||
file.read (&hdr, sizeof (StorageHeader));
|
||||
|
||||
// check the magic
|
||||
if (hdr.magic != kStorageMagic && hdr.magic != kStorageMagicUB) {
|
||||
if (tryReload ()) {
|
||||
return true;
|
||||
}
|
||||
return error (isGraph, isDebug, file, "Unable to read magic of %s (filename: '%s').", type.name, filename);
|
||||
}
|
||||
|
||||
// check the path-numbers
|
||||
if (!isGraph && hdr.length != graph.length ()) {
|
||||
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Mismatch number of nodes (got: '%d', need: '%d').", type.name, filename, hdr.length, graph.length ());
|
||||
}
|
||||
|
||||
// check the count
|
||||
if (hdr.length == 0 || hdr.length > kMaxNodes || hdr.length < kMaxNodeLinks) {
|
||||
if (tryReload ()) {
|
||||
return true;
|
||||
}
|
||||
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Paths length is overflowed (got: '%d').", type.name, filename, hdr.length);
|
||||
}
|
||||
|
||||
// check the version
|
||||
if (hdr.version > type.version && isGraph) {
|
||||
ctrl.msg ("Graph version mismatch %s (filename: '%s'). Version number differs (got: '%d', need: '%d') Please, upgrade %s.", type.name, filename, hdr.version, type.version, product.name);
|
||||
}
|
||||
else if (hdr.version != type.version && !isGraph) {
|
||||
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Version number differs (got: '%d', need: '%d').", type.name, filename, hdr.version, type.version);
|
||||
}
|
||||
|
||||
// save graph version
|
||||
if (isGraph) {
|
||||
graph.setGraphHeader (&hdr);
|
||||
}
|
||||
|
||||
// check the storage type
|
||||
if ((hdr.options & type.option) != type.option) {
|
||||
return error (isGraph, isDebug, file, "Incorrect storage format for %s (filename: '%s').", type.name, filename);
|
||||
}
|
||||
auto compressedSize = static_cast <size_t> (hdr.compressed);
|
||||
auto numberNodes = static_cast <size_t> (hdr.length);
|
||||
|
||||
SmallArray <uint8_t> compressed (compressedSize + sizeof (uint8_t) * ULZ::Excess);
|
||||
|
||||
// graph is not resized upon load
|
||||
if (isGraph) {
|
||||
resizeData (numberNodes);
|
||||
}
|
||||
else {
|
||||
resizeData (hdr.uncompressed / sizeof (U));
|
||||
}
|
||||
|
||||
// read compressed data
|
||||
if (file.read (compressed.data (), sizeof (uint8_t), compressedSize) == compressedSize) {
|
||||
|
||||
// try to uncompress
|
||||
if (ulz.uncompress (compressed.data (), hdr.compressed, reinterpret_cast <uint8_t *> (data.data ()), hdr.uncompressed) == ULZ::UncompressFailure) {
|
||||
return error (isGraph, isDebug, file, "Unable to decompress ULZ data for %s (filename: '%s').", type.name, filename);
|
||||
}
|
||||
else {
|
||||
|
||||
if (outOptions) {
|
||||
outOptions = &hdr.options;
|
||||
}
|
||||
|
||||
// author of graph.. save
|
||||
if ((hdr.options & StorageOption::Exten) && exten != nullptr) {
|
||||
auto extenSize = sizeof (ExtenHeader);
|
||||
auto actuallyRead = file.read (exten, extenSize) * extenSize;
|
||||
|
||||
if (isGraph) {
|
||||
resetRetries ();
|
||||
|
||||
ExtenHeader extenHeader;
|
||||
strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author));
|
||||
|
||||
if (extenSize <= actuallyRead) {
|
||||
// write modified by, only if the name is different
|
||||
if (!strings.isEmpty (extenHeader.author) && strncmp (extenHeader.author, exten->modified, cr::bufsize (extenHeader.author)) != 0) {
|
||||
strings.copy (extenHeader.modified, exten->modified, cr::bufsize (exten->modified));
|
||||
}
|
||||
}
|
||||
else {
|
||||
strings.copy (extenHeader.modified, "(none)", cr::bufsize (exten->modified));
|
||||
}
|
||||
extenHeader.mapSize = exten->mapSize;
|
||||
|
||||
// tell graph about exten header
|
||||
graph.setExtenHeader (&extenHeader);
|
||||
}
|
||||
}
|
||||
ctrl.msg ("Loaded Bots %s data v%d (Memory: %.2fMB).", type.name, hdr.version, static_cast <float> (data.capacity () * sizeof (U)) / 1024.0f / 1024.0f);
|
||||
file.close ();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return error (isGraph, isDebug, file, "Unable to read ULZ data for %s (filename: '%s').", type.name, filename);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename U> bool BotStorage::save (const SmallArray <U> &data, ExtenHeader *exten, int32_t passOptions) {
|
||||
auto type = guessType <U> ();
|
||||
|
||||
// append additional options
|
||||
if (passOptions != 0) {
|
||||
type.option |= passOptions;
|
||||
}
|
||||
auto isGraph = !!(type.option & StorageOption::Graph);
|
||||
|
||||
// do not allow to save graph with less than 8 nodes
|
||||
if (isGraph && graph.length () < kMaxNodeLinks) {
|
||||
ctrl.msg ("Can't save graph data with less than %d nodes. Please add some more before saving.", kMaxNodeLinks);
|
||||
return false;
|
||||
}
|
||||
String filename = buildPath (storageToBotFile (type.option));
|
||||
|
||||
if (data.empty ()) {
|
||||
logger.error ("Unable to save %s file. Empty data. (filename: '%s').", type.name, filename);
|
||||
return false;
|
||||
}
|
||||
else if (isGraph) {
|
||||
for (auto &path : graph) {
|
||||
path.display = 0.0f;
|
||||
path.light = illum.getLightLevel (path.origin);
|
||||
}
|
||||
}
|
||||
|
||||
// open the file
|
||||
File file (filename, "wb");
|
||||
|
||||
// no open no fun
|
||||
if (!file) {
|
||||
logger.error ("Unable to open %s file for writing (filename: '%s').", type.name, filename);
|
||||
return false;
|
||||
}
|
||||
auto rawLength = data.length () * sizeof (U);
|
||||
SmallArray <uint8_t> compressed (rawLength + sizeof (uint8_t) * ULZ::Excess);
|
||||
|
||||
// try to compress
|
||||
auto compressedLength = static_cast <size_t> (ulz.compress (reinterpret_cast <uint8_t *> (data.data ()), static_cast <int32_t> (rawLength), reinterpret_cast <uint8_t *> (compressed.data ())));
|
||||
|
||||
if (compressedLength > 0) {
|
||||
StorageHeader hdr {};
|
||||
|
||||
hdr.magic = kStorageMagic;
|
||||
hdr.version = type.version;
|
||||
hdr.options = type.option;
|
||||
hdr.length = graph.length ();
|
||||
hdr.compressed = static_cast <int32_t> (compressedLength);
|
||||
hdr.uncompressed = static_cast <int32_t> (rawLength);
|
||||
|
||||
file.write (&hdr, sizeof (StorageHeader));
|
||||
file.write (compressed.data (), sizeof (uint8_t), compressedLength);
|
||||
|
||||
// add extension
|
||||
if ((type.option & StorageOption::Exten) && exten != nullptr) {
|
||||
file.write (exten, sizeof (ExtenHeader));
|
||||
}
|
||||
extern ConVar cv_debug;
|
||||
|
||||
// notify only about graph
|
||||
if (isGraph || cv_debug.bool_ ()) {
|
||||
ctrl.msg ("Successfully saved Bots %s data.", type.name);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error ("Unable to compress %s data (filename: '%s').", type.name, filename);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename ...Args> bool BotStorage::error (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args) {
|
||||
auto result = strings.format (fmt, cr::forward <Args> (args)...);
|
||||
|
||||
// display error only for graph file
|
||||
if (isGraph || isDebug) {
|
||||
logger.error (result);
|
||||
}
|
||||
|
||||
// if graph reset paths
|
||||
if (isGraph) {
|
||||
bots.kickEveryone (true);
|
||||
graph.reset ();
|
||||
}
|
||||
file.close ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename U> BotStorage::SaveLoadData BotStorage::guessType () {
|
||||
if constexpr (cr::is_same <U, FloydWarshallAlgo::Matrix>::value) {
|
||||
return { "Pathmatrix", StorageOption::Matrix, StorageVersion::Matrix };
|
||||
}
|
||||
else if constexpr (cr::is_same <U, BotPractice::DangerSaveRestore>::value) {
|
||||
return { "Practice", StorageOption::Practice, StorageVersion::Practice };
|
||||
}
|
||||
else if constexpr (cr::is_same <U, GraphVistable::VisStorage>::value) {
|
||||
return { "Vistable", StorageOption::Vistable, StorageVersion::Vistable };
|
||||
}
|
||||
else if constexpr (cr::is_same <U, Path>::value) {
|
||||
return { "Graph", StorageOption::Graph, StorageVersion::Graph };
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
String BotStorage::buildPath (int32_t file, bool isMemoryLoad) {
|
||||
using FilePath = Twin <String, String>;
|
||||
|
||||
static HashMap <int32_t, FilePath> paths = {
|
||||
{ BotFile::Vistable, FilePath ("train", "vis")},
|
||||
{ BotFile::Practice, FilePath ("train", "prc")},
|
||||
{ BotFile::Pathmatrix, FilePath ("train", "pmx")},
|
||||
{ BotFile::LogFile, FilePath ("logs", "txt")},
|
||||
{ BotFile::Graph, FilePath ("graph", "graph")},
|
||||
{ BotFile::PodbotPWF, FilePath ("pwf", "pwf")},
|
||||
{ BotFile::EbotEWP, FilePath ("ewp", "ewp")},
|
||||
};
|
||||
|
||||
static StringArray path;
|
||||
path.clear ();
|
||||
|
||||
// if not memory file we're don't need game dir
|
||||
if (!isMemoryLoad) {
|
||||
path.emplace (game.getRunningModName ());
|
||||
}
|
||||
|
||||
// allways append addons/product
|
||||
path.emplace ("addons");
|
||||
path.emplace (product.folder);
|
||||
|
||||
// the datadir
|
||||
path.emplace ("data");
|
||||
|
||||
// append real filepath
|
||||
path.emplace (paths[file].first);
|
||||
|
||||
// if file is logfile use correct logfile name with date
|
||||
if (file == BotFile::LogFile) {
|
||||
time_t ticks = time (&ticks);
|
||||
tm timeinfo {};
|
||||
|
||||
plat.loctime (&timeinfo, &ticks);
|
||||
auto timebuf = strings.chars ();
|
||||
|
||||
strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo);
|
||||
path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second));
|
||||
}
|
||||
else {
|
||||
String mapName = game.getMapName ();
|
||||
path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second));
|
||||
}
|
||||
|
||||
// finally use correct path separarators for us
|
||||
return String::join (path, PATH_SEP);
|
||||
}
|
||||
|
||||
int32_t BotStorage::storageToBotFile (int32_t options) {
|
||||
// converts storage option to stroage filename
|
||||
|
||||
if (options & StorageOption::Graph) {
|
||||
return BotFile::Graph;
|
||||
}
|
||||
else if (options & StorageOption::Matrix) {
|
||||
return BotFile::Pathmatrix;
|
||||
}
|
||||
else if (options & StorageOption::Vistable) {
|
||||
return BotFile::Vistable;
|
||||
}
|
||||
else if (options & StorageOption::Practice) {
|
||||
return BotFile::Practice;
|
||||
}
|
||||
return BotFile::Graph;
|
||||
}
|
||||
|
||||
void BotStorage::unlinkFromDisk () {
|
||||
// this function removes graph file from the hard disk
|
||||
|
||||
StringArray unlinkable;
|
||||
bots.kickEveryone (true);
|
||||
|
||||
// if we're delete graph, delete all corresponding to it files
|
||||
unlinkable.emplace (buildPath (BotFile::Graph)); // graph itself
|
||||
unlinkable.emplace (buildPath (BotFile::Practice)); // corresponding to practice
|
||||
unlinkable.emplace (buildPath (BotFile::Vistable)); // corresponding to vistable
|
||||
unlinkable.emplace (buildPath (BotFile::Pathmatrix)); // corresponding to matrix
|
||||
|
||||
for (const auto &item : unlinkable) {
|
||||
if (File::exists (item)) {
|
||||
plat.removeFile (item.chars ());
|
||||
ctrl.msg ("File %s, has been deleted from the hard disk", item);
|
||||
}
|
||||
else {
|
||||
logger.error ("Unable to open %s", item);
|
||||
}
|
||||
}
|
||||
graph.reset (); // re-intialize points
|
||||
}
|
||||
|
||||
#endif // BOT_STORAGE_EXPLICIT_INSTANTIATIONS
|
||||
|
|
@ -538,75 +538,6 @@ String BotSupport::getCurrentDateTime () {
|
|||
return String (timebuf);
|
||||
}
|
||||
|
||||
String BotSupport::buildPath (int32_t file, bool isMemoryLoad) {
|
||||
using FilePath = Twin <String, String>;
|
||||
|
||||
static HashMap <int32_t, FilePath> paths = {
|
||||
{ BotFile::Vistable, FilePath ("train", "vis")},
|
||||
{ BotFile::Practice, FilePath ("train", "prc")},
|
||||
{ BotFile::Pathmatrix, FilePath ("train", "pmx")},
|
||||
{ BotFile::LogFile, FilePath ("logs", "txt")},
|
||||
{ BotFile::Graph, FilePath ("graph", "graph")},
|
||||
{ BotFile::PodbotPWF, FilePath ("pwf", "pwf")},
|
||||
{ BotFile::EbotEWP, FilePath ("ewp", "ewp")},
|
||||
};
|
||||
|
||||
static StringArray path;
|
||||
path.clear ();
|
||||
|
||||
// if not memory file we're don't need game dir
|
||||
if (!isMemoryLoad) {
|
||||
path.emplace (game.getRunningModName ());
|
||||
}
|
||||
|
||||
// allways append addons/product
|
||||
path.emplace ("addons");
|
||||
path.emplace (product.folder);
|
||||
|
||||
// the datadir
|
||||
path.emplace ("data");
|
||||
|
||||
// append real filepath
|
||||
path.emplace (paths[file].first);
|
||||
|
||||
// if file is logfile use correct logfile name with date
|
||||
if (file == BotFile::LogFile) {
|
||||
time_t ticks = time (&ticks);
|
||||
tm timeinfo {};
|
||||
|
||||
plat.loctime (&timeinfo, &ticks);
|
||||
auto timebuf = strings.chars ();
|
||||
|
||||
strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo);
|
||||
path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second));
|
||||
}
|
||||
else {
|
||||
String mapName = game.getMapName ();
|
||||
path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second));
|
||||
}
|
||||
|
||||
// finally use correct path separarators for us
|
||||
return String::join (path, plat.win ? "\\" : "/");
|
||||
}
|
||||
|
||||
// converts storage option to stroage filename
|
||||
|
||||
int32_t BotSupport::storageToBotFile (StorageOption options) {
|
||||
if (options & StorageOption::Graph) {
|
||||
return BotFile::Graph;
|
||||
}
|
||||
else if (options & StorageOption::Matrix) {
|
||||
return BotFile::Pathmatrix;
|
||||
}
|
||||
else if (options & StorageOption::Vistable) {
|
||||
return BotFile::Vistable;
|
||||
}
|
||||
else if (options & StorageOption::Practice) {
|
||||
return BotFile::Practice;
|
||||
}
|
||||
return BotFile::Graph;
|
||||
}
|
||||
|
||||
int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) {
|
||||
const auto send = [&] (const Twin <const uint8_t *, size_t> &msg) -> int32_t {
|
||||
return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength);
|
||||
|
|
|
|||
178
src/vistable.cpp
Normal file
178
src/vistable.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
||||
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
#include <yapb.h>
|
||||
|
||||
|
||||
void GraphVistable::rebuild () {
|
||||
if (!m_rebuild) {
|
||||
return;
|
||||
}
|
||||
m_length = graph.length ();
|
||||
|
||||
TraceResult tr {};
|
||||
uint8_t res, shift;
|
||||
|
||||
if (!graph.exists (m_sliceIndex)) {
|
||||
m_sliceIndex = 0;
|
||||
}
|
||||
|
||||
if (!graph.exists (m_curIndex)) {
|
||||
m_curIndex = 0;
|
||||
}
|
||||
const auto &vis = graph[m_curIndex];
|
||||
|
||||
auto sourceCrouch = vis.origin;
|
||||
auto sourceStand = vis.origin;
|
||||
|
||||
if (vis.flags & NodeFlag::Crouch) {
|
||||
sourceCrouch.z += 12.0f;
|
||||
sourceStand.z += 18.0f + 28.0f;
|
||||
}
|
||||
else {
|
||||
sourceCrouch.z += -18.0f + 12.0f;
|
||||
sourceStand.z += 28.0f;
|
||||
}
|
||||
auto end = m_sliceIndex + rg.get (250, 400);
|
||||
|
||||
if (end > m_length) {
|
||||
end = m_length;
|
||||
}
|
||||
uint16_t standCount = 0, crouchCount = 0;
|
||||
|
||||
for (int i = m_sliceIndex; i < end; ++i) {
|
||||
const auto &path = graph[i];
|
||||
|
||||
// first check ducked visibility
|
||||
Vector dest = path.origin;
|
||||
|
||||
game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr);
|
||||
|
||||
// check if line of sight to object is not blocked (i.e. visible)
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
res = VisIndex::Stand;
|
||||
}
|
||||
else {
|
||||
res = VisIndex::None;
|
||||
}
|
||||
res <<= 1;
|
||||
|
||||
game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr);
|
||||
|
||||
// check if line of sight to object is not blocked (i.e. visible)
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
res |= VisIndex::Stand;
|
||||
}
|
||||
|
||||
if (res != VisIndex::None) {
|
||||
dest = path.origin;
|
||||
|
||||
// first check ducked visibility
|
||||
if (path.flags & NodeFlag::Crouch) {
|
||||
dest.z += 18.0f + 28.0f;
|
||||
}
|
||||
else {
|
||||
dest.z += 28.0f;
|
||||
}
|
||||
game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr);
|
||||
|
||||
// check if line of sight to object is not blocked (i.e. visible)
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
res |= VisIndex::Crouch;;
|
||||
}
|
||||
else {
|
||||
res &= VisIndex::Stand;;
|
||||
}
|
||||
game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr);
|
||||
|
||||
// check if line of sight to object is not blocked (i.e. visible)
|
||||
if (!cr::fequal (tr.flFraction, 1.0f)) {
|
||||
res |= VisIndex::Stand;
|
||||
}
|
||||
else {
|
||||
res &= VisIndex::Crouch;;
|
||||
}
|
||||
}
|
||||
shift = static_cast <uint8_t> ((path.number % 4) << 1);
|
||||
|
||||
m_vistable[vis.number * m_length + path.number] &= ~static_cast <uint8_t> (VisIndex::Any << shift);
|
||||
m_vistable[vis.number * m_length + path.number] |= res << shift;
|
||||
|
||||
if (!(res & VisIndex::Crouch)) {
|
||||
++crouchCount;
|
||||
}
|
||||
|
||||
if (!(res & VisIndex::Stand)) {
|
||||
++standCount;
|
||||
}
|
||||
}
|
||||
graph[vis.number].vis.crouch = crouchCount;
|
||||
graph[vis.number].vis.stand = standCount;
|
||||
|
||||
if (end == m_length) {
|
||||
m_sliceIndex = 0;
|
||||
m_curIndex++;
|
||||
}
|
||||
else {
|
||||
m_sliceIndex += rg.get (250, 400);
|
||||
}
|
||||
|
||||
// notify host about rebuilding
|
||||
if (m_notifyMsgTimestamp > 0.0f && m_notifyMsgTimestamp < game.time () && end == m_length) {
|
||||
game.print ("Rebuilding vistable... %d%% done.", m_curIndex * 100 / m_length);
|
||||
m_notifyMsgTimestamp = game.time () + 1.0f;
|
||||
}
|
||||
|
||||
if (m_curIndex == m_length && end == m_length) {
|
||||
m_rebuild = false;
|
||||
m_notifyMsgTimestamp = 0.0f;
|
||||
|
||||
save ();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphVistable::startRebuild () {
|
||||
m_rebuild = true;
|
||||
m_notifyMsgTimestamp = game.time ();
|
||||
}
|
||||
|
||||
bool GraphVistable::visible (int srcIndex, int destIndex, VisIndex vis) {
|
||||
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
|
||||
return false;
|
||||
}
|
||||
return !(((m_vistable[srcIndex * m_length + destIndex] >> ((destIndex % 4) << 1)) & vis) == vis);
|
||||
}
|
||||
|
||||
void GraphVistable::load () {
|
||||
m_rebuild = true;
|
||||
m_length = graph.length ();
|
||||
|
||||
m_sliceIndex = 0;
|
||||
m_curIndex = 0;
|
||||
m_notifyMsgTimestamp = 0.0f;
|
||||
|
||||
if (!m_length) {
|
||||
return;
|
||||
}
|
||||
bool dataLoaded = bstor.load <VisStorage> (m_vistable);
|
||||
|
||||
// if loaded, do not recalculate visibility
|
||||
if (dataLoaded) {
|
||||
m_rebuild = false;
|
||||
}
|
||||
else {
|
||||
m_vistable.resize (cr::sqrf (m_length));
|
||||
m_notifyMsgTimestamp = game.time ();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphVistable::save () {
|
||||
if (!graph.length () || graph.hasChanged () || m_rebuild) {
|
||||
return;
|
||||
}
|
||||
bstor.save <VisStorage> (m_vistable);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue