Node graphs for deathmatch maps

From Valve Developer Community
Jump to navigation Jump to search

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.