mirror of
https://github.com/alliedmodders/hl2sdk.git
synced 2025-09-20 12:36:05 +08:00
Added original SDK code for Alien Swarm.
This commit is contained in:
538
game/server/TemplateEntities.cpp
Normal file
538
game/server/TemplateEntities.cpp
Normal file
@ -0,0 +1,538 @@
|
||||
//========= Copyright (c) 1996-2005, Valve Corporation, All rights reserved. ============//
|
||||
//
|
||||
// Purpose: Template entities are used by spawners to create copies of entities
|
||||
// that were configured by the level designer. This allows us to spawn
|
||||
// entities with arbitrary sets of key/value data and entity I/O
|
||||
// connections.
|
||||
//
|
||||
// Template entities are marked with a special spawnflag which causes
|
||||
// them not to spawn, but to be saved as a string containing all the
|
||||
// map data (keyvalues and I/O connections) from the BSP. Template
|
||||
// entities are looked up by name by the spawner, which copies the
|
||||
// map data into a local string (that's how the template data is saved
|
||||
// and restored). Once all the entities in the map have been activated,
|
||||
// the template database is freed.
|
||||
//
|
||||
//=============================================================================//
|
||||
|
||||
#include "cbase.h"
|
||||
#include "igamesystem.h"
|
||||
#include "mapentities_shared.h"
|
||||
#include "point_template.h"
|
||||
#include "eventqueue.h"
|
||||
#include "TemplateEntities.h"
|
||||
#include "utldict.h"
|
||||
#include "entitydefs.h"
|
||||
|
||||
// memdbgon must be the last include file in a .cpp file!!!
|
||||
#include "tier0/memdbgon.h"
|
||||
|
||||
ConVar template_debug( "template_debug", "0" );
|
||||
|
||||
// This is appended to key's values that will need to be unique in template instances
|
||||
const char *ENTITYIO_FIXUP_STRING = "&0000";
|
||||
|
||||
int MapEntity_GetNumKeysInEntity( const char *pEntData );
|
||||
|
||||
struct TemplateEntityData_t
|
||||
{
|
||||
const char *pszName;
|
||||
char *pszMapData;
|
||||
string_t iszMapData;
|
||||
int iMapDataLength;
|
||||
bool bNeedsEntityIOFixup; // If true, this template has entity I/O in its mapdata that needs fixup before spawning.
|
||||
char *pszFixedMapData; // A single copy of this template that we used to fix up the Entity I/O whenever someone wants a fixed version of this template
|
||||
|
||||
int m_nHammerID; // Used to update the template in Foundry
|
||||
|
||||
DECLARE_SIMPLE_DATADESC();
|
||||
};
|
||||
|
||||
BEGIN_SIMPLE_DATADESC( TemplateEntityData_t )
|
||||
//DEFINE_FIELD( pszName, FIELD_STRING ), // Saved custom, see below
|
||||
//DEFINE_FIELD( pszMapData, FIELD_STRING ), // Saved custom, see below
|
||||
DEFINE_FIELD( iszMapData, FIELD_STRING ),
|
||||
DEFINE_FIELD( iMapDataLength, FIELD_INTEGER ),
|
||||
DEFINE_FIELD( bNeedsEntityIOFixup, FIELD_BOOLEAN ),
|
||||
|
||||
//DEFINE_FIELD( pszFixedMapData, FIELD_STRING ), // Not saved at all
|
||||
END_DATADESC()
|
||||
|
||||
struct grouptemplate_t
|
||||
{
|
||||
CEntityMapData *pMapDataParser;
|
||||
char pszName[MAPKEY_MAXLENGTH];
|
||||
int iIndex;
|
||||
bool bChangeTargetname;
|
||||
};
|
||||
|
||||
static CUtlVector<TemplateEntityData_t *> g_Templates;
|
||||
|
||||
int g_iCurrentTemplateInstance;
|
||||
|
||||
void Templates_FreeTemplate( TemplateEntityData_t *pTemplate )
|
||||
{
|
||||
free((void *)pTemplate->pszName);
|
||||
free(pTemplate->pszMapData);
|
||||
if ( pTemplate->pszFixedMapData )
|
||||
{
|
||||
free(pTemplate->pszFixedMapData);
|
||||
}
|
||||
|
||||
free(pTemplate);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Saves the given entity's keyvalue data for later use by a spawner.
|
||||
// Returns the index into the templates.
|
||||
//-----------------------------------------------------------------------------
|
||||
int Templates_Add(CBaseEntity *pEntity, const char *pszMapData, int nLen, int nHammerID)
|
||||
{
|
||||
const char *pszName = STRING(pEntity->GetEntityName());
|
||||
if ((!pszName) || (!strlen(pszName)))
|
||||
{
|
||||
DevWarning(1, "RegisterTemplateEntity: template entity with no name, class %s\n", pEntity->GetClassname());
|
||||
return -1;
|
||||
}
|
||||
|
||||
TemplateEntityData_t *pEntData = (TemplateEntityData_t *)malloc(sizeof(TemplateEntityData_t));
|
||||
pEntData->m_nHammerID = nHammerID;
|
||||
pEntData->pszName = strdup( pszName );
|
||||
|
||||
// We may modify the values of the keys in this mapdata chunk later on to fix Entity I/O
|
||||
// connections. For this reason, we need to ensure we have enough memory to do that.
|
||||
int iKeys = MapEntity_GetNumKeysInEntity( pszMapData );
|
||||
int iExtraSpace = (strlen(ENTITYIO_FIXUP_STRING)+1) * iKeys;
|
||||
|
||||
// Extra 1 because the mapdata passed in isn't null terminated
|
||||
pEntData->iMapDataLength = nLen + iExtraSpace + 1;
|
||||
pEntData->pszMapData = (char *)malloc( pEntData->iMapDataLength );
|
||||
memcpy(pEntData->pszMapData, pszMapData, nLen + 1);
|
||||
pEntData->pszMapData[nLen] = '\0';
|
||||
|
||||
// We don't alloc these suckers right now because that gives us no time to
|
||||
// tweak them for Entity I/O purposes.
|
||||
pEntData->iszMapData = NULL_STRING;
|
||||
pEntData->bNeedsEntityIOFixup = false;
|
||||
pEntData->pszFixedMapData = NULL;
|
||||
|
||||
return g_Templates.AddToTail(pEntData);
|
||||
}
|
||||
|
||||
|
||||
void Templates_RemoveByHammerID( int nHammerID )
|
||||
{
|
||||
for ( int i=g_Templates.Count()-1; i >= 0; i-- )
|
||||
{
|
||||
if ( g_Templates[i]->m_nHammerID == nHammerID )
|
||||
{
|
||||
Templates_FreeTemplate( g_Templates[i] );
|
||||
g_Templates.Remove( i );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Returns true if the specified index needs to be fixed up to be unique
|
||||
// when the template is spawned.
|
||||
//-----------------------------------------------------------------------------
|
||||
bool Templates_IndexRequiresEntityIOFixup( int iIndex )
|
||||
{
|
||||
Assert( iIndex < g_Templates.Count() );
|
||||
return g_Templates[iIndex]->bNeedsEntityIOFixup;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Looks up a template entity by its index in the templates
|
||||
// Used by point_templates because they often have multiple templates with the same name
|
||||
//-----------------------------------------------------------------------------
|
||||
string_t Templates_FindByIndex( int iIndex )
|
||||
{
|
||||
Assert( iIndex < g_Templates.Count() );
|
||||
|
||||
// First time through we alloc the mapdata onto the pool.
|
||||
// It's safe to do it now because this isn't called until post Entity I/O cleanup.
|
||||
if ( g_Templates[iIndex]->iszMapData == NULL_STRING )
|
||||
{
|
||||
g_Templates[iIndex]->iszMapData = AllocPooledString( g_Templates[iIndex]->pszMapData );
|
||||
}
|
||||
|
||||
return g_Templates[iIndex]->iszMapData;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose:
|
||||
//-----------------------------------------------------------------------------
|
||||
int Templates_GetStringSize( int iIndex )
|
||||
{
|
||||
Assert( iIndex < g_Templates.Count() );
|
||||
return g_Templates[iIndex]->iMapDataLength;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Looks up a template entity by name, returning the map data blob as
|
||||
// a null-terminated string containing key/value pairs.
|
||||
// NOTE: This can't handle multiple templates with the same targetname.
|
||||
//-----------------------------------------------------------------------------
|
||||
string_t Templates_FindByTargetName(const char *pszName)
|
||||
{
|
||||
int nCount = g_Templates.Count();
|
||||
for (int i = 0; i < nCount; i++)
|
||||
{
|
||||
TemplateEntityData_t *pTemplate = g_Templates.Element(i);
|
||||
if ( !stricmp(pTemplate->pszName, pszName) )
|
||||
return Templates_FindByIndex( i );
|
||||
}
|
||||
|
||||
return NULL_STRING;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: A CPointTemplate has asked us to reconnect all the entity I/O links
|
||||
// inside it's templates. Go through the keys and add look for values
|
||||
// that match a name within the group's entity names. Append %d to any
|
||||
// found values, which will later be filled out by a unique identifier
|
||||
// whenever the template is instanced.
|
||||
//-----------------------------------------------------------------------------
|
||||
void Templates_ReconnectIOForGroup( CPointTemplate *pGroup )
|
||||
{
|
||||
int iCount = pGroup->GetNumTemplates();
|
||||
if ( !iCount )
|
||||
return;
|
||||
|
||||
// First assemble a list of the targetnames of all the templates in the group.
|
||||
// We need to store off the original names here, because we're going to change
|
||||
// them as we go along.
|
||||
CUtlVector< grouptemplate_t > GroupTemplates;
|
||||
int i;
|
||||
for ( i = 0; i < iCount; i++ )
|
||||
{
|
||||
grouptemplate_t newGroupTemplate;
|
||||
newGroupTemplate.iIndex = pGroup->GetTemplateIndexForTemplate(i);
|
||||
newGroupTemplate.pMapDataParser = new CEntityMapData( g_Templates[ newGroupTemplate.iIndex ]->pszMapData, g_Templates[ newGroupTemplate.iIndex ]->iMapDataLength );
|
||||
Assert( newGroupTemplate.pMapDataParser );
|
||||
newGroupTemplate.pMapDataParser->ExtractValue( "targetname", newGroupTemplate.pszName );
|
||||
newGroupTemplate.bChangeTargetname = false;
|
||||
|
||||
GroupTemplates.AddToTail( newGroupTemplate );
|
||||
}
|
||||
|
||||
if (pGroup->AllowNameFixup())
|
||||
{
|
||||
char keyName[MAPKEY_MAXLENGTH];
|
||||
char value[MAPKEY_MAXLENGTH];
|
||||
char valueclipped[MAPKEY_MAXLENGTH];
|
||||
|
||||
// Now go through all the entities in the group and parse their mapdata keyvalues.
|
||||
// We're looking for any values that match targetnames of any of the group entities.
|
||||
for ( i = 0; i < iCount; i++ )
|
||||
{
|
||||
// We need to know what instance of each key we're changing.
|
||||
// Store a table of the count of the keys we've run into.
|
||||
CUtlDict< int, int > KeyInstanceCount;
|
||||
CEntityMapData *mapData = GroupTemplates[i].pMapDataParser;
|
||||
|
||||
// Loop through our keys
|
||||
if ( !mapData->GetFirstKey(keyName, value) )
|
||||
continue;
|
||||
|
||||
do
|
||||
{
|
||||
// Ignore targetnames
|
||||
if ( !stricmp( keyName, "targetname" ) )
|
||||
continue;
|
||||
|
||||
// Add to the count for this
|
||||
int idx = KeyInstanceCount.Find( keyName );
|
||||
if ( idx == KeyInstanceCount.InvalidIndex() )
|
||||
{
|
||||
idx = KeyInstanceCount.Insert( keyName, 0 );
|
||||
}
|
||||
KeyInstanceCount[idx]++;
|
||||
|
||||
// Entity I/O values are stored as "Targetname,<data>", so we need to see if there's a ',' in the string
|
||||
char *sValue = value;
|
||||
|
||||
// FIXME: This is very brittle. Any key with a , will not be found.
|
||||
char delimiter = VMF_IOPARAM_STRING_DELIMITER;
|
||||
if( strchr( value, delimiter ) == NULL )
|
||||
{
|
||||
delimiter = ',';
|
||||
}
|
||||
|
||||
char *s = strchr( value, delimiter );
|
||||
if ( s )
|
||||
{
|
||||
// Grab just the targetname of the receiver
|
||||
Q_strncpy( valueclipped, value, (s - value+1) );
|
||||
sValue = valueclipped;
|
||||
}
|
||||
|
||||
// Loop through our group templates
|
||||
for ( int iTName = 0; iTName < iCount; iTName++ )
|
||||
{
|
||||
char *pName = GroupTemplates[iTName].pszName;
|
||||
if ( stricmp( pName, sValue ) )
|
||||
continue;
|
||||
|
||||
if ( template_debug.GetInt() )
|
||||
{
|
||||
Msg("Template Connection Found: Key %s (\"%s\") in entity named \"%s\"(%d) matches entity %d's targetname\n", keyName, sValue, GroupTemplates[i].pszName, i, iTName );
|
||||
}
|
||||
|
||||
char newvalue[MAPKEY_MAXLENGTH];
|
||||
|
||||
// Get the current key instance. (-1 because it's this one we're changing)
|
||||
int nKeyInstance = KeyInstanceCount[idx] - 1;
|
||||
|
||||
// Add our IO value to the targetname
|
||||
// We need to append it if this isn't an Entity I/O value, or prepend it to the ',' if it is
|
||||
if ( s )
|
||||
{
|
||||
Q_strncpy( newvalue, valueclipped, MAPKEY_MAXLENGTH );
|
||||
Q_strncat( newvalue, ENTITYIO_FIXUP_STRING, sizeof(newvalue), COPY_ALL_CHARACTERS );
|
||||
Q_strncat( newvalue, s, sizeof(newvalue), COPY_ALL_CHARACTERS );
|
||||
mapData->SetValue( keyName, newvalue, nKeyInstance );
|
||||
}
|
||||
else
|
||||
{
|
||||
Q_strncpy( newvalue, sValue, MAPKEY_MAXLENGTH );
|
||||
Q_strncat( newvalue, ENTITYIO_FIXUP_STRING, sizeof(newvalue), COPY_ALL_CHARACTERS );
|
||||
mapData->SetValue( keyName, newvalue, nKeyInstance );
|
||||
}
|
||||
|
||||
// Remember we changed this targetname
|
||||
GroupTemplates[iTName].bChangeTargetname = true;
|
||||
|
||||
// Set both entity's flags telling them their template needs fixup when it's spawned
|
||||
g_Templates[ GroupTemplates[i].iIndex ]->bNeedsEntityIOFixup = true;
|
||||
g_Templates[ GroupTemplates[iTName].iIndex ]->bNeedsEntityIOFixup = true;
|
||||
}
|
||||
}
|
||||
while ( mapData->GetNextKey(keyName, value) );
|
||||
}
|
||||
|
||||
// Now change targetnames for all entities that need them changed
|
||||
for ( i = 0; i < iCount; i++ )
|
||||
{
|
||||
char value[MAPKEY_MAXLENGTH];
|
||||
|
||||
if ( GroupTemplates[i].bChangeTargetname )
|
||||
{
|
||||
CEntityMapData *mapData = GroupTemplates[i].pMapDataParser;
|
||||
mapData->ExtractValue( "targetname", value );
|
||||
Q_strncat( value, ENTITYIO_FIXUP_STRING, sizeof(value), COPY_ALL_CHARACTERS );
|
||||
mapData->SetValue( "targetname", value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete our group parsers
|
||||
for ( i = 0; i < iCount; i++ )
|
||||
{
|
||||
delete GroupTemplates[i].pMapDataParser;
|
||||
}
|
||||
GroupTemplates.Purge();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Someone's about to start instancing a new group of entities.
|
||||
// Generate a unique identifier for this group.
|
||||
//-----------------------------------------------------------------------------
|
||||
void Templates_StartUniqueInstance( void )
|
||||
{
|
||||
g_iCurrentTemplateInstance++;
|
||||
|
||||
// Make sure there's enough room to fit it into the string
|
||||
int iMax = pow(10.0f, (int)((strlen(ENTITYIO_FIXUP_STRING) - 1))); // -1 for the &
|
||||
if ( g_iCurrentTemplateInstance >= iMax )
|
||||
{
|
||||
// We won't hit this.
|
||||
Assert(0);
|
||||
// Hopefully there were still be instance number 0 around.
|
||||
g_iCurrentTemplateInstance = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Someone wants to spawn an instance of a template that requires
|
||||
// entity IO fixup. Fill out the pMapData with a copy of the template
|
||||
// with unique key/values where the template requires them.
|
||||
//-----------------------------------------------------------------------------
|
||||
char *Templates_GetEntityIOFixedMapData( int iIndex )
|
||||
{
|
||||
Assert( Templates_IndexRequiresEntityIOFixup( iIndex ) );
|
||||
|
||||
// First time through?
|
||||
if ( !g_Templates[iIndex]->pszFixedMapData )
|
||||
{
|
||||
g_Templates[iIndex]->pszFixedMapData = new char[g_Templates[iIndex]->iMapDataLength];
|
||||
Q_strncpy( g_Templates[iIndex]->pszFixedMapData, g_Templates[iIndex]->pszMapData, g_Templates[iIndex]->iMapDataLength );
|
||||
}
|
||||
|
||||
int iFixupSize = strlen(ENTITYIO_FIXUP_STRING); // strlen("&0000\0") = 5!
|
||||
char *sOurFixup = new char[iFixupSize+1]; // do alloc room here for the null terminator
|
||||
Q_snprintf( sOurFixup, iFixupSize+1, "%c%.4d", ENTITYIO_FIXUP_STRING[0], g_iCurrentTemplateInstance );
|
||||
|
||||
// Now rip through the map data string and replace any instances of the fixup string with our unique identifier
|
||||
char *c = g_Templates[iIndex]->pszFixedMapData;
|
||||
do
|
||||
{
|
||||
if ( *c == ENTITYIO_FIXUP_STRING[0] )
|
||||
{
|
||||
// Make sure it's our fixup string
|
||||
bool bValid = true;
|
||||
for ( int i = 1; i < iFixupSize; i++ )
|
||||
{
|
||||
// Look for any number, because we've already used this string
|
||||
if ( !(*(c+i) >= '0' && *(c+i) <= '9') )
|
||||
{
|
||||
// Some other string
|
||||
bValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stomp it with our unique string
|
||||
if ( bValid )
|
||||
{
|
||||
memcpy( c, sOurFixup, iFixupSize );
|
||||
c += iFixupSize;
|
||||
}
|
||||
}
|
||||
c++;
|
||||
} while (*c);
|
||||
|
||||
return g_Templates[iIndex]->pszFixedMapData;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Frees all the template data. Called on level shutdown.
|
||||
//-----------------------------------------------------------------------------
|
||||
void Templates_RemoveAll(void)
|
||||
{
|
||||
int nCount = g_Templates.Count();
|
||||
for (int i = 0; i < nCount; i++)
|
||||
{
|
||||
TemplateEntityData_t *pTemplate = g_Templates.Element(i);
|
||||
Templates_FreeTemplate( pTemplate );
|
||||
}
|
||||
|
||||
g_Templates.RemoveAll();
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Hooks in the template manager's callbacks.
|
||||
//-----------------------------------------------------------------------------
|
||||
class CTemplatesHook : public CAutoGameSystem
|
||||
{
|
||||
public:
|
||||
CTemplatesHook( char const *name ) : CAutoGameSystem( name )
|
||||
{
|
||||
}
|
||||
|
||||
virtual void LevelShutdownPostEntity( void )
|
||||
{
|
||||
Templates_RemoveAll();
|
||||
}
|
||||
};
|
||||
|
||||
CTemplatesHook g_TemplateEntityHook( "CTemplatesHook" );
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// TEMPLATE SAVE / RESTORE
|
||||
//-----------------------------------------------------------------------------
|
||||
static short TEMPLATE_SAVE_RESTORE_VERSION = 1;
|
||||
|
||||
class CTemplate_SaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler
|
||||
{
|
||||
public:
|
||||
const char *GetBlockName()
|
||||
{
|
||||
return "Templates";
|
||||
}
|
||||
|
||||
//---------------------------------
|
||||
|
||||
void Save( ISave *pSave )
|
||||
{
|
||||
pSave->WriteInt( &g_iCurrentTemplateInstance );
|
||||
|
||||
short nCount = g_Templates.Count();
|
||||
pSave->WriteShort( &nCount );
|
||||
for ( int i = 0; i < nCount; i++ )
|
||||
{
|
||||
TemplateEntityData_t *pTemplate = g_Templates[i];
|
||||
pSave->WriteAll( pTemplate );
|
||||
pSave->WriteString( pTemplate->pszName );
|
||||
pSave->WriteString( pTemplate->pszMapData );
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------
|
||||
|
||||
void WriteSaveHeaders( ISave *pSave )
|
||||
{
|
||||
pSave->WriteShort( &TEMPLATE_SAVE_RESTORE_VERSION );
|
||||
}
|
||||
|
||||
//---------------------------------
|
||||
|
||||
void ReadRestoreHeaders( IRestore *pRestore )
|
||||
{
|
||||
// No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so.
|
||||
short version;
|
||||
pRestore->ReadShort( &version );
|
||||
m_fDoLoad = ( version == TEMPLATE_SAVE_RESTORE_VERSION );
|
||||
}
|
||||
|
||||
//---------------------------------
|
||||
|
||||
void Restore( IRestore *pRestore, bool createPlayers )
|
||||
{
|
||||
if ( m_fDoLoad )
|
||||
{
|
||||
Templates_RemoveAll();
|
||||
g_Templates.Purge();
|
||||
g_iCurrentTemplateInstance = pRestore->ReadInt();
|
||||
|
||||
int iTemplates = pRestore->ReadShort();
|
||||
while ( iTemplates-- )
|
||||
{
|
||||
TemplateEntityData_t *pNewTemplate = (TemplateEntityData_t *)malloc(sizeof(TemplateEntityData_t));
|
||||
pRestore->ReadAll( pNewTemplate );
|
||||
|
||||
int sizeData = 0;//pRestore->SkipHeader();
|
||||
char szName[MAPKEY_MAXLENGTH];
|
||||
pRestore->ReadString( szName, MAPKEY_MAXLENGTH, sizeData );
|
||||
pNewTemplate->pszName = strdup( szName );
|
||||
//sizeData = pRestore->SkipHeader();
|
||||
pNewTemplate->pszMapData = (char *)malloc( pNewTemplate->iMapDataLength );
|
||||
pRestore->ReadString( pNewTemplate->pszMapData, pNewTemplate->iMapDataLength, sizeData );
|
||||
|
||||
// Set this to NULL so it'll be created the first time it gets used
|
||||
pNewTemplate->pszFixedMapData = NULL;
|
||||
|
||||
g_Templates.AddToTail( pNewTemplate );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_fDoLoad;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
CTemplate_SaveRestoreBlockHandler g_Template_SaveRestoreBlockHandler;
|
||||
|
||||
//-------------------------------------
|
||||
|
||||
ISaveRestoreBlockHandler *GetTemplateSaveRestoreBlockHandler()
|
||||
{
|
||||
return &g_Template_SaveRestoreBlockHandler;
|
||||
}
|
Reference in New Issue
Block a user