You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
11 KiB
418 lines
11 KiB
5 years ago
|
//========= Copyright Valve Corporation, All rights reserved. ============//
|
||
|
//
|
||
|
// The copyright to the contents herein is the property of Valve, L.L.C.
|
||
|
// The contents may be used and/or copied only with the written permission of
|
||
|
// Valve, L.L.C., or in accordance with the terms and conditions stipulated in
|
||
|
// the agreement/contract under which the contents have been supplied.
|
||
|
//
|
||
|
// $Header: $
|
||
|
// $NoKeywords: $
|
||
|
//
|
||
|
//=============================================================================
|
||
|
|
||
|
|
||
|
// Valve includes
|
||
|
#include "appframework/tier2app.h"
|
||
|
#include "filesystem.h"
|
||
|
#include "icommandline.h"
|
||
|
#include "tier2/p4helpers.h"
|
||
|
#include "p4lib/ip4.h"
|
||
|
#include "tier1/KeyValues.h"
|
||
|
#include "tier1/utlbuffer.h"
|
||
|
#include "bsplib.h"
|
||
|
#include "lumpfiles.h"
|
||
|
#include "filesystem_tools.h"
|
||
|
#include "cmdlib.h"
|
||
|
|
||
|
#ifdef _DEBUG
|
||
|
#include <windows.h>
|
||
|
#undef GetCurrentDirectory
|
||
|
#endif
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Standard spew functions
|
||
|
//-----------------------------------------------------------------------------
|
||
|
static SpewRetval_t SpewStdout( SpewType_t spewType, char const *pMsg )
|
||
|
{
|
||
|
if ( !pMsg )
|
||
|
return SPEW_CONTINUE;
|
||
|
|
||
|
#ifdef _DEBUG
|
||
|
OutputDebugString( pMsg );
|
||
|
#endif
|
||
|
|
||
|
printf( pMsg );
|
||
|
fflush( stdout );
|
||
|
|
||
|
return ( spewType == SPEW_ASSERT ) ? SPEW_DEBUGGER : SPEW_CONTINUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// The application object
|
||
|
//-----------------------------------------------------------------------------
|
||
|
class CMkEntityPatchApp : public CTier2SteamApp
|
||
|
{
|
||
|
typedef CTier2SteamApp BaseClass;
|
||
|
|
||
|
public:
|
||
|
// Methods of IApplication
|
||
|
virtual bool Create();
|
||
|
virtual bool PreInit( );
|
||
|
virtual int Main();
|
||
|
virtual void Destroy() {}
|
||
|
|
||
|
void PrintHelp( );
|
||
|
|
||
|
private:
|
||
|
};
|
||
|
|
||
|
|
||
|
DEFINE_CONSOLE_STEAM_APPLICATION_OBJECT( CMkEntityPatchApp );
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// The application object
|
||
|
//-----------------------------------------------------------------------------
|
||
|
bool CMkEntityPatchApp::Create()
|
||
|
{
|
||
|
SpewOutputFunc( SpewStdout );
|
||
|
|
||
|
AppSystemInfo_t appSystems[] =
|
||
|
{
|
||
|
{ "p4lib.dll", P4_INTERFACE_VERSION },
|
||
|
{ "", "" } // Required to terminate the list
|
||
|
};
|
||
|
|
||
|
return AddSystems( appSystems );
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
//
|
||
|
//-----------------------------------------------------------------------------
|
||
|
bool CMkEntityPatchApp::PreInit( )
|
||
|
{
|
||
|
MathLib_Init( 2.2f, 2.2f, 0.0f, 2.0f, false, false, false, false );
|
||
|
|
||
|
if ( !BaseClass::PreInit() )
|
||
|
return false;
|
||
|
|
||
|
if ( !g_pFullFileSystem )
|
||
|
{
|
||
|
Error( "// ERROR: sfmgen is missing a required interface!\n" );
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Add paths...
|
||
|
if ( !SetupSearchPaths( NULL, false, true ) )
|
||
|
return false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Print help
|
||
|
//-----------------------------------------------------------------------------
|
||
|
void CMkEntityPatchApp::PrintHelp( )
|
||
|
{
|
||
|
Msg( "Usage: mkentitypatch [-nop4] [-vproject <path to gameinfo.txt>] <in .bsp file>\n" );
|
||
|
Msg( "\t-nop4\t: [Optional] Disables auto perforce checkout/add.\n" );
|
||
|
Msg( "\t-vproject\t: [Optional] Specifies path to a gameinfo.txt file (which mod to build for).\n" );
|
||
|
Msg( "\t Source .BSP file whose entity lump you wish to patch.\n" );
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// Computes a full directory
|
||
|
//-----------------------------------------------------------------------------
|
||
|
static void ComputeFullPath( const char *pRelativeDir, char *pFullPath, int nBufLen )
|
||
|
{
|
||
|
if ( !Q_IsAbsolutePath( pRelativeDir ) )
|
||
|
{
|
||
|
char pDir[MAX_PATH];
|
||
|
if ( g_pFullFileSystem->GetCurrentDirectory( pDir, sizeof(pDir) ) )
|
||
|
{
|
||
|
Q_ComposeFileName( pDir, pRelativeDir, pFullPath, nBufLen );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Q_strncpy( pFullPath, pRelativeDir, nBufLen );
|
||
|
}
|
||
|
|
||
|
Q_StripTrailingSlash( pFullPath );
|
||
|
|
||
|
// Ensure the output directory exists
|
||
|
g_pFullFileSystem->CreateDirHierarchy( pFullPath );
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// The application object
|
||
|
//-----------------------------------------------------------------------------
|
||
|
entity_t *FindEntity( KeyValues *pEntity )
|
||
|
{
|
||
|
int nHammerId = pEntity->GetInt( "id", INT_MIN );
|
||
|
if ( nHammerId != INT_MIN )
|
||
|
{
|
||
|
// First, look for hammerid
|
||
|
for ( int i = 0; i < num_entities; ++i )
|
||
|
{
|
||
|
int nId = IntForKeyWithDefault( &entities[i], "hammerid", INT_MIN );
|
||
|
if ( nId == nHammerId )
|
||
|
return &entities[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Unfortunately, hammmerid appears to be a relatively new feature. Now, we must
|
||
|
// look for target name
|
||
|
int nMatch = -1;
|
||
|
const char *pTargetName = pEntity->GetString( "targetname" );
|
||
|
if ( pTargetName && pTargetName[0] )
|
||
|
{
|
||
|
// First, look for hammerid
|
||
|
for ( int i = 0; i < num_entities; ++i )
|
||
|
{
|
||
|
const char *pMatchTargetName = ValueForKey( &entities[i], "targetname" );
|
||
|
if ( !pMatchTargetName || !pMatchTargetName[0] )
|
||
|
continue;
|
||
|
if ( !V_stricmp( pTargetName, pMatchTargetName ) )
|
||
|
{
|
||
|
if ( nMatch >= 0 )
|
||
|
{
|
||
|
//Warning( "Encountered multiple entities that matched targetname %s!\n", pTargetName );
|
||
|
//return false;
|
||
|
nMatch = -1; // force a fallback to scanning classname and origin
|
||
|
break;
|
||
|
}
|
||
|
else
|
||
|
nMatch = i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( nMatch >= 0 )
|
||
|
return &entities[nMatch];
|
||
|
|
||
|
// No target name? Well, let's try classname and origin.
|
||
|
const char *pClassName = pEntity->GetString( "classname" );
|
||
|
if ( pClassName && pClassName[0] )
|
||
|
{
|
||
|
// First, look for hammerid
|
||
|
for ( int i = 0; i < num_entities; ++i )
|
||
|
{
|
||
|
const char *pMatchClassName = ValueForKey( &entities[i], "classname" );
|
||
|
if ( !pMatchClassName || !pMatchClassName[0] )
|
||
|
continue;
|
||
|
if ( V_stricmp( pClassName, pMatchClassName ) )
|
||
|
continue;
|
||
|
|
||
|
const char *pOrigin = "(na)";
|
||
|
if ( V_stricmp( pClassName, "worldspawn" ) ) // allow worldspawn to match all
|
||
|
{
|
||
|
pOrigin = pEntity->GetString( "origin" );
|
||
|
const char *pMatchOrigin = ValueForKey( &entities[i], "origin" );
|
||
|
if ( !pMatchOrigin || !pMatchOrigin[0] )
|
||
|
continue;
|
||
|
if ( V_stricmp( pOrigin, pMatchOrigin ) )
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( nMatch >= 0 )
|
||
|
{
|
||
|
Warning( "Encountered multiple entities that matched classname %s, origin %s!\n", pClassName, pOrigin );
|
||
|
return false;
|
||
|
}
|
||
|
nMatch = i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( nMatch >= 0 )
|
||
|
return &entities[nMatch];
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
bool InsertEntity( entity_t *pEntity, KeyValues *pEntityKeys )
|
||
|
{
|
||
|
CUtlVector<KeyValues *> vecKVs;
|
||
|
for ( KeyValues *pKey = pEntityKeys->GetFirstValue(); pKey; pKey = pKey->GetNextValue() )
|
||
|
{
|
||
|
vecKVs.AddToTail( pKey );
|
||
|
}
|
||
|
|
||
|
FOR_EACH_VEC_BACK( vecKVs, i )
|
||
|
{
|
||
|
epair_t *e = (epair_t*)malloc( sizeof(epair_t) );
|
||
|
memset (e, 0, sizeof(epair_t));
|
||
|
|
||
|
const char *pName = vecKVs[i]->GetName();
|
||
|
if ( strlen(pName) >= MAX_KEY-1 )
|
||
|
{
|
||
|
Warning( "ParseEpar: token %s too long", pName );
|
||
|
return false;
|
||
|
}
|
||
|
e->key = copystring(pName);
|
||
|
|
||
|
const char *pValue = vecKVs[i]->GetString();
|
||
|
if ( strlen(pValue) >= MAX_VALUE-1 )
|
||
|
{
|
||
|
Warning( "ParseEpar: token %s too long", pValue );
|
||
|
return false;
|
||
|
}
|
||
|
e->value = copystring(pValue);
|
||
|
|
||
|
// strip trailing spaces
|
||
|
StripTrailing( e->key );
|
||
|
StripTrailing( e->value );
|
||
|
e->next = pEntity->epairs;
|
||
|
pEntity->epairs = e;
|
||
|
}
|
||
|
|
||
|
// Flatten everything ( specifically, 'connection' keys, necessary to
|
||
|
// make the patch file have the same format as the commentary files )
|
||
|
for ( KeyValues *pKey = pEntityKeys->GetFirstTrueSubKey(); pKey; pKey = pKey->GetNextTrueSubKey() )
|
||
|
{
|
||
|
InsertEntity( pEntity, pKey );
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool InsertEntity( KeyValues *pEntity )
|
||
|
{
|
||
|
entity_t &entity = entities[ num_entities++ ];
|
||
|
return InsertEntity( &entity, pEntity );
|
||
|
}
|
||
|
|
||
|
bool ReplaceEntity( KeyValues *pEntity )
|
||
|
{
|
||
|
entity_t *pReplace = FindEntity( pEntity );
|
||
|
if ( !pReplace )
|
||
|
{
|
||
|
Warning( "Tried to replace an entity %s, origin %s, but couldn't find the original!\n", pEntity->GetString( "classname" ), pEntity->GetString( "origin" ) );
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
epair_t *pNext;
|
||
|
for ( epair_t *e = pReplace->epairs; e; e = pNext )
|
||
|
{
|
||
|
pNext = e->next;
|
||
|
free( e->key );
|
||
|
free( e->value );
|
||
|
free( e );
|
||
|
}
|
||
|
pReplace->epairs = NULL;
|
||
|
|
||
|
return InsertEntity( pReplace, pEntity );
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------
|
||
|
// The application object
|
||
|
//-----------------------------------------------------------------------------
|
||
|
int CMkEntityPatchApp::Main()
|
||
|
{
|
||
|
// Backward compat for bsplib
|
||
|
g_pFileSystem = g_pFullFileSystem;
|
||
|
|
||
|
// This bit of hackery allows us to access files on the harddrive
|
||
|
g_pFullFileSystem->AddSearchPath( "", "LOCAL", PATH_ADD_TO_HEAD );
|
||
|
|
||
|
if ( CommandLine()->CheckParm( "-h" ) || CommandLine()->CheckParm( "-help" ) || CommandLine()->ParmCount() == 1 )
|
||
|
{
|
||
|
PrintHelp();
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// The file name is the last argument
|
||
|
const char *pBSPFile = CommandLine()->GetParm( CommandLine()->ParmCount() - 1 );
|
||
|
if ( !pBSPFile || pBSPFile[0] == 0 || pBSPFile[0] == '-' )
|
||
|
{
|
||
|
PrintHelp();
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
char pFullPath[MAX_PATH];
|
||
|
ComputeFullPath( pBSPFile, pFullPath, sizeof(pFullPath) );
|
||
|
|
||
|
char pBSPFileName[MAX_PATH];
|
||
|
char pPatchFileName[MAX_PATH];
|
||
|
char pOutputFileName[MAX_PATH];
|
||
|
V_strcpy( pBSPFileName, pFullPath );
|
||
|
V_strcpy( pPatchFileName, pFullPath );
|
||
|
V_SetExtension( pBSPFileName, ".bsp", sizeof(pBSPFileName) );
|
||
|
V_SetExtension( pPatchFileName, ".pat", sizeof(pPatchFileName) );
|
||
|
GenerateLumpFileName( pFullPath, pOutputFileName, sizeof(pOutputFileName), LUMP_ENTITIES );
|
||
|
|
||
|
if ( !g_pFullFileSystem->FileExists( pBSPFileName ) )
|
||
|
{
|
||
|
Warning( "BSP file %s doesn't exist!\n", pBSPFileName );
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if ( !g_pFullFileSystem->FileExists( pPatchFileName ) )
|
||
|
{
|
||
|
Warning( "BSP patch file %s doesn't exist!\n", pPatchFileName );
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
KeyValues *pKeyValues = new KeyValues( "patch" );
|
||
|
if ( !pKeyValues->LoadFromFile( g_pFullFileSystem, pPatchFileName ) )
|
||
|
{
|
||
|
Warning( "Error parsing patch file %s!\n", pPatchFileName );
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
LoadBSPFile( pFullPath );
|
||
|
ParseEntities();
|
||
|
|
||
|
for( int i = 0; i < num_entities; i++ )
|
||
|
{
|
||
|
entity_t *pCur = &entities[i];
|
||
|
epair_t *pNext = NULL;
|
||
|
epair_t *pPrev = NULL;
|
||
|
for ( epair_t *e = pCur->epairs; e; e = pNext )
|
||
|
{
|
||
|
pNext = e->next;
|
||
|
e->next = pPrev;
|
||
|
pPrev = e;
|
||
|
}
|
||
|
pCur->epairs = pPrev;
|
||
|
}
|
||
|
|
||
|
for ( KeyValues *pKey = pKeyValues->GetFirstTrueSubKey(); pKey; pKey = pKey->GetNextTrueSubKey() )
|
||
|
{
|
||
|
const char *pKeyName = pKey->GetName();
|
||
|
if ( !V_stricmp( pKeyName, "entity" ) )
|
||
|
{
|
||
|
if ( !InsertEntity( pKey ) )
|
||
|
return 0;
|
||
|
}
|
||
|
else if ( !V_stricmp( pKeyName, "replace_entity" ) )
|
||
|
{
|
||
|
if ( !ReplaceEntity( pKey ) )
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Do Perforce Stuff
|
||
|
if ( CommandLine()->FindParm( "-nop4" ) )
|
||
|
{
|
||
|
g_p4factory->SetDummyMode( true );
|
||
|
}
|
||
|
|
||
|
g_p4factory->SetOpenFileChangeList( "Entity Patch Files" );
|
||
|
|
||
|
CP4AutoAddFile p4AddBSP( pBSPFileName );
|
||
|
CP4AutoAddFile p4AddPatch( pPatchFileName );
|
||
|
CP4AutoEditAddFile p4AddOutput( pOutputFileName );
|
||
|
|
||
|
UnparseEntities();
|
||
|
WriteLumpToFile( pBSPFileName, LUMP_ENTITIES, 0, dentdata.Base(), dentdata.Count() );
|
||
|
|
||
|
pKeyValues->deleteThis();
|
||
|
|
||
|
return -1;
|
||
|
}
|