Resetting the Map

From Valve Developer Community
Jump to: navigation, search
Dead End - Icon.png
This article has no links to other VDC articles. Please help improve this article by adding links that are relevant to the context within the existing text.
January 2024

Introduction

While you can pretty much copy and paste the provided here into your mod - it's best to read through it and gain an understanding of what's going on, to that end I'm going to explain each section of code as I deal with it. What I won't do is tell you where to find the files mentioned, it should be simple enough if you have the SDK open when reading this article.

The goal of this article is to enable a mod to restart a round by resetting all map entities to their default locations, the code provided is for the Source Only SDK (not the HL2MP SDK), it may still work in the HL2MP SDK but it has not been tested!

Note: I have had success with this. It can be seen in use in The Battlegrounds II. The code is available with each release or at svn://bgmod.com/code/ - Draco Houston, a BG2 coder.

There are a couple of things we need to do to restart a round, we need to get a copy of the map entity list, store a filter for entities we don't want reset and finally add some logic to gamerules that specifies what to do with all that stored information.

Creating the entity filter

As there is no built in filter class that is suitable for us to use we need to create our own. So, add two new empty files to your server project: mapfilter.cpp and mapfilter.h. - I recommend creating a seperate mod folder for any new files you add to the solution and also adding a prefix to the file names to distinguish between valve code and mod code.

Mapfilter.h

#include "cbase.h"
#include "mapentities.h"
#include "UtlSortVector.h"
 
#ifndef CMAPENTITYFILTER_H
#define CMAPENTITYFILTER_H
 
typedef const char* strptr;
 
static bool StrLessThan(  const strptr &src1,  const strptr &src2, void *pCtx ) 
{
	if( strcmp(src1, src2) >= 0)
		return false;
	else
		return true;
}
 
class CMapEntityFilter : public IMapEntityFilter
{
public:
	// constructor
	CMapEntityFilter();
	// deconstructor
	~CMapEntityFilter();
	
	// used to check if we should reset an entity or not
	virtual bool ShouldCreateEntity( const char *pClassname );
	// creates the next entity in our stored list.
	virtual CBaseEntity* CreateNextEntity( const char *pClassname );
	// add an entity to our list
	void AddKeep( const char*);
 
private:
	// our list of entities to keep
	CUtlSortVector< const char* > *keepList;
};
 
#endif 

MapFilter.cpp

#include "cbase.h"
#include "mapfilter.h"
 
// Constructor
CMapEntityFilter::CMapEntityFilter() 
{
	keepList = new CUtlSortVector< const char *> (StrLessThan);
}
 
// Deconstructor
CMapEntityFilter::~CMapEntityFilter() 
{
	delete keepList;
}
 
// [bool] ShouldCreateEntity [char]
// Purpose   : Used to check if the passed in entity is on our stored list
// Arguments : The classname of an entity 
// Returns   : Boolean value - if we have it stored, we return false.
 
bool CMapEntityFilter::ShouldCreateEntity( const char *pClassname ) 
{
	//Check if the entity is in our keep list.
	if( keepList->Find( pClassname ) >= 0 )
		return false;
	else
		return true;
}
 
// [CBaseEntity] CreateNextEntity [char]
// Purpose   : Creates an entity
// Arguments : The classname of an entity
// Returns   : A pointer to the new entity
 
CBaseEntity* CMapEntityFilter::CreateNextEntity( const char *pClassname ) 
{
	return CreateEntityByName( pClassname);
}
 
// [void] AddKeep [char]
// Purpose   : Adds the passed in value to our list of items to keep
// Arguments : The class name of an entity
// Returns   : Void
 
void CMapEntityFilter::AddKeep( const char *sz) 
{
	keepList->Insert(sz);
}

This code is based on code provided by stefaniii of the VERC forums, the original code and discussion can be found in the following post: Tutorial: Creating a Roundtimer (VERC forums) (http://www.chatbear.com/board.plm?a=viewthread&t=549,1104207081,14733&b=4991&id=&v=flatold&s=20).

Putting it all together

So, we have the building blocks in place it's time to make the magic happen! We need to make some changes to our gamerules, namely we want to add a RestartRound() function. So open up sdk_gamerules.h

// only include the mapfilter if we're in the server dll.
#ifdef GAME_DLL
	#include "mapfilter.h"
#endif

Add the include and surrounding #if block to the top of the file, then using a similar #if block declare the restart round function and an instance of the filter class - like so:

#ifdef GAME_DLL
	// restart round function declaration
	void RestartRound(void);
 
	// map entity filter instance.
	CMapEntityFilter filter;
#endif

With that done save sdk_gamerules.h and open sdk_gamerules.cpp.

Somewhere sensible (I'd recommend the constructor) put the following:

	filter.AddKeep("worldspawn");
	filter.AddKeep("soundent");
	filter.AddKeep("sdk_gamerules");
	filter.AddKeep("scene_manager");
	filter.AddKeep("predicted_viewmodel");
	filter.AddKeep("sdk_team_manager");
	filter.AddKeep("event_queue_saveload_proxy");
	filter.AddKeep("player_manager");
	filter.AddKeep("player");
	filter.AddKeep("info_player_start");
	filter.AddKeep("ai_network");

This creates our list of entities to keep, with further work your mod may require more of these to be added, simply use

filter.AddKeep("entity_name");

Though I'd hope that is fairly obvious by now!

Now for the core of the matter, the RestartRound() function!

// [void] RestartRound [void]
// Purpose   : Trigger the resetting of all map entities and the respawning of all players.
// Arguments : Void
// Returns   : Void
 
void CSDKGameRules::RestartRound()
{
	CBaseEntity *pEnt;
	CBaseEntity *tmpEnt;
 
	// find the first entity in the entity list
	pEnt = gEntList.FirstEnt();
 
	// as long as we've got a valid pointer, keep looping through the list
	while (pEnt != NULL) {
		if (filter.ShouldCreateEntity (pEnt->GetClassname() ) )
		{
			// if we don't need to keep the entity, we remove it from the list
			tmpEnt = gEntList.NextEnt (pEnt);
			UTIL_Remove (pEnt);
			pEnt = tmpEnt;
		}	
		else
		{
			// if we need to keep it, we move on to the next entity
			pEnt = gEntList.NextEnt (pEnt);
		}
	} 
 
        // force the entities we've set to be removed to actually be removed
        gEntList.CleanupDeleteList();
 
	// with any unrequired entities removed, we use MapEntity_ParseAllEntities to reparse the map entities
	// this in effect causes them to spawn back to their normal position.
	MapEntity_ParseAllEntities( engine->GetMapEntitiesString(), &filter, true);
 
	// print a message to all clients telling them that the round is restarting
	UTIL_ClientPrintAll( HUD_PRINTCENTER, "Round restarting..." );
 
	// now we've got all our entities back in place and looking pretty, we need to respawn all the players
	for (int i = 1; i <= gpGlobals->maxClients; i++ )
	{
		CBaseEntity *plr = UTIL_PlayerByIndex( i );
 
		if ( plr )
		{
			plr->Spawn();
		} else {
			break;
		}
	} 
	roundRestart = false;		
}

The comments should explain what this function does and how it does it - there is one thing to note, the function does not deal with client side temporary effects (debris, decals etc). I will not cover that in this tutorial right now but an update may be forthcoming in the long run.

There are likely better, more efficient ways of doing this, but I've found this method works without any hassle and very little effort. The code contained in this article works very well when used in conjunction with Creating a Roundtimer.

UPDATE : now uses engine->GetMapEntitiesString() instead of the custom cache of the entity list. Forced the entities removed with UTIL_Remove to actually be removed from the list as soon as possible.