2019-07-27 17:36:24 +03:00
//
2020-06-12 18:52:38 +03:00
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
2023-01-22 19:12:03 +06:00
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
2019-07-27 17:36:24 +03:00
//
2020-11-03 08:57:12 +03:00
// SPDX-License-Identifier: MIT
2019-07-27 17:36:24 +03:00
//
# include <yapb.h>
2020-06-12 20:05:07 +03:00
ConVar cv_graph_fixcamp ( " yb_graph_fixcamp " , " 1 " , " Specifies whether bot should not 'fix' camp directions of camp waypoints when loading old PWF format. " ) ;
2020-07-11 20:34:26 +03:00
ConVar cv_graph_url ( " yb_graph_url " , product . download . chars ( ) , " Specifies the URL from bots will be able to download graph in case of missing local one. Set to empty, if no downloads needed. " , false , 0.0f , 0.0f ) ;
2022-01-23 21:15:21 +06:00
ConVar cv_graph_auto_save_count ( " yb_graph_auto_save_count " , " 15 " , " Every N graph nodes placed on map, the graph will be saved automatically (without checks) . " , true, 0.0f, kMaxNodes) ;
2022-12-22 21:34:39 +03:00
ConVar cv_graph_draw_distance ( " yb_graph_draw_distance " , " 400 " , " Maximum distance to draw graph nodes from editor viewport. " , true , 64.0f , 3072.0f ) ;
2019-07-27 17:36:24 +03:00
2020-09-26 18:41:53 +03:00
void BotGraph : : reset ( ) {
2019-07-27 17:36:24 +03:00
// this function initialize the graph structures..
m_loadAttempts = 0 ;
m_editFlags = 0 ;
2022-01-16 01:57:21 +03:00
m_autoSaveCount = 0 ;
2019-07-27 17:36:24 +03:00
2020-01-08 18:29:28 +03:00
m_learnVelocity = nullptr ;
2019-08-24 12:43:42 +03:00
m_learnPosition = nullptr ;
2020-01-08 18:29:28 +03:00
m_lastNode = nullptr ;
2019-07-27 17:36:24 +03:00
m_pathDisplayTime = 0.0f ;
m_arrowDisplayTime = 0.0f ;
m_autoPathDistance = 250.0f ;
m_hasChanged = false ;
2020-01-08 18:29:28 +03:00
m_narrowChecked = false ;
2019-07-27 17:36:24 +03:00
// reset highest recorded damage
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
m_highestDamage [ team ] = 1 ;
}
2022-10-04 12:30:14 +03:00
m_graphAuthor . clear ( ) ;
m_graphModified . clear ( ) ;
2019-07-27 17:36:24 +03:00
}
int BotGraph : : clearConnections ( int index ) {
// this function removes the useless paths connections from and to node pointed by index. This is based on code from POD-bot MM from KWo
if ( ! exists ( index ) ) {
return 0 ;
}
int numFixedLinks = 0 ;
// wrapper form unassiged paths
auto clearPath = [ & ] ( int from , int to ) {
unassignPath ( from , to ) ;
+ + numFixedLinks ;
} ;
if ( bots . hasBotsOnline ( ) ) {
bots . kickEveryone ( true ) ;
}
struct Connection {
2020-11-23 00:06:18 +03:00
int index { } ;
int number { } ;
int distance { } ;
float angles { } ;
2019-07-27 17:36:24 +03:00
public :
Connection ( ) {
reset ( ) ;
}
public :
void reset ( ) {
index = kInvalidNodeIndex ;
number = kInvalidNodeIndex ;
2019-08-04 18:22:01 +03:00
distance = kInfiniteDistanceLong ;
2019-07-27 17:36:24 +03:00
angles = 0.0f ;
}
} ;
auto & path = m_paths [ index ] ;
Connection sorted [ kMaxNodeLinks ] ;
Connection top ;
for ( int i = 0 ; i < kMaxNodeLinks ; + + i ) {
auto & cur = sorted [ i ] ;
const auto & link = path . links [ i ] ;
cur . number = i ;
cur . index = link . index ;
cur . distance = link . distance ;
if ( cur . index = = kInvalidNodeIndex ) {
2019-08-04 18:22:01 +03:00
cur . distance = kInfiniteDistanceLong ;
2019-07-27 17:36:24 +03:00
}
if ( cur . distance < top . distance ) {
top . distance = link . distance ;
top . number = i ;
top . index = cur . index ;
}
}
if ( top . number = = kInvalidNodeIndex ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Cannot find path to the closest connected node to node number %d. " , index ) ;
2019-07-27 17:36:24 +03:00
return numFixedLinks ;
}
bool sorting = false ;
// sort paths from the closest node to the farest away one...
do {
sorting = false ;
for ( int i = 0 ; i < kMaxNodeLinks - 1 ; + + i ) {
if ( sorted [ i ] . distance > sorted [ i + 1 ] . distance ) {
cr : : swap ( sorted [ i ] , sorted [ i + 1 ] ) ;
sorting = true ;
}
}
} while ( sorting ) ;
// calculate angles related to the angle of the closeset connected node
for ( auto & cur : sorted ) {
if ( cur . index = = kInvalidNodeIndex ) {
2019-08-04 18:22:01 +03:00
cur . distance = kInfiniteDistanceLong ;
2019-07-27 17:36:24 +03:00
cur . angles = 360.0f ;
}
else if ( exists ( cur . index ) ) {
cur . angles = ( ( m_paths [ cur . index ] . origin - path . origin ) . angles ( ) - ( m_paths [ sorted [ 0 ] . index ] . origin - path . origin ) . angles ( ) ) . y ;
if ( cur . angles < 0.0f ) {
cur . angles + = 360.0f ;
}
}
}
// sort the paths from the lowest to the highest angle (related to the vector closest node - checked index)...
do {
sorting = false ;
for ( int i = 0 ; i < kMaxNodeLinks - 1 ; + + i ) {
if ( sorted [ i ] . index ! = kInvalidNodeIndex & & sorted [ i ] . angles > sorted [ i + 1 ] . angles ) {
cr : : swap ( sorted [ i ] , sorted [ i + 1 ] ) ;
sorting = true ;
}
}
} while ( sorting ) ;
// reset top state
top . reset ( ) ;
// printing all the stuff causes reliable message overflow
ctrl . setRapidOutput ( true ) ;
// check pass 0
auto inspect_p0 = [ & ] ( const int id ) - > bool {
if ( id < 2 ) {
return false ;
}
auto & cur = sorted [ id ] , & prev = sorted [ id - 1 ] , & prev2 = sorted [ id - 2 ] ;
if ( cur . index = = kInvalidNodeIndex | | prev . index = = kInvalidNodeIndex | | prev2 . index = = kInvalidNodeIndex ) {
return false ;
}
// store the highest index which should be tested later...
top . index = cur . index ;
top . distance = cur . distance ;
top . angles = cur . angles ;
if ( cur . angles - prev2 . angles < 80.0f ) {
// leave alone ladder connections and don't remove jump connections..
if ( ( ( path . flags & NodeFlag : : Ladder ) & & ( m_paths [ prev . index ] . flags & NodeFlag : : Ladder ) ) | | ( path . links [ prev . number ] . flags & PathFlag : : Jump ) ) {
return false ;
}
if ( ( cur . distance + prev2 . distance ) * 1.1f / 2.0f < static_cast < float > ( prev . distance ) ) {
if ( path . links [ prev . number ] . index = = prev . index ) {
ctrl . msg ( " Removing a useless (P.0.1) connection from index = %d to %d. " , index , prev . index ) ;
// unassign this path
clearPath ( index , prev . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ prev . index ] . links [ j ] . index = = index & & ! ( m_paths [ prev . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.0.2) connection from index = %d to %d. " , prev . index , index ) ;
// unassign this path
clearPath ( prev . index , j ) ;
}
}
prev . index = kInvalidNodeIndex ;
for ( int j = id - 1 ; j < kMaxNodeLinks - 1 ; + + j ) {
sorted [ j ] = cr : : move ( sorted [ j + 1 ] ) ;
}
sorted [ kMaxNodeLinks - 1 ] . index = kInvalidNodeIndex ;
// do a second check
return true ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.0) connection from index = %d to %d. " , index , prev . index ) ;
return false ;
}
}
}
return false ;
} ;
for ( int i = 2 ; i < kMaxNodeLinks ; + + i ) {
while ( inspect_p0 ( i ) ) { }
}
// check pass 1
if ( exists ( top . index ) & & exists ( sorted [ 0 ] . index ) & & exists ( sorted [ 1 ] . index ) ) {
if ( ( sorted [ 1 ] . angles - top . angles < 80.0f | | 360.0f - ( sorted [ 1 ] . angles - top . angles ) < 80.0f ) & & ( ! ( m_paths [ sorted [ 0 ] . index ] . flags & NodeFlag : : Ladder ) | | ! ( path . flags & NodeFlag : : Ladder ) ) & & ! ( path . links [ sorted [ 0 ] . number ] . flags & PathFlag : : Jump ) ) {
if ( ( sorted [ 1 ] . distance + top . distance ) * 1.1f / 2.0f < static_cast < float > ( sorted [ 0 ] . distance ) ) {
if ( path . links [ sorted [ 0 ] . number ] . index = = sorted [ 0 ] . index ) {
ctrl . msg ( " Removing a useless (P.1.1) connection from index = %d to %d. " , index , sorted [ 0 ] . index ) ;
// unassign this path
clearPath ( index , sorted [ 0 ] . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ sorted [ 0 ] . index ] . links [ j ] . index = = index & & ! ( m_paths [ sorted [ 0 ] . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.1.2) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
// unassign this path
clearPath ( sorted [ 0 ] . index , j ) ;
}
}
sorted [ 0 ] . index = kInvalidNodeIndex ;
for ( int j = 0 ; j < kMaxNodeLinks - 1 ; + + j ) {
sorted [ j ] = cr : : move ( sorted [ j + 1 ] ) ;
}
sorted [ kMaxNodeLinks - 1 ] . index = kInvalidNodeIndex ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.1) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
}
}
}
}
top . reset ( ) ;
// check pass 2
auto inspect_p2 = [ & ] ( const int id ) - > bool {
if ( id < 1 ) {
return false ;
}
auto & cur = sorted [ id ] , & prev = sorted [ id - 1 ] ;
if ( cur . index = = kInvalidNodeIndex | | prev . index = = kInvalidNodeIndex ) {
return false ;
}
if ( cur . angles - prev . angles < 40.0f ) {
if ( prev . distance < static_cast < float > ( cur . distance * 1.1f ) ) {
// leave alone ladder connections and don't remove jump connections..
if ( ( ( path . flags & NodeFlag : : Ladder ) & & ( m_paths [ cur . index ] . flags & NodeFlag : : Ladder ) ) | | ( path . links [ cur . number ] . flags & PathFlag : : Jump ) ) {
return false ;
}
if ( path . links [ cur . number ] . index = = cur . index ) {
ctrl . msg ( " Removing a useless (P.2.1) connection from index = %d to %d. " , index , cur . index ) ;
// unassign this path
clearPath ( index , cur . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ cur . index ] . links [ j ] . index = = index & & ! ( m_paths [ cur . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.2.2) connection from index = %d to %d. " , cur . index , index ) ;
// unassign this path
clearPath ( cur . index , j ) ;
}
}
cur . index = kInvalidNodeIndex ;
for ( int j = id - 1 ; j < kMaxNodeLinks - 1 ; + + j ) {
sorted [ j ] = cr : : move ( sorted [ j + 1 ] ) ;
}
sorted [ kMaxNodeLinks - 1 ] . index = kInvalidNodeIndex ;
return true ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.2) connection from index = %d to %d. " , index , cur . index ) ;
}
}
else if ( cur . distance < static_cast < float > ( prev . distance * 1.1f ) ) {
// leave alone ladder connections and don't remove jump connections..
if ( ( ( path . flags & NodeFlag : : Ladder ) & & ( m_paths [ prev . index ] . flags & NodeFlag : : Ladder ) ) | | ( path . links [ prev . number ] . flags & PathFlag : : Jump ) ) {
return false ;
}
if ( path . links [ prev . number ] . index = = prev . index ) {
ctrl . msg ( " Removing a useless (P.2.3) connection from index = %d to %d. " , index , prev . index ) ;
// unassign this path
clearPath ( index , prev . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ prev . index ] . links [ j ] . index = = index & & ! ( m_paths [ prev . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.2.4) connection from index = %d to %d. " , prev . index , index ) ;
// unassign this path
clearPath ( prev . index , j ) ;
}
}
prev . index = kInvalidNodeIndex ;
for ( int j = id - 1 ; j < kMaxNodeLinks - 1 ; + + j ) {
sorted [ j ] = cr : : move ( sorted [ j + 1 ] ) ;
}
sorted [ kMaxNodeLinks - 1 ] . index = kInvalidNodeIndex ;
// do a second check
return true ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.2) connection from index = %d to %d. " , index , prev . index ) ;
}
}
}
else {
top = cur ;
}
return false ;
} ;
for ( int i = 1 ; i < kMaxNodeLinks ; + + i ) {
while ( inspect_p2 ( i ) ) { }
}
// check pass 3
if ( exists ( top . index ) & & exists ( sorted [ 0 ] . index ) ) {
if ( ( top . angles - sorted [ 0 ] . angles < 40.0f | | ( 360.0f - top . angles - sorted [ 0 ] . angles ) < 40.0f ) & & ( ! ( m_paths [ sorted [ 0 ] . index ] . flags & NodeFlag : : Ladder ) | | ! ( path . flags & NodeFlag : : Ladder ) ) & & ! ( path . links [ sorted [ 0 ] . number ] . flags & PathFlag : : Jump ) ) {
if ( top . distance * 1.1f < static_cast < float > ( sorted [ 0 ] . distance ) ) {
if ( path . links [ sorted [ 0 ] . number ] . index = = sorted [ 0 ] . index ) {
ctrl . msg ( " Removing a useless (P.3.1) connection from index = %d to %d. " , index , sorted [ 0 ] . index ) ;
// unassign this path
clearPath ( index , sorted [ 0 ] . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ sorted [ 0 ] . index ] . links [ j ] . index = = index & & ! ( m_paths [ sorted [ 0 ] . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.3.2) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
// unassign this path
clearPath ( sorted [ 0 ] . index , j ) ;
}
}
sorted [ 0 ] . index = kInvalidNodeIndex ;
for ( int j = 0 ; j < kMaxNodeLinks - 1 ; + + j ) {
sorted [ j ] = cr : : move ( sorted [ j + 1 ] ) ;
}
sorted [ kMaxNodeLinks - 1 ] . index = kInvalidNodeIndex ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.3) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
}
}
else if ( sorted [ 0 ] . distance * 1.1f < static_cast < float > ( top . distance ) & & ! ( path . links [ top . number ] . flags & PathFlag : : Jump ) ) {
if ( path . links [ top . number ] . index = = top . index ) {
ctrl . msg ( " Removing a useless (P.3.3) connection from index = %d to %d. " , index , sorted [ 0 ] . index ) ;
// unassign this path
clearPath ( index , top . number ) ;
for ( int j = 0 ; j < kMaxNodeLinks ; + + j ) {
if ( m_paths [ top . index ] . links [ j ] . index = = index & & ! ( m_paths [ top . index ] . links [ j ] . flags & PathFlag : : Jump ) ) {
ctrl . msg ( " Removing a useless (P.3.4) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
// unassign this path
clearPath ( top . index , j ) ;
}
}
sorted [ 0 ] . index = kInvalidNodeIndex ;
}
else {
ctrl . msg ( " Failed to remove a useless (P.3) connection from index = %d to %d. " , sorted [ 0 ] . index , index ) ;
}
}
}
}
ctrl . setRapidOutput ( false ) ;
return numFixedLinks ;
}
2021-09-20 13:50:00 +03:00
int BotGraph : : getBspSize ( ) {
MemFile file ( strings . format ( " maps/%s.bsp " , game . getMapName ( ) ) ) ;
if ( file ) {
return static_cast < int > ( file . length ( ) ) ;
}
return 0 ;
}
2019-07-27 17:36:24 +03:00
void BotGraph : : addPath ( int addIndex , int pathIndex , float distance ) {
if ( ! exists ( addIndex ) | | ! exists ( pathIndex ) ) {
return ;
}
auto & path = m_paths [ addIndex ] ;
// don't allow paths get connected twice
for ( const auto & link : path . links ) {
if ( link . index = = pathIndex ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Denied path creation from %d to %d (path already exists). " , addIndex , pathIndex ) ;
2019-07-27 17:36:24 +03:00
return ;
}
}
// check for free space in the connection indices
for ( auto & link : path . links ) {
if ( link . index = = kInvalidNodeIndex ) {
link . index = static_cast < int16 > ( pathIndex ) ;
link . distance = cr : : abs ( static_cast < int > ( distance ) ) ;
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Path added from %d to %d. " , addIndex , pathIndex ) ;
2019-07-27 17:36:24 +03:00
return ;
}
}
// there wasn't any free space. try exchanging it with a long-distance path
2019-08-04 18:22:01 +03:00
int maxDistance = - kInfiniteDistanceLong ;
2019-07-27 17:36:24 +03:00
int slot = kInvalidNodeIndex ;
for ( int i = 0 ; i < kMaxNodeLinks ; + + i ) {
if ( path . links [ i ] . distance > maxDistance ) {
maxDistance = path . links [ i ] . distance ;
slot = i ;
}
}
if ( slot ! = kInvalidNodeIndex ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Path added from %d to %d. " , addIndex , pathIndex ) ;
2019-07-27 17:36:24 +03:00
path . links [ slot ] . index = static_cast < int16 > ( pathIndex ) ;
path . links [ slot ] . distance = cr : : abs ( static_cast < int > ( distance ) ) ;
}
}
int BotGraph : : getFarest ( const Vector & origin , float maxDistance ) {
// find the farest node to that origin, and return the index to this node
int index = kInvalidNodeIndex ;
maxDistance = cr : : square ( maxDistance ) ;
for ( const auto & path : m_paths ) {
2021-09-20 13:50:00 +03:00
float distance = path . origin . distanceSq ( origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance > maxDistance ) {
index = path . number ;
maxDistance = distance ;
}
}
return index ;
}
int BotGraph : : getNearestNoBuckets ( const Vector & origin , float minDistance , int flags ) {
// find the nearest node to that origin and return the index
// fallback and go thru wall the nodes...
int index = kInvalidNodeIndex ;
minDistance = cr : : square ( minDistance ) ;
for ( const auto & path : m_paths ) {
if ( flags ! = - 1 & & ! ( path . flags & flags ) ) {
continue ; // if flag not -1 and node has no this flag, skip node
}
2021-09-20 13:50:00 +03:00
float distance = path . origin . distanceSq ( origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < minDistance ) {
index = path . number ;
minDistance = distance ;
}
}
return index ;
}
2023-01-22 19:12:03 +06:00
int BotGraph : : getEditorNearest ( ) {
2019-07-27 17:36:24 +03:00
if ( ! hasEditFlag ( GraphEdit : : On ) ) {
return kInvalidNodeIndex ;
}
return getNearestNoBuckets ( m_editor - > v . origin , 50.0f ) ;
}
int BotGraph : : getNearest ( const Vector & origin , float minDistance , int flags ) {
// find the nearest node to that origin and return the index
auto & bucket = getNodesInBucket ( origin ) ;
if ( bucket . empty ( ) ) {
return getNearestNoBuckets ( origin , minDistance , flags ) ;
}
int index = kInvalidNodeIndex ;
auto minDistanceSq = cr : : square ( minDistance ) ;
for ( const auto & at : bucket ) {
if ( flags ! = - 1 & & ! ( m_paths [ at ] . flags & flags ) ) {
continue ; // if flag not -1 and node has no this flag, skip node
}
2021-09-20 13:50:00 +03:00
float distance = origin . distanceSq ( m_paths [ at ] . origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < minDistanceSq ) {
index = at ;
minDistanceSq = distance ;
}
}
// nothing found, try to find without buckets
if ( index = = kInvalidNodeIndex ) {
return getNearestNoBuckets ( origin , minDistance , flags ) ;
}
return index ;
}
IntArray BotGraph : : searchRadius ( float radius , const Vector & origin , int maxCount ) {
// returns all nodes within radius from position
IntArray result ;
auto & bucket = getNodesInBucket ( origin ) ;
if ( bucket . empty ( ) ) {
result . push ( getNearestNoBuckets ( origin , radius ) ) ;
2020-11-23 00:06:18 +03:00
return result ;
2019-07-27 17:36:24 +03:00
}
radius = cr : : square ( radius ) ;
for ( const auto & at : bucket ) {
if ( maxCount ! = - 1 & & static_cast < int > ( result . length ( ) ) > maxCount ) {
break ;
}
2021-09-20 13:50:00 +03:00
if ( origin . distanceSq ( m_paths [ at ] . origin ) < radius ) {
2019-07-27 17:36:24 +03:00
result . push ( at ) ;
}
}
2020-11-23 00:06:18 +03:00
return result ;
2019-07-27 17:36:24 +03:00
}
void BotGraph : : add ( int type , const Vector & pos ) {
2019-08-29 23:31:54 +03:00
// @todo: remove type magic numbers
2019-07-27 17:36:24 +03:00
if ( game . isNullEntity ( m_editor ) ) {
return ;
}
int index = kInvalidNodeIndex ;
Path * path = nullptr ;
bool addNewNode = true ;
2019-08-18 21:00:00 +03:00
Vector newOrigin = pos . empty ( ) ? m_editor - > v . origin : pos ;
2019-07-27 17:36:24 +03:00
if ( bots . hasBotsOnline ( ) ) {
bots . kickEveryone ( true ) ;
}
m_hasChanged = true ;
switch ( type ) {
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : Camp :
2023-01-22 19:12:03 +06:00
index = getEditorNearest ( ) ;
2019-08-29 23:31:54 +03:00
if ( index ! = kInvalidNodeIndex ) {
path = & m_paths [ index ] ;
if ( path - > flags & NodeFlag : : Camp ) {
path - > start = m_editor - > v . v_angle . get2d ( ) ;
// play "done" sound...
game . playSound ( m_editor , " common/wpn_hudon.wav " ) ;
return ;
}
}
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : CampEnd :
2023-01-22 19:12:03 +06:00
index = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( index ! = kInvalidNodeIndex ) {
path = & m_paths [ index ] ;
if ( ! ( path - > flags & NodeFlag : : Camp ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " This is not camping node. " ) ;
2019-07-27 17:36:24 +03:00
return ;
}
path - > end = m_editor - > v . v_angle . get2d ( ) ;
// play "done" sound...
game . playSound ( m_editor , " common/wpn_hudon.wav " ) ;
}
return ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : JumpStart :
2023-01-22 19:12:03 +06:00
index = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( index ! = kInvalidNodeIndex & & m_paths [ index ] . number > = 0 ) {
2021-09-20 13:50:00 +03:00
float distance = m_editor - > v . origin . distance ( m_paths [ index ] . origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < 50.0f ) {
addNewNode = false ;
path = & m_paths [ index ] ;
path - > origin = ( path - > origin + m_learnPosition ) * 0.5f ;
}
}
else {
newOrigin = m_learnPosition ;
}
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : JumpEnd :
2023-01-22 19:12:03 +06:00
index = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( index ! = kInvalidNodeIndex & & m_paths [ index ] . number > = 0 ) {
2021-09-20 13:50:00 +03:00
float distance = m_editor - > v . origin . distance ( m_paths [ index ] . origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < 50.0f ) {
addNewNode = false ;
path = & m_paths [ index ] ;
int connectionFlags = 0 ;
for ( const auto & link : path - > links ) {
connectionFlags + = link . flags ;
}
if ( connectionFlags = = 0 ) {
path - > origin = ( path - > origin + m_editor - > v . origin ) * 0.5f ;
}
}
}
break ;
}
if ( addNewNode ) {
2023-01-22 19:12:03 +06:00
auto nearest = getEditorNearest ( ) ;
2019-08-29 23:31:54 +03:00
// do not allow to place waypoints "inside" waypoints, make at leat 10 units range
2021-09-20 13:50:00 +03:00
if ( exists ( nearest ) & & newOrigin . distanceSq ( m_paths [ nearest ] . origin ) < cr : : square ( 10.0f ) ) {
2019-08-29 23:31:54 +03:00
ctrl . msg ( " Can't add node. It's way to near to %d node. Please move some units anywhere. " , nearest ) ;
return ;
}
2019-07-27 17:36:24 +03:00
// need to remove limit?
if ( m_paths . length ( ) > = kMaxNodes ) {
return ;
}
2022-01-16 01:57:21 +03:00
m_paths . emplace ( ) ;
2019-07-27 17:36:24 +03:00
2020-06-12 18:52:38 +03:00
index = length ( ) - 1 ;
2019-07-27 17:36:24 +03:00
path = & m_paths [ index ] ;
path - > number = index ;
path - > flags = 0 ;
// store the origin (location) of this node
path - > origin = newOrigin ;
2019-08-18 21:00:00 +03:00
path - > start = nullptr ;
path - > end = nullptr ;
2019-07-27 17:36:24 +03:00
path - > display = 0.0f ;
path - > light = 0.0f ;
for ( auto & link : path - > links ) {
link . index = kInvalidNodeIndex ;
link . distance = 0 ;
link . flags = 0 ;
2019-08-18 21:00:00 +03:00
link . velocity = nullptr ;
2019-07-27 17:36:24 +03:00
}
2022-01-16 01:57:21 +03:00
// autosave nodes here and there
2022-01-23 21:15:21 +06:00
if ( cv_graph_auto_save_count . bool_ ( ) & & + + m_autoSaveCount > = cv_graph_auto_save_count . int_ ( ) ) {
2022-01-16 01:57:21 +03:00
if ( saveGraphData ( ) ) {
ctrl . msg ( " Nodes has been autosaved... " ) ;
}
else {
2022-01-16 01:58:06 +03:00
ctrl . msg ( " Can't autosave graph data... " ) ;
2022-01-16 01:57:21 +03:00
}
m_autoSaveCount = 0 ;
}
2019-07-27 17:36:24 +03:00
// store the last used node for the auto node code...
m_lastNode = m_editor - > v . origin ;
}
2021-09-20 13:50:00 +03:00
if ( type = = NodeAddFlag : : JumpStart ) {
2019-07-27 17:36:24 +03:00
m_lastJumpNode = index ;
}
2021-09-20 13:50:00 +03:00
else if ( type = = NodeAddFlag : : JumpEnd ) {
float distance = m_paths [ m_lastJumpNode ] . origin . distance ( m_editor - > v . origin ) ;
2019-07-27 17:36:24 +03:00
addPath ( m_lastJumpNode , index , distance ) ;
for ( auto & link : m_paths [ m_lastJumpNode ] . links ) {
if ( link . index = = index ) {
link . flags | = PathFlag : : Jump ;
link . velocity = m_learnVelocity ;
break ;
}
}
calculatePathRadius ( index ) ;
return ;
}
if ( ! path | | path - > number = = kInvalidNodeIndex ) {
return ;
}
if ( m_editor - > v . flags & FL_DUCKING ) {
path - > flags | = NodeFlag : : Crouch ; // set a crouch node
}
if ( m_editor - > v . movetype = = MOVETYPE_FLY ) {
path - > flags | = NodeFlag : : Ladder ;
}
else if ( m_isOnLadder ) {
path - > flags | = NodeFlag : : Ladder ;
}
switch ( type ) {
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : TOnly :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : Crossing ;
path - > flags | = NodeFlag : : TerroristOnly ;
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : CTOnly :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : Crossing ;
path - > flags | = NodeFlag : : CTOnly ;
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : NoHostage :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : NoHostage ;
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : Rescue :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : Rescue ;
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : Camp :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : Crossing ;
path - > flags | = NodeFlag : : Camp ;
path - > start = m_editor - > v . v_angle ;
break ;
2021-09-20 13:50:00 +03:00
case NodeAddFlag : : Goal :
2019-07-27 17:36:24 +03:00
path - > flags | = NodeFlag : : Goal ;
break ;
}
// Ladder nodes need careful connections
if ( path - > flags & NodeFlag : : Ladder ) {
2019-08-04 18:22:01 +03:00
float minDistance = kInfiniteDistance ;
2019-07-27 17:36:24 +03:00
int destIndex = kInvalidNodeIndex ;
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-07-27 17:36:24 +03:00
// calculate all the paths to this new node
for ( const auto & calc : m_paths ) {
if ( calc . number = = index ) {
continue ; // skip the node that was just added
}
// other ladder nodes should connect to this
if ( calc . flags & NodeFlag : : Ladder ) {
// check if the node is reachable from the new one
game . testLine ( newOrigin , calc . origin , TraceIgnore : : Monsters , m_editor , & tr ) ;
if ( cr : : fequal ( tr . flFraction , 1.0f ) & & cr : : abs ( newOrigin . x - calc . origin . x ) < 64.0f & & cr : : abs ( newOrigin . y - calc . origin . y ) < 64.0f & & cr : : abs ( newOrigin . z - calc . origin . z ) < m_autoPathDistance ) {
2021-09-20 13:50:00 +03:00
float distance = newOrigin . distance ( calc . origin ) ;
2019-07-27 17:36:24 +03:00
addPath ( index , calc . number , distance ) ;
addPath ( calc . number , index , distance ) ;
}
}
else {
// check if the node is reachable from the new one
if ( isNodeReacheable ( newOrigin , calc . origin ) | | isNodeReacheable ( calc . origin , newOrigin ) ) {
2021-09-20 13:50:00 +03:00
float distance = newOrigin . distance ( calc . origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < minDistance ) {
destIndex = calc . number ;
minDistance = distance ;
}
}
}
}
if ( exists ( destIndex ) ) {
// check if the node is reachable from the new one (one-way)
if ( isNodeReacheable ( newOrigin , m_paths [ destIndex ] . origin ) ) {
2021-09-20 13:50:00 +03:00
addPath ( index , destIndex , newOrigin . distance ( m_paths [ destIndex ] . origin ) ) ;
2019-07-27 17:36:24 +03:00
}
// check if the new one is reachable from the node (other way)
if ( isNodeReacheable ( m_paths [ destIndex ] . origin , newOrigin ) ) {
2021-09-20 13:50:00 +03:00
addPath ( destIndex , index , newOrigin . distance ( m_paths [ destIndex ] . origin ) ) ;
2019-07-27 17:36:24 +03:00
}
}
}
else {
// calculate all the paths to this new node
for ( const auto & calc : m_paths ) {
if ( calc . number = = index ) {
continue ; // skip the node that was just added
}
// check if the node is reachable from the new one (one-way)
if ( isNodeReacheable ( newOrigin , calc . origin ) ) {
2021-09-20 13:50:00 +03:00
addPath ( index , calc . number , calc . origin . distance ( newOrigin ) ) ;
2019-07-27 17:36:24 +03:00
}
// check if the new one is reachable from the node (other way)
if ( isNodeReacheable ( calc . origin , newOrigin ) ) {
2021-09-20 13:50:00 +03:00
addPath ( calc . number , index , calc . origin . distance ( newOrigin ) ) ;
2019-07-27 17:36:24 +03:00
}
}
clearConnections ( index ) ;
}
game . playSound ( m_editor , " weapons/xbow_hit1.wav " ) ;
calculatePathRadius ( index ) ; // calculate the wayzone of this node
}
void BotGraph : : erase ( int target ) {
m_hasChanged = true ;
if ( m_paths . empty ( ) ) {
return ;
}
if ( bots . hasBotsOnline ( ) ) {
bots . kickEveryone ( true ) ;
}
2023-01-22 19:12:03 +06:00
const int index = ( target = = kInvalidNodeIndex ) ? getEditorNearest ( ) : target ;
2019-07-27 17:36:24 +03:00
if ( ! exists ( index ) ) {
return ;
}
auto & path = m_paths [ index ] ;
// unassign paths that points to this nodes
for ( auto & connected : m_paths ) {
for ( auto & link : connected . links ) {
if ( link . index = = index ) {
link . index = kInvalidNodeIndex ;
link . flags = 0 ;
link . distance = 0 ;
2019-08-18 21:00:00 +03:00
link . velocity = nullptr ;
2019-07-27 17:36:24 +03:00
}
}
}
// relink nodes so the index will match path number
for ( auto & relink : m_paths ) {
// if pathnumber bigger than deleted node...
if ( relink . number > index ) {
- - relink . number ;
}
for ( auto & neighbour : relink . links ) {
if ( neighbour . index > index ) {
- - neighbour . index ;
}
}
}
m_paths . remove ( path ) ;
game . playSound ( m_editor , " weapons/mine_activate.wav " ) ;
}
void BotGraph : : toggleFlags ( int toggleFlag ) {
// this function allow manually changing flags
2023-01-22 19:12:03 +06:00
int index = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( index ! = kInvalidNodeIndex ) {
if ( m_paths [ index ] . flags & toggleFlag ) {
m_paths [ index ] . flags & = ~ toggleFlag ;
}
2020-10-03 16:39:09 +03:00
else {
2019-07-27 17:36:24 +03:00
if ( toggleFlag = = NodeFlag : : Sniper & & ! ( m_paths [ index ] . flags & NodeFlag : : Camp ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Cannot assign sniper flag to node %d. This is not camp node. " , index ) ;
2019-07-27 17:36:24 +03:00
return ;
}
m_paths [ index ] . flags | = toggleFlag ;
}
// play "done" sound...
game . playSound ( m_editor , " common/wpn_hudon.wav " ) ;
}
}
void BotGraph : : setRadius ( int index , float radius ) {
// this function allow manually setting the zone radius
2023-01-22 19:12:03 +06:00
int node = exists ( index ) ? index : getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( node ! = kInvalidNodeIndex ) {
m_paths [ node ] . radius = static_cast < float > ( radius ) ;
// play "done" sound...
game . playSound ( m_editor , " common/wpn_hudon.wav " ) ;
2020-08-22 01:38:59 +03:00
ctrl . msg ( " Node %d has been set to radius %.2f. " , node , radius ) ;
2019-07-27 17:36:24 +03:00
}
}
bool BotGraph : : isConnected ( int a , int b ) {
// this function checks if node A has a connection to node B
2019-07-28 15:47:46 +03:00
if ( ! exists ( a ) | | ! exists ( b ) ) {
return false ;
}
2019-07-27 17:36:24 +03:00
for ( const auto & link : m_paths [ a ] . links ) {
if ( link . index = = b ) {
return true ;
}
}
return false ;
}
int BotGraph : : getFacingIndex ( ) {
2019-08-31 22:55:16 +03:00
// find the waypoint the user is pointing at
2019-07-27 17:36:24 +03:00
2019-08-31 22:55:16 +03:00
Twin < int32 , float > result { kInvalidNodeIndex , 5.32f } ;
2023-01-22 19:12:03 +06:00
auto nearestNode = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
2019-08-31 22:55:16 +03:00
// check bounds from eyes of editor
const auto & editorEyes = m_editor - > v . origin + m_editor - > v . view_ofs ;
2019-07-27 17:36:24 +03:00
2019-08-31 22:55:16 +03:00
for ( const auto & path : m_paths ) {
2019-07-27 17:36:24 +03:00
2019-08-31 22:55:16 +03:00
// skip nearest waypoint to editor, since this used mostly for adding / removing paths
if ( path . number = = nearestNode ) {
continue ;
}
2019-08-29 23:31:54 +03:00
2019-08-31 22:55:16 +03:00
const auto & to = path . origin - m_editor - > v . origin ;
auto angles = ( to . angles ( ) - m_editor - > v . v_angle ) . clampAngles ( ) ;
2019-08-29 23:31:54 +03:00
2019-08-31 22:55:16 +03:00
// skip the waypoints that are too far away from us, and we're not looking at them directly
if ( to . lengthSq ( ) > cr : : square ( 500.0f ) | | cr : : abs ( angles . y ) > result . second ) {
continue ;
}
// check if visible, (we're not using visiblity tables here, as they not valid at time of waypoint editing)
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-08-31 22:55:16 +03:00
game . testLine ( editorEyes , path . origin , TraceIgnore : : Everything , m_editor , & tr ) ;
2019-08-29 23:31:54 +03:00
2019-08-31 22:55:16 +03:00
if ( ! cr : : fequal ( tr . flFraction , 1.0f ) ) {
2019-08-29 23:31:54 +03:00
continue ;
}
2019-08-31 22:55:16 +03:00
const float bestAngle = angles . y ;
2019-07-27 17:36:24 +03:00
2019-08-31 22:55:16 +03:00
angles = - m_editor - > v . v_angle ;
angles . x = - angles . x ;
angles = ( angles + ( ( path . origin - Vector ( 0.0f , 0.0f , ( path . flags & NodeFlag : : Crouch ) ? 17.0f : 34.0f ) ) - editorEyes ) . angles ( ) ) . clampAngles ( ) ;
if ( angles . x > 0.0f ) {
continue ;
2019-07-27 17:36:24 +03:00
}
2019-08-31 22:55:16 +03:00
result = { path . number , bestAngle } ;
2019-07-27 17:36:24 +03:00
}
2019-08-31 22:55:16 +03:00
return result . first ;
2019-07-27 17:36:24 +03:00
}
void BotGraph : : pathCreate ( char dir ) {
// this function allow player to manually create a path from one node to another
2023-01-22 19:12:03 +06:00
int nodeFrom = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( nodeFrom = = kInvalidNodeIndex ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Unable to find nearest node in 50 units. " ) ;
2019-07-27 17:36:24 +03:00
return ;
}
int nodeTo = m_facingAtIndex ;
if ( ! exists ( nodeTo ) ) {
if ( exists ( m_cacheNodeIndex ) ) {
nodeTo = m_cacheNodeIndex ;
}
else {
2019-08-07 15:25:14 +03:00
ctrl . msg ( " Unable to find destination node. " ) ;
2019-07-27 17:36:24 +03:00
return ;
}
}
if ( nodeTo = = nodeFrom ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Unable to connect node with itself. " ) ;
2019-07-27 17:36:24 +03:00
return ;
}
2021-09-20 13:50:00 +03:00
float distance = m_paths [ nodeFrom ] . origin . distance ( m_paths [ nodeTo ] . origin ) ;
2019-07-27 17:36:24 +03:00
if ( dir = = PathConnection : : Outgoing ) {
addPath ( nodeFrom , nodeTo , distance ) ;
}
else if ( dir = = PathConnection : : Incoming ) {
addPath ( nodeTo , nodeFrom , distance ) ;
}
else {
addPath ( nodeFrom , nodeTo , distance ) ;
addPath ( nodeTo , nodeFrom , distance ) ;
}
game . playSound ( m_editor , " common/wpn_hudon.wav " ) ;
m_hasChanged = true ;
}
void BotGraph : : erasePath ( ) {
// this function allow player to manually remove a path from one node to another
2023-01-22 19:12:03 +06:00
int nodeFrom = getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( nodeFrom = = kInvalidNodeIndex ) {
ctrl . msg ( " Unable to find nearest node in 50 units. " ) ;
return ;
}
int nodeTo = m_facingAtIndex ;
if ( ! exists ( nodeTo ) ) {
if ( exists ( m_cacheNodeIndex ) ) {
nodeTo = m_cacheNodeIndex ;
}
else {
2019-08-07 15:25:14 +03:00
ctrl . msg ( " Unable to find destination node. " ) ;
2019-07-27 17:36:24 +03:00
return ;
}
}
2019-08-29 23:31:54 +03:00
// helper
auto destroy = [ ] ( PathLink & link ) - > void {
link . index = kInvalidNodeIndex ;
link . distance = 0 ;
link . flags = 0 ;
link . velocity = nullptr ;
} ;
2019-07-27 17:36:24 +03:00
for ( auto & link : m_paths [ nodeFrom ] . links ) {
if ( link . index = = nodeTo ) {
2019-08-29 23:31:54 +03:00
destroy ( link ) ;
2019-07-27 17:36:24 +03:00
game . playSound ( m_editor , " weapons/mine_activate.wav " ) ;
return ;
}
}
// not found this way ? check for incoming connections then
2019-08-29 23:31:54 +03:00
cr : : swap ( nodeFrom , nodeTo ) ;
2019-07-27 17:36:24 +03:00
for ( auto & link : m_paths [ nodeFrom ] . links ) {
if ( link . index = = nodeTo ) {
2019-08-29 23:31:54 +03:00
destroy ( link ) ;
2019-07-27 17:36:24 +03:00
game . playSound ( m_editor , " weapons/mine_activate.wav " ) ;
return ;
}
}
2019-08-07 15:25:14 +03:00
ctrl . msg ( " There is already no path on this node. " ) ;
2019-07-27 17:36:24 +03:00
}
void BotGraph : : cachePoint ( int index ) {
2023-01-22 19:12:03 +06:00
int node = exists ( index ) ? index : getEditorNearest ( ) ;
2019-07-27 17:36:24 +03:00
if ( node = = kInvalidNodeIndex ) {
m_cacheNodeIndex = kInvalidNodeIndex ;
ctrl . msg ( " Cached node cleared (nearby point not found in 50 units range). " ) ;
return ;
}
m_cacheNodeIndex = node ;
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d has been put into memory. " , m_cacheNodeIndex ) ;
2019-07-27 17:36:24 +03:00
}
2020-08-23 11:08:27 +03:00
void BotGraph : : setAutoPathDistance ( const float distance ) {
m_autoPathDistance = distance ;
if ( cr : : fzero ( distance ) ) {
ctrl . msg ( " Autopathing is now disabled. " ) ;
}
else {
ctrl . msg ( " Autopath distance is set to %.2f. " , distance ) ;
}
}
2020-08-31 14:52:12 +03:00
void BotGraph : : showStats ( ) {
int terrPoints = 0 ;
int ctPoints = 0 ;
int goalPoints = 0 ;
int rescuePoints = 0 ;
int campPoints = 0 ;
int sniperPoints = 0 ;
int noHostagePoints = 0 ;
for ( const auto & path : m_paths ) {
if ( path . flags & NodeFlag : : TerroristOnly ) {
+ + terrPoints ;
}
if ( path . flags & NodeFlag : : CTOnly ) {
+ + ctPoints ;
}
if ( path . flags & NodeFlag : : Goal ) {
+ + goalPoints ;
}
if ( path . flags & NodeFlag : : Rescue ) {
+ + rescuePoints ;
}
if ( path . flags & NodeFlag : : Camp ) {
+ + campPoints ;
}
if ( path . flags & NodeFlag : : Sniper ) {
+ + sniperPoints ;
}
if ( path . flags & NodeFlag : : NoHostage ) {
+ + noHostagePoints ;
}
}
ctrl . msg ( " Nodes: %d - T Points: %d " , m_paths . length ( ) , terrPoints ) ;
ctrl . msg ( " CT Points: %d - Goal Points: %d " , ctPoints , goalPoints ) ;
ctrl . msg ( " Rescue Points: %d - Camp Points: %d " , rescuePoints , campPoints ) ;
ctrl . msg ( " Block Hostage Points: %d - Sniper Points: %d " , noHostagePoints , sniperPoints ) ;
}
2022-09-21 14:47:36 +03:00
void BotGraph : : showFileInfo ( ) {
ctrl . msg ( " header: " ) ;
ctrl . msg ( " magic: %d " , m_graphHeader . magic ) ;
ctrl . msg ( " version: %d " , m_graphHeader . version ) ;
ctrl . msg ( " node_count: %d " , m_graphHeader . length ) ;
ctrl . msg ( " compressed_size: %dkB " , m_graphHeader . compressed / 1024 ) ;
ctrl . msg ( " uncompressed_size: %dkB " , m_graphHeader . uncompressed / 1024 ) ;
ctrl . msg ( " options: %d " , m_graphHeader . options ) ; // display as string ?
ctrl . msg ( " " ) ;
ctrl . msg ( " extensions: " ) ;
ctrl . msg ( " author: %s " , m_extenHeader . author ) ;
ctrl . msg ( " modified_by: %s " , m_extenHeader . modified ) ;
ctrl . msg ( " bsp_size: %d " , m_extenHeader . mapSize ) ;
}
2019-07-27 17:36:24 +03:00
void BotGraph : : calculatePathRadius ( int index ) {
// calculate "wayzones" for the nearest node (meaning a dynamic distance area to vary node origin)
auto & path = m_paths [ index ] ;
Vector start , direction ;
if ( ( path . flags & ( NodeFlag : : Ladder | NodeFlag : : Goal | NodeFlag : : Camp | NodeFlag : : Rescue | NodeFlag : : Crouch ) ) | | m_jumpLearnNode ) {
path . radius = 0.0f ;
return ;
}
for ( const auto & test : path . links ) {
if ( test . index ! = kInvalidNodeIndex & & ( m_paths [ test . index ] . flags & NodeFlag : : Ladder ) ) {
path . radius = 0.0f ;
return ;
}
}
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-07-27 17:36:24 +03:00
bool wayBlocked = false ;
2021-09-16 16:09:51 +03:00
for ( int32 scanDistance = 32 ; scanDistance < 128 ; scanDistance + = 16 ) {
2021-09-16 16:16:00 +03:00
auto scan = static_cast < float > ( scanDistance ) ;
2019-07-27 17:36:24 +03:00
start = path . origin ;
2021-09-16 16:16:00 +03:00
direction = Vector ( 0.0f , 0.0f , 0.0f ) . forward ( ) * scan ;
2019-07-27 17:36:24 +03:00
direction = direction . angles ( ) ;
2021-09-16 16:16:00 +03:00
path . radius = scan ;
2019-07-27 17:36:24 +03:00
2021-09-16 16:09:51 +03:00
for ( int32 circleRadius = 0 ; circleRadius < 360 ; circleRadius + = 20 ) {
2019-08-12 14:16:28 +03:00
const auto & forward = direction . forward ( ) ;
2019-07-27 17:36:24 +03:00
2021-09-16 16:16:00 +03:00
auto radiusStart = start + forward * scan ;
auto radiusEnd = start + forward * scan ;
2019-07-27 17:36:24 +03:00
game . testHull ( radiusStart , radiusEnd , TraceIgnore : : Monsters , head_hull , nullptr , & tr ) ;
if ( tr . flFraction < 1.0f ) {
game . testLine ( radiusStart , radiusEnd , TraceIgnore : : Monsters , nullptr , & tr ) ;
2019-09-21 23:20:33 +03:00
if ( tr . pHit & & strncmp ( " func_door " , tr . pHit - > v . classname . chars ( ) , 9 ) = = 0 ) {
2019-07-27 17:36:24 +03:00
path . radius = 0.0f ;
wayBlocked = true ;
break ;
}
wayBlocked = true ;
path . radius - = 16.0f ;
break ;
}
2021-09-16 16:16:00 +03:00
auto dropStart = start + forward * scan ;
auto dropEnd = dropStart - Vector ( 0.0f , 0.0f , scan + 60.0f ) ;
2019-07-27 17:36:24 +03:00
game . testHull ( dropStart , dropEnd , TraceIgnore : : Monsters , head_hull , nullptr , & tr ) ;
if ( tr . flFraction > = 1.0f ) {
wayBlocked = true ;
path . radius - = 16.0f ;
break ;
}
2021-09-16 16:16:00 +03:00
dropStart = start - forward * scan ;
dropEnd = dropStart - Vector ( 0.0f , 0.0f , scan + 60.0f ) ;
2019-07-27 17:36:24 +03:00
game . testHull ( dropStart , dropEnd , TraceIgnore : : Monsters , head_hull , nullptr , & tr ) ;
if ( tr . flFraction > = 1.0f ) {
wayBlocked = true ;
path . radius - = 16.0f ;
break ;
}
radiusEnd . z + = 34.0f ;
game . testHull ( radiusStart , radiusEnd , TraceIgnore : : Monsters , head_hull , nullptr , & tr ) ;
if ( tr . flFraction < 1.0f ) {
wayBlocked = true ;
path . radius - = 16.0f ;
break ;
}
2021-09-16 16:09:51 +03:00
direction . y = cr : : normalizeAngles ( direction . y + static_cast < float > ( circleRadius ) ) ;
2019-07-27 17:36:24 +03:00
}
if ( wayBlocked ) {
break ;
}
}
path . radius - = 16.0f ;
if ( path . radius < 0.0f ) {
path . radius = 0.0f ;
}
}
void BotGraph : : loadPractice ( ) {
if ( m_paths . empty ( ) ) {
return ;
}
// reset highest recorded damage
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
m_highestDamage [ team ] = 1 ;
}
bool dataLoaded = loadStorage < Practice > ( " prc " , " Practice " , StorageOption : : Practice , StorageVersion : : Practice , m_practice , nullptr , nullptr ) ;
2020-06-12 18:52:38 +03:00
int count = length ( ) ;
2019-07-27 17:36:24 +03:00
// set's the highest damage if loaded ok
if ( dataLoaded ) {
auto practice = m_practice . data ( ) ;
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
for ( int i = 0 ; i < count ; + + i ) {
for ( int j = 0 ; j < count ; + + j ) {
if ( i = = j ) {
if ( ( practice + ( i * count ) + j ) - > damage [ team ] > m_highestDamage [ team ] ) {
m_highestDamage [ team ] = ( practice + ( i * count ) + j ) - > damage [ team ] ;
}
}
}
}
}
return ;
}
auto practice = m_practice . data ( ) ;
// initialize table by hand to correct values, and NOT zero it out
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
for ( int i = 0 ; i < count ; + + i ) {
for ( int j = 0 ; j < count ; + + j ) {
( practice + ( i * count ) + j ) - > index [ team ] = kInvalidNodeIndex ;
( practice + ( i * count ) + j ) - > damage [ team ] = 0 ;
( practice + ( i * count ) + j ) - > value [ team ] = 0 ;
}
}
}
}
void BotGraph : : savePractice ( ) {
if ( m_paths . empty ( ) | | m_hasChanged ) {
return ;
}
saveStorage < Practice > ( " prc " , " Practice " , StorageOption : : Practice , StorageVersion : : Practice , m_practice , nullptr ) ;
}
void BotGraph : : loadVisibility ( ) {
m_needsVisRebuild = true ;
if ( m_paths . empty ( ) ) {
return ;
}
bool dataLoaded = loadStorage < uint8 > ( " vis " , " Visibility " , StorageOption : : Vistable , StorageVersion : : Vistable , m_vistable , nullptr , nullptr ) ;
// if loaded, do not recalculate visibility
if ( dataLoaded ) {
m_needsVisRebuild = false ;
}
}
void BotGraph : : saveVisibility ( ) {
if ( m_paths . empty ( ) | | m_hasChanged | | m_needsVisRebuild ) {
return ;
}
saveStorage < uint8 > ( " vis " , " Visibility " , StorageOption : : Vistable , StorageVersion : : Vistable , m_vistable , nullptr ) ;
}
bool BotGraph : : loadPathMatrix ( ) {
if ( m_paths . empty ( ) ) {
return false ;
}
bool dataLoaded = loadStorage < Matrix > ( " pmx " , " Pathmatrix " , StorageOption : : Matrix , StorageVersion : : Matrix , m_matrix , nullptr , nullptr ) ;
// do not rebuild if loaded
if ( dataLoaded ) {
return true ;
}
auto count = length ( ) ;
auto matrix = m_matrix . data ( ) ;
for ( int i = 0 ; i < count ; + + i ) {
for ( int j = 0 ; j < count ; + + j ) {
( matrix + i * count + j ) - > dist = SHRT_MAX ;
( matrix + i * count + j ) - > index = kInvalidNodeIndex ;
}
}
for ( int i = 0 ; i < count ; + + i ) {
for ( const auto & link : m_paths [ i ] . links ) {
if ( ! exists ( link . index ) ) {
continue ;
}
( matrix + ( i * count ) + link . index ) - > dist = static_cast < int16 > ( link . distance ) ;
( matrix + ( i * count ) + link . index ) - > index = static_cast < int16 > ( link . index ) ;
}
}
for ( int i = 0 ; i < count ; + + i ) {
( matrix + ( i * count ) + i ) - > dist = 0 ;
}
for ( int k = 0 ; k < count ; + + k ) {
for ( int i = 0 ; i < count ; + + i ) {
for ( int j = 0 ; j < count ; + + j ) {
int distance = ( matrix + ( i * count ) + k ) - > dist + ( matrix + ( k * count ) + j ) - > dist ;
if ( distance < ( matrix + ( i * count ) + j ) - > dist ) {
( matrix + ( i * count ) + j ) - > dist = static_cast < int16 > ( distance ) ;
( matrix + ( i * count ) + j ) - > index = static_cast < int16 > ( ( matrix + ( i * count ) + k ) - > index ) ;
}
}
}
}
savePathMatrix ( ) ; // save path matrix to file for faster access
return true ;
}
void BotGraph : : savePathMatrix ( ) {
if ( m_paths . empty ( ) ) {
return ;
}
saveStorage < Matrix > ( " pmx " , " Pathmatrix " , StorageOption : : Matrix , StorageVersion : : Matrix , m_matrix , nullptr ) ;
}
void BotGraph : : initLightLevels ( ) {
// this function get's the light level for each waypoin on the map
// no nodes ? no light levels, and only one-time init
if ( m_paths . empty ( ) | | ! cr : : fzero ( m_paths [ 0 ] . light ) ) {
return ;
}
// update light levels for all nodes
for ( auto & path : m_paths ) {
path . light = illum . getLightLevel ( path . origin ) ;
}
// disable lightstyle animations on finish (will be auto-enabled on mapchange)
illum . enableAnimation ( false ) ;
2020-01-08 18:29:28 +03:00
}
void BotGraph : : initNarrowPlaces ( ) {
// this function checks all nodes if they are inside narrow places. this is used to prevent
// bots to track hidden enemies in narrow places and prevent bots from throwing flashbangs or
// other grenades inside bad places.
// no nodes ?
if ( m_paths . empty ( ) | | m_narrowChecked ) {
return ;
}
2020-07-11 20:34:26 +03:00
constexpr int32 kNarrowPlacesMinGraphVersion = 2 ;
// if version 2 or higher, narrow places already initialized and saved into file
2022-09-21 14:47:36 +03:00
if ( m_graphHeader . version > = kNarrowPlacesMinGraphVersion ) {
2020-07-11 20:34:26 +03:00
m_narrowChecked = true ;
return ;
}
2020-01-08 18:29:28 +03:00
TraceResult tr ;
const auto distance = 178.0f ;
const auto worldspawn = game . getStartEntity ( ) ;
const auto offset = Vector ( 0.0f , 0.0f , 16.0f ) ;
// check olny paths that have not too much connections
for ( auto & path : m_paths ) {
// skip any goals and camp points
if ( path . flags & ( NodeFlag : : Camp | NodeFlag : : Goal ) ) {
continue ;
}
int linkCount = 0 ;
for ( const auto & link : path . links ) {
if ( link . index = = kInvalidNodeIndex | | link . index = = path . number ) {
continue ;
}
if ( + + linkCount > kMaxNodeLinks / 2 ) {
break ;
}
}
// skip nodes with too much connections, this indicated we're not in narrow place
if ( linkCount > kMaxNodeLinks / 2 ) {
continue ;
}
int accumWeight = 0 ;
// we could use this one!
for ( const auto & link : path . links ) {
if ( link . index = = kInvalidNodeIndex | | link . index = = path . number ) {
continue ;
}
const Vector & ang = ( ( path . origin - m_paths [ link . index ] . origin ) . normalize ( ) * distance ) . angles ( ) ;
Vector forward , right , upward ;
ang . angleVectors ( & forward , & right , & upward ) ;
// helper lambda
2020-06-12 18:52:38 +03:00
auto directionCheck = [ & ] ( const Vector & to ) - > bool {
2020-01-08 18:29:28 +03:00
game . testLine ( path . origin + offset , to , TraceIgnore : : None , nullptr , & tr ) ;
// check if we're hit worldspawn entity
if ( tr . pHit = = worldspawn & & tr . flFraction < 1.0f ) {
2020-06-12 18:52:38 +03:00
return true ;
2020-01-08 18:29:28 +03:00
}
2020-06-12 18:52:38 +03:00
return false ;
2020-01-08 18:29:28 +03:00
} ;
2020-06-12 18:52:38 +03:00
if ( directionCheck ( - forward * distance ) ) {
accumWeight + = 1 ;
}
if ( directionCheck ( right * distance ) ) {
accumWeight + = 1 ;
}
if ( directionCheck ( - right * distance ) ) {
accumWeight + = 1 ;
}
2020-01-08 18:29:28 +03:00
2020-06-12 18:52:38 +03:00
if ( directionCheck ( upward * distance ) ) {
accumWeight + = 1 ;
}
2020-01-08 18:29:28 +03:00
}
path . flags & = ~ NodeFlag : : Narrow ;
if ( accumWeight > 1 ) {
path . flags | = NodeFlag : : Narrow ;
}
}
m_narrowChecked = true ;
2019-07-27 17:36:24 +03:00
}
void BotGraph : : initNodesTypes ( ) {
m_terrorPoints . clear ( ) ;
m_ctPoints . clear ( ) ;
m_goalPoints . clear ( ) ;
m_campPoints . clear ( ) ;
m_rescuePoints . clear ( ) ;
m_sniperPoints . clear ( ) ;
m_visitedGoals . clear ( ) ;
for ( const auto & path : m_paths ) {
if ( path . flags & NodeFlag : : TerroristOnly ) {
m_terrorPoints . push ( path . number ) ;
}
else if ( path . flags & NodeFlag : : CTOnly ) {
m_ctPoints . push ( path . number ) ;
}
else if ( path . flags & NodeFlag : : Goal ) {
m_goalPoints . push ( path . number ) ;
}
else if ( path . flags & NodeFlag : : Camp ) {
m_campPoints . push ( path . number ) ;
}
else if ( path . flags & NodeFlag : : Sniper ) {
m_sniperPoints . push ( path . number ) ;
}
else if ( path . flags & NodeFlag : : Rescue ) {
m_rescuePoints . push ( path . number ) ;
}
}
}
bool BotGraph : : convertOldFormat ( ) {
MemFile fp ( getOldFormatGraphName ( true ) ) ;
2020-06-12 18:52:38 +03:00
PODGraphHeader header { } ;
2019-09-21 23:20:33 +03:00
plat . bzero ( & header , sizeof ( header ) ) ;
2019-07-27 17:36:24 +03:00
// save for faster access
2020-01-08 18:29:28 +03:00
auto map = game . getMapName ( ) ;
2019-07-27 17:36:24 +03:00
if ( fp ) {
if ( fp . read ( & header , sizeof ( header ) ) = = 0 ) {
return false ;
}
if ( strncmp ( header . header , kPodbotMagic , cr : : bufsize ( kPodbotMagic ) ) = = 0 ) {
if ( header . fileVersion ! = StorageVersion : : Podbot ) {
return false ;
}
2019-09-21 23:20:33 +03:00
else if ( ! strings . matches ( header . mapName , map ) ) {
2019-07-27 17:36:24 +03:00
return false ;
}
else {
if ( header . pointNumber = = 0 | | header . pointNumber > kMaxNodes ) {
return false ;
}
2020-09-26 18:41:53 +03:00
reset ( ) ;
2019-07-27 17:36:24 +03:00
m_paths . clear ( ) ;
for ( int i = 0 ; i < header . pointNumber ; + + i ) {
Path path { } ;
PODPath podpath { } ;
if ( fp . read ( & podpath , sizeof ( PODPath ) ) = = 0 ) {
return false ;
}
convertFromPOD ( path , podpath ) ;
// more checks of node quality
if ( path . number < 0 | | path . number > header . pointNumber ) {
return false ;
}
// add to node array
m_paths . push ( cr : : move ( path ) ) ;
}
2023-03-13 15:20:13 +03:00
fp . close ( ) ;
// save new format in case loaded older one
if ( ! m_paths . empty ( ) ) {
ctrl . msg ( " Converting old PWF to new format Graph. " ) ;
m_graphAuthor = header . author ;
// clean editor so graph will be saved with header's author
auto editor = m_editor ;
m_editor = nullptr ;
auto result = saveGraphData ( ) ;
m_editor = editor ;
return result ;
}
2019-07-27 17:36:24 +03:00
}
}
else {
return false ;
}
}
else {
return false ;
}
2023-03-13 15:20:13 +03:00
return false ;
2019-07-27 17:36:24 +03:00
}
2020-06-12 18:52:38 +03:00
template < typename U > bool BotGraph : : saveStorage ( StringRef ext , StringRef name , StorageOption options , StorageVersion version , const SmallArray < U > & data , ExtenHeader * exten ) {
bool isGraph = ! ! ( options & StorageOption : : Graph ) ;
2019-07-27 17:36:24 +03:00
String filename ;
2020-10-11 14:01:19 +03:00
filename . assignf ( " %s.%s " , game . getMapName ( ) , ext ) . lowercase ( ) ;
2019-07-27 17:36:24 +03:00
if ( data . empty ( ) ) {
2020-06-12 18:52:38 +03:00
logger . error ( " Unable to save %s file. Empty data. (filename: '%s'). " , name , filename ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
else if ( isGraph ) {
for ( auto & path : m_paths ) {
path . display = 0.0f ;
path . light = illum . getLightLevel ( path . origin ) ;
}
}
// open the file
2020-06-12 18:52:38 +03:00
File file ( strings . format ( " %s%s/%s " , getDataDirectory ( false ) , isGraph ? " graph " : " train " , filename ) , " wb " ) ;
2019-07-27 17:36:24 +03:00
// no open no fun
if ( ! file ) {
2020-06-12 18:52:38 +03:00
logger . error ( " Unable to open %s file for writing (filename: '%s'). " , name , filename ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
2020-06-12 18:52:38 +03:00
int32 rawLength = data . template length < int32 > ( ) * sizeof ( U ) ;
2019-07-29 23:11:49 +03:00
SmallArray < uint8 > compressed ( rawLength + sizeof ( uint8 ) * ULZ : : Excess ) ;
2019-07-27 17:36:24 +03:00
// try to compress
2020-07-11 20:34:26 +03:00
auto compressedLength = ulz . compress ( reinterpret_cast < uint8 * > ( data . data ( ) ) , rawLength , reinterpret_cast < uint8 * > ( compressed . data ( ) ) ) ;
2019-07-27 17:36:24 +03:00
if ( compressedLength > 0 ) {
2020-06-12 18:52:38 +03:00
StorageHeader hdr { } ;
2019-07-27 17:36:24 +03:00
hdr . magic = kStorageMagic ;
hdr . version = version ;
hdr . options = options ;
2020-06-12 18:52:38 +03:00
hdr . length = length ( ) ;
2019-07-27 17:36:24 +03:00
hdr . compressed = compressedLength ;
hdr . uncompressed = rawLength ;
file . write ( & hdr , sizeof ( StorageHeader ) ) ;
file . write ( compressed . data ( ) , sizeof ( uint8 ) , compressedLength ) ;
2020-02-08 00:03:52 +03:00
// add extension
if ( ( options & StorageOption : : Exten ) & & exten ! = nullptr ) {
file . write ( exten , sizeof ( ExtenHeader ) ) ;
2019-07-27 17:36:24 +03:00
}
2020-10-12 20:59:48 +03:00
ctrl . msg ( " Successfully saved Bots %s data. " , name ) ;
2019-07-27 17:36:24 +03:00
}
else {
2020-06-12 18:52:38 +03:00
logger . error ( " Unable to compress %s data (filename: '%s'). " , name , filename ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
return true ;
}
2020-06-12 18:52:38 +03:00
template < typename U > bool BotGraph : : loadStorage ( StringRef ext , StringRef name , StorageOption options , StorageVersion version , SmallArray < U > & data , ExtenHeader * exten , int32 * outOptions ) {
2019-07-27 17:36:24 +03:00
String filename ;
2020-06-12 18:52:38 +03:00
filename . assignf ( " %s.%s " , game . getMapName ( ) , ext ) . lowercase ( ) ;
2019-07-27 17:36:24 +03:00
// graphs can be downloaded...
2020-06-12 18:52:38 +03:00
bool isGraph = ! ! ( options & StorageOption : : Graph ) ;
MemFile file ( strings . format ( " %s%s/%s " , getDataDirectory ( true ) , isGraph ? " graph " : " train " , filename ) ) ; // open the file
2019-07-27 17:36:24 +03:00
2021-09-10 19:42:24 +03:00
data . clear ( ) ;
data . shrink ( ) ;
2019-07-27 17:36:24 +03:00
// 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 ) ;
} ;
// allocate non-graph data, so we're will be ok in case if loading will fail
if ( ! isGraph ) {
resizeData ( m_paths . length ( ) * m_paths . length ( ) ) ;
}
// if graph & attempted to load multiple times, bail out, we're failed
if ( isGraph & & + + m_loadAttempts > 2 ) {
m_loadAttempts = 0 ;
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Unable to load %s (filename: '%s') . Download process has failed as well . No nodes has been found . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
// downloader for graph
auto download = [ & ] ( ) - > bool {
2020-07-11 20:34:26 +03:00
if ( ! graph . canDownload ( ) ) {
2020-06-12 20:05:07 +03:00
return false ;
}
2020-07-11 20:34:26 +03:00
auto downloadAddress = cv_graph_url . str ( ) ;
2020-06-12 18:52:38 +03:00
auto toDownload = strings . format ( " %sgraph/%s " , getDataDirectory ( false ) , filename ) ;
2020-06-12 20:05:07 +03:00
auto fromDownload = strings . format ( " http://%s/graph/%s " , downloadAddress , filename ) ;
2019-07-27 17:36:24 +03:00
// try to download
if ( http . downloadFile ( fromDownload , toDownload ) ) {
2020-10-12 20:59:48 +03:00
ctrl . msg ( " %s file '%s' successfully downloaded. Processing... " , name , filename ) ;
2019-07-27 17:36:24 +03:00
return true ;
}
else {
2020-10-12 20:59:48 +03:00
ctrl . msg ( " Can't download '%s' from '%s' to '%s'... (%d). " , filename , fromDownload , toDownload , http . getLastStatusCode ( ) ) ;
2019-07-27 17:36:24 +03:00
}
return false ;
} ;
// tries to reload or open pwf file
auto tryReload = [ & ] ( ) - > bool {
file . close ( ) ;
if ( ! isGraph ) {
return false ;
}
if ( download ( ) ) {
2020-02-08 00:03:52 +03:00
return loadStorage < U > ( ext , name , options , version , data , exten , outOptions ) ;
2019-07-27 17:36:24 +03:00
}
if ( convertOldFormat ( ) ) {
2020-02-08 00:03:52 +03:00
return loadStorage < U > ( ext , name , options , version , data , exten , outOptions ) ;
2019-07-27 17:36:24 +03:00
}
return false ;
} ;
// no open no fun
if ( ! file ) {
if ( tryReload ( ) ) {
return true ;
}
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Unable to open %s file for reading (filename: '%s') . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
// read the header
2020-06-12 18:52:38 +03:00
StorageHeader hdr { } ;
2019-07-27 17:36:24 +03:00
file . read ( & hdr , sizeof ( StorageHeader ) ) ;
// check the magic
2020-03-12 14:32:23 +03:00
if ( hdr . magic ! = kStorageMagic & & hdr . magic ! = kStorageMagicUB ) {
2019-07-27 17:36:24 +03:00
if ( tryReload ( ) ) {
return true ;
}
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Unable to read magic of %s (filename: '%s') . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
// check the path-numbers
if ( ! isGraph & & hdr . length ! = length ( ) ) {
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Damaged %s (filename: '%s') . Mismatch number of nodes ( got : ' % d ' , need : ' % d ' ) . " , name, filename, hdr.length, m_paths.length ()) ;
2019-07-27 17:36:24 +03:00
}
// check the count
2022-10-06 17:01:18 +03:00
if ( hdr . length = = 0 | | hdr . length > kMaxNodes | | hdr . length < kMaxNodeLinks ) {
2019-07-27 17:36:24 +03:00
if ( tryReload ( ) ) {
return true ;
}
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Damaged %s (filename: '%s') . Paths length is overflowed ( got : ' % d ' ) . " , name, filename, hdr.length) ;
2019-07-27 17:36:24 +03:00
}
// check the version
2022-10-03 11:20:06 +03:00
if ( hdr . version > version & & isGraph ) {
ctrl . msg ( " Graph version mismatch %s (filename: '%s'). Version number differs (got: '%d', need: '%d') Please, upgrade %s. " , name , filename , hdr . version , version , product . name ) ;
}
else if ( hdr . version > version & & ! isGraph ) {
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Damaged %s (filename: '%s') . Version number differs ( got : ' % d ' , need : ' % d ' ) . " , name, filename, hdr.version, version) ;
2019-07-27 17:36:24 +03:00
}
2020-07-11 20:34:26 +03:00
// save graph version
if ( isGraph ) {
2022-09-21 14:47:36 +03:00
memcpy ( & m_graphHeader , & hdr , sizeof ( StorageHeader ) ) ;
2020-07-11 20:34:26 +03:00
}
2019-07-27 17:36:24 +03:00
// check the storage type
if ( ( hdr . options & options ) ! = options ) {
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Incorrect storage format for %s (filename: '%s') . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
2019-07-29 23:11:49 +03:00
SmallArray < uint8 > compressed ( hdr . compressed + sizeof ( uint8 ) * ULZ : : Excess ) ;
2019-07-27 17:36:24 +03:00
// graph is not resized upon load
if ( isGraph ) {
resizeData ( hdr . length ) ;
}
// read compressed data
if ( file . read ( compressed . data ( ) , sizeof ( uint8 ) , hdr . compressed ) = = static_cast < size_t > ( hdr . compressed ) ) {
// try to uncompress
2020-07-11 20:34:26 +03:00
if ( ulz . uncompress ( compressed . data ( ) , hdr . compressed , reinterpret_cast < uint8 * > ( data . data ( ) ) , hdr . uncompressed ) = = ULZ : : UncompressFailure ) {
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Unable to decompress ULZ data for %s (filename: '%s') . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
else {
if ( outOptions ) {
outOptions = & hdr . options ;
}
// author of graph.. save
2020-02-08 00:03:52 +03:00
if ( ( hdr . options & StorageOption : : Exten ) & & exten ! = nullptr ) {
2022-10-03 11:20:06 +03:00
auto extenSize = sizeof ( ExtenHeader ) ;
auto actuallyRead = file . read ( exten , extenSize ) * extenSize ;
2022-09-21 14:47:36 +03:00
if ( isGraph ) {
strings . copy ( m_extenHeader . author , exten - > author , cr : : bufsize ( exten - > author ) ) ;
2022-10-03 11:20:06 +03:00
if ( extenSize < = actuallyRead ) {
2022-10-10 01:51:32 +06:00
// write modified by, only if the name is different
if ( ! strings . isEmpty ( m_extenHeader . author ) & & strncmp ( m_extenHeader . author , exten - > modified , cr : : bufsize ( m_extenHeader . author ) ) ! = 0 ) {
strings . copy ( m_extenHeader . modified , exten - > modified , cr : : bufsize ( exten - > modified ) ) ;
}
2022-09-21 14:47:36 +03:00
}
else {
strings . copy ( m_extenHeader . modified , " (none) " , cr : : bufsize ( exten - > modified ) ) ;
}
m_extenHeader . mapSize = exten - > mapSize ;
}
2019-07-27 17:36:24 +03:00
}
2020-10-12 20:59:48 +03:00
ctrl . msg ( " Successfully loaded Bots %s data v%d (%d/%.2fMB). " , name , hdr . version , m_paths . length ( ) , static_cast < float > ( data . capacity ( ) * sizeof ( U ) ) / 1024.0f / 1024.0f ) ;
2019-07-27 17:36:24 +03:00
file . close ( ) ;
return true ;
}
}
else {
2020-06-12 18:52:38 +03:00
return raiseLoadingError ( isGraph , file , " Unable to read ULZ data for %s (filename: '%s') . " , name, filename) ;
2019-07-27 17:36:24 +03:00
}
}
bool BotGraph : : loadGraphData ( ) {
2020-02-08 00:03:52 +03:00
ExtenHeader exten { } ;
2019-07-27 17:36:24 +03:00
int32 outOptions = 0 ;
2022-09-21 14:47:36 +03:00
m_graphHeader = { } ;
m_extenHeader = { } ;
2019-07-27 17:36:24 +03:00
// check if loaded
2020-02-08 00:03:52 +03:00
bool dataLoaded = loadStorage < Path > ( " graph " , " Graph " , StorageOption : : Graph , StorageVersion : : Graph , m_paths , & exten , & outOptions ) ;
2019-07-27 17:36:24 +03:00
if ( dataLoaded ) {
2020-09-26 18:41:53 +03:00
reset ( ) ;
2019-07-27 17:36:24 +03:00
initBuckets ( ) ;
// add data to buckets
for ( const auto & path : m_paths ) {
addToBucket ( path . origin , path . number ) ;
}
2020-02-08 00:03:52 +03:00
if ( ( outOptions & StorageOption : : Official ) | | strncmp ( exten . author , " official " , 8 ) = = 0 | | strlen ( exten . author ) < 2 ) {
2022-10-06 17:01:18 +03:00
m_graphAuthor . assign ( product . name ) ;
2019-07-27 17:36:24 +03:00
}
else {
2022-09-21 14:47:36 +03:00
m_graphAuthor . assign ( exten . author ) ;
}
2022-10-03 11:20:06 +03:00
StringRef modified = exten . modified ;
2022-09-21 14:47:36 +03:00
2022-10-03 11:20:06 +03:00
if ( ! modified . empty ( ) & & ! modified . contains ( " (none) " ) ) {
2022-09-21 14:47:36 +03:00
m_graphModified . assign ( exten . modified ) ;
2019-07-27 17:36:24 +03:00
}
2022-09-21 14:47:36 +03:00
2019-07-27 17:36:24 +03:00
initNodesTypes ( ) ;
loadPathMatrix ( ) ;
loadVisibility ( ) ;
loadPractice ( ) ;
2020-02-08 00:03:52 +03:00
if ( exten . mapSize > 0 ) {
2021-09-20 13:50:00 +03:00
int mapSize = getBspSize ( ) ;
2020-02-08 00:03:52 +03:00
if ( mapSize ! = exten . mapSize ) {
2020-10-12 20:59:48 +03:00
ctrl . msg ( " Warning: Graph data is probably not for this map. Please check bots behaviour. " ) ;
2020-02-08 00:03:52 +03:00
}
}
2020-06-12 18:52:38 +03:00
cv_debug_goal . set ( kInvalidNodeIndex ) ;
2019-07-27 17:36:24 +03:00
return true ;
}
return false ;
}
2020-07-11 20:34:26 +03:00
bool BotGraph : : canDownload ( ) {
return ! strings . isEmpty ( cv_graph_url . str ( ) ) ;
}
2019-07-27 17:36:24 +03:00
bool BotGraph : : saveGraphData ( ) {
2020-02-08 00:03:52 +03:00
auto options = StorageOption : : Graph | StorageOption : : Exten ;
2022-10-06 17:01:18 +03:00
String editorName ;
2019-07-27 17:36:24 +03:00
2022-09-21 14:47:36 +03:00
if ( game . isNullEntity ( m_editor ) & & ! m_graphAuthor . empty ( ) ) {
2022-10-06 17:01:18 +03:00
editorName = m_graphAuthor ;
2019-07-27 17:36:24 +03:00
2020-07-11 20:34:26 +03:00
if ( ! game . isDedicated ( ) ) {
options | = StorageOption : : Recovered ;
}
2019-07-27 17:36:24 +03:00
}
else if ( ! game . isNullEntity ( m_editor ) ) {
2022-10-06 17:01:18 +03:00
editorName = m_editor - > v . netname . chars ( ) ;
2019-07-27 17:36:24 +03:00
}
else {
2022-10-06 17:01:18 +03:00
editorName = product . name ;
2019-07-27 17:36:24 +03:00
}
2020-02-08 00:03:52 +03:00
2019-07-27 17:36:24 +03:00
// mark as official
2022-10-06 17:01:18 +03:00
if ( editorName . startsWith ( product . name ) ) {
2019-07-27 17:36:24 +03:00
options | = StorageOption : : Official ;
}
2020-02-08 00:03:52 +03:00
ExtenHeader exten { } ;
2022-09-21 14:47:36 +03:00
2022-10-10 01:51:32 +06:00
// only modify the author if no author currently assigned to graph file
2022-10-06 17:01:18 +03:00
if ( m_graphAuthor . empty ( ) | | strings . isEmpty ( m_extenHeader . author ) ) {
strings . copy ( exten . author , editorName . chars ( ) , cr : : bufsize ( exten . author ) ) ;
2022-09-21 14:47:36 +03:00
}
else {
strings . copy ( exten . author , m_extenHeader . author , cr : : bufsize ( exten . author ) ) ;
}
2022-10-06 14:50:58 +03:00
// only update modified by, if name differs
2022-10-06 17:01:18 +03:00
if ( m_graphAuthor ! = editorName & & ! strings . isEmpty ( m_extenHeader . author ) ) {
strings . copy ( exten . modified , editorName . chars ( ) , cr : : bufsize ( exten . author ) ) ;
2022-10-06 14:50:58 +03:00
}
2021-09-20 13:50:00 +03:00
exten . mapSize = getBspSize ( ) ;
2020-02-08 00:03:52 +03:00
2020-07-11 20:34:26 +03:00
// ensure narrow places saved into file
m_narrowChecked = false ;
initNarrowPlaces ( ) ;
2020-02-08 00:03:52 +03:00
return saveStorage < Path > ( " graph " , " Graph " , static_cast < StorageOption > ( options ) , StorageVersion : : Graph , m_paths , & exten ) ;
2019-07-27 17:36:24 +03:00
}
void BotGraph : : saveOldFormat ( ) {
2019-09-14 23:13:55 +03:00
PODGraphHeader header { } ;
2019-09-21 23:20:33 +03:00
strings . copy ( header . header , kPodbotMagic , sizeof ( kPodbotMagic ) ) ;
strings . copy ( header . author , m_editor - > v . netname . chars ( ) , cr : : bufsize ( header . author ) ) ;
strings . copy ( header . mapName , game . getMapName ( ) , cr : : bufsize ( header . mapName ) ) ;
2019-07-27 17:36:24 +03:00
header . mapName [ 31 ] = 0 ;
header . fileVersion = StorageVersion : : Podbot ;
2020-06-12 18:52:38 +03:00
header . pointNumber = length ( ) ;
2019-07-27 17:36:24 +03:00
File fp ;
// file was opened
if ( fp . open ( getOldFormatGraphName ( ) , " wb " ) ) {
// write the node header to the file...
fp . write ( & header , sizeof ( header ) ) ;
// save the node paths...
for ( const auto & path : m_paths ) {
PODPath pod { } ;
2019-08-07 15:25:14 +03:00
convertToPOD ( path , pod ) ;
2019-07-27 17:36:24 +03:00
fp . write ( & pod , sizeof ( PODPath ) ) ;
}
fp . close ( ) ;
}
else {
logger . error ( " Error writing '%s.pwf' node file. " , game . getMapName ( ) ) ;
}
}
const char * BotGraph : : getOldFormatGraphName ( bool isMemoryFile ) {
static String buffer ;
2020-10-05 18:59:40 +03:00
buffer . assignf ( " %s/pwf/%s.pwf " , getDataDirectory ( isMemoryFile ) , game . getMapName ( ) ) ;
2019-07-27 17:36:24 +03:00
if ( File : : exists ( buffer ) ) {
return buffer . chars ( ) ;
}
2020-10-05 18:59:40 +03:00
return strings . format ( " %s/pwf/%s.pwf " , getDataDirectory ( isMemoryFile ) , game . getMapName ( ) ) ;
2019-07-27 17:36:24 +03:00
}
float BotGraph : : calculateTravelTime ( float maxSpeed , const Vector & src , const Vector & origin ) {
// this function returns 2D traveltime to a position
2021-09-20 13:50:00 +03:00
return origin . distance2d ( src ) / maxSpeed ;
2019-07-27 17:36:24 +03:00
}
bool BotGraph : : isNodeReacheable ( const Vector & src , const Vector & destination ) {
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-07-27 17:36:24 +03:00
2021-09-20 13:50:00 +03:00
float distance = destination . distance ( src ) ;
2019-07-27 17:36:24 +03:00
// is the destination not close enough?
if ( distance > m_autoPathDistance ) {
return false ;
}
// check if we go through a func_illusionary, in which case return false
game . testHull ( src , destination , TraceIgnore : : Monsters , head_hull , m_editor , & tr ) ;
2019-09-21 23:20:33 +03:00
if ( tr . pHit & & strcmp ( " func_illusionary " , tr . pHit - > v . classname . chars ( ) ) = = 0 ) {
2019-07-27 17:36:24 +03:00
return false ; // don't add pathnodes through func_illusionaries
}
// check if this node is "visible"...
game . testLine ( src , destination , TraceIgnore : : Monsters , m_editor , & tr ) ;
// if node is visible from current position (even behind head)...
2020-06-12 18:52:38 +03:00
if ( tr . pHit & & ( tr . flFraction > = 1.0f | | strncmp ( " func_door " , tr . pHit - > v . classname . chars ( ) , 9 ) = = 0 ) ) {
2019-07-27 17:36:24 +03:00
// if it's a door check if nothing blocks behind
2019-09-14 23:13:55 +03:00
if ( strncmp ( " func_door " , tr . pHit - > v . classname . chars ( ) , 9 ) = = 0 ) {
2019-07-27 17:36:24 +03:00
game . testLine ( tr . vecEndPos , destination , TraceIgnore : : Monsters , tr . pHit , & tr ) ;
if ( tr . flFraction < 1.0f ) {
return false ;
}
}
// check for special case of both nodes being in water...
if ( engfuncs . pfnPointContents ( src ) = = CONTENTS_WATER & & engfuncs . pfnPointContents ( destination ) = = CONTENTS_WATER ) {
return true ; // then they're reachable each other
}
// is dest node higher than src? (45 is max jump height)
if ( destination . z > src . z + 45.0f ) {
Vector sourceNew = destination ;
Vector destinationNew = destination ;
destinationNew . z = destinationNew . z - 50.0f ; // straight down 50 units
game . testLine ( sourceNew , destinationNew , TraceIgnore : : Monsters , m_editor , & tr ) ;
// check if we didn't hit anything, if not then it's in mid-air
if ( tr . flFraction > = 1.0 ) {
return false ; // can't reach this one
}
}
// check if distance to ground drops more than step height at points between source and destination...
Vector direction = ( destination - src ) . normalize ( ) ; // 1 unit long
Vector check = src , down = src ;
down . z = down . z - 1000.0f ; // straight down 1000 units
game . testLine ( check , down , TraceIgnore : : Monsters , m_editor , & tr ) ;
float lastHeight = tr . flFraction * 1000.0f ; // height from ground
2021-09-20 13:50:00 +03:00
distance = destination . distance ( check ) ; // distance from goal
2019-07-27 17:36:24 +03:00
while ( distance > 10.0f ) {
// move 10 units closer to the goal...
check = check + ( direction * 10.0f ) ;
down = check ;
down . z = down . z - 1000.0f ; // straight down 1000 units
game . testLine ( check , down , TraceIgnore : : Monsters , m_editor , & tr ) ;
float height = tr . flFraction * 1000.0f ; // height from ground
// is the current height greater than the step height?
if ( height < lastHeight - 18.0f ) {
return false ; // can't get there without jumping...
}
lastHeight = height ;
2021-09-20 13:50:00 +03:00
distance = destination . distance ( check ) ; // distance from goal
2019-07-27 17:36:24 +03:00
}
return true ;
}
return false ;
}
void BotGraph : : rebuildVisibility ( ) {
if ( ! m_needsVisRebuild ) {
return ;
}
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-07-27 17:36:24 +03:00
uint8 res , shift ;
for ( const auto & vis : m_paths ) {
Vector sourceDuck = vis . origin ;
Vector sourceStand = vis . origin ;
if ( vis . flags & NodeFlag : : Crouch ) {
sourceDuck . z + = 12.0f ;
sourceStand . z + = 18.0f + 28.0f ;
}
else {
sourceDuck . z + = - 18.0f + 12.0f ;
sourceStand . z + = 28.0f ;
}
uint16 standCount = 0 , crouchCount = 0 ;
for ( const auto & path : m_paths ) {
// first check ducked visibility
Vector dest = path . origin ;
game . testLine ( sourceDuck , 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 ) | | tr . fStartSolid ) {
res = 1 ;
}
else {
res = 0 ;
}
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 ) | | tr . fStartSolid ) {
res | = 1 ;
}
if ( res ! = 0 ) {
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 ( sourceDuck , 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 ) | | tr . fStartSolid ) {
res | = 2 ;
}
else {
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 ) | | tr . fStartSolid ) {
res | = 1 ;
}
else {
res & = 2 ;
}
}
shift = ( path . number % 4 ) < < 1 ;
m_vistable [ vis . number * m_paths . length ( ) + path . number ] & = ~ ( 3 < < shift ) ;
m_vistable [ vis . number * m_paths . length ( ) + path . number ] | = res < < shift ;
if ( ! ( res & 2 ) ) {
+ + crouchCount ;
}
if ( ! ( res & 1 ) ) {
+ + standCount ;
}
}
m_paths [ vis . number ] . vis . crouch = crouchCount ;
m_paths [ vis . number ] . vis . stand = standCount ;
}
m_needsVisRebuild = false ;
saveVisibility ( ) ;
}
bool BotGraph : : isVisible ( int srcIndex , int destIndex ) {
if ( ! exists ( srcIndex ) | | ! exists ( destIndex ) ) {
return false ;
}
uint8 res = m_vistable [ srcIndex * m_paths . length ( ) + destIndex ] ;
res > > = ( destIndex % 4 ) < < 1 ;
return ! ( ( res & 3 ) = = 3 ) ;
}
bool BotGraph : : isDuckVisible ( int srcIndex , int destIndex ) {
if ( ! exists ( srcIndex ) | | ! exists ( destIndex ) ) {
return false ;
}
uint8 res = m_vistable [ srcIndex * m_paths . length ( ) + destIndex ] ;
res > > = ( destIndex % 4 ) < < 1 ;
return ! ( ( res & 2 ) = = 2 ) ;
}
bool BotGraph : : isStandVisible ( int srcIndex , int destIndex ) {
if ( ! exists ( srcIndex ) | | ! exists ( destIndex ) ) {
return false ;
}
uint8 res = m_vistable [ srcIndex * m_paths . length ( ) + destIndex ] ;
res > > = ( destIndex % 4 ) < < 1 ;
return ! ( ( res & 1 ) = = 1 ) ;
}
void BotGraph : : frame ( ) {
// this function executes frame of graph operation code.
if ( game . isNullEntity ( m_editor ) ) {
return ; // this function is only valid with editor, and in graph enabled mode.
}
// keep the clipping mode enabled, or it can be turned off after new round has started
if ( graph . hasEditFlag ( GraphEdit : : Noclip ) & & util . isAlive ( m_editor ) ) {
m_editor - > v . movetype = MOVETYPE_NOCLIP ;
}
2019-08-04 18:22:01 +03:00
float nearestDistance = kInfiniteDistance ;
2019-07-27 17:36:24 +03:00
int nearestIndex = kInvalidNodeIndex ;
// check if it's time to add jump node
if ( m_jumpLearnNode ) {
if ( ! m_endJumpPoint ) {
if ( m_editor - > v . button & IN_JUMP ) {
2021-09-20 13:50:00 +03:00
add ( NodeAddFlag : : JumpStart ) ;
2019-07-27 17:36:24 +03:00
2019-08-18 21:00:00 +03:00
m_timeJumpStarted = game . time ( ) ;
2019-07-27 17:36:24 +03:00
m_endJumpPoint = true ;
}
else {
m_learnVelocity = m_editor - > v . velocity ;
m_learnPosition = m_editor - > v . origin ;
}
}
2019-08-18 21:00:00 +03:00
else if ( ( ( m_editor - > v . flags & FL_ONGROUND ) | | m_editor - > v . movetype = = MOVETYPE_FLY ) & & m_timeJumpStarted + 0.1f < game . time ( ) & & m_endJumpPoint ) {
2021-09-20 13:50:00 +03:00
add ( NodeAddFlag : : JumpEnd ) ;
2019-07-27 17:36:24 +03:00
m_jumpLearnNode = false ;
m_endJumpPoint = false ;
}
}
// check if it's a auto-add-node mode enabled
if ( hasEditFlag ( GraphEdit : : Auto ) & & ( m_editor - > v . flags & ( FL_ONGROUND | FL_PARTIALGROUND ) ) ) {
// find the distance from the last used node
2021-09-20 13:50:00 +03:00
float distance = m_lastNode . distanceSq ( m_editor - > v . origin ) ;
2019-07-27 17:36:24 +03:00
2019-08-18 21:00:00 +03:00
if ( distance > cr : : square ( 128.0f ) ) {
2019-07-27 17:36:24 +03:00
// check that no other reachable nodes are nearby...
for ( const auto & path : m_paths ) {
if ( isNodeReacheable ( m_editor - > v . origin , path . origin ) ) {
2021-09-20 13:50:00 +03:00
distance = path . origin . distanceSq ( m_editor - > v . origin ) ;
2019-07-27 17:36:24 +03:00
if ( distance < nearestDistance ) {
nearestDistance = distance ;
}
}
}
// make sure nearest node is far enough away...
if ( nearestDistance > = cr : : square ( 128.0f ) ) {
2021-09-20 13:50:00 +03:00
add ( NodeAddFlag : : Normal ) ; // place a node here
2019-07-27 17:36:24 +03:00
}
}
}
m_facingAtIndex = getFacingIndex ( ) ;
// reset the minimal distance changed before
2019-08-04 18:22:01 +03:00
nearestDistance = kInfiniteDistance ;
2019-07-27 17:36:24 +03:00
// now iterate through all nodes in a map, and draw required ones
for ( auto & path : m_paths ) {
2021-09-20 13:50:00 +03:00
float distance = path . origin . distance ( m_editor - > v . origin ) ;
2019-07-27 17:36:24 +03:00
// check if node is whitin a distance, and is visible
2022-01-21 23:39:26 +03:00
if ( distance < cv_graph_draw_distance . float_ ( ) & & ( ( util . isVisible ( path . origin , m_editor ) & & util . isInViewCone ( path . origin , m_editor ) ) | | ! util . isAlive ( m_editor ) | | distance < 64.0f ) ) {
2019-07-27 17:36:24 +03:00
// check the distance
if ( distance < nearestDistance ) {
nearestIndex = path . number ;
nearestDistance = distance ;
}
2022-01-21 23:39:26 +03:00
if ( path . display + 1.0f < game . time ( ) ) {
2019-07-27 17:36:24 +03:00
float nodeHeight = 0.0f ;
// check the node height
if ( path . flags & NodeFlag : : Crouch ) {
nodeHeight = 36.0f ;
}
else {
nodeHeight = 72.0f ;
}
float nodeHalfHeight = nodeHeight * 0.5f ;
// all nodes are by default are green
2020-06-12 18:52:38 +03:00
Color nodeColor { - 1 , - 1 , - 1 } ;
2019-07-27 17:36:24 +03:00
// colorize all other nodes
2023-01-18 22:32:38 +03:00
if ( path . flags & NodeFlag : : Goal ) {
2020-06-12 18:52:38 +03:00
nodeColor = { 128 , 0 , 255 } ;
2019-07-27 17:36:24 +03:00
}
else if ( path . flags & NodeFlag : : Ladder ) {
2020-06-12 18:52:38 +03:00
nodeColor = { 128 , 64 , 0 } ;
2019-07-27 17:36:24 +03:00
}
else if ( path . flags & NodeFlag : : Rescue ) {
2020-06-12 18:52:38 +03:00
nodeColor = { 255 , 255 , 255 } ;
2019-07-27 17:36:24 +03:00
}
2023-01-18 22:32:38 +03:00
else if ( path . flags & NodeFlag : : Camp ) {
if ( path . flags & NodeFlag : : TerroristOnly ) {
nodeColor = { 255 , 160 , 160 } ;
}
else if ( path . flags & NodeFlag : : CTOnly ) {
nodeColor = { 255 , 160 , 255 } ;
}
else {
nodeColor = { 0 , 255 , 255 } ;
}
}
else if ( path . flags & NodeFlag : : TerroristOnly ) {
nodeColor = { 255 , 0 , 0 } ;
}
else if ( path . flags & NodeFlag : : CTOnly ) {
nodeColor = { 0 , 0 , 255 } ;
}
2019-07-27 17:36:24 +03:00
else {
2020-06-12 18:52:38 +03:00
nodeColor = { 0 , 255 , 0 } ;
2019-07-27 17:36:24 +03:00
}
// colorize additional flags
2020-06-12 18:52:38 +03:00
Color nodeFlagColor { - 1 , - 1 , - 1 } ;
2019-07-27 17:36:24 +03:00
// check the colors
if ( path . flags & NodeFlag : : Sniper ) {
2020-06-12 18:52:38 +03:00
nodeFlagColor = { 130 , 87 , 0 } ;
2019-07-27 17:36:24 +03:00
}
else if ( path . flags & NodeFlag : : NoHostage ) {
2020-06-12 18:52:38 +03:00
nodeFlagColor = { 255 , 255 , 255 } ;
2019-07-27 17:36:24 +03:00
}
2023-01-18 22:32:38 +03:00
else if ( path . flags & NodeFlag : : Lift ) {
nodeFlagColor = { 255 , 0 , 255 } ;
2019-07-27 17:36:24 +03:00
}
int nodeWidth = 14 ;
if ( exists ( m_facingAtIndex ) & & path . number = = m_facingAtIndex ) {
nodeWidth * = 2 ;
}
// draw node without additional flags
if ( nodeFlagColor . red = = - 1 ) {
game . drawLine ( m_editor , path . origin - Vector ( 0 , 0 , nodeHalfHeight ) , path . origin + Vector ( 0 , 0 , nodeHalfHeight ) , nodeWidth + 1 , 0 , nodeColor , 250 , 0 , 10 ) ;
}
// draw node with flags
else {
game . drawLine ( m_editor , path . origin - Vector ( 0 , 0 , nodeHalfHeight ) , path . origin - Vector ( 0 , 0 , nodeHalfHeight - nodeHeight * 0.75f ) , nodeWidth , 0 , nodeColor , 250 , 0 , 10 ) ; // draw basic path
game . drawLine ( m_editor , path . origin - Vector ( 0 , 0 , nodeHalfHeight - nodeHeight * 0.75f ) , path . origin + Vector ( 0 , 0 , nodeHalfHeight ) , nodeWidth , 0 , nodeFlagColor , 250 , 0 , 10 ) ; // draw additional path
}
2019-08-18 21:00:00 +03:00
path . display = game . time ( ) ;
2019-07-27 17:36:24 +03:00
}
}
}
if ( nearestIndex = = kInvalidNodeIndex ) {
return ;
}
// draw arrow to a some importaint nodes
if ( exists ( m_findWPIndex ) | | exists ( m_cacheNodeIndex ) | | exists ( m_facingAtIndex ) ) {
// check for drawing code
2019-08-18 21:00:00 +03:00
if ( m_arrowDisplayTime + 0.5f < game . time ( ) ) {
2019-07-27 17:36:24 +03:00
// finding node - pink arrow
if ( m_findWPIndex ! = kInvalidNodeIndex ) {
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , m_editor - > v . origin , m_paths [ m_findWPIndex ] . origin , 10 , 0 , { 128 , 0 , 128 } , 200 , 0 , 5 , DrawLine : : Arrow ) ;
2019-07-27 17:36:24 +03:00
}
// cached node - yellow arrow
if ( m_cacheNodeIndex ! = kInvalidNodeIndex ) {
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , m_editor - > v . origin , m_paths [ m_cacheNodeIndex ] . origin , 10 , 0 , { 255 , 255 , 0 } , 200 , 0 , 5 , DrawLine : : Arrow ) ;
2019-07-27 17:36:24 +03:00
}
// node user facing at - white arrow
if ( m_facingAtIndex ! = kInvalidNodeIndex ) {
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , m_editor - > v . origin , m_paths [ m_facingAtIndex ] . origin , 10 , 0 , { 255 , 255 , 255 } , 200 , 0 , 5 , DrawLine : : Arrow ) ;
2019-07-27 17:36:24 +03:00
}
2019-08-18 21:00:00 +03:00
m_arrowDisplayTime = game . time ( ) ;
2019-07-27 17:36:24 +03:00
}
}
// draw a paths, camplines and danger directions for nearest node
2022-01-21 23:39:26 +03:00
if ( nearestDistance < = 56.0f & & m_pathDisplayTime < game . time ( ) ) {
m_pathDisplayTime = game . time ( ) + 0.96f ;
2019-07-27 17:36:24 +03:00
2020-10-03 16:39:09 +03:00
// create path pointer for faster access
auto & path = m_paths [ nearestIndex ] ;
2019-07-27 17:36:24 +03:00
// draw the camplines
if ( path . flags & NodeFlag : : Camp ) {
float height = 36.0f ;
// check if it's a source
if ( path . flags & NodeFlag : : Crouch ) {
height = 18.0f ;
}
2019-08-12 14:16:28 +03:00
const auto & source = Vector ( path . origin . x , path . origin . y , path . origin . z + height ) ; // source
const auto & start = path . origin + Vector ( path . start . x , path . start . y , 0.0f ) . forward ( ) * 500.0f ; // camp start
const auto & end = path . origin + Vector ( path . end . x , path . end . y , 0.0f ) . forward ( ) * 500.0f ; // camp end
2019-07-27 17:36:24 +03:00
// draw it now
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , source , start , 10 , 0 , { 255 , 0 , 0 } , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , source , end , 10 , 0 , { 255 , 0 , 0 } , 200 , 0 , 10 ) ;
2019-07-27 17:36:24 +03:00
}
// draw the connections
for ( const auto & link : path . links ) {
if ( link . index = = kInvalidNodeIndex ) {
continue ;
}
// jump connection
if ( link . flags & PathFlag : : Jump ) {
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , path . origin , m_paths [ link . index ] . origin , 5 , 0 , { 255 , 0 , 128 } , 200 , 0 , 10 ) ;
2019-07-27 17:36:24 +03:00
}
else if ( isConnected ( link . index , nearestIndex ) ) { // twoway connection
2020-06-12 18:52:38 +03:00
game . drawLine ( m_editor , path . origin , m_paths [ link . index ] . origin , 5 , 0 , { 255 , 255 , 0 } , 200 , 0 , 10 ) ;
2019-07-27 17:36:24 +03:00
}
else { // oneway connection
2023-01-18 22:32:38 +03:00
game . drawLine ( m_editor , path . origin , m_paths [ link . index ] . origin , 5 , 0 , { 255 , 255 , 255 } , 200 , 0 , 10 ) ;
2019-07-27 17:36:24 +03:00
}
}
// now look for oneway incoming connections
for ( const auto & connected : m_paths ) {
if ( isConnected ( connected . number , path . number ) & & ! isConnected ( path . number , connected . number ) ) {
2023-01-18 22:32:38 +03:00
game . drawLine ( m_editor , path . origin , connected . origin , 5 , 0 , { 0 , 192 , 96 } , 200 , 0 , 10 ) ;
2019-07-27 17:36:24 +03:00
}
}
// draw the radius circle
Vector origin = ( path . flags & NodeFlag : : Crouch ) ? path . origin : path . origin - Vector ( 0.0f , 0.0f , 18.0f ) ;
2020-06-12 18:52:38 +03:00
Color radiusColor { 0 , 0 , 255 } ;
2019-07-27 17:36:24 +03:00
// if radius is nonzero, draw a full circle
if ( path . radius > 0.0f ) {
2020-06-12 18:52:38 +03:00
float sqr = cr : : sqrtf ( cr : : square ( path . radius ) * 0.5f ) ;
2022-01-21 23:39:26 +03:00
2019-07-27 17:36:24 +03:00
game . drawLine ( m_editor , origin + Vector ( path . radius , 0.0f , 0.0f ) , origin + Vector ( sqr , - sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( sqr , - sqr , 0.0f ) , origin + Vector ( 0.0f , - path . radius , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( 0.0f , - path . radius , 0.0f ) , origin + Vector ( - sqr , - sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( - sqr , - sqr , 0.0f ) , origin + Vector ( - path . radius , 0.0f , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( - path . radius , 0.0f , 0.0f ) , origin + Vector ( - sqr , sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( - sqr , sqr , 0.0f ) , origin + Vector ( 0.0f , path . radius , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( 0.0f , path . radius , 0.0f ) , origin + Vector ( sqr , sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( sqr , sqr , 0.0f ) , origin + Vector ( path . radius , 0.0f , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
}
else {
float sqr = cr : : sqrtf ( 32.0f ) ;
game . drawLine ( m_editor , origin + Vector ( sqr , - sqr , 0.0f ) , origin + Vector ( - sqr , sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
game . drawLine ( m_editor , origin + Vector ( - sqr , - sqr , 0.0f ) , origin + Vector ( sqr , sqr , 0.0f ) , 5 , 0 , radiusColor , 200 , 0 , 10 ) ;
}
// draw the danger directions
if ( ! m_hasChanged ) {
2022-04-24 19:28:36 +06:00
int dangerIndexT = getDangerIndex ( Team : : Terrorist , nearestIndex , nearestIndex ) ;
int dangerIndexCT = getDangerIndex ( Team : : CT , nearestIndex , nearestIndex ) ;
2019-07-27 17:36:24 +03:00
2022-04-24 19:28:36 +06:00
if ( exists ( dangerIndexT ) ) {
game . drawLine ( m_editor , path . origin , m_paths [ dangerIndexT ] . origin , 15 , 0 , { 255 , 0 , 0 } , 200 , 0 , 10 , DrawLine : : Arrow ) ; // draw a red arrow to this index's danger point
}
if ( exists ( dangerIndexCT ) ) {
game . drawLine ( m_editor , path . origin , m_paths [ dangerIndexCT ] . origin , 15 , 0 , { 0 , 0 , 255 } , 200 , 0 , 10 , DrawLine : : Arrow ) ; // draw a blue arrow to this index's danger point
2019-07-27 17:36:24 +03:00
}
}
2022-01-21 23:39:26 +03:00
static int channel = 0 ;
auto sendHudMessage = [ ] ( Color color , float x , float y , edict_t * to , StringRef text ) {
MessageWriter ( MSG_ONE_UNRELIABLE , SVC_TEMPENTITY , nullptr , to )
. writeByte ( TE_TEXTMESSAGE )
. writeByte ( channel + + & 0xff ) // channel
. writeShort ( MessageWriter : : fs16 ( x , 13.0f ) ) // x
. writeShort ( MessageWriter : : fs16 ( y , 13.0f ) ) // y
. writeByte ( 0 ) // effect
. writeByte ( color . red ) // r1
. writeByte ( color . green ) // g1
. writeByte ( color . blue ) // b1
. writeByte ( 1 ) // a1
. writeByte ( color . red ) // r2
. writeByte ( color . green ) // g2
. writeByte ( color . blue ) // b2
. writeByte ( 1 ) // a2
. writeShort ( 0 ) // fadeintime
. writeShort ( 0 ) // fadeouttime
. writeShort ( MessageWriter : : fu16 ( 1.0f , 8.0f ) ) // holdtime
. writeString ( text . chars ( ) ) ;
if ( channel > 3 ) {
channel = 0 ;
}
} ;
// very helpful stuff..
auto getNodeData = [ & ] ( StringRef type , int node ) - > String {
String message , flags ;
2019-07-27 17:36:24 +03:00
2022-01-21 23:39:26 +03:00
const auto & path = m_paths [ node ] ;
2019-07-27 17:36:24 +03:00
bool jumpPoint = false ;
// iterate through connections and find, if it's a jump path
for ( const auto & link : path . links ) {
// check if we got a valid connection
if ( link . index ! = kInvalidNodeIndex & & ( link . flags & PathFlag : : Jump ) ) {
jumpPoint = true ;
}
}
2022-01-21 23:39:26 +03:00
flags . assignf ( " %s%s%s%s%s%s%s%s%s%s%s%s " , ( path . flags & NodeFlag : : Lift ) ? " LIFT " : " " , ( path . flags & NodeFlag : : Crouch ) ? " CROUCH " : " " , ( path . flags & NodeFlag : : Camp ) ? " CAMP " : " " , ( path . flags & NodeFlag : : TerroristOnly ) ? " TERRORIST " : " " , ( path . flags & NodeFlag : : CTOnly ) ? " CT " : " " , ( path . flags & NodeFlag : : Sniper ) ? " SNIPER " : " " , ( path . flags & NodeFlag : : Goal ) ? " GOAL " : " " , ( path . flags & NodeFlag : : Ladder ) ? " LADDER " : " " , ( path . flags & NodeFlag : : Rescue ) ? " RESCUE " : " " , ( path . flags & NodeFlag : : DoubleJump ) ? " JUMPHELP " : " " , ( path . flags & NodeFlag : : NoHostage ) ? " NOHOSTAGE " : " " , jumpPoint ? " JUMP " : " " ) ;
2019-07-27 17:36:24 +03:00
2022-01-21 23:39:26 +03:00
if ( flags . empty ( ) ) {
flags . assign ( " (none) " ) ;
2020-06-12 18:52:38 +03:00
}
2019-08-29 23:31:54 +03:00
2022-01-21 23:39:26 +03:00
// show the information about that point
message . assignf ( " %s node: \n "
" Node %d of %d, Radius: %.1f, Light: %.1f \n "
" Flags: %s \n "
" Origin: (%.1f, %.1f, %.1f) \n " , type , node , m_paths . length ( ) - 1 , path . radius , path . light , flags , path . origin . x , path . origin . y , path . origin . z ) ;
return message ;
2019-08-29 23:31:54 +03:00
} ;
2019-07-27 17:36:24 +03:00
// display some information
2022-01-21 23:39:26 +03:00
sendHudMessage ( { 255 , 255 , 255 } , 0.0f , 0.025f , m_editor , getNodeData ( " Current " , nearestIndex ) ) ;
// check if we need to show the cached point index
if ( m_cacheNodeIndex ! = kInvalidNodeIndex ) {
sendHudMessage ( { 255 , 255 , 255 } , 0.28f , 0.16f , m_editor , getNodeData ( " Cached " , m_cacheNodeIndex ) ) ;
}
2019-07-27 17:36:24 +03:00
2022-01-21 23:39:26 +03:00
// check if we need to show the facing point index
if ( m_facingAtIndex ! = kInvalidNodeIndex ) {
sendHudMessage ( { 255 , 255 , 255 } , 0.28f , 0.025f , m_editor , getNodeData ( " Facing " , m_facingAtIndex ) ) ;
}
String timeMessage = strings . format ( " Map: %s, Time: %s \n " , game . getMapName ( ) , util . getCurrentDateTime ( ) ) ;
2019-07-27 17:36:24 +03:00
// if node is not changed display experience also
if ( ! m_hasChanged ) {
int dangerIndexCT = getDangerIndex ( Team : : CT , nearestIndex , nearestIndex ) ;
int dangerIndexT = getDangerIndex ( Team : : Terrorist , nearestIndex , nearestIndex ) ;
2022-01-21 23:39:26 +03:00
String practice ;
practice . assignf ( " Node practice data (index / damage): \n "
" CT: %d / %d \n "
" T: %d / %d \n \n " , dangerIndexCT , dangerIndexCT ! = kInvalidNodeIndex ? getDangerDamage ( Team : : CT , nearestIndex , dangerIndexCT ) : 0 , dangerIndexT , dangerIndexT ! = kInvalidNodeIndex ? getDangerDamage ( Team : : Terrorist , nearestIndex , dangerIndexT ) : 0 ) ;
2019-07-27 17:36:24 +03:00
2022-01-21 23:39:26 +03:00
sendHudMessage ( { 255 , 255 , 255 } , 0.0f , 0.16f , m_editor , practice + timeMessage ) ;
}
else {
sendHudMessage ( { 255 , 255 , 255 } , 0.0f , 0.16f , m_editor , timeMessage ) ;
2019-07-27 17:36:24 +03:00
}
}
}
bool BotGraph : : isConnected ( int index ) {
for ( const auto & path : m_paths ) {
if ( path . number = = index ) {
continue ;
}
for ( const auto & test : path . links ) {
if ( test . index = = index ) {
return true ;
}
}
}
return false ;
}
bool BotGraph : : checkNodes ( bool teleportPlayer ) {
auto teleport = [ & ] ( const Path & path ) - > void {
if ( teleportPlayer ) {
engfuncs . pfnSetOrigin ( m_editor , path . origin ) ;
setEditFlag ( GraphEdit : : On | GraphEdit : : Noclip ) ;
}
} ;
int terrPoints = 0 ;
int ctPoints = 0 ;
int goalPoints = 0 ;
int rescuePoints = 0 ;
ctrl . setFromConsole ( true ) ;
for ( const auto & path : m_paths ) {
int connections = 0 ;
if ( path . number ! = static_cast < int > ( m_paths . index ( path ) ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d path differs from index %d. " , path . number , m_paths . index ( path ) ) ;
2019-07-27 17:36:24 +03:00
break ;
}
for ( const auto & test : path . links ) {
if ( test . index ! = kInvalidNodeIndex ) {
if ( test . index > length ( ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d connected with invalid node %d. " , path . number , test . index ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
+ + connections ;
break ;
}
}
if ( connections = = 0 ) {
if ( ! isConnected ( path . number ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d isn't connected with any other node. " , path . number ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
}
if ( path . flags & NodeFlag : : Camp ) {
2019-08-18 21:00:00 +03:00
if ( path . end . empty ( ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d camp-endposition not set. " , path . number ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
}
else if ( path . flags & NodeFlag : : TerroristOnly ) {
+ + terrPoints ;
}
else if ( path . flags & NodeFlag : : CTOnly ) {
+ + ctPoints ;
}
else if ( path . flags & NodeFlag : : Goal ) {
+ + goalPoints ;
}
else if ( path . flags & NodeFlag : : Rescue ) {
+ + rescuePoints ;
}
for ( const auto & test : path . links ) {
if ( test . index ! = kInvalidNodeIndex ) {
if ( ! exists ( test . index ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d path index %d out of range. " , path . number , test . index ) ;
2019-07-27 17:36:24 +03:00
teleport ( path ) ;
return false ;
}
else if ( test . index = = path . number ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Node %d path index %d points to itself. " , path . number , test . index ) ;
2019-07-27 17:36:24 +03:00
teleport ( path ) ;
return false ;
}
}
}
}
if ( game . mapIs ( MapFlags : : HostageRescue ) ) {
if ( rescuePoints = = 0 ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " You didn't set a rescue point. " ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
}
if ( terrPoints = = 0 ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " You didn't set any terrorist important point. " ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
else if ( ctPoints = = 0 ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " You didn't set any CT important point. " ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
else if ( goalPoints = = 0 ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " You didn't set any goal point. " ) ;
2019-07-27 17:36:24 +03:00
return false ;
}
// perform DFS instead of floyd-warshall, this shit speedup this process in a bit
PathWalk walk ;
2020-12-18 20:06:05 +03:00
walk . init ( m_paths . length ( ) ) ;
2019-07-27 17:36:24 +03:00
Array < bool > visited ;
visited . resize ( m_paths . length ( ) ) ;
// first check incoming connectivity, initialize the "visited" table
for ( auto & visit : visited ) {
visit = false ;
}
2020-11-23 00:06:18 +03:00
walk . add ( 0 ) ; // always check from node number 0
2019-07-27 17:36:24 +03:00
while ( ! walk . empty ( ) ) {
// pop a node from the stack
const int current = walk . first ( ) ;
walk . shift ( ) ;
visited [ current ] = true ;
for ( const auto & link : m_paths [ current ] . links ) {
int index = link . index ;
// skip this node as it's already visited
if ( exists ( index ) & & ! visited [ index ] ) {
visited [ index ] = true ;
2020-11-23 00:06:18 +03:00
walk . add ( index ) ;
2019-07-27 17:36:24 +03:00
}
}
}
for ( const auto & path : m_paths ) {
if ( ! visited [ path . number ] ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Path broken from node 0 to node %d. " , path . number ) ;
2019-07-27 17:36:24 +03:00
teleport ( path ) ;
return false ;
}
}
// then check outgoing connectivity
Array < IntArray > outgoingPaths ; // store incoming paths for speedup
outgoingPaths . resize ( m_paths . length ( ) ) ;
for ( const auto & path : m_paths ) {
outgoingPaths [ path . number ] . resize ( m_paths . length ( ) + 1 ) ;
for ( const auto & link : path . links ) {
if ( exists ( link . index ) ) {
outgoingPaths [ link . index ] . push ( path . number ) ;
}
}
}
// initialize the "visited" table
for ( auto & visit : visited ) {
visit = false ;
}
walk . clear ( ) ;
2020-11-23 00:06:18 +03:00
walk . add ( 0 ) ; // always check from node number 0
2019-07-27 17:36:24 +03:00
while ( ! walk . empty ( ) ) {
const int current = walk . first ( ) ; // pop a node from the stack
walk . shift ( ) ;
for ( auto & outgoing : outgoingPaths [ current ] ) {
if ( visited [ outgoing ] ) {
continue ; // skip this node as it's already visited
}
visited [ outgoing ] = true ;
2020-11-23 00:06:18 +03:00
walk . add ( outgoing ) ;
2019-07-27 17:36:24 +03:00
}
}
for ( const auto & path : m_paths ) {
if ( ! visited [ path . number ] ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Path broken from node %d to node 0. " , path . number ) ;
2019-07-27 17:36:24 +03:00
teleport ( path ) ;
return false ;
}
}
return true ;
}
int BotGraph : : getPathDist ( int srcIndex , int destIndex ) {
if ( ! exists ( srcIndex ) | | ! exists ( destIndex ) ) {
return 1 ;
}
return ( m_matrix . data ( ) + ( srcIndex * length ( ) ) + destIndex ) - > dist ;
}
void BotGraph : : setVisited ( int index ) {
if ( ! exists ( index ) ) {
return ;
}
if ( ! isVisited ( index ) & & ( m_paths [ index ] . flags & NodeFlag : : Goal ) ) {
m_visitedGoals . push ( index ) ;
}
}
void BotGraph : : clearVisited ( ) {
m_visitedGoals . clear ( ) ;
}
bool BotGraph : : isVisited ( int index ) {
for ( auto & visited : m_visitedGoals ) {
if ( visited = = index ) {
return true ;
}
}
return false ;
}
void BotGraph : : addBasic ( ) {
// this function creates basic node types on map
// first of all, if map contains ladder points, create it
2019-08-18 21:00:00 +03:00
game . searchEntities ( " classname " , " func_ladder " , [ & ] ( edict_t * ent ) {
2019-07-27 17:36:24 +03:00
Vector ladderLeft = ent - > v . absmin ;
Vector ladderRight = ent - > v . absmax ;
ladderLeft . z = ladderRight . z ;
2019-09-21 23:20:33 +03:00
TraceResult tr { } ;
2019-07-27 17:36:24 +03:00
Vector up , down , front , back ;
2019-08-18 21:00:00 +03:00
const Vector & diff = ( ( ladderLeft - ladderRight ) ^ Vector ( 0.0f , 0.0f , 0.0f ) ) . normalize ( ) * 15.0f ;
2021-09-16 16:09:51 +03:00
front = back = game . getEntityOrigin ( ent ) ;
2019-07-27 17:36:24 +03:00
front = front + diff ; // front
back = back - diff ; // back
up = down = front ;
down . z = ent - > v . absmax . z ;
game . testHull ( down , up , TraceIgnore : : Monsters , point_hull , nullptr , & tr ) ;
if ( engfuncs . pfnPointContents ( up ) = = CONTENTS_SOLID | | ! cr : : fequal ( tr . flFraction , 1.0f ) ) {
up = down = back ;
down . z = ent - > v . absmax . z ;
}
game . testHull ( down , up - Vector ( 0.0f , 0.0f , 1000.0f ) , TraceIgnore : : Monsters , point_hull , nullptr , & tr ) ;
up = tr . vecEndPos ;
Vector point = up + Vector ( 0.0f , 0.0f , 39.0f ) ;
m_isOnLadder = true ;
do {
if ( getNearestNoBuckets ( point , 50.0f ) = = kInvalidNodeIndex ) {
2021-09-20 13:50:00 +03:00
add ( NodeAddFlag : : NoHostage , point ) ;
2019-07-27 17:36:24 +03:00
}
point . z + = 160 ;
} while ( point . z < down . z - 40.0f ) ;
point = down + Vector ( 0.0f , 0.0f , 38.0f ) ;
if ( getNearestNoBuckets ( point , 50.0f ) = = kInvalidNodeIndex ) {
2021-09-20 13:50:00 +03:00
add ( NodeAddFlag : : NoHostage , point ) ;
2019-07-27 17:36:24 +03:00
}
m_isOnLadder = false ;
2019-08-18 21:00:00 +03:00
return EntitySearchResult : : Continue ;
} ) ;
2019-07-27 17:36:24 +03:00
2019-08-18 21:00:00 +03:00
auto autoCreateForEntity = [ ] ( int type , const char * entity ) {
game . searchEntities ( " classname " , entity , [ & ] ( edict_t * ent ) {
2021-09-16 16:09:51 +03:00
const Vector & pos = game . getEntityOrigin ( ent ) ;
2019-07-27 17:36:24 +03:00
if ( graph . getNearestNoBuckets ( pos , 50.0f ) = = kInvalidNodeIndex ) {
graph . add ( type , pos ) ;
}
2019-08-18 21:00:00 +03:00
return EntitySearchResult : : Continue ;
} ) ;
2019-07-27 17:36:24 +03:00
} ;
2021-09-20 13:50:00 +03:00
autoCreateForEntity ( NodeAddFlag : : Normal , " info_player_deathmatch " ) ; // then terrortist spawnpoints
autoCreateForEntity ( NodeAddFlag : : Normal , " info_player_start " ) ; // then add ct spawnpoints
autoCreateForEntity ( NodeAddFlag : : Normal , " info_vip_start " ) ; // then vip spawnpoint
autoCreateForEntity ( NodeAddFlag : : Normal , " armoury_entity " ) ; // weapons on the map ?
2019-07-27 17:36:24 +03:00
2021-09-20 13:50:00 +03:00
autoCreateForEntity ( NodeAddFlag : : Rescue , " func_hostage_rescue " ) ; // hostage rescue zone
autoCreateForEntity ( NodeAddFlag : : Rescue , " info_hostage_rescue " ) ; // hostage rescue zone (same as above)
2019-07-27 17:36:24 +03:00
2021-09-20 13:50:00 +03:00
autoCreateForEntity ( NodeAddFlag : : Goal , " func_bomb_target " ) ; // bombspot zone
autoCreateForEntity ( NodeAddFlag : : Goal , " info_bomb_target " ) ; // bombspot zone (same as above)
autoCreateForEntity ( NodeAddFlag : : Goal , " hostage_entity " ) ; // hostage entities
2022-01-03 07:18:36 +01:00
autoCreateForEntity ( NodeAddFlag : : Goal , " monster_scientist " ) ; // hostage entities (same as above)
2021-09-20 13:50:00 +03:00
autoCreateForEntity ( NodeAddFlag : : Goal , " func_vip_safetyzone " ) ; // vip rescue (safety) zone
autoCreateForEntity ( NodeAddFlag : : Goal , " func_escapezone " ) ; // terrorist escape zone
2019-07-27 17:36:24 +03:00
}
void BotGraph : : eraseFromDisk ( ) {
// this function removes graph file from the hard disk
StringArray forErase ;
2021-09-20 13:50:00 +03:00
bots . kickEveryone ( true ) ;
2020-02-08 00:03:52 +03:00
auto map = game . getMapName ( ) ;
auto data = getDataDirectory ( ) ;
2019-07-27 17:36:24 +03:00
// if we're delete graph, delete all corresponding to it files
2020-10-05 18:59:40 +03:00
forErase . push ( strings . format ( " %spwf/%s.pwf " , data , map ) ) ; // graph itself
2020-11-23 00:06:18 +03:00
forErase . push ( strings . format ( " %strain/%s.prc " , data , map ) ) ; // corresponding to practice
2020-06-12 18:52:38 +03:00
forErase . push ( strings . format ( " %strain/%s.vis " , data , map ) ) ; // corresponding to vistable
forErase . push ( strings . format ( " %strain/%s.pmx " , data , map ) ) ; // corresponding to matrix
2020-02-08 00:03:52 +03:00
forErase . push ( strings . format ( " %sgraph/%s.graph " , data , map ) ) ; // new format graph
2019-07-27 17:36:24 +03:00
for ( const auto & item : forErase ) {
if ( File : : exists ( item ) ) {
2019-07-28 21:42:32 +03:00
plat . removeFile ( item . chars ( ) ) ;
2020-10-12 20:59:48 +03:00
ctrl . msg ( " File %s, has been deleted from the hard disk " , item ) ;
2019-07-27 17:36:24 +03:00
}
else {
2020-06-12 18:52:38 +03:00
logger . error ( " Unable to open %s " , item ) ;
2019-07-27 17:36:24 +03:00
}
}
2020-09-26 18:41:53 +03:00
reset ( ) ; // reintialize points
2019-07-27 17:36:24 +03:00
m_paths . clear ( ) ;
}
const char * BotGraph : : getDataDirectory ( bool isMemoryFile ) {
static String buffer ;
buffer . clear ( ) ;
if ( isMemoryFile ) {
2020-06-12 18:52:38 +03:00
buffer . assignf ( " addons/%s/data/ " , product . folder ) ;
2019-07-27 17:36:24 +03:00
}
else {
2020-06-15 11:28:42 +03:00
buffer . assignf ( " %s/addons/%s/data/ " , game . getRunningModName ( ) , product . folder ) ;
2019-07-27 17:36:24 +03:00
}
return buffer . chars ( ) ;
}
2019-08-24 12:43:42 +03:00
void BotGraph : : setBombOrigin ( bool reset , const Vector & pos ) {
2019-07-27 17:36:24 +03:00
// this function stores the bomb position as a vector
2020-06-12 18:52:38 +03:00
if ( ! game . mapIs ( MapFlags : : Demolition ) | | ! bots . isBombPlanted ( ) ) {
return ;
}
2019-07-27 17:36:24 +03:00
if ( reset ) {
2019-08-24 12:43:42 +03:00
m_bombOrigin = nullptr ;
2019-07-27 17:36:24 +03:00
bots . setBombPlanted ( false ) ;
return ;
}
if ( ! pos . empty ( ) ) {
2019-08-24 12:43:42 +03:00
m_bombOrigin = pos ;
2019-07-27 17:36:24 +03:00
return ;
}
2020-06-12 18:52:38 +03:00
bool wasFound = false ;
2020-12-15 15:28:58 +03:00
auto bombModel = conf . getBombModelName ( ) ;
2019-08-18 21:00:00 +03:00
game . searchEntities ( " classname " , " grenade " , [ & ] ( edict_t * ent ) {
2020-12-15 15:28:58 +03:00
if ( util . isModel ( ent , bombModel ) ) {
2021-09-16 16:09:51 +03:00
m_bombOrigin = game . getEntityOrigin ( ent ) ;
2020-06-12 18:52:38 +03:00
wasFound = true ;
2019-08-18 21:00:00 +03:00
return EntitySearchResult : : Break ;
2019-07-27 17:36:24 +03:00
}
2019-08-18 21:00:00 +03:00
return EntitySearchResult : : Continue ;
} ) ;
2020-06-12 18:52:38 +03:00
if ( ! wasFound ) {
m_bombOrigin = nullptr ;
bots . setBombPlanted ( false ) ;
}
2019-07-27 17:36:24 +03:00
}
void BotGraph : : startLearnJump ( ) {
m_jumpLearnNode = true ;
}
void BotGraph : : setSearchIndex ( int index ) {
m_findWPIndex = index ;
if ( exists ( m_findWPIndex ) ) {
2019-11-30 23:18:37 +03:00
ctrl . msg ( " Showing direction to node %d. " , m_findWPIndex ) ;
2019-07-27 17:36:24 +03:00
}
else {
m_findWPIndex = kInvalidNodeIndex ;
}
}
BotGraph : : BotGraph ( ) {
m_endJumpPoint = false ;
m_needsVisRebuild = false ;
m_jumpLearnNode = false ;
m_hasChanged = false ;
2020-10-01 10:43:51 +03:00
m_narrowChecked = false ;
2019-07-27 17:36:24 +03:00
m_timeJumpStarted = 0.0f ;
m_lastJumpNode = kInvalidNodeIndex ;
m_cacheNodeIndex = kInvalidNodeIndex ;
m_findWPIndex = kInvalidNodeIndex ;
m_facingAtIndex = kInvalidNodeIndex ;
m_loadAttempts = 0 ;
m_isOnLadder = false ;
m_terrorPoints . clear ( ) ;
m_ctPoints . clear ( ) ;
m_goalPoints . clear ( ) ;
m_campPoints . clear ( ) ;
m_rescuePoints . clear ( ) ;
m_sniperPoints . clear ( ) ;
m_loadAttempts = 0 ;
m_editFlags = 0 ;
2020-07-11 20:34:26 +03:00
2019-07-27 17:36:24 +03:00
m_pathDisplayTime = 0.0f ;
m_arrowDisplayTime = 0.0f ;
m_autoPathDistance = 250.0f ;
m_matrix . clear ( ) ;
m_practice . clear ( ) ;
m_editor = nullptr ;
}
void BotGraph : : initBuckets ( ) {
for ( int x = 0 ; x < kMaxBucketsInsidePos ; + + x ) {
for ( int y = 0 ; y < kMaxBucketsInsidePos ; + + y ) {
for ( int z = 0 ; z < kMaxBucketsInsidePos ; + + z ) {
m_buckets [ x ] [ y ] [ z ] . reserve ( kMaxNodesInsideBucket ) ;
m_buckets [ x ] [ y ] [ z ] . clear ( ) ;
}
}
}
}
void BotGraph : : addToBucket ( const Vector & pos , int index ) {
const auto & bucket = locateBucket ( pos ) ;
m_buckets [ bucket . x ] [ bucket . y ] [ bucket . z ] . push ( index ) ;
}
void BotGraph : : eraseFromBucket ( const Vector & pos , int index ) {
const auto & bucket = locateBucket ( pos ) ;
auto & data = m_buckets [ bucket . x ] [ bucket . y ] [ bucket . z ] ;
for ( size_t i = 0 ; i < data . length ( ) ; + + i ) {
if ( data [ i ] = = index ) {
data . erase ( i , 1 ) ;
break ;
}
}
}
BotGraph : : Bucket BotGraph : : locateBucket ( const Vector & pos ) {
constexpr auto size = static_cast < float > ( kMaxNodes * 2 ) ;
return {
cr : : abs ( static_cast < int > ( ( pos . x + size ) / kMaxBucketSize ) ) ,
cr : : abs ( static_cast < int > ( ( pos . y + size ) / kMaxBucketSize ) ) ,
cr : : abs ( static_cast < int > ( ( pos . z + size ) / kMaxBucketSize ) )
} ;
}
2019-07-29 23:11:49 +03:00
const SmallArray < int32 > & BotGraph : : getNodesInBucket ( const Vector & pos ) {
2019-07-27 17:36:24 +03:00
const auto & bucket = locateBucket ( pos ) ;
return m_buckets [ bucket . x ] [ bucket . y ] [ bucket . z ] ;
}
void BotGraph : : updateGlobalPractice ( ) {
// this function called after each end of the round to update knowledge about most dangerous nodes for each team.
// no nodes, no experience used or nodes edited or being edited?
if ( m_paths . empty ( ) | | m_hasChanged ) {
return ; // no action
}
bool adjustValues = false ;
auto practice = m_practice . data ( ) ;
// get the most dangerous node for this position for both teams
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
int bestIndex = kInvalidNodeIndex ; // best index to store
2020-10-03 16:39:09 +03:00
2019-07-27 17:36:24 +03:00
for ( int i = 0 ; i < length ( ) ; + + i ) {
2020-10-03 16:39:09 +03:00
int maxDamage = 0 ;
2019-07-27 17:36:24 +03:00
bestIndex = kInvalidNodeIndex ;
for ( int j = 0 ; j < length ( ) ; + + j ) {
if ( i = = j ) {
continue ;
}
int actDamage = getDangerDamage ( team , i , j ) ;
if ( actDamage > maxDamage ) {
maxDamage = actDamage ;
bestIndex = j ;
}
}
if ( maxDamage > kMaxPracticeDamageValue ) {
adjustValues = true ;
}
( practice + ( i * length ( ) ) + i ) - > index [ team ] = static_cast < int16 > ( bestIndex ) ;
}
}
2020-06-12 18:52:38 +03:00
constexpr auto kHalfDamageVal = static_cast < int > ( kMaxPracticeDamageValue * 0.5 ) ;
2019-07-27 17:36:24 +03:00
// adjust values if overflow is about to happen
if ( adjustValues ) {
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
for ( int i = 0 ; i < length ( ) ; + + i ) {
for ( int j = 0 ; j < length ( ) ; + + j ) {
if ( i = = j ) {
continue ;
}
2020-06-12 18:52:38 +03:00
( practice + ( i * length ( ) ) + j ) - > damage [ team ] = static_cast < int16 > ( cr : : clamp ( getDangerDamage ( team , i , j ) - kHalfDamageVal , 0 , kMaxPracticeDamageValue ) ) ;
2019-07-27 17:36:24 +03:00
}
}
}
}
for ( int team = Team : : Terrorist ; team < kGameTeamNum ; + + team ) {
2020-06-12 18:52:38 +03:00
m_highestDamage [ team ] = cr : : clamp ( m_highestDamage [ team ] - kHalfDamageVal , 1 , kMaxPracticeDamageValue ) ;
2019-07-27 17:36:24 +03:00
}
}
void BotGraph : : unassignPath ( int from , int to ) {
auto & link = m_paths [ from ] . links [ to ] ;
link . index = kInvalidNodeIndex ;
link . distance = 0 ;
link . flags = 0 ;
2019-08-18 21:00:00 +03:00
link . velocity = nullptr ;
2019-07-27 17:36:24 +03:00
setEditFlag ( GraphEdit : : On ) ;
m_hasChanged = true ;
}
void BotGraph : : convertFromPOD ( Path & path , const PODPath & pod ) {
path = { } ;
path . number = pod . number ;
path . flags = pod . flags ;
path . origin = pod . origin ;
path . start = Vector ( pod . csx , pod . csy , 0.0f ) ;
path . end = Vector ( pod . cex , pod . cey , 0.0f ) ;
2020-06-12 18:52:38 +03:00
if ( cv_graph_fixcamp . bool_ ( ) ) {
2019-07-27 17:36:24 +03:00
convertCampDirection ( path ) ;
}
path . radius = pod . radius ;
path . light = 0.0f ;
path . display = 0.0f ;
for ( int i = 0 ; i < kMaxNodeLinks ; + + i ) {
path . links [ i ] . index = pod . index [ i ] ;
path . links [ i ] . distance = pod . distance [ i ] ;
path . links [ i ] . flags = pod . conflags [ i ] ;
path . links [ i ] . velocity = pod . velocity [ i ] ;
}
path . vis . stand = pod . vis . stand ;
path . vis . crouch = pod . vis . crouch ;
}
2019-08-07 15:25:14 +03:00
void BotGraph : : convertToPOD ( const Path & path , PODPath & pod ) {
2019-07-27 17:36:24 +03:00
pod = { } ;
pod . number = path . number ;
pod . flags = path . flags ;
pod . origin = path . origin ;
pod . radius = path . radius ;
pod . csx = path . start . x ;
pod . csy = path . start . y ;
pod . cex = path . end . x ;
pod . cey = path . end . y ;
for ( int i = 0 ; i < kMaxNodeLinks ; + + i ) {
pod . index [ i ] = path . links [ i ] . index ;
pod . distance [ i ] = path . links [ i ] . distance ;
pod . conflags [ i ] = path . links [ i ] . flags ;
pod . velocity [ i ] = path . links [ i ] . velocity ;
}
pod . vis . stand = path . vis . stand ;
pod . vis . crouch = path . vis . crouch ;
}
void BotGraph : : convertCampDirection ( Path & path ) {
// this function converts old vector based camp directions to angles, note that podbotmm graph
// are already saved with angles, and converting this stuff may result strange look directions.
if ( m_paths . empty ( ) ) {
return ;
}
const Vector & offset = path . origin + Vector ( 0.0f , 0.0f , ( path . flags & NodeFlag : : Crouch ) ? 15.0f : 17.0f ) ;
path . start = ( Vector ( path . start . x , path . start . y , path . origin . z ) - offset ) . angles ( ) ;
path . end = ( Vector ( path . end . x , path . end . y , path . origin . z ) - offset ) . angles ( ) ;
path . start . x = - path . start . x ;
path . end . x = - path . end . x ;
path . start . clampAngles ( ) ;
path . end . clampAngles ( ) ;
}
int BotGraph : : getDangerIndex ( int team , int start , int goal ) {
if ( team ! = Team : : Terrorist & & team ! = Team : : CT ) {
return kInvalidNodeIndex ;
}
// realiablity check
if ( ! exists ( start ) | | ! exists ( goal ) ) {
return kInvalidNodeIndex ;
}
return ( m_practice . data ( ) + ( start * length ( ) ) + goal ) - > index [ team ] ;
}
int BotGraph : : getDangerValue ( int team , int start , int goal ) {
if ( team ! = Team : : Terrorist & & team ! = Team : : CT ) {
return 0 ;
}
// reliability check
if ( ! exists ( start ) | | ! exists ( goal ) ) {
return 0 ;
}
return ( m_practice . data ( ) + ( start * length ( ) ) + goal ) - > value [ team ] ;
}
int BotGraph : : getDangerDamage ( int team , int start , int goal ) {
if ( team ! = Team : : Terrorist & & team ! = Team : : CT ) {
return 0 ;
}
// reliability check
if ( ! exists ( start ) | | ! exists ( goal ) ) {
return 0 ;
}
return ( m_practice . data ( ) + ( start * length ( ) ) + goal ) - > damage [ team ] ;
}
void BotGraph : : setDangerValue ( int team , int start , int goal , int value ) {
if ( team ! = Team : : Terrorist & & team ! = Team : : CT ) {
return ;
}
// reliability check
if ( ! exists ( start ) | | ! exists ( goal ) ) {
return ;
}
( m_practice . data ( ) + ( start * length ( ) ) + goal ) - > value [ team ] = static_cast < int16 > ( value ) ;
}
void BotGraph : : setDangerDamage ( int team , int start , int goal , int value ) {
if ( team ! = Team : : Terrorist & & team ! = Team : : CT ) {
return ;
}
// reliability check
if ( ! exists ( start ) | | ! exists ( goal ) ) {
return ;
}
( m_practice . data ( ) + ( start * length ( ) ) + goal ) - > damage [ team ] = static_cast < int16 > ( value ) ;
}