Node graphs for deathmatch maps
You can help by adding links to this article from other relevant articles.
January 2024
If a mod wishes to use maps that have no nodegraph (e.g. HL2DM maps), and has no access to the uncompiled .vmf
map (e.g. HL2DM maps), then any NPCs they spawn are going to be severely limited in their navigation ability. This article describes a technique for creating a node graph for an already-compiled map, by manually saving node coordinates to a text file, then building a complete .ain
nodegraph from that text file when reloading the map. As a valid .ain
is produced, this technique need only be done once per map, and indeed, the .txt
files need not be distributed with the mod, although the .ain
files will be required.
This technique is designed not to interfere with existing node graphs, or the map_edit node placement process. The first section will describe how to load a text file containing coordinates, create nodes at each specified point, then build & save a node graph. The second section will cover how to create the text file list of coordinates in the first place.
Graph creation
At the end of the public
section of ai_networkmanager.h, add the following function:
void SetRebuildNeeded(bool b=true) { m_bNeedGraphRebuild = b; }
We are going to need a way of specifying a rebuild (and save) are needed from a static context, so this function is required.
Around line 892 of ai_networkmanager.cpp, you should see the following if
block:
if ( CAI_NetworkManager::IsAIFileCurrent( STRING( gpGlobals->mapname ) ) ) { pNetwork->LoadNetworkGraph(); if ( !g_bAIDisabledByUser ) { CAI_BaseNPC::m_nDebugBits &= ~bits_debugDisableAI; } }
Change the if
to an else if
, and immediately before this block, add this if
block:
if ( CAI_NetworkManager::IsTextFileNewer( STRING( gpGlobals->mapname ) ) ) { char szNodeTextFilename[MAX_PATH];// text node coordinate filename Q_snprintf( szNodeTextFilename, sizeof( szNodeTextFilename ), "maps/graphs/%s%s.txt", STRING( gpGlobals->mapname ), GetPlatformExt() ); CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); if ( filesystem->ReadFile( szNodeTextFilename, "game", buf ) ) { Msg("Parsing .txt node file!\n"); if (!buf.Size()) return; const int maxLen = 64; char line[ maxLen ]; CUtlVector<char*> floats; int num = 0; // loop through every line of the file, read it in while( true ) { buf.GetLine( line, maxLen ); if ( Q_strlen(line) <= 0 ) break; // reached the end of the file // we've read in a string containing 3 tab separated floats // we need to split this into 3 floats, which we put in a vector V_SplitString( line, " ", floats ); Vector origin( atof( floats[0] ), atof( floats[1] ), atof( floats[2] ) ); //Msg("Parsed Vector(%.2f, %.2f, %.2f)\n",origin.x,origin.y,origin.z); floats.PurgeAndDeleteElements(); // now we want to create a CNodeEnt (info_node) at the location these coordinates describe CNodeEnt *pNode = (CNodeEnt*)CreateNoSpawn( "info_node", origin, vec3_angle, NULL ); if ( pNode ) {// setting this index stops it moaning, doesn't seem to affect anything else though pNode->m_NodeData.nWCNodeID = g_pAINetworkManager->GetEditOps()->m_nNextWCIndex; pNode->Spawn(); // spawning it adds it into the node graph num ++; } if ( !buf.IsValid() ) break; } // when the DelayedInit is reached, will rebuild the graph from these nodes, // and save it out in the .ain file so we don't need to do this next time pNetwork->SetRebuildNeeded(); Msg("Created %i nodes from text file\n",num); } }
Now we just need to define & declare this IsTextFileNewer
function. In ai_networkmanager.h, just under
static bool IsAIFileCurrent( const char *szMapName );
add
static bool IsTextFileNewer( const char *szMapName );
Lastly in ai_networkmanager.cpp, find IsAIFileCurrent
, and just after that function, add
//----------------------------------------------------------------------------- // Purpose: Returns true if there's a node text file, and its newer than the nodegraph file //----------------------------------------------------------------------------- bool CAI_NetworkManager::IsTextFileNewer( const char *szMapName ) { char szGraphFilename[MAX_PATH]; char szTextFilename[MAX_PATH]; Q_snprintf( szGraphFilename, sizeof( szGraphFilename ), "maps/graphs/%s%s.ain", szMapName, GetPlatformExt() ); Q_snprintf( szTextFilename, sizeof( szTextFilename ), "maps/graphs/%s%s.txt", szMapName, GetPlatformExt() ); if ( !filesystem->FileExists( szTextFilename ) ) return false; // if there's no text file, we clearly CANT use it int iCompare; if ( engine->CompareFileTime( szTextFilename, szGraphFilename, &iCompare ) ) { if ( iCompare > 0 ) { Msg("Text file is newer\n"); return true; // text file is newer } } Msg("Network file is newer\n"); return false; }
If no .ain
nodegraph is found, or if the .txt
file exists and is newer than the .ain
, it will load the .txt
file from the maps/graphs/
folder, and will create info_nodes
at every point it specifies. A rebuild (and save) of the nodegraph will then be scheduled.
Manual Coordinate Specification
The code will now create a node graph based upon the coordinates of a .txt file, but a method is needed of populating that text file with reliable coordinates. The method listed here is by no means as complex as node placement in map_edit mode. It will require the user to set sv_cheats 1
, and then use the new cheat startnodes
to begin node placement. The commands addnode
and delnode
will add/remove a node where the player is currently standing, and a visual marker will be created. The text file will be saved when the user enters the savenodes
command. If desired, this process can occur in a multiplayer game with multiple (developer) players present, all placing nodes, to speed up the process, but if this occurs, ensure that they all properly understand nodegraph placement, or the quality of the graph will suffer.
In your gamerules (probably hl2mp_gamerules.cpp), add a few includes (server only)
#include "Sprite.h" #include "utlbuffer.h" #include "filesystem.h"
Now paste in these 4 ConCommands
and their relevant functions somewhere between #ifndef CLIENT_DLL
and #endif
. If necessary, place these defines yourself at the end of the file, and paste it there.
static bool bInNodePlacement = false; static CUtlVector<CBaseEntity*> pNodes; CBaseEntity *CreateNode(Vector origin) { CBaseEntity *pEnt = CSprite::SpriteCreate( "sprites/glow01.vmt", origin, false ); pNodes.AddToTail(pEnt); return pEnt; } void CC_StartNodes( void ) { if ( bInNodePlacement ) return; // if the text file already exists, load its current nodes char szNodeTextFilename[MAX_PATH]; Q_snprintf( szNodeTextFilename, sizeof( szNodeTextFilename ), "maps/graphs/%s%s.txt", STRING( gpGlobals->mapname ), GetPlatformExt() ); CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); if ( filesystem->ReadFile( szNodeTextFilename, "game", buf ) ) { if (!buf.Size()) return; const int maxLen = 64; char line[ maxLen ]; CUtlVector<char*> floats; int num = 0; // loop through every line of the file, read it in while( true ) { buf.GetLine( line, maxLen ); if ( Q_strlen(line) <= 0 ) break; // reached the end of the file // we've read in a string containing 3 tab separated floats // we need to split this into 3 floats, which we put in a vector V_SplitString( line, " ", floats ); Vector origin( atof( floats[0] ), atof( floats[1] ), atof( floats[2] ) ); floats.PurgeAndDeleteElements(); CreateNode(origin); num ++; if ( !buf.IsValid() ) break; } } bInNodePlacement = true; UTIL_ClientPrintAll(HUD_PRINTTALK,"Entered node placement mode. Use addnode and delnode commands to place / remove nodes at your feet. Use savenodes command to finish.\n"); } static ConCommand cc_startnodes("startnodes", CC_StartNodes, "Start manually placing nodegraph elements, with addnode and delnode commands. Finish with savenodes.", FCVAR_CHEAT); void CC_SaveNodes( void ) { if ( !bInNodePlacement ) return; // save the nodes char szNodeTextFilename[MAX_PATH]; Q_snprintf( szNodeTextFilename, sizeof( szNodeTextFilename ), "maps/graphs/%s%s.txt", STRING( gpGlobals->mapname ), GetPlatformExt() ); CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); for ( int i=0; i<pNodes.Size(); i++ ) { buf.PutString( UTIL_VarArgs("%f %f %f\n",pNodes[i]->GetAbsOrigin().x, pNodes[i]->GetAbsOrigin().y, pNodes[i]->GetAbsOrigin().z) ); } filesystem->WriteFile(szNodeTextFilename,"default_write_path",buf); // clean up & exit node mode pNodes.PurgeAndDeleteElements(); bInNodePlacement = false; UTIL_ClientPrintAll(HUD_PRINTTALK,"Saved nodes & exited node placement mode. Reload map to build nodegraph.\n"); } static ConCommand cc_savenodes("savenodes", CC_SaveNodes, "Finish manually placing nodegraph elements, and save the .txt", FCVAR_CHEAT); void CC_AddNode( void ) { if ( !bInNodePlacement ) return; CBasePlayer *pPlayer = UTIL_PlayerByIndex( UTIL_GetCommandClientIndex() ); Vector vecOrigin = pPlayer->GetAbsOrigin() + Vector(0,0,10); CreateNode(vecOrigin); UTIL_ClientPrintAll(HUD_PRINTCONSOLE,UTIL_VarArgs("Added node at %.0f, %.0f, %.0f\n",vecOrigin.x,vecOrigin.y,vecOrigin.z)); } static ConCommand cc_addnode("addnode", CC_AddNode, "Manually place a nodegraph element at your feet. First, set startnodes.", FCVAR_CHEAT); void CC_UndoNode( void ) { if ( !bInNodePlacement ) return; if ( pNodes.Size() > 0 ) { UTIL_Remove( pNodes.Tail() ); pNodes.Remove( pNodes.Size()-1 ); UTIL_ClientPrintAll(HUD_PRINTCONSOLE,"Last-placed node removed\n"); } else UTIL_ClientPrintAll(HUD_PRINTTALK,"Node list is empty\n"); } static ConCommand cc_delnode("undonode", CC_UndoNode, "Delete the last placed node. First, set startnodes.", FCVAR_CHEAT);
Node placement guide
To place nodes, load the map, type sv_cheats 1
in the console, followed by startnodes
. May I recommend binding the addnode
and undonode
commands to (eg) the middle mouse and an easily accessible key. Run around the map, placing nodes in a sensible manner, then type savenodes. At this point it would be sensible to quit (or minimise the game), and check that maps/graphs/mapname.txt
exists. Restart or restore the game, and load the same map again. Your nodegraph should be created, and if all is well, you may delete the text file, although keeping it is recommended, in case you need to make changes later.
Conclusion
Once a graph is created, NPCs should be able to navigate any map without problems. Freely redistributable nodegraphs for all the default hl2dm maps are available here, created using this technique. While the use of these graphs will not require the implementation of any of the changes described here, creating nodegraphs for any other maps certainly will. Note that for any maps for which you or a member of your team is the author, the node graph should still be created in the original manner, by filling the map with info_nodes
.