yapb-noob-edition/src/analyze.cpp
jeefo 3358168fad
aim: look at last enemy origin when just loosed focus
fix: saving bot practice took enormous amount of time when changing level on large practice database
refactor: changed random number generator usage
2024-04-25 15:03:39 +03:00

400 lines
12 KiB
C++

//
// YaPB, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright © YaPB Project Developers <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
ConVar cv_graph_analyze_auto_start ("graph_analyze_auto_start", "1", "Autostart analyzer if all other cases are failed.");
ConVar cv_graph_analyze_auto_save ("graph_analyze_auto_save", "1", "Auto save results of analysis to graph file. And re-add bots.");
ConVar cv_graph_analyze_distance ("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 ("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 ("graph_analyze_fps", "30.0", "The FPS at which analyzer process is running. This keeps game from freezing during analyzing.", true, 25.0f, 99.0f);
ConVar cv_graph_analyze_clean_paths_on_finish ("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 ("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 ("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) {
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 analysis.");
}
else {
m_updateInterval = 0.0f;
}
}
void GraphAnalyze::update () {
if (cr::fzero (m_updateInterval) || !m_isAnalyzing) {
return;
}
if (m_updateInterval >= game.time ()) {
return;
}
else {
displayOverlayMessage ();
}
// 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;
const auto range = cv_graph_analyze_distance.as <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;
m_basicsCreated = 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 ("Completed map analysis.");
// auto save bots graph
if (cv_graph_analyze_auto_save) {
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 ();
ctrl.enableDrawModels (false);
cv_quota.revert ();
}
}
void GraphAnalyze::optimize () {
if (graph.length () == 0) {
return;
}
if (!cv_graph_analyze_optimize_nodes_on_finish) {
return;
}
cleanup ();
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]
&& !AStarAlgo::cantSkipNode (path.number, link.index, true)) {
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);
}
}
// clear the useless connections
if (cv_graph_analyze_clean_paths_on_finish) {
for (auto i = 0; i < graph.length (); ++i) {
graph.clearConnections (i);
}
}
}
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::displayOverlayMessage () {
auto listenserverEdict = game.getLocalEntity ();
if (game.isNullEntity (listenserverEdict) || !m_isAnalyzing) {
return;
}
constexpr StringRef analyzeHudMesssage =
"+-----------------------------------------------------------------+\n"
" Map analysis for bots is in progress. Please Wait.. \n"
"+-----------------------------------------------------------------+\n";
static hudtextparms_t textParams {};
textParams.channel = 1;
textParams.x = -1.0f;
textParams.y = -1.0f;
textParams.effect = 1;
textParams.r1 = textParams.r2 = static_cast <uint8_t> (255);
textParams.g1 = textParams.g2 = static_cast <uint8_t> (31);
textParams.b1 = textParams.b2 = static_cast <uint8_t> (75);
textParams.a1 = textParams.a2 = static_cast <uint8_t> (0);
textParams.fadeinTime = 0.0078125f;
textParams.fadeoutTime = 0.0078125f;
textParams.holdTime = m_updateInterval;
textParams.fxTime = 0.25f;
game.sendHudMessage (listenserverEdict, textParams, analyzeHudMesssage);
}
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) && !util.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.as <float> () + frametime) <= 1.0f / frametime) {
m_updateInterval = game.time () + frametime * 0.06f;
}
}
void GraphAnalyze::markGoals () {
if (!cv_graph_analyze_mark_goals_on_finish) {
return;
}
auto updateNodeFlags = [] (int type, StringRef classname) {
game.searchEntities ("classname", classname, [&] (edict_t *ent) {
for (auto &path : graph) {
const auto &bb = path.origin + Vector (1.0f, 1.0f, 1.0f);
if (ent->v.absmin.x > bb.x || ent->v.absmin.y > bb.y) {
continue;
}
if (ent->v.absmax.x < bb.x || ent->v.absmax.y < bb.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
}
}