//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: unusedcontent.cpp : Defines the entry point for the console application. // //=============================================================================// #include "cbase.h" #include #include #include #include #include "tier0/dbg.h" #pragma warning( disable : 4018 ) #include "utlrbtree.h" #include "utlvector.h" #include "utldict.h" #include "filesystem.h" #include "FileSystem_Tools.h" #include "FileSystem_Helpers.h" #include "KeyValues.h" #include "cmdlib.h" #include "scriplib.h" #include "tier0/icommandline.h" #include "tier1/fmtstr.h" bool uselogfile = false; bool spewdeletions = false; bool showreferencedfiles = false; bool immediatedelete = false; bool printwhitelist = false; bool showmapfileusage = false; static char modname[MAX_PATH]; static char g_szReslistDir[ MAX_PATH ] = "reslists/"; namespace UnusedContent { class CCleanupUtlSymbolTable; //----------------------------------------------------------------------------- // forward declarations //----------------------------------------------------------------------------- class CUtlSymbolTable; //----------------------------------------------------------------------------- // This is a symbol, which is a easier way of dealing with strings. //----------------------------------------------------------------------------- typedef unsigned int UtlSymId_t; #define UC_UTL_INVAL_SYMBOL ((UnusedContent::UtlSymId_t)~0) class CUtlSymbol { public: // constructor, destructor CUtlSymbol() : m_Id(UTL_INVAL_SYMBOL) {} CUtlSymbol( UtlSymId_t id ) : m_Id(id) {} CUtlSymbol( char const* pStr ); CUtlSymbol( CUtlSymbol const& sym ) : m_Id(sym.m_Id) {} // operator= CUtlSymbol& operator=( CUtlSymbol const& src ) { m_Id = src.m_Id; return *this; } // operator== bool operator==( CUtlSymbol const& src ) const { return m_Id == src.m_Id; } bool operator==( char const* pStr ) const; // Is valid? bool IsValid() const { return m_Id != UTL_INVAL_SYMBOL; } // Gets at the symbol operator UtlSymId_t const() const { return m_Id; } // Gets the string associated with the symbol char const* String( ) const; // Modules can choose to disable the static symbol table so to prevent accidental use of them. static void DisableStaticSymbolTable(); protected: UtlSymId_t m_Id; // Initializes the symbol table static void Initialize(); // returns the current symbol table static CUtlSymbolTable* CurrTable(); // The standard global symbol table static CUtlSymbolTable* s_pSymbolTable; static bool s_bAllowStaticSymbolTable; friend class UnusedContent::CCleanupUtlSymbolTable; }; //----------------------------------------------------------------------------- // CUtlSymbolTable: // description: // This class defines a symbol table, which allows us to perform mappings // of strings to symbols and back. The symbol class itself contains // a static version of this class for creating global strings, but this // class can also be instanced to create local symbol tables. //----------------------------------------------------------------------------- class CUtlSymbolTable { public: // constructor, destructor CUtlSymbolTable( int growSize = 0, int initSize = 32, bool caseInsensitive = false ); ~CUtlSymbolTable(); // Finds and/or creates a symbol based on the string CUtlSymbol AddString( char const* pString ); // Finds the symbol for pString CUtlSymbol Find( char const* pString ); // Look up the string associated with a particular symbol char const* String( CUtlSymbol id ) const; // Remove all symbols in the table. void RemoveAll(); int GetNumStrings( void ) const { return m_Lookup.Count(); } protected: class CStringPoolIndex { public: inline CStringPoolIndex() { } inline CStringPoolIndex( unsigned int iPool, unsigned int iOffset ) { m_iPool = iPool; m_iOffset = iOffset; } inline bool operator==( const CStringPoolIndex &other ) const { return m_iPool == other.m_iPool && m_iOffset == other.m_iOffset; } unsigned int m_iPool; // Index into m_StringPools. unsigned int m_iOffset; // Index into the string pool. }; // Stores the symbol lookup CUtlRBTree m_Lookup; typedef struct { int m_TotalLen; // How large is int m_SpaceUsed; char m_Data[1]; } StringPool_t; // stores the string data CUtlVector m_StringPools; private: int FindPoolWithSpace( int len ) const; const char* StringFromIndex( const CStringPoolIndex &index ) const; // Less function, for sorting strings static bool SymLess( CStringPoolIndex const& i1, CStringPoolIndex const& i2 ); // case insensitive less function static bool SymLessi( CStringPoolIndex const& i1, CStringPoolIndex const& i2 ); }; //=========== (C) Copyright 1999 Valve, L.L.C. 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. // // Purpose: Defines a symbol table // // $Header: $ // $NoKeywords: $ //============================================================================= #pragma warning (disable:4514) #define INVALID_STRING_INDEX CStringPoolIndex( 0xFFFF, 0xFFFF ) #define MIN_STRING_POOL_SIZE 2048 //----------------------------------------------------------------------------- // globals //----------------------------------------------------------------------------- CUtlSymbolTable* CUtlSymbol::s_pSymbolTable = 0; bool CUtlSymbol::s_bAllowStaticSymbolTable = true; //----------------------------------------------------------------------------- // symbol methods //----------------------------------------------------------------------------- void CUtlSymbol::Initialize() { // If this assert fails, then the module that this call is in has chosen to disallow // use of the static symbol table. Usually, it's to prevent confusion because it's easy // to accidentally use the global symbol table when you really want to use a specific one. Assert( s_bAllowStaticSymbolTable ); // necessary to allow us to create global symbols static bool symbolsInitialized = false; if (!symbolsInitialized) { s_pSymbolTable = new CUtlSymbolTable; symbolsInitialized = true; } } //----------------------------------------------------------------------------- // Purpose: Singleton to delete table on exit from module //----------------------------------------------------------------------------- class CCleanupUtlSymbolTable { public: ~CCleanupUtlSymbolTable() { delete CUtlSymbol::s_pSymbolTable; CUtlSymbol::s_pSymbolTable = NULL; } }; static CCleanupUtlSymbolTable g_CleanupSymbolTable; CUtlSymbolTable* CUtlSymbol::CurrTable() { Initialize(); return s_pSymbolTable; } //----------------------------------------------------------------------------- // string->symbol->string //----------------------------------------------------------------------------- CUtlSymbol::CUtlSymbol( char const* pStr ) { m_Id = CurrTable()->AddString( pStr ); } char const* CUtlSymbol::String( ) const { return CurrTable()->String(m_Id); } void CUtlSymbol::DisableStaticSymbolTable() { s_bAllowStaticSymbolTable = false; } //----------------------------------------------------------------------------- // checks if the symbol matches a string //----------------------------------------------------------------------------- bool CUtlSymbol::operator==( char const* pStr ) const { if (m_Id == UTL_INVAL_SYMBOL) return false; return strcmp( String(), pStr ) == 0; } //----------------------------------------------------------------------------- // symbol table stuff //----------------------------------------------------------------------------- struct LessCtx_t { char const* m_pUserString; CUtlSymbolTable* m_pTable; LessCtx_t( ) : m_pUserString(0), m_pTable(0) {} }; static LessCtx_t g_LessCtx; inline const char* CUtlSymbolTable::StringFromIndex( const CStringPoolIndex &index ) const { Assert( index.m_iPool < m_StringPools.Count() ); Assert( index.m_iOffset < m_StringPools[index.m_iPool]->m_TotalLen ); return &m_StringPools[index.m_iPool]->m_Data[index.m_iOffset]; } bool CUtlSymbolTable::SymLess( CStringPoolIndex const& i1, CStringPoolIndex const& i2 ) { char const* str1 = (i1 == INVALID_STRING_INDEX) ? g_LessCtx.m_pUserString : g_LessCtx.m_pTable->StringFromIndex( i1 ); char const* str2 = (i2 == INVALID_STRING_INDEX) ? g_LessCtx.m_pUserString : g_LessCtx.m_pTable->StringFromIndex( i2 ); return strcmp( str1, str2 ) < 0; } bool CUtlSymbolTable::SymLessi( CStringPoolIndex const& i1, CStringPoolIndex const& i2 ) { char const* str1 = (i1 == INVALID_STRING_INDEX) ? g_LessCtx.m_pUserString : g_LessCtx.m_pTable->StringFromIndex( i1 ); char const* str2 = (i2 == INVALID_STRING_INDEX) ? g_LessCtx.m_pUserString : g_LessCtx.m_pTable->StringFromIndex( i2 ); return strcmpi( str1, str2 ) < 0; } //----------------------------------------------------------------------------- // constructor, destructor //----------------------------------------------------------------------------- CUtlSymbolTable::CUtlSymbolTable( int growSize, int initSize, bool caseInsensitive ) : m_Lookup( growSize, initSize, caseInsensitive ? SymLessi : SymLess ), m_StringPools( 8 ) { } CUtlSymbolTable::~CUtlSymbolTable() { } CUtlSymbol CUtlSymbolTable::Find( char const* pString ) { if (!pString) return CUtlSymbol(); // Store a special context used to help with insertion g_LessCtx.m_pUserString = pString; g_LessCtx.m_pTable = this; // Passing this special invalid symbol makes the comparison function // use the string passed in the context UtlSymId_t idx = m_Lookup.Find( INVALID_STRING_INDEX ); return CUtlSymbol( idx ); } int CUtlSymbolTable::FindPoolWithSpace( int len ) const { for ( int i=0; i < m_StringPools.Count(); i++ ) { StringPool_t *pPool = m_StringPools[i]; if ( (pPool->m_TotalLen - pPool->m_SpaceUsed) >= len ) { return i; } } return -1; } //----------------------------------------------------------------------------- // Finds and/or creates a symbol based on the string //----------------------------------------------------------------------------- CUtlSymbol CUtlSymbolTable::AddString( char const* pString ) { if (!pString) return CUtlSymbol( UTL_INVAL_SYMBOL ); CUtlSymbol id = Find( pString ); if (id.IsValid()) return id; int len = strlen(pString) + 1; // Find a pool with space for this string, or allocate a new one. int iPool = FindPoolWithSpace( len ); if ( iPool == -1 ) { // Add a new pool. int newPoolSize = max( len, MIN_STRING_POOL_SIZE ); StringPool_t *pPool = (StringPool_t*)malloc( sizeof( StringPool_t ) + newPoolSize - 1 ); pPool->m_TotalLen = newPoolSize; pPool->m_SpaceUsed = 0; iPool = m_StringPools.AddToTail( pPool ); } // Copy the string in. StringPool_t *pPool = m_StringPools[iPool]; Assert( pPool->m_SpaceUsed < 0xFFFF ); // This should never happen, because if we had a string > 64k, it // would have been given its entire own pool. unsigned int iStringOffset = pPool->m_SpaceUsed; memcpy( &pPool->m_Data[pPool->m_SpaceUsed], pString, len ); pPool->m_SpaceUsed += len; // didn't find, insert the string into the vector. CStringPoolIndex index; index.m_iPool = iPool; index.m_iOffset = iStringOffset; UtlSymId_t idx = m_Lookup.Insert( index ); return CUtlSymbol( idx ); } //----------------------------------------------------------------------------- // Look up the string associated with a particular symbol //----------------------------------------------------------------------------- char const* CUtlSymbolTable::String( CUtlSymbol id ) const { if (!id.IsValid()) return ""; Assert( m_Lookup.IsValidIndex((UtlSymId_t)id) ); return StringFromIndex( m_Lookup[id] ); } //----------------------------------------------------------------------------- // Remove all symbols in the table. //----------------------------------------------------------------------------- void CUtlSymbolTable::RemoveAll() { m_Lookup.RemoveAll(); for ( int i=0; i < m_StringPools.Count(); i++ ) free( m_StringPools[i] ); m_StringPools.RemoveAll(); } } // Namespace UnusedContent struct AnalysisData { UnusedContent::CUtlSymbolTable symbols; }; char *directories_to_check[] = { "", "bin", "maps", "materials", "models", "scenes", "scripts", "sound", "hl2", }; char *directories_to_ignore[] = // don't include these dirs in the others list { "reslists", "reslists_temp", "logs", "media", "downloads", "save", "screenshots", "testscripts", "logos" }; enum { REFERENCED_NO = 0, REFERENCED_WHITELIST, REFERENCED_GAME }; struct FileEntry { FileEntry() { sym = UC_UTL_INVAL_SYMBOL; size = 0; referenced = REFERENCED_NO; } UnusedContent::CUtlSymbol sym; unsigned int size; int referenced; }; struct ReferencedFile { ReferencedFile() { sym = UC_UTL_INVAL_SYMBOL; } ReferencedFile( const ReferencedFile& src ) { sym = src.sym; maplist.RemoveAll(); int c = src.maplist.Count(); for ( int i = 0; i < c; ++i ) { maplist.AddToTail( src.maplist[ i ] ); } } ReferencedFile & operator =( const ReferencedFile& src ) { if ( this == &src ) return *this; sym = src.sym; maplist.RemoveAll(); int c = src.maplist.Count(); for ( int i = 0; i < c; ++i ) { maplist.AddToTail( src.maplist[ i ] ); } return *this; } UnusedContent::CUtlSymbol sym; CUtlVector< UnusedContent::CUtlSymbol > maplist; }; static AnalysisData g_Analysis; IFileSystem *filesystem = NULL; static CUniformRandomStream g_Random; IUniformRandomStream *random = &g_Random; static bool spewed = false; SpewRetval_t SpewFunc( SpewType_t type, char const *pMsg ) { spewed = true; printf( "%s", pMsg ); OutputDebugString( pMsg ); if ( type == SPEW_ERROR ) { printf( "\n" ); OutputDebugString( "\n" ); exit(-1); } return SPEW_CONTINUE; } char *va( const char *fmt, ... ) { static char string[ 8192 ]; va_list va; va_start( va, fmt ); vsprintf( string, fmt, va ); va_end( va ); return string; } //----------------------------------------------------------------------------- // Purpose: // Input : depth - // *fmt - // ... - //----------------------------------------------------------------------------- void vprint( int depth, const char *fmt, ... ) { char string[ 8192 ]; va_list va; va_start( va, fmt ); vsprintf( string, fmt, va ); va_end( va ); FILE *fp = NULL; if ( uselogfile ) { fp = fopen( "log.txt", "ab" ); } while ( depth-- > 0 ) { printf( " " ); OutputDebugString( " " ); if ( fp ) { fprintf( fp, " " ); } } ::printf( "%s", string ); OutputDebugString( string ); if ( fp ) { char *p = string; while ( *p ) { if ( *p == '\n' ) { fputc( '\r', fp ); } fputc( *p, fp ); p++; } fclose( fp ); } } void logprint( char const *logfile, const char *fmt, ... ) { char string[ 8192 ]; va_list va; va_start( va, fmt ); vsprintf( string, fmt, va ); va_end( va ); FILE *fp = NULL; UnusedContent::CUtlSymbol sym = g_Analysis.symbols.Find( logfile ); static CUtlRBTree< UnusedContent::CUtlSymbol, int > previousfiles( 0, 0, DefLessFunc( UnusedContent::CUtlSymbol ) ); if ( previousfiles.Find( sym ) == previousfiles.InvalidIndex() ) { previousfiles.Insert( sym ); fp = fopen( logfile, "wb" ); } else { fp = fopen( logfile, "ab" ); } if ( fp ) { char *p = string; while ( *p ) { if ( *p == '\n' ) { fputc( '\r', fp ); } fputc( *p, fp ); p++; } fclose( fp ); } } void Con_Printf( const char *fmt, ... ) { va_list args; static char output[1024]; va_start( args, fmt ); vprintf( fmt, args ); vsprintf( output, fmt, args ); vprint( 0, output ); } bool ShouldCheckDir( char const *dirname ); bool ShouldIgnoreDir( const char *dirname ); void BuildFileList_R( int depth, CUtlVector< FileEntry >& files, CUtlVector< FileEntry > * otherfiles, char const *dir, char const *wild, int skipchars ) { WIN32_FIND_DATA wfd; char directory[ 256 ]; char filename[ 256 ]; HANDLE ff; bool canrecurse = true; if ( !Q_stricmp( wild, "..." ) ) { canrecurse = true; sprintf( directory, "%s%s%s", dir[0] == '\\' ? dir + 1 : dir, dir[0] != 0 ? "\\" : "", "*.*" ); } else { sprintf( directory, "%s%s%s", dir, dir[0] != 0 ? "\\" : "", wild ); } int dirlen = Q_strlen( dir ); if ( ( ff = FindFirstFile( directory, &wfd ) ) == INVALID_HANDLE_VALUE ) return; do { if ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) { bool useOtherFiles = false; if ( wfd.cFileName[ 0 ] == '.' ) continue; if ( depth == 0 && !ShouldCheckDir( wfd.cFileName ) && otherfiles ) { if ( !ShouldIgnoreDir( wfd.cFileName ) ) { useOtherFiles = true; } } if ( !canrecurse ) continue; // Recurse down directory if ( dir[0] ) { sprintf( filename, "%s\\%s", dir, wfd.cFileName ); } else { sprintf( filename, "%s", wfd.cFileName ); } BuildFileList_R( depth + 1, useOtherFiles ? *otherfiles: files, NULL, filename, wild, skipchars ); } else { if (!stricmp(wfd.cFileName, "vssver.scc")) continue; char filename[ MAX_PATH ]; if ( dirlen <= skipchars ) { Q_snprintf( filename, sizeof( filename ), "%s", wfd.cFileName ); } else { Q_snprintf( filename, sizeof( filename ), "%s\\%s", &dir[ skipchars ], wfd.cFileName ); } _strlwr( filename ); Q_FixSlashes( filename ); UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( filename ); FileEntry entry; entry.sym = sym; int size = g_pFileSystem->Size( filename ); entry.size = size >= 0 ? (unsigned int)size : 0; files.AddToTail( entry ); if ( !( files.Count() % 3000 ) ) { vprint( 0, "...found %i files\n", files.Count() ); } } } while ( FindNextFile( ff, &wfd ) ); } void BuildFileList( int depth, CUtlVector< FileEntry >& files, CUtlVector< FileEntry > * otherfiles, char const *rootdir, int skipchars ) { files.RemoveAll(); Assert( otherfiles ); otherfiles->RemoveAll(); BuildFileList_R( depth, files, otherfiles, rootdir, "...", skipchars ); } void BuildFileListWildcard( int depth, CUtlVector< FileEntry >& files, char const *rootdir, char const *wildcard, int skipchars ) { files.RemoveAll(); BuildFileList_R( depth, files, NULL, rootdir, wildcard, skipchars ); } static CUtlVector< UnusedContent::CUtlSymbol > g_DirList; static CUtlVector< UnusedContent::CUtlSymbol > g_IgnoreDir; bool ShouldCheckDir( char const *dirname ) { int c = g_DirList.Count(); for ( int i = 0; i < c; ++i ) { char const *check = g_Analysis.symbols.String( g_DirList[ i ] ); if ( !Q_stricmp( dirname, check ) ) return true; } vprint( 1, "Skipping dir %s\n", dirname ); return false; } bool ShouldIgnoreDir( const char *dirname ) { int c = g_IgnoreDir.Count(); for ( int i = 0; i < c; ++i ) { char const *check = g_Analysis.symbols.String( g_IgnoreDir[ i ] ); if ( Q_stristr( dirname, "reslists" ) ) { vprint( 1, "Ignoring dir %s\n", dirname ); return true; } if ( !Q_stricmp( dirname, check ) ) { vprint( 1, "Ignoring dir %s\n", dirname ); return true; } } return false; } void AddCheckdir( char const *dirname ) { UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( dirname ); g_DirList.AddToTail( sym ); vprint( 1, "AddCheckdir[ \"%s\" ]\n", dirname ); } void AddIgnoredir( const char *dirname ) { UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( dirname ); g_IgnoreDir.AddToTail( sym ); vprint( 1, "AddIgnoredir[ \"%s\" ]\n", dirname ); } #define UNUSEDCONTENT_CFG "unusedcontent.cfg" void BuildCheckdirList() { vprint( 0, "Checking for dirlist\n" ); // Search for unusedcontent.cfg file if ( g_pFileSystem->FileExists( UNUSEDCONTENT_CFG, "GAME") ) { KeyValues *kv = new KeyValues( UNUSEDCONTENT_CFG ); if ( kv ) { if ( kv->LoadFromFile( g_pFileSystem, UNUSEDCONTENT_CFG, "GAME" ) ) { for ( KeyValues *sub = kv->GetFirstSubKey(); sub; sub = sub->GetNextKey() ) { if ( !Q_stricmp( sub->GetName(), "dir" ) ) { AddCheckdir( sub->GetString() ); } else if ( !Q_stricmp( sub->GetName(), "ignore" ) ) { AddIgnoredir( sub->GetString() ); } else { vprint( 1, "Unknown subkey '%s' in %s\n", sub->GetName(), UNUSEDCONTENT_CFG ); } } } kv->deleteThis(); } } else { int c = ARRAYSIZE( directories_to_check ); int i; for ( i = 0; i < c; ++i ) { AddCheckdir( directories_to_check[ i ] ); } // add the list of dirs to ignore from the others lists c = ARRAYSIZE( directories_to_ignore ); for ( i = 0; i < c; ++i ) { AddIgnoredir( directories_to_ignore[ i ] ); } } } static CUtlRBTree< UnusedContent::CUtlSymbol, int > g_WhiteList( 0, 0, DefLessFunc( UnusedContent::CUtlSymbol ) ); #define WHITELIST_FILE "whitelist.cfg" static int wl_added = 0; static int wl_removed = 0; void AddToWhiteList( char const *path ) { vprint( 2, "+\t'%s'\n", path ); char dir[ 512 ]; Q_strncpy( dir, path, sizeof( dir ) ); // Get the base filename from the path _strlwr( dir ); Q_FixSlashes( dir ); CUtlVector< FileEntry > files; char *lastslash = strrchr( dir, '\\' ); if ( lastslash == 0 ) { BuildFileListWildcard( 1, files, "", dir, 0 ); } else { char *wild = lastslash + 1; *lastslash = 0; BuildFileListWildcard( 1, files, dir, wild, 0 ); } int c = files.Count(); for ( int i = 0; i < c; ++i ) { UnusedContent::CUtlSymbol sym = files[ i ].sym; if ( g_WhiteList.Find( sym ) == g_WhiteList.InvalidIndex() ) { g_WhiteList.Insert( sym ); ++wl_added; } } } void RemoveFromWhiteList( char const *path ) { vprint( 2, "-\t'%s'\n", path ); char dir[ 512 ]; Q_strncpy( dir, path, sizeof( dir ) ); // Get the base filename from the path _strlwr( dir ); Q_FixSlashes( dir ); CUtlVector< FileEntry > files; char *lastslash = strrchr( dir, '\\' ); if ( lastslash == 0 ) { BuildFileListWildcard( 1, files, "", dir, 0 ); } else { char *wild = lastslash + 1; *lastslash = 0; BuildFileListWildcard( 1, files, dir, wild, 0 ); } int c = files.Count(); for ( int i = 0; i < c; ++i ) { UnusedContent::CUtlSymbol sym = files[ i ].sym; int idx = g_WhiteList.Find( sym ); if ( idx != g_WhiteList.InvalidIndex() ) { g_WhiteList.RemoveAt( idx ); ++wl_removed; } } } void BuildWhiteList() { // Search for unusedcontent.cfg file if ( !g_pFileSystem->FileExists( WHITELIST_FILE ) ) { vprint( 1, "Running with no whitelist.cfg file!!!\n" ); return; } vprint( 1, "\nBuilding whitelist\n" ); KeyValues *kv = new KeyValues( WHITELIST_FILE ); if ( kv ) { if ( kv->LoadFromFile( g_pFileSystem, WHITELIST_FILE, NULL ) ) { for ( KeyValues *sub = kv->GetFirstSubKey(); sub; sub = sub->GetNextKey() ) { if ( !Q_stricmp( sub->GetName(), "add" ) ) { AddToWhiteList( sub->GetString() ); } else if ( !Q_stricmp( sub->GetName(), "remove" ) ) { RemoveFromWhiteList( sub->GetString() ); } else { vprint( 1, "Unknown subkey '%s' in %s\n", sub->GetName(), WHITELIST_FILE ); } } } kv->deleteThis(); } if ( verbose || printwhitelist ) { vprint( 1, "Whitelist:\n\n" ); for ( int i = g_WhiteList.FirstInorder(); i != g_WhiteList.InvalidIndex(); i = g_WhiteList.NextInorder( i ) ) { UnusedContent::CUtlSymbol& sym = g_WhiteList[ i ]; char const *resolved = g_Analysis.symbols.String( sym ); vprint( 2, " %s\n", resolved ); } } // dump the whitelist file list anyway { filesystem->RemoveFile( "whitelist_files.txt", "GAME" ); for ( int i = g_WhiteList.FirstInorder(); i != g_WhiteList.InvalidIndex(); i = g_WhiteList.NextInorder( i ) ) { UnusedContent::CUtlSymbol& sym = g_WhiteList[ i ]; char const *resolved = g_Analysis.symbols.String( sym ); logprint( "whitelist_files.txt", "\"%s\"\n", resolved ); } } vprint( 1, "Whitelist resolves to %d files (added %i/removed %i)\n\n", g_WhiteList.Count(), wl_added, wl_removed ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void printusage( void ) { vprint( 0, "usage: unusedcontent maplistfile\n\ \t Note that you must have generated the reslistsfile output via the engine first!!!\n\ \t-d = spew command prompt deletion instructions to deletions.bat\n\ \t-v = verbose output\n\ \t-l = log to file log.txt\n\ \t-r = print out all referenced files\n\ \t-m = generate referenced.csv with map counts\n\ \t-w = print out whitelist\n\ \t-i = delete unused files immediately\n\ \t-f : specify reslists folder, 'reslists' assumed by default\n\ \t\tmaps/\n\ \t\tmaterials/\n\ \t\tmodels/\n\ \t\tsounds/\n\ \ne.g.: unusedcontent -r maplist.txt\n" ); // Exit app exit( 1 ); } void ParseFilesFromResList( UnusedContent::CUtlSymbol & resfilesymbol, CUtlRBTree< ReferencedFile, int >& files, char const *resfile ) { int addedStrings = 0; int resourcesConsidered = 0; int offset = Q_strlen( gamedir ); char basedir[MAX_PATH]; Q_strncpy( basedir, gamedir, sizeof( basedir ) ); if ( !Q_StripLastDir( basedir, sizeof( basedir ) ) ) Error( "Can't get basedir from %s.", gamedir ); FileHandle_t resfilehandle; resfilehandle = g_pFileSystem->Open( resfile, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != resfilehandle ) { // Read in the entire file int length = g_pFileSystem->Size(resfilehandle); if ( length > 0 ) { char *pStart = (char *)new char[ length + 1 ]; if ( pStart && ( length == g_pFileSystem->Read(pStart, length, resfilehandle) ) ) { pStart[ length ] = 0; char *pFileList = pStart; char token[512]; while ( 1 ) { pFileList = ParseFile( pFileList, token, NULL ); if ( !pFileList ) break; if ( strlen( token ) > 0 ) { char szFileName[ 256 ]; Q_snprintf( szFileName, sizeof( szFileName ), "%s%s", basedir, token ); _strlwr( szFileName ); Q_FixSlashes( szFileName ); while ( szFileName[ strlen( szFileName ) - 1 ] == '\n' || szFileName[ strlen( szFileName ) - 1 ] == '\r' ) { szFileName[ strlen( szFileName ) - 1 ] = 0; } if ( Q_strnicmp( szFileName, gamedir, offset ) ) continue; char *pFile = szFileName + offset; ++resourcesConsidered; ReferencedFile rf; rf.sym = g_Analysis.symbols.AddString( pFile ); int idx = files.Find( rf ); if ( idx == files.InvalidIndex() ) { ++addedStrings; rf.maplist.AddToTail( resfilesymbol ); files.Insert( rf ); } else { // ReferencedFile & slot = files[ idx ]; if ( slot.maplist.Find( resfilesymbol ) == slot.maplist.InvalidIndex() ) { slot.maplist.AddToTail( resfilesymbol ); } } } } } delete[] pStart; } g_pFileSystem->Close(resfilehandle); } int filesFound = addedStrings; vprint( 1, "Found %i new resources (%i total) in %s\n", filesFound, resourcesConsidered, resfile ); } bool BuildReferencedFileList( CUtlVector< UnusedContent::CUtlSymbol >& resfiles, CUtlRBTree< ReferencedFile, int >& files, const char *resfile ) { // Load the reslist file FileHandle_t resfilehandle; resfilehandle = g_pFileSystem->Open( resfile, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != resfilehandle ) { // Read in and parse mapcycle.txt int length = g_pFileSystem->Size(resfilehandle); if ( length > 0 ) { char *pStart = (char *)new char[ length + 1 ]; if ( pStart && ( length == g_pFileSystem->Read(pStart, length, resfilehandle) ) ) { pStart[ length ] = 0; char *pFileList = pStart; while ( 1 ) { char szResList[ 256 ]; pFileList = COM_Parse( pFileList ); if ( strlen( com_token ) <= 0 ) break; Q_snprintf(szResList, sizeof( szResList ), "%s%s.lst", g_szReslistDir, com_token ); _strlwr( szResList ); Q_FixSlashes( szResList ); if ( !g_pFileSystem->FileExists( szResList ) ) { vprint( 0, "Couldn't find %s\n", szResList ); continue; } UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( szResList ); resfiles.AddToTail( sym ); } } delete[] pStart; } g_pFileSystem->Close(resfilehandle); } else { Error( "Unable to open reslist file %s\n", resfile ); exit( -1 ); } if ( g_pFileSystem->FileExists( CFmtStr( "%sall.lst", g_szReslistDir ) ) ) { UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( CFmtStr( "%sall.lst", g_szReslistDir ) ); resfiles.AddToTail( sym ); } if ( g_pFileSystem->FileExists( CFmtStr( "%sengine.lst", g_szReslistDir ) ) ) { UnusedContent::CUtlSymbol sym = g_Analysis.symbols.AddString( CFmtStr( "%sengine.lst", g_szReslistDir ) ); resfiles.AddToTail( sym ); } // Do we have any resfiles? if ( resfiles.Count() <= 0 ) { vprint( 0, "%s didn't have any actual .lst files in the reslists folder, have you run the engine with %s\n", resfile, "-makereslists -usereslistfile maplist.txt" ); return false; } vprint( 0, "Parsed %i reslist files\n", resfiles.Count() ); // Now load in each res file int c = resfiles.Count(); for ( int i = 0; i < c; ++i ) { UnusedContent::CUtlSymbol& filename = resfiles[ i ]; char fn[ 256 ]; Q_strncpy( fn, g_Analysis.symbols.String( filename ), sizeof( fn ) ); ParseFilesFromResList( filename, files, fn ); } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CheckLogFile( void ) { if ( uselogfile ) { _unlink( "log.txt" ); vprint( 0, " Outputting to log.txt\n" ); } } void PrintHeader() { vprint( 0, "Valve Software - unusedcontent.exe (%s)\n", __DATE__ ); vprint( 0, "--- Compares reslists with actual game content tree to show unreferenced content and stats ---\n" ); } static bool ReferencedFileLessFunc( const ReferencedFile &lhs, const ReferencedFile &rhs ) { char const *s1 = g_Analysis.symbols.String( lhs.sym ); char const *s2 = g_Analysis.symbols.String( rhs.sym ); return Q_stricmp( s1, s2 ) < 0; } static bool FileEntryLessFunc( const FileEntry &lhs, const FileEntry &rhs ) { char const *s1 = g_Analysis.symbols.String( lhs.sym ); char const *s2 = g_Analysis.symbols.String( rhs.sym ); return Q_stricmp( s1, s2 ) < 0; } static bool RefFileLessFunc( const ReferencedFile &lhs, const ReferencedFile &rhs ) { char const *s1 = g_Analysis.symbols.String( lhs.sym ); char const *s2 = g_Analysis.symbols.String( rhs.sym ); return Q_stricmp( s1, s2 ) < 0; } struct DirEntry { DirEntry() { total = 0; unreferenced = 0; whitelist = 0; } double total; double unreferenced; double whitelist; }; void Correlate( CUtlRBTree< ReferencedFile, int >& referencedfiles, CUtlVector< FileEntry >& contentfiles, const char *modname ) { int i; int c = contentfiles.Count(); double totalDiskSize = 0; double totalReferencedDiskSize = 0; double totalWhiteListDiskSize = 0; for ( i = 0; i < c; ++i ) { totalDiskSize += contentfiles [ i ].size; } vprint( 0, "Content tree size on disk %s\n", Q_pretifymem( totalDiskSize, 3 ) ); // Analysis is to walk tree and see which files on disk are referenced in the .lst files // Need a fast lookup from file symbol to referenced list CUtlRBTree< ReferencedFile, int > tree( 0, 0, ReferencedFileLessFunc ); c = referencedfiles.Count(); for ( i = 0 ; i < c; ++i ) { tree.Insert( referencedfiles[ i ] ); } // Now walk the on disk file and see check off resources which are in referenced c = contentfiles.Count(); int invalidindex = tree.InvalidIndex(); unsigned int refcounted = 0; unsigned int whitelisted = 0; filesystem->RemoveFile( CFmtStr( "%swhitelist.lst", g_szReslistDir ), "GAME" ); for ( i = 0; i < c; ++i ) { FileEntry & entry = contentfiles[ i ]; ReferencedFile foo; foo.sym = entry.sym; bool gameref = tree.Find( foo ) != invalidindex; char const *fn = g_Analysis.symbols.String( entry.sym ); bool whitelist = g_WhiteList.Find( entry.sym ) != g_WhiteList.InvalidIndex(); if ( gameref || whitelist ) { entry.referenced = gameref ? REFERENCED_GAME : REFERENCED_WHITELIST; totalReferencedDiskSize += entry.size; if ( entry.referenced == REFERENCED_WHITELIST ) { logprint( CFmtStr( "%swhitelist.lst", g_szReslistDir ), "\"%s\\%s\"\n", modname, fn ); totalWhiteListDiskSize += entry.size; ++whitelisted; } ++refcounted; } } vprint( 0, "Found %i referenced (%i whitelist) files in tree, %s\n", refcounted, whitelisted, Q_pretifymem( totalReferencedDiskSize, 2 ) ); vprint( 0, "%s appear unused\n", Q_pretifymem( totalDiskSize - totalReferencedDiskSize, 2 ) ); // Now sort and dump the unreferenced ones.. vprint( 0, "Sorting unreferenced files list...\n" ); CUtlRBTree< FileEntry, int > unreftree( 0, 0, FileEntryLessFunc ); for ( i = 0; i < c; ++i ) { FileEntry & entry = contentfiles[ i ]; if ( entry.referenced != REFERENCED_NO ) continue; unreftree.Insert( entry ); } // Now walk the unref tree in order i = unreftree.FirstInorder(); invalidindex = unreftree.InvalidIndex(); int index = 0; while ( i != invalidindex ) { FileEntry & entry = unreftree[ i ]; if ( showreferencedfiles ) { vprint( 1, "%6i %12s: %s\n", ++index, Q_pretifymem( entry.size, 2 ), g_Analysis.symbols.String( entry.sym ) ); } i = unreftree.NextInorder( i ); } if ( showmapfileusage ) { vprint( 0, "Writing referenced.csv...\n" ); // Now walk the list of referenced files and print out how many and which maps reference them i = tree.FirstInorder(); invalidindex = tree.InvalidIndex(); index = 0; while ( i != invalidindex ) { ReferencedFile & entry = tree[ i ]; char ext[ 32 ]; Q_ExtractFileExtension( g_Analysis.symbols.String( entry.sym ), ext, sizeof( ext ) ); logprint( "referenced.csv", "\"%s\",\"%s\",%d", g_Analysis.symbols.String( entry.sym ), ext, entry.maplist.Count() ); int mapcount = entry.maplist.Count(); for ( int j = 0 ; j < mapcount; ++j ) { char basemap[ 128 ]; Q_FileBase( g_Analysis.symbols.String( entry.maplist[ j ] ), basemap, sizeof( basemap ) ); logprint( "referenced.csv", ",\"%s\"", basemap ); } logprint( "referenced.csv", "\n" ); i = tree.NextInorder( i ); } } vprint( 0, "\nBuilding directory summary list...\n" ); // Now build summaries by root branch off of gamedir (e.g., for sound, materials, models, etc.) CUtlDict< DirEntry, int > directories; invalidindex = directories.InvalidIndex(); for ( i = 0; i < c; ++i ) { FileEntry & entry = contentfiles[ i ]; // Get the dir name char const *dirname = g_Analysis.symbols.String( entry.sym ); const char *backslash = strstr( dirname, "\\" ); char dir[ 256 ]; if ( !backslash ) { dir[0] = 0; } else { Q_strncpy( dir, dirname, backslash - dirname + 1); } int idx = directories.Find( dir ); if ( idx == invalidindex ) { DirEntry foo; idx = directories.Insert( dir, foo ); } DirEntry & de = directories[ idx ]; de.total += entry.size; if ( entry.referenced == REFERENCED_NO ) { de.unreferenced += entry.size; } if ( entry.referenced == REFERENCED_WHITELIST ) { de.whitelist += entry.size; } } if ( spewdeletions ) { // Spew deletion commands to console if ( immediatedelete ) { vprint( 0, "\n\nDeleting files...\n" ); } else { vprint( 0, "\n\nGenerating deletions.bat\n" ); } i = unreftree.FirstInorder(); invalidindex = unreftree.InvalidIndex(); float deletionSize = 0.0f; int deletionCount = 0; while ( i != invalidindex ) { FileEntry & entry = unreftree[ i ]; i = unreftree.NextInorder( i ); // Don't delete stuff that's in the white list if ( g_WhiteList.Find( entry.sym ) != g_WhiteList.InvalidIndex() ) { if ( verbose ) { vprint( 0, "whitelist blocked deletion of %s\n", g_Analysis.symbols.String( entry.sym ) ); } continue; } ++deletionCount; deletionSize += entry.size; if ( immediatedelete ) { if ( _chmod( g_Analysis.symbols.String( entry.sym ), _S_IWRITE ) == -1 ) { vprint( 0, "Could not find file %s\n", g_Analysis.symbols.String( entry.sym ) ); } if ( _unlink( g_Analysis.symbols.String( entry.sym ) ) == -1 ) { vprint( 0, "Could not delete file %s\n", g_Analysis.symbols.String( entry.sym ) ); } if ( deletionCount % 1000 == 0 ) { vprint( 0, "...deleted %i files\n", deletionCount ); } } else { logprint( "deletions.bat", "del \"%s\" /f\n", g_Analysis.symbols.String( entry.sym ) ); } } vprint( 0, "\nFile deletion (%d files, %s)\n\n", deletionCount, Q_pretifymem(deletionSize, 2) ); } double grand_total = 0; double grand_total_unref = 0; double grand_total_white = 0; char totalstring[ 20 ]; char unrefstring[ 20 ]; char refstring[ 20 ]; char whiteliststring[ 20 ]; vprint( 0, "---------------------------------------- Summary ----------------------------------------\n" ); vprint( 0, "% 15s % 15s % 15s % 15s %12s\n", "Referenced", "WhiteListed", "Unreferenced", "Total", "Directory" ); // Now walk the dictionary in order i = directories.First(); while ( i != invalidindex ) { DirEntry & de = directories[ i ]; double remainder = de.total - de.unreferenced; float percent_unref = 0.0f; float percent_white = 0.0f; if ( de.total > 0 ) { percent_unref = 100.0f * (float)de.unreferenced / (float)de.total; percent_white = 100.0f * (float)de.whitelist / (float)de.total; } Q_strncpy( totalstring, Q_pretifymem( de.total, 2 ), sizeof( totalstring ) ); Q_strncpy( unrefstring, Q_pretifymem( de.unreferenced, 2 ), sizeof( unrefstring ) ); Q_strncpy( refstring, Q_pretifymem( remainder, 2 ), sizeof( refstring ) ); Q_strncpy( whiteliststring, Q_pretifymem( de.whitelist, 2 ), sizeof( whiteliststring ) ); vprint( 0, "%15s (%8.3f%%) %15s (%8.3f%%) %15s (%8.3f%%) %15s => dir: %s\n", refstring, 100.0f - percent_unref, whiteliststring, percent_white, unrefstring, percent_unref, totalstring, directories.GetElementName( i ) ); grand_total += de.total; grand_total_unref += de.unreferenced; grand_total_white += de.whitelist; i = directories.Next( i ); } Q_strncpy( totalstring, Q_pretifymem( grand_total, 2 ), sizeof( totalstring ) ); Q_strncpy( unrefstring, Q_pretifymem( grand_total_unref, 2 ), sizeof( unrefstring ) ); Q_strncpy( refstring, Q_pretifymem( grand_total - grand_total_unref, 2 ), sizeof( refstring ) ); Q_strncpy( whiteliststring, Q_pretifymem( grand_total_white, 2 ), sizeof( whiteliststring ) ); double percent_unref = 100.0 * grand_total_unref / grand_total; double percent_white = 100.0 * grand_total_white / grand_total; vprint( 0, "-----------------------------------------------------------------------------------------\n" ); vprint( 0, "%15s (%8.3f%%) %15s (%8.3f%%) %15s (%8.3f%%) %15s\n", refstring, 100.0f - percent_unref, whiteliststring, percent_white, unrefstring, percent_unref, totalstring ); } //----------------------------------------------------------------------------- // Purpose: // Input : argc - // argv[] - // Output : int //----------------------------------------------------------------------------- int main( int argc, char* argv[] ) { SpewOutputFunc( SpewFunc ); SpewActivate( "unusedcontent", 2 ); CommandLine()->CreateCmdLine( argc, argv ); int i=1; for ( i ; iRemoveAllSearchPaths(); g_pFullFileSystem->AddSearchPath(gamedir, "GAME"); Q_strlower( gamedir ); Q_FixSlashes( gamedir ); // //ProcessMaterialsDirectory( vmtdir ); // find out the mod dir name Q_strncpy( modname, gamedir, sizeof(modname) ); modname[ strlen(modname) - 1] = 0; if ( strrchr( modname, '\\' ) ) { Q_strncpy( modname, strrchr( modname, '\\' ) + 1, sizeof(modname) ); } else { Q_strncpy( modname, "", sizeof(modname) ); } vprint( 1, "Mod Name:%s\n", modname); BuildCheckdirList(); BuildWhiteList(); vprint( 0, "Building aggregate file list from resfile output\n" ); CUtlRBTree< ReferencedFile, int > referencedfiles( 0, 0, RefFileLessFunc ); CUtlVector< UnusedContent::CUtlSymbol > resfiles; BuildReferencedFileList( resfiles, referencedfiles, resfile ); vprint( 0, "found %i files\n\n", referencedfiles.Count() ); vprint( 0, "Building list of all game content files\n" ); CUtlVector< FileEntry > contentfiles; CUtlVector< FileEntry > otherfiles; BuildFileList( 0, contentfiles, &otherfiles, "", 0 ); vprint( 0, "found %i files in content tree\n\n", contentfiles.Count() ); Correlate( referencedfiles, contentfiles, modname ); // now output the files not referenced in the whitelist or general reslists filesystem->RemoveFile( CFmtStr( "%sunreferenced_files.lst", g_szReslistDir ), "GAME" ); int c = otherfiles.Count(); for ( i = 0; i < c; ++i ) { FileEntry & entry = otherfiles[ i ]; char const *name = g_Analysis.symbols.String( entry.sym ); logprint( CFmtStr( "%sunreferenced_files.lst", g_szReslistDir ), "\"%s\\%s\"\n", modname, name ); } // also include the files from deletions.bat, as we don't actually run that now c = contentfiles.Count(); for ( i = 0; i < c; ++i ) { FileEntry & entry = contentfiles[ i ]; if ( entry.referenced != REFERENCED_NO ) continue; char const *fn = g_Analysis.symbols.String( entry.sym ); logprint( CFmtStr( "%sunreferenced_files.lst", g_szReslistDir ), "\"%s\\%s\"\n", modname, fn ); } FileSystem_Term(); return 0; }