Resetting Maps and Entities
Introduction
The example code provided can be used by copying and pasting, but it is best to ensure you read through the rest of the text to gain a proper understanding of the process behind it. Each section of code is explained thoroughly in the body of text and via comments. It is recommended to have the source code open as you read through the article so you can easily find the relevant sections.
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 was originally for the Source Only SDK, but has been tested in the HL2MP SDK to ensure it works.
The first thing covered are the files that should be created, these contain the list of entities to keep and a custom entity filter.
Server Side
Add two new empty files to the server project: mapfilter.cpp and mapfilter.h. It is recommended that any new files are created in a separate folder from the core SDK codebase.
Mapfilter.h
// general definitions and includes #include "cbase.h" #include "mapentities.h" #include "utllinkedlist.h"
#ifndef CMAPENTITYFILTER_H #define CMAPENTITYFILTER_H
The following is a generic list of entities that should be preserved between resets, be sure that the last entry is set to NULL. Add any mod specific entities (such as custom game rules) that do not want to be recreated on reset.
// These entities are preserved each round restart. The rest are removed and recreated. static const char *s_PreserveEnts[] = { "ai_network", "ai_hint", "ambient_generic", "sdk_gamerules", "sdk_team_manager", "player_manager", "env_soundscape", "env_soundscape_proxy", "env_soundscape_triggerable", "env_sun", "env_wind", "env_fog_controller", "func_brush", "func_wall", "func_illusionary", "func_rotating", "hl2mp_gamerules", "infodecal", "info_projecteddecal", "info_node", "info_target", "info_node_hint", "info_spectator", "info_map_parameters", "keyframe_rope", "move_rope", "info_ladder", "player", "point_viewcontrol", "scene_manager", "shadow_control", "sky_camera", "soundent", "trigger_soundscape", "viewmodel", "predicted_viewmodel", "worldspawn", "point_devshot_camera", "", NULL,// END Marker };
CMapEntityRef is a purely helper class, it is accessed through the linked list that is declared extern beneath the class definition.
class CMapEntityRef { public: int m_iEdict; int m_iSerialNumber; };
extern CUtlLinkedList<CMapEntityRef, unsigned short> g_MapEntityRefs;
The following is our custom map entity filter, this gets used on initial level load and is used to fill the linked list with entity data.
class CMapLoadEntityFilter : public IMapEntityFilter { public:
virtual bool ShouldCreateEntity( const char *pClassname ) { // During map load, create all the entities. return true; }
virtual CBaseEntity* CreateNextEntity( const char *pClassname ) { // create each entity in turn and an instance of CMapEntityRef CBaseEntity *pRet = CreateEntityByName( pClassname );
CMapEntityRef ref; ref.m_iEdict = -1; ref.m_iSerialNumber = -1;
// if the new entity is valid store the entity information in ref if ( pRet ) { ref.m_iEdict = pRet->entindex();
if ( pRet->edict() ) ref.m_iSerialNumber = pRet->edict()->m_NetworkSerialNumber; }
// add the new ref to the linked list and return the entity g_MapEntityRefs.AddToTail( ref ); return pRet; } };
#endif
MapFilter.cpp
All that's done here is the declaration of our linked list
#include "cbase.h" #include "mapfilter.h"
// memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h"
CUtlLinkedList<CMapEntityRef, unsigned short> g_MapEntityRefs;
CServerGameDLL
Find CServerGameDLL::LevelInit_ParseAllEntities
in sdk_gameinterface.cpp
, which is called when a level is first loaded and where the first addition is made. Add the following code to the function:
// Load the entities and build up a new list of the map entities and their starting state in here. g_MapEntityRefs.Purge(); CMapLoadEntityFilter filter; MapEntity_ParseAllEntities( pMapEntities, &filter );
GameRules
Now add a function called CleanMap
to the gamerules, this is the function that will be called when the map has to be reset (due to a round end etc). The function FindInList
is used in at a couple of points, the following code show's the function definition.
// FindInList searches the provided array for the compare string // we use it to check against our list of preserved entities (s_PreserveEnts) bool FindInList(const char *s_List[], const char *compare) { int index = 0;
while(s_List[index]) { if ( Q_strcmp(s_List[index], compare) == 0 ) return true;
index++; }
return false; }
The following code makes up the entire CleanUpMap
function, it is divided into smaller sections to allow for clarification of some of the finer points.
void CSDKGameRules::CleanMap() { // Recreate all the map entities from the map data (preserving their indices), // then remove everything else except the players.
// Get rid of all entities except players. CBaseEntity *pCur = gEntList.FirstEnt(); while ( pCur ) { if ( !FindInList( s_PreserveEnts, pCur->GetClassname() ) ) { UTIL_Remove( pCur ); }
pCur = gEntList.NextEnt( pCur ); }
// Really remove the entities so we can have access to their slots below. gEntList.CleanupDeleteList();
The following is an inline class declaration, it is meant to be here - it acts in a similar fashion to the previous CMapLoadEntityFilter, but rather than storing entity values it checks against the stored values to decide how to recreate entities.
// Now reload the map entities. class CMapEntityFilter : public IMapEntityFilter { public: virtual bool ShouldCreateEntity( const char *pClassname ) { // Don't recreate the preserved entities. if ( !FindInList( s_PreserveEnts, pClassname ) ) { return true; } else { // Increment our iterator since it's not going to call CreateNextEntity for this ent. if ( m_iIterator != g_MapEntityRefs.InvalidIndex() ) m_iIterator = g_MapEntityRefs.Next( m_iIterator );
return false; } }
virtual CBaseEntity* CreateNextEntity( const char *pClassname ) { if ( m_iIterator == g_MapEntityRefs.InvalidIndex() ) { // We should never reach this point - g_MapEntityRefs should have been filled // when we loaded the map due to the use of CHDNMapLoadFilter, but we cover ourselves // by checking here. Assert( m_iIterator != g_MapEntityRefs.InvalidIndex() ); return NULL; } else { CMapEntityRef &ref = g_MapEntityRefs[m_iIterator]; m_iIterator = g_MapEntityRefs.Next( m_iIterator ); // Seek to the next entity.
if ( ref.m_iEdict == -1 || engine->PEntityOfEntIndex( ref.m_iEdict ) ) { // the entities previous edict has been used for whatever reason, // so just create it and use any spare edict slot return CreateEntityByName( pClassname ); } else { // The entity's edict slot was free, so we put it back where it came from. return CreateEntityByName( pClassname, ref.m_iEdict ); } } }
public: int m_iIterator; // Iterator into g_MapEntityRefs. };
The final task of this function is to call MapEntity_ParseAllEntities
using the new filter class, this guarantees that only the entities that need to be recreated will be.
CMapEntityFilter filter; filter.m_iIterator = g_MapEntityRefs.Head();
// final task, trigger the recreation of any entities that need it. MapEntity_ParseAllEntities( engine->GetMapEntitiesString(), &filter, true ); }
An important stage is to fire an event that signifies when the map has been restarted, this will allow the clients to pick up and perform their own clean up - the code below is a demonstration of this, be sure to add the event to ModEvents.res
IGameEvent * event = gameeventmanager->CreateEvent( "game_round_restart" ); if ( event ) { DevMsg(1, "Fired game_round_restart event\n"); // fire the event gameeventmanager->FireEvent( event ); }
Client Side
As part of the reset process client side entities and decals also have to be cleared up, this is done by listening for the game_round_restart
event that was fired by the server during the reset process. The best place to do this is in clientmode_shared.cpp.
Add a listener to ClientModeShared::Init()
gameeventmanager->AddListener( this, "game_round_restart", false );
Finally put the following at the end of ClientModeShared::FireGameEvent
else if ( Q_strcmp( "game_round_restart", eventname ) == 0 ) { // recreate all client side physics props C_PhysPropClientside::RecreateAll();
// Just tell engine to clear decals engine->ClientCmd( "r_cleardecals\n" );
tempents->Clear();
//stop any looping sounds enginesound->StopAllSounds( true );
Soundscape_OnStopAllSounds(); // Tell the soundscape system. }