2023-05-02 09:42:43 +03:00
//
2023-05-24 23:41:23 +03:00
// YaPB, based on PODBot by Markus Klinge ("CountFloyd").
// Copyright © YaPB Project Developers <yapb@jeefo.net>.
2023-05-02 09:42:43 +03:00
//
// SPDX-License-Identifier: MIT
//
# include <yapb.h>
2023-07-21 21:43:36 +03:00
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. " ) ;
2023-05-02 09:42:43 +03:00
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 ;
}
2023-05-12 22:12:22 +03:00
ctrl . msg ( " Starting map analysis. " ) ;
2023-05-02 09:42:43 +03:00
}
else {
m_updateInterval = 0.0f ;
}
}
void GraphAnalyze : : update ( ) {
if ( cr : : fzero ( m_updateInterval ) | | ! m_isAnalyzing ) {
return ;
}
if ( m_updateInterval > = game . time ( ) ) {
return ;
}
2024-01-19 00:03:45 +03:00
else {
displayOverlayMessage ( ) ;
}
2023-05-02 09:42:43 +03:00
// 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 ;
2023-06-24 02:36:51 +03:00
const auto range = cv_graph_analyze_distance . float_ ( ) ;
2023-05-02 09:42:43 +03:00
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 ;
2023-05-06 20:14:03 +03:00
m_basicsCreated = false ;
2023-05-02 09:42:43 +03:00
}
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 ) ;
2023-05-12 22:12:22 +03:00
ctrl . msg ( " Completed map analysis. " ) ;
2023-05-02 09:42:43 +03:00
2023-05-06 20:14:03 +03:00
// auto save bots graph
2023-05-02 09:42:43 +03:00
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 ( ) ;
2024-01-19 00:03:45 +03:00
ctrl . enableDrawModels ( false ) ;
2023-05-02 09:42:43 +03:00
cv_quota . revert ( ) ;
}
}
void GraphAnalyze : : optimize ( ) {
if ( graph . length ( ) = = 0 ) {
return ;
}
if ( ! cv_graph_analyze_optimize_nodes_on_finish . bool_ ( ) ) {
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 ) {
2024-02-16 00:57:41 +03:00
if ( graph . exists ( link . index ) & & ! m_optimizedNodes [ link . index ]
& & ! AStarAlgo : : cantSkipNode ( path . number , link . index , true ) ) {
2023-05-02 09:42:43 +03:00
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 ) ;
}
}
2024-01-19 00:03:45 +03:00
// clear the useless connections
if ( cv_graph_analyze_clean_paths_on_finish . bool_ ( ) ) {
for ( auto i = 0 ; i < graph . length ( ) ; + + i ) {
graph . clearConnections ( i ) ;
}
}
2023-05-02 09:42:43 +03:00
}
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 ) ;
}
}
}
2024-01-19 00:03:45 +03:00
void GraphAnalyze : : displayOverlayMessage ( ) {
auto listenserverEdict = game . getLocalEntity ( ) ;
if ( game . isNullEntity ( listenserverEdict ) | | ! m_isAnalyzing ) {
return ;
}
constexpr StringRef analyzeHudMesssage =
2024-01-26 19:52:00 +03:00
" +-----------------------------------------------------------------+ \n "
" Map analysis for bots is in progress. Please Wait.. \n "
" +-----------------------------------------------------------------+ \n " ;
2024-01-19 00:03:45 +03:00
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 ) ;
}
2023-05-02 09:42:43 +03:00
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
2023-06-24 02:36:51 +03:00
if ( ! cr : : fequal ( tr . flFraction , 1.0f ) & & ! util . isShootableBreakable ( tr . pHit ) ) {
2023-05-02 09:42:43 +03:00
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 ;
2024-02-16 00:57:41 +03:00
if ( ( graph . isNodeReacheable ( targetPos , testPos )
& & graph . isNodeReacheable ( testPos , targetPos ) ) | | ( graph . isNodeReacheableWithJump ( testPos , targetPos )
& & graph . isNodeReacheableWithJump ( targetPos , testPos ) ) ) {
2023-05-02 09:42:43 +03:00
graph . add ( NodeAddFlag : : Normal , m_isCrouch ? Vector { nextPos . x , nextPos . y , nextPos . z - 9.0f } : nextPos ) ;
}
}
}
void GraphAnalyze : : setUpdateInterval ( ) {
2024-01-26 19:52:00 +03:00
const auto frametime = globals - > frametime ;
2023-05-02 09:42:43 +03:00
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 ;
}
2024-01-26 19:52:00 +03:00
auto updateNodeFlags = [ ] ( int type , StringRef classname ) {
game . searchEntities ( " classname " , classname , [ & ] ( edict_t * ent ) {
2023-05-02 09:42:43 +03:00
for ( auto & path : graph ) {
2024-01-26 19:52:00 +03:00
const auto & bb = path . origin + Vector ( 1.0f , 1.0f , 1.0f ) ;
2023-05-02 09:42:43 +03:00
2024-01-26 19:52:00 +03:00
if ( ent - > v . absmin . x > bb . x | | ent - > v . absmin . y > bb . y ) {
2023-05-02 09:42:43 +03:00
continue ;
}
2024-01-26 19:52:00 +03:00
if ( ent - > v . absmax . x < bb . x | | ent - > v . absmax . y < bb . y ) {
2023-05-02 09:42:43 +03:00
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
}
}