Resetting the Map
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.