source-engine/utils/unusedcontent/unusedcontent.cpp
FluorescentCIAAfricanAmerican 3bf9df6b27 1
2020-04-22 12:56:21 -04:00

1767 lines
43 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: unusedcontent.cpp : Defines the entry point for the console application.
//
//=============================================================================//
#include "cbase.h"
#include <stdio.h>
#include <windows.h>
#include <io.h>
#include <sys/stat.h>
#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<CStringPoolIndex, unsigned int> m_Lookup;
typedef struct
{
int m_TotalLen; // How large is
int m_SpaceUsed;
char m_Data[1];
} StringPool_t;
// stores the string data
CUtlVector<StringPool_t*> 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 <reslistdir> : 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 ; i<argc ; i++)
{
if ( argv[ i ][ 0 ] == '-' )
{
switch( argv[ i ][ 1 ] )
{
case 'l':
uselogfile = true;
break;
case 'v':
verbose = true;
break;
case 'r':
showreferencedfiles = true;
break;
case 'd':
spewdeletions = true;
break;
case 'i':
immediatedelete = true;
break;
case 'w':
printwhitelist = true;
break;
case 'm':
showmapfileusage = true;
break;
case 'g':
// Just skip -game
Assert( !Q_stricmp( argv[ i ], "-game" ) );
++i;
break;
case 'f':
// grab reslists folder
{
++i;
Q_strncpy( g_szReslistDir, argv[ i ], sizeof( g_szReslistDir ) );
Q_strlower( g_szReslistDir );
Q_FixSlashes( g_szReslistDir );
Q_AppendSlash( g_szReslistDir, sizeof( g_szReslistDir ) );
}
break;
default:
printusage();
break;
}
}
}
if ( argc < 3 || ( i != argc ) )
{
PrintHeader();
printusage();
return 0;
}
CheckLogFile();
PrintHeader();
vprint( 0, " Using reslist dir '%s'\n", g_szReslistDir );
vprint( 0, " Looking for extraneous content...\n" );
char resfile[ 256 ];
strcpy( resfile, argv[ i - 1 ] );
vprint( 0, " Comparing results of resfile (%s) with files under current directory...\n", resfile );
char workingdir[ 256 ];
workingdir[0] = 0;
Q_getwd( workingdir, sizeof( workingdir ) );
// If they didn't specify -game on the command line, use VPROJECT.
CmdLib_InitFileSystem( workingdir );
filesystem = (IFileSystem *)(CmdLib_GetFileSystemFactory()( FILESYSTEM_INTERFACE_VERSION, NULL ));
if ( !filesystem )
{
AssertMsg( 0, "Failed to create/get IFileSystem" );
return 1;
}
g_pFullFileSystem->RemoveAllSearchPaths();
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;
}