//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: localization_check.cpp : Defines the entry point for the console application. // // // What the tool does: // // Load g_pSoundEmitterSystem, vgui::localize, soundcombiner system, vcd system // // Catalog all files in closecaption/ folder // Iterate all known vcds // // 1) For each sound emitted by a vcd not marked as CC_DISABLED, verify that there's an entry in the localization table // 2) For each combined sound in a vcd, make sure there's a valid entry in the localization table // 3) For each combined sound, verify that the english version combined .wav file has the proper checksum // 4) Note any files in the closecaption folder which are orphaned after parsing the above // 5) If hl2_french directories etc. exist, then compare combined .wav files with localized versions and // see if localized checksum tag differs from US one, or warn if tag missing, but complain if .wav duration is different. // // UNDONE:re-create combined .wav files in english to the extent that the checksums mismatch? // //===========================================================================// #include "cbase.h" #include #include #include #include #include #include "tier0/dbg.h" #include "utldict.h" #include "filesystem.h" #include "KeyValues.h" #include "cmdlib.h" #include "scriplib.h" #include "appframework/tier3app.h" #include "vstdlib/random.h" #include "SoundEmitterSystem/isoundemittersystembase.h" #include "choreoscene.h" #include "choreoevent.h" #include "choreochannel.h" #include "choreoactor.h" #include "iscenetokenprocessor.h" #include "ifaceposersound.h" #include "snd_audio_source.h" #include "snd_wave_source.h" #include "AudioWaveOutput.h" #include "isoundcombiner.h" #include "tier0/icommandline.h" #include #include "vgui/ivgui.h" #include "soundchars.h" #include "sentence.h" #include "tier2/riff.h" #include "utlbuffer.h" #include "FileSystem_Helpers.h" #include "pacifier.h" #include "phonemeextractor/PhonemeExtractor.h" #include "UnicodeFileHelpers.h" using namespace vgui; bool uselogfile = false; bool regenerate = false; bool regenerate_quiet = false; bool regenerate_all = false; // user hit a to y/n/all prompt bool generate_usage = false; bool nuke = false; bool checkscriptsounds = false; bool build_cc = false; bool build_script = false; bool wavcheck = false; bool extractphonemes = false; bool checkforloops = false; bool importcaptions = false; bool checkfordups = false; bool makecopybatch = false; bool syncducking = false; char sounddir[ 512 ]; char importfile[ 512 ]; // for -i processing char fromdir[ 512 ]; char todir[ 512 ]; struct AnalysisData { CUtlSymbolTable symbols; }; IFileSystem *filesystem = NULL; static AnalysisData g_Analysis; static CUniformRandomStream g_Random; IUniformRandomStream *random = &g_Random; static bool spewed = false; static bool forceextract = false; #define SOUND_DURATION_TOLERANCE 0.1f // 100 msec of slop void vprint( int depth, const char *fmt, ... ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void printusage( void ) { vprint( 0, "usage: localization_check languagename\n\ \t-v = verbose output\n\ \t-l = log to file log.txt\n\ \t-u = generate usage data for .vcds based on -makereslists maplist.txt files\n\ \t-b = generate nuke.bat which will nuke all of the unreferenced .vcds from your tree\n\ \t-s = generate list of unused sounds.txt entries\n\ \t-c = build cc fixed/fixed2.txt files\n\ \t-r = regenerate missing/mismatched combined wav files\n\ \t\t-q = quiet mode during regenerate\n\ languagename = check combined language files for existence and duration, 'english' for no extra checks\n\ \t-x = build script of dialog from .vcds\n\ \t-w sounddir = spew csv of wave files in directory, including sound and cc info\n\ \t-e sounddir = do textless phoneme processing on files in dir and subdirs\n\ \t-f with above, forces extraction even if wav already has phonemes (danger!)\n\ \t-i = import unicode wavename/caption into a new closecaption_test.txt file\n\ \t-d = check for duplicated unicode strings\n\ \t-p = pull raw english txt out of closecaption document, for spellchecking\n\ \t-m = given a directory of .wav files finds the full directory path they should live in based on english\n\ \t-a english_sound_dir localized_sound_dir = sets voice duck for sounds in localized_sound_dir to match the values in the english dir\n\ \t-loop = warn on any sound files in specified directory having loop markers in the .wav\n\ \ne.g.: localization_check -l -w npc/metropolice/vo -r french\n" ); // Exit app exit( 1 ); } //----------------------------------------------------------------------------- // Purpose: Helper for parsing scene data file //----------------------------------------------------------------------------- class CSceneTokenProcessor : public ISceneTokenProcessor { public: const char *CurrentToken( void ); bool GetToken( bool crossline ); bool TokenAvailable( void ); void Error( const char *fmt, ... ); }; //----------------------------------------------------------------------------- // Purpose: // Output : const char //----------------------------------------------------------------------------- const char *CSceneTokenProcessor::CurrentToken( void ) { return token; } //----------------------------------------------------------------------------- // Purpose: // Input : crossline - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CSceneTokenProcessor::GetToken( bool crossline ) { return ::GetToken( crossline ) ? true : false; } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CSceneTokenProcessor::TokenAvailable( void ) { return ::TokenAvailable() ? true : false; } static char *va( char const *fmt, ... ) { static char string[ 2048 ]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( string, sizeof(string), fmt, argptr ); va_end( argptr ); return string; } void cleanquotes( char *text ) { char *out = text; while ( *out ) { if ( *out == '\"' ) { *out++ = '\''; } else { ++out; } } } //----------------------------------------------------------------------------- // Purpose: // Input : *fmt - // ... - //----------------------------------------------------------------------------- void CSceneTokenProcessor::Error( const char *fmt, ... ) { char string[ 2048 ]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( string, sizeof(string), fmt, argptr ); va_end( argptr ); Warning( "%s", string ); Assert(0); } static CSceneTokenProcessor g_TokenProcessor; SpewRetval_t SpewFunc( SpewType_t type, char const *pMsg ) { spewed = true; printf( "%s", pMsg ); OutputDebugString( pMsg ); if ( type == SPEW_ERROR ) { printf( "\n" ); OutputDebugString( "\n" ); } return SPEW_CONTINUE; } 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; static bool first = true; if ( first ) { first = false; 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 nuke_print( int depth, const char *fmt, ... ) { char string[ 8192 ]; va_list va; va_start( va, fmt ); vsprintf( string, fmt, va ); va_end( va ); static bool first = false; FILE *fp = NULL; char const *nukefile = "nuke.bat"; if ( first ) { first = false; fp = fopen( nukefile, "wb" ); } else { fp = fopen( nukefile, "ab" ); } while ( depth-- > 0 ) { fprintf( fp, " " ); } char *p = string; while ( *p ) { if ( *p == '\n' ) { fputc( '\r', fp ); } fputc( *p, fp ); p++; } fclose( fp ); } //----------------------------------------------------------------------------- // 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 Con_Printf( const char *fmt, ... ) { va_list args; static char output[1024]; va_start( args, fmt ); Q_vsnprintf( output, sizeof( output ), fmt, args ); va_end( args ); vprint( 0, output ); } void BuildFileList_R( CUtlVector< CUtlSymbol >& files, char const *dir, char const *extension ) { WIN32_FIND_DATA wfd; char directory[ 256 ]; char filename[ MAX_PATH ]; HANDLE ff; sprintf( directory, "%s\\*.*", dir ); if ( ( ff = FindFirstFile( directory, &wfd ) ) == INVALID_HANDLE_VALUE ) return; int extlen = strlen( extension ); do { if ( wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) { if ( wfd.cFileName[ 0 ] == '.' ) continue; // Recurse down directory sprintf( filename, "%s\\%s", dir, wfd.cFileName ); BuildFileList_R( files, filename, extension ); } else { int len = strlen( wfd.cFileName ); if ( len > extlen ) { if ( !stricmp( &wfd.cFileName[ len - extlen ], extension ) ) { Q_snprintf( filename, sizeof( filename ), "%s\\%s", dir, wfd.cFileName ); _strlwr( filename ); Q_FixSlashes( filename ); CUtlSymbol sym = g_Analysis.symbols.AddString( filename ); files.AddToTail( sym ); if ( !( files.Count() % 3000 ) ) { vprint( 0, "...found %i .%s files\n", files.Count(), extension ); } } } } } while ( FindNextFile( ff, &wfd ) ); } void BuildFileList( CUtlVector< CUtlSymbol >& files, char const *rootdir, char const *extension ) { files.RemoveAll(); BuildFileList_R( files, rootdir, extension ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CheckLogFile( void ) { if ( uselogfile ) { _unlink( "log.txt" ); vprint( 0, " Outputting to log.txt\n" ); } } void PrintHeader() { vprint( 0, "Valve Software - localization_check.exe (%s)\n", __DATE__ ); vprint( 0, "--- Voice Wav File .vcd Checker ---\n" ); } char const *FacePoser_TranslateSoundNameGender( char const *soundname, gender_t gender ) { if ( Q_stristr( soundname, ".wav" ) ) return PSkipSoundChars( soundname ); return PSkipSoundChars( g_pSoundEmitterSystem->GetWavFileForSound( soundname, gender ) ); } //----------------------------------------------------------------------------- // Purpose: Implements the RIFF i/o interface on stdio //----------------------------------------------------------------------------- class StdIOReadBinary : public IFileReadBinary { public: int open( const char *pFileName ) { return (int)g_pFullFileSystem->Open( pFileName, "rb" ); } int read( void *pOutput, int size, int file ) { if ( !file ) return 0; return g_pFullFileSystem->Read( pOutput, size, (FileHandle_t)file ); } void seek( int file, int pos ) { if ( !file ) return; g_pFullFileSystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD ); } unsigned int tell( int file ) { if ( !file ) return 0; return g_pFullFileSystem->Tell( (FileHandle_t)file ); } unsigned int size( int file ) { if ( !file ) return 0; return g_pFullFileSystem->Size( (FileHandle_t)file ); } void close( int file ) { if ( !file ) return; g_pFullFileSystem->Close( (FileHandle_t)file ); } }; class StdIOWriteBinary : public IFileWriteBinary { public: int create( const char *pFileName ) { g_pFullFileSystem->SetFileWritable( pFileName, true, "GAME" ); return (int)g_pFullFileSystem->Open( pFileName, "wb" ); } int write( void *pData, int size, int file ) { return g_pFullFileSystem->Write( pData, size, (FileHandle_t)file ); } void close( int file ) { g_pFullFileSystem->Close( (FileHandle_t)file ); } void seek( int file, int pos ) { g_pFullFileSystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD ); } unsigned int tell( int file ) { return g_pFullFileSystem->Tell( (FileHandle_t)file ); } }; static StdIOWriteBinary io_out; static StdIOReadBinary io_in; #define RIFF_WAVE MAKEID('W','A','V','E') #define WAVE_FMT MAKEID('f','m','t',' ') #define WAVE_DATA MAKEID('d','a','t','a') #define WAVE_FACT MAKEID('f','a','c','t') #define WAVE_CUE MAKEID('c','u','e',' ') //----------------------------------------------------------------------------- // Purpose: // Input : &walk - //----------------------------------------------------------------------------- static void ParseSentence( CSentence& sentence, IterateRIFF &walk ) { CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); buf.EnsureCapacity( walk.ChunkSize() ); walk.ChunkRead( buf.Base() ); buf.SeekPut( CUtlBuffer::SEEK_HEAD, walk.ChunkSize() ); sentence.InitFromDataChunk( buf.Base(), buf.TellPut() ); } bool LoadSentenceFromWavFileUsingIO( char const *wavfile, CSentence& sentence, IFileReadBinary& io, void *formatbuffer = NULL, int* formatsize = NULL, int *datasize = NULL ) { int insize = 0; if ( formatsize ) { insize = *formatsize; *formatsize = 0; } if ( datasize ) { *datasize = 0; } sentence.Reset(); InFileRIFF riff( wavfile, io ); // UNDONE: Don't use printf to handle errors if ( riff.RIFFName() != RIFF_WAVE ) { return false; } // set up the iterator for the whole file (root RIFF is a chunk) IterateRIFF walk( riff, riff.RIFFSize() ); // This chunk must be first as it contains the wave's format // break out when we've parsed it bool found = false; while ( walk.ChunkAvailable( ) ) { switch( walk.ChunkName() ) { case WAVE_FMT: { if ( formatbuffer && formatsize ) { if ( walk.ChunkSize() <= insize ) { *formatsize = walk.ChunkSize(); walk.ChunkRead( formatbuffer ); } else { Error( "oops, format tag too big!!!" ); } } } break; case WAVE_VALVEDATA: { found = true; ParseSentence( sentence, walk ); } break; case WAVE_DATA: { if ( datasize ) { *datasize = walk.ChunkSize(); } } break; } walk.ChunkNext(); } return true; } bool LoadSentenceFromWavFile( char const *wavfile, CSentence& sentence, void *formatbuffer = NULL, int* formatsize = NULL, int *dataSize = NULL ) { return LoadSentenceFromWavFileUsingIO( wavfile, sentence, io_in, formatbuffer, formatsize, dataSize ); } bool ValidateCombinedFileCheckSum( char const *outfilename, char const *cctoken, gender_t gender, CUtlRBTree< CChoreoEvent * >& sorted, CUtlRBTree< CUtlSymbol, int >& referencedcaptionwaves ) { CUtlVector< CombinerEntry > work; char actualfile[ 512 ]; g_pSoundEmitterSystem->GenderExpandString( gender, outfilename, actualfile, sizeof( actualfile ) ); if ( Q_strlen( actualfile ) <= 0 ) { return false; } int i = sorted.FirstInorder(); if ( i != sorted.InvalidIndex() ) { CChoreoEvent *e = sorted[ i ]; float startoffset = e->GetStartTime(); do { e = sorted[ i ]; float curoffset = e->GetStartTime(); CombinerEntry ce; Q_snprintf( ce.wavefile, sizeof( ce.wavefile ), "sound/%s", FacePoser_TranslateSoundNameGender( e->GetParameters(), gender ) ); ce.startoffset = curoffset - startoffset; work.AddToTail( ce ); i = sorted.NextInorder( i ); } while ( i != sorted.InvalidIndex() ); int c = work.Count(); char worklist[ 2048 ]; worklist[ 0 ] = 0; for ( i = 0; i < c; ++i ) { CombinerEntry &item = work[ i ]; Q_strncat( worklist, item.wavefile, sizeof( worklist ), COPY_ALL_CHARACTERS ); if ( i != c - 1 ) { Q_strncat( worklist, ", ", sizeof( worklist ), COPY_ALL_CHARACTERS ); } } logprint( "cc_combined.txt", "combined .wav '%s': %s\n", actualfile, worklist ); } bool valid = soundcombiner->IsCombinedFileChecksumValid( g_pFullFileSystem, actualfile, work ); if ( !valid ) { vprint( 0, "combined file (%s) checksum mismatch for '%s'\n event '%s' of scene '%s'\n", actualfile, cctoken, sorted[0]->GetName(), sorted[0]->GetScene()->GetFilename() ); if ( regenerate ) { bool dothisfile = false; if ( !regenerate_all ) { vprint( 0, "Regenerate '%s'? (Yes/No/All)", actualfile ); char ch = getch(); if ( ch == 'y' || ch == 'Y' ) { dothisfile = true; } if ( ch == 'a' || ch == 'A' ) { regenerate_all = true; dothisfile = true; } vprint( 0, "\n" ); } else { dothisfile = true; } if ( dothisfile ) { bool success = soundcombiner->CombineSoundFiles( g_pFullFileSystem, actualfile, work ); vprint( 1, "%s: %s\n", actualfile, success ? "succeeded" : "FAILED" ); } } } else { if ( regenerate ) { vprint( 0, "combined file (%s) checksum still matches for %s, skipping rebuild...\n", actualfile, cctoken ); } } // Mark the file as referenced // char fn[ 512 ]; Q_snprintf( fn, sizeof( fn ), "%s%s", gamedir, actualfile ); _strlwr( fn ); Q_FixSlashes( fn ); CUtlSymbol sym = g_Analysis.symbols.AddString( fn ); if ( referencedcaptionwaves.Find( sym ) == referencedcaptionwaves.InvalidIndex() ) { referencedcaptionwaves.Insert( sym ); } return valid; } static bool EventStartTimeLessFunc( CChoreoEvent * const &p1, CChoreoEvent * const &p2 ) { CChoreoEvent *w1; CChoreoEvent *w2; w1 = const_cast< CChoreoEvent * >( p1 ); w2 = const_cast< CChoreoEvent * >( p2 ); return w1->GetStartTime() < w2->GetStartTime(); } static bool SymbolLessFunc( const CUtlSymbol & p1, const CUtlSymbol &p2 ) { if ( Q_stricmp( g_Analysis.symbols.String( p1 ), g_Analysis.symbols.String( p2 ) ) < 0 ) return true; return false; } bool ValidateCombinedSoundCheckSum( CChoreoEvent *e, CUtlRBTree< CUtlSymbol, int >& referencedcaptionwaves ) { if ( !e || e->GetType() != CChoreoEvent::SPEAK ) return false; bool genderwildcard = e->IsCombinedUsingGenderToken(); char outfilename[ 512 ]; Q_memset( outfilename, 0, sizeof( outfilename ) ); if ( !e->ComputeCombinedBaseFileName( outfilename, sizeof( outfilename ), genderwildcard ) ) { vprint( 0, "Unable to regenerate wav file name for combined sound (%s)\n", e->GetCloseCaptionToken() ); return false; } bool checksumvalid = false; CUtlRBTree< CChoreoEvent * > eventList( 0, 0, EventStartTimeLessFunc ); if ( !e->GetChannel()->GetSortedCombinedEventList( e->GetCloseCaptionToken(), eventList ) ) { vprint( 0, "Unable to generated combined event list (%s)\n", e->GetCloseCaptionToken() ); return false; } if ( genderwildcard ) { checksumvalid = ValidateCombinedFileCheckSum( outfilename, e->GetCloseCaptionToken(), GENDER_MALE, eventList, referencedcaptionwaves ); checksumvalid &= ValidateCombinedFileCheckSum( outfilename, e->GetCloseCaptionToken(), GENDER_FEMALE, eventList, referencedcaptionwaves ); } else { checksumvalid = ValidateCombinedFileCheckSum( outfilename, e->GetCloseCaptionToken(), GENDER_NONE, eventList, referencedcaptionwaves ); } return checksumvalid; } struct PerMapVCDS { PerMapVCDS() { } PerMapVCDS( const PerMapVCDS& src ) { int i = src.vcds.FirstInorder(); while ( i != src.vcds.InvalidIndex() ) { vcds.Insert( src.vcds[ i ] ); i = src.vcds.NextInorder( i ); } } class CTree : public CUtlRBTree< CUtlSymbol > { public: CTree() : CUtlRBTree< CUtlSymbol >( 0, 0, DefLessFunc( CUtlSymbol ) ) { } CTree &operator=( const CTree &from ) { CopyFrom( from ); return *this; } }; CTree vcds; }; CUtlDict< PerMapVCDS, int > g_PerMapVCDS; CUtlDict< CUtlSymbol, int > g_FirstMapForVCD; void ParseVCDFilesFromResList( CUtlVector< CUtlSymbol >& vcdsinreslist, char const *resfile ) { char gd[ 256 ]; Q_strncpy( gd, gamedir, sizeof( gd ) ); Q_StripTrailingSlash( gd ); _strlwr( gd ); Q_FixSlashes( gd ); int gdlen = strlen( gd ); char resbase[ 512 ]; Q_FileBase( resfile, resbase, sizeof( resbase ) ); int addedStrings = 0; int resourcesConsidered = 0; FileHandle_t resfilehandle; resfilehandle = g_pFullFileSystem->Open( resfile, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != resfilehandle ) { // Read in the entire file int length = g_pFullFileSystem->Size(resfilehandle); if ( length > 0 ) { char *pStart = (char *)new char[ length + 1 ]; if ( pStart && ( length == g_pFullFileSystem->Read(pStart, length, resfilehandle) ) ) { pStart[ length ] = 0; char *pFileList = pStart; char tokenFile[512]; while ( 1 ) { pFileList = ParseFile( pFileList, tokenFile, NULL ); if ( !pFileList ) break; if ( strlen( tokenFile ) > 0 ) { char szFileName[ 256 ]; Q_strncpy( szFileName, tokenFile, sizeof( szFileName ) ); _strlwr( szFileName ); Q_FixSlashes( szFileName ); while ( szFileName[ strlen( szFileName ) - 1 ] == '\n' || szFileName[ strlen( szFileName ) - 1 ] == '\r' ) { szFileName[ strlen( szFileName ) - 1 ] = 0; } char *pFile = szFileName; if ( !Q_strnicmp( szFileName, gd, gdlen ) ) { pFile = szFileName + gdlen + 1; } else { // Ack //vprint( 1, "File %s not under game directory but in reslist, skipping!!!\n", szFileName ); pFileList = ParseFile( pFileList, tokenFile, NULL ); continue; } ++resourcesConsidered; // Is it a .vcd? if ( !Q_stristr( pFile, ".vcd" ) ) continue; char symname[ 512 ]; Q_snprintf( symname, sizeof( symname ), "%s%s", gamedir, pFile ); _strlwr( symname ); Q_FixSlashes( symname ); CUtlSymbol sym = g_Analysis.symbols.AddString( symname ); int idx = vcdsinreslist.Find( sym ); if ( idx == vcdsinreslist.InvalidIndex() ) { ++addedStrings; // This is the first time this vcd was encountered, remember which map we are in PerMapVCDS e; e.vcds.Insert( sym ); g_PerMapVCDS.Insert( resbase, e ); CUtlSymbol mapsym = g_Analysis.symbols.AddString( resbase ); g_FirstMapForVCD.Insert( symname, mapsym ); vcdsinreslist.AddToTail( sym ); } } } } delete[] pStart; } g_pFullFileSystem->Close(resfilehandle); } // int filesFound = addedStrings; // vprint( 1, "\rFound %i new resources (%7i total) in %64s", filesFound, resourcesConsidered, resfile ); } #define MAPLIST_FILE "maplist.txt" void AddFileToList( CUtlVector< CUtlSymbol >& list, char const *filename ) { char fn[ 512 ]; Q_strncpy( fn, filename, sizeof( fn ) ); _strlwr( fn ); Q_FixSlashes( fn ); CUtlSymbol sym = g_Analysis.symbols.AddString( fn ); list.AddToTail( sym ); } void BuildVCDAndMapNameListsFromReslists( CUtlVector< CUtlSymbol >& vcdsinreslist ) { // Load all .rst files in the reslists folder CUtlVector< CUtlSymbol > reslists; // If maplist.txt exists, use it, otherwise bool loaded = false; if ( g_pFullFileSystem->FileExists( MAPLIST_FILE ) ) { // Parse the true list from the maplist.txt file // and add engine.lst and all.lst at the very end // Load them in FileHandle_t resfilehandle; resfilehandle = g_pFullFileSystem->Open( MAPLIST_FILE, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != resfilehandle ) { // Read in and parse mapcycle.txt int length = g_pFullFileSystem->Size(resfilehandle); if ( length > 0 ) { char *pStart = (char *)new char[ length + 1 ]; if ( pStart && ( length == g_pFullFileSystem->Read(pStart, length, resfilehandle) ) ) { pStart[ length ] = 0; const char *pFileList = pStart; while ( 1 ) { char szMap[ 512 ]; pFileList = ParseFile( pFileList, com_token, NULL ); if ( strlen( com_token ) <= 0 ) break; Q_strncpy(szMap, com_token, sizeof(szMap)); // Any more tokens on this line? //while ( TokenWaiting( pFileList ) ) //{ // pFileList = ParseFile( pFileList, com_token, NULL ); //} char fn[ 512 ]; Q_snprintf( fn, sizeof( fn ), "%sreslists/%s.lst", gamedir, szMap ); AddFileToList( reslists, fn ); } } delete[] pStart; AddFileToList( reslists, va( "%sreslists/engine.lst", gamedir ) ); AddFileToList( reslists, va( "%sreslists/all.lst", gamedir ) ); loaded = true; } g_pFullFileSystem->Close(resfilehandle); } } if ( !loaded ) { char reslistdir[ 512 ]; Q_snprintf( reslistdir, sizeof( reslistdir ), "%sreslists", gamedir ); BuildFileList_R( reslists, reslistdir, ".lst" ); } int c = reslists.Count(); StartPacifier( "ParseVCDFilesFromResList: " ); for ( int i = 0; i < c; ++i ) { UpdatePacifier( (float)( i + 1 ) / (float)c ); ParseVCDFilesFromResList( vcdsinreslist, g_Analysis.symbols.String( reslists[ i ] ) ); } EndPacifier( true ); } void CheckUnusedVcds( CUtlVector< CUtlSymbol >& vcdsinreslist, CUtlVector< CUtlSymbol >& vcdfiles ) { vprint( 1, "Checking for orphaned vcd files\n" ); // For each reslist, load in the filenames, looking for .vcds vprint( 1, "Found %i .vcd files referenced (%i total)\n", vcdsinreslist.Count(), vcdfiles.Count() ); // For each vcd in the min list, see if it's in the sublist int i; int c = vcdfiles.Count(); int invalid_index = vcdsinreslist.InvalidIndex(); int unrefcount = 0; for ( i = 0; i < c; ++i ) { CUtlSymbol& sym = vcdfiles[ i ]; if ( vcdsinreslist.Find( sym ) == invalid_index ) { ++unrefcount; vprint( 1, " unref .vcd: %s\n", g_Analysis.symbols.String( sym ) ); if ( nuke ) { nuke_print( 0, "del %s /f\n", g_Analysis.symbols.String( sym ) ); } } } // For each reslist, load in the filenames, looking for .vcds vprint( 1, "Found %i unreferenced vcds (%i total)\n", unrefcount, vcdfiles.Count() ); } void ParseUsedSoundsFromSndFile( CUtlRBTree< int, int >& usedsounds, char const *sndfile ) { char gd[ 256 ]; Q_strncpy( gd, gamedir, sizeof( gd ) ); Q_StripTrailingSlash( gd ); _strlwr( gd ); Q_FixSlashes( gd ); int addedStrings = 0; int resourcesConsidered = 0; FileHandle_t resfilehandle; resfilehandle = g_pFullFileSystem->Open( sndfile, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != resfilehandle ) { // Read in the entire file int length = g_pFullFileSystem->Size(resfilehandle); if ( length > 0 ) { char *pStart = (char *)new char[ length + 1 ]; if ( pStart && ( length == g_pFullFileSystem->Read(pStart, length, resfilehandle) ) ) { pStart[ length ] = 0; char *pFileList = pStart; char tokenFile[512]; while ( 1 ) { pFileList = ParseFile( pFileList, tokenFile, NULL ); if ( !pFileList ) break; if ( strlen( tokenFile ) > 0 ) { char soundname[ 256 ]; Q_strncpy( soundname, tokenFile, sizeof( soundname ) ); _strlwr( soundname ); ++resourcesConsidered; int index = g_pSoundEmitterSystem->GetSoundIndex( soundname ); if ( !g_pSoundEmitterSystem->IsValidIndex( index ) ) { vprint( 1, "---> Sound %s doesn't exist in g_pSoundEmitterSystemsystem!!!\n", soundname ); continue; } int idx = usedsounds.Find( index ); if ( idx == usedsounds.InvalidIndex() ) { ++addedStrings; usedsounds.Insert( index ); } } } } delete[] pStart; } g_pFullFileSystem->Close(resfilehandle); } vprint( 1, "Found %i new resources (%i total) in %s\n", addedStrings, resourcesConsidered, sndfile ); } bool SplitCommand( wchar_t const **ppIn, wchar_t *cmd, wchar_t *args ); void SpewDuplicatedText( char const *lang, const char *entry, const wchar_t *str ) { const wchar_t *curpos = str; wchar_t cleaned[ 4096 ]; wchar_t *out = cleaned; for ( ; curpos && *curpos != L'\0'; ++curpos ) { wchar_t cmd[ 256 ]; wchar_t args[ 256 ]; if ( SplitCommand( &curpos, cmd, args ) ) { continue; } // Only copy non command, non-whitespace characters if ( iswspace( *curpos ) ) { continue; } *out++ = *curpos; } *out = L'\0'; int len = wcslen( cleaned ); if ( len < 5 ) return; // Now see how many characters from the first 50% of the text are also in the second 50% int halflen = len / 2; int foundcount = 0; for ( int i = 0; i < halflen; ++i ) { wchar_t ch[3]; ch[0] = cleaned[ i ]; ch[1] = cleaned[ i + 1 ]; ch[2] = L'\0'; if ( wcsstr( &cleaned[ halflen ], ch ) ) { ++foundcount; } } if ( foundcount > 0.7 * halflen ) { logprint( "cc_duplicatedtext.txt", "%s: Suspect token %s\n", lang, entry ); } } void CheckDuplcatedText( void ) { g_pFullFileSystem->RemoveFile( "cc_duplicatedtext.txt", "GAME" ); for ( int lang = 0; lang < CC_NUM_LANGUAGES; ++lang ) { char language[ 256 ]; Q_strncpy( language, CSentence::NameForLanguage( lang ), sizeof( language ) ); vprint( 0, "adding langauge file for '%s'\n", language ); g_pVGuiLocalize->AddFile( "resource/closecaption_english.txt" ); char fn[ 256 ]; Q_snprintf( fn, sizeof( fn ), "resource/closecaption_%s.txt", language ); g_pVGuiLocalize->AddFile( fn ); // Now check for closecaption_xxx.txt entries which are orphaned because there isn't an existing sound script entry in use for them StringIndex_t str = g_pVGuiLocalize->GetFirstStringIndex(); while ( str != INVALID_LOCALIZE_STRING_INDEX ) { char const *keyname = g_pVGuiLocalize->GetNameByIndex( str ); if ( keyname ) { const wchar_t *value = g_pVGuiLocalize->GetValueByIndex( str ); SpewDuplicatedText( language, keyname, value ); } str = g_pVGuiLocalize->GetNextStringIndex( str ); } } } static bool IsAllSpaces( const wchar_t *stream ) { const wchar_t *p = stream; while ( *p != L'\0' ) { if ( !iswspace( *p ) ) return false; p++; } return true; } void SpewEnglishText( const wchar_t *str ) { const wchar_t *curpos = str; wchar_t cleaned[ 4096 ]; wchar_t *out = cleaned; for ( ; curpos && *curpos != L'\0'; ++curpos ) { wchar_t cmd[ 256 ]; wchar_t args[ 256 ]; if ( SplitCommand( &curpos, cmd, args ) ) { continue; } *out++ = *curpos; } *out = L'\0'; if ( IsAllSpaces( cleaned ) ) return; char ansi[ 4096 ]; g_pVGuiLocalize->ConvertUnicodeToANSI( cleaned, ansi, sizeof( ansi ) ); logprint( "cc_english.txt", "\"%s\"\n", ansi ); } void ExtractEnglish() { g_pFullFileSystem->RemoveFile( "cc_english.txt", "GAME" ); // Now check for closecaption_xxx.txt entries which are orphaned because there isn't an existing sound script entry in use for them StringIndex_t str = g_pVGuiLocalize->GetFirstStringIndex(); while ( str != INVALID_LOCALIZE_STRING_INDEX ) { char const *keyname = g_pVGuiLocalize->GetNameByIndex( str ); if ( keyname ) { const wchar_t *value = g_pVGuiLocalize->GetValueByIndex( str ); SpewEnglishText( value ); } str = g_pVGuiLocalize->GetNextStringIndex( str ); } } void CheckUnusedSounds() { vprint( 1, "Checking for unused sounds.txt entries\n" ); CUtlRBTree< int, int > usedsounds( 0, 0, DefLessFunc(int) ); // Load all .snd files in the reslists folder CUtlVector< CUtlSymbol > sndlists; char reslistdir[ 512 ]; Q_snprintf( reslistdir, sizeof( reslistdir ), "%sreslists", gamedir ); BuildFileList_R( sndlists, reslistdir, ".snd" ); int c = sndlists.Count(); for ( int i = 0; i < c; ++i ) { ParseUsedSoundsFromSndFile( usedsounds, g_Analysis.symbols.String( sndlists[ i ] ) ); } // For each reslist, load in the filenames, looking for .vcds vprint( 1, "Found %i unique sounds referenced\n", usedsounds.Count() ); // For each vcd in the min list, see if it's in the sublist c = g_pSoundEmitterSystem->GetSoundCount(); int unrefcount = 0; int invalidindex = usedsounds.InvalidIndex(); CUtlRBTree< int, int > usedscripts( 0, 0, DefLessFunc(int) ); for ( int i = 0; i < c; ++i ) { int slot = usedsounds.Find( i ); if ( invalidindex == slot ) { ++unrefcount; char const *soundname = g_pSoundEmitterSystem->GetSoundName( i ); vprint( 1, " unref: %s : %s\n", soundname, g_pSoundEmitterSystem->GetSourceFileForSound( i ) ); } else { int scriptindex = g_pSoundEmitterSystem->FindSoundScript( g_pSoundEmitterSystem->GetSourceFileForSound( i ) ); if ( scriptindex != -1 ) { slot = usedscripts.Find( scriptindex ); if ( usedscripts.InvalidIndex() == slot ) { usedscripts.Insert( scriptindex ); } } } } // For each reslist, load in the filenames, looking for .vcds vprint( 1, "Found %i unreferenced sounds (%i total)\n", unrefcount, c ); c = g_pSoundEmitterSystem->GetNumSoundScripts(); for ( int i = 0; i < c; ++i ) { char const *scriptname = g_pSoundEmitterSystem->GetSoundScriptName( i ); int slot = usedscripts.Find( i ); if ( usedscripts.InvalidIndex() == slot ) { vprint( 1, " No sounds fron script %s are being used, should delete from manifest!!!\n", scriptname ); } } if ( !build_cc ) return; g_pFullFileSystem->RemoveFile( "fixed.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "fixed2.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "todo.csv", "GAME" ); g_pFullFileSystem->RemoveFile( "cc_add.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "cc_delete.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "cc_foundphonemes.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "cc_combined.txt", "GAME" ); logprint( "todo.csv", "\"CC_TOKEN\",\"TEXT\",\"WAVE FILE\"\n" ); // Now check for closecaption_xxx.txt entries which are orphaned because there isn't an existing sound script entry in use for them StringIndex_t str = g_pVGuiLocalize->GetFirstStringIndex(); while ( str != INVALID_LOCALIZE_STRING_INDEX ) { char const *keyname = g_pVGuiLocalize->GetNameByIndex( str ); if ( keyname ) { wchar_t *value = g_pVGuiLocalize->GetValueByIndex( str ); char ansi[ 512 ]; g_pVGuiLocalize->ConvertUnicodeToANSI( value, ansi, sizeof( ansi ) ); // See if key exists in g_pSoundEmitterSystem system int soundindex = g_pSoundEmitterSystem->GetSoundIndex( keyname ); if( soundindex == -1 ) { vprint( 1, " cc token %s not in current g_pSoundEmitterSystem scripts\n", keyname ); // Just write it back out as is... logprint( "fixed2.txt", "\t\"%s\"\t\t\"%s\"\n", keyname, ansi ); } else { // See if it's referenced int slot = usedsounds.Find( soundindex ); if ( usedsounds.InvalidIndex() == slot ) { vprint( 1, " cc token %s exists, but the sound is not used by the game\n", keyname ); logprint( "cc_delete.txt", "\"%s\"\n", keyname ); } else { // Now try to find a better bit of text CSoundParametersInternal *internal = g_pSoundEmitterSystem->InternalGetParametersForSound( soundindex ); if ( internal && internal->NumSoundNames() > 0 ) { CUtlSymbol &symwave = internal->GetSoundNames()[ 0 ].symbol; char const *wavname = g_pSoundEmitterSystem->GetWaveName( symwave ); if ( wavname && ( Q_stristr( wavname, "vo/" ) || Q_stristr( wavname, "vo\\" ) ) ) { // See if 1) it's marked as !!! and try to figure out the text from .wav files... if ( !Q_strnicmp( ansi, "!!!", 3 ) ) { CSentence sentence; if ( LoadSentenceFromWavFile( va( "sound/%s", PSkipSoundChars( wavname ) ), sentence ) ) { if ( Q_strlen( sentence.GetText() ) > 0 ) { Q_snprintf( ansi, sizeof( ansi ), "%s", sentence.GetText() ); cleanquotes( ansi ); logprint( "cc_foundphonemes.txt", "\t\"%s\"\t\t\"%s\"\n", keyname, ansi ); } } } logprint( "fixed.txt", "\t\"%s\"\t\t\"%s\"\n", keyname, ansi ); for ( int w = 0; w < internal->NumSoundNames() ; ++w ) { wavname = g_pSoundEmitterSystem->GetWaveName( internal->GetSoundNames()[ w ].symbol ); logprint( "todo.csv", "\"%s\",\"%s\",\"%s\"\n", keyname, ansi, va( "sound/%s", PSkipSoundChars( wavname ) ) ); } } } else { logprint( "fixed.txt", "\t\"%s\"\t\t\"%s\"\n", keyname, ansi ); } } } } str = g_pVGuiLocalize->GetNextStringIndex( str ); } // Now walk through all of the sounds that were used, but not in the localization file and and those, too c = g_pSoundEmitterSystem->GetSoundCount(); for ( int i = 0; i < c; ++i ) { int slot = usedsounds.Find( i ); if ( usedsounds.InvalidIndex() == slot ) continue; char const *soundname = g_pSoundEmitterSystem->GetSoundName( i ); // See if it exists in the localization file wchar_t *text = g_pVGuiLocalize->Find( soundname ); if ( text ) { continue; } else { char ansi[ 512 ]; Q_snprintf( ansi, sizeof( ansi ), "!!!%s", soundname ); // Now try to find a better bit of text CSoundParametersInternal *internal = g_pSoundEmitterSystem->InternalGetParametersForSound( i ); if ( internal && internal->NumSoundNames() > 0 ) { CUtlSymbol &symwave = internal->GetSoundNames()[ 0 ].symbol; char const *wavname = g_pSoundEmitterSystem->GetWaveName( symwave ); if ( wavname && ( Q_stristr( wavname, "vo/" ) || Q_stristr( wavname, "vo\\" ) ) ) { CSentence sentence; if ( LoadSentenceFromWavFile( va( "sound/%s", PSkipSoundChars( wavname ) ), sentence ) ) { if ( Q_strlen( sentence.GetText() ) > 0 ) { Q_snprintf( ansi, sizeof( ansi ), "%s", sentence.GetText() ); cleanquotes( ansi ); } } // Add an entry for stuff in vo/ logprint( "fixed.txt", "\t\"%s\"\t\t\"%s\"\n", soundname, ansi ); for ( int w = 0; w < internal->NumSoundNames() ; ++w ) { wavname = g_pSoundEmitterSystem->GetWaveName( internal->GetSoundNames()[ w ].symbol ); logprint( "todo.csv", "\"%s\",\"%s\",\"%s\"\n", soundname, ansi, va( "sound/%s", PSkipSoundChars( wavname ) ) ); } logprint( "cc_add.txt", "\"%s\"\n", soundname ); } } } } } // Removes commas from text void RemoveCommas( char *in ) { char *out = in; while ( out && *out ) { if ( *in == ',' ) { *out++ = ';'; in++; } else { *out++ = *in++; } } *out = 0; } void SpewScript( char const *vcdname, CUtlRBTree< CChoreoEvent *, int >& list ) { if ( !build_script ) return; if ( list.Count() == 0 ) return; logprint( "script.txt", "VCD( %s )\n\n", vcdname ); for ( int i = list.FirstInorder(); i != list.InvalidIndex(); i = list.NextInorder( i ) ) { CChoreoEvent *e = list[ i ]; if ( e->GetCloseCaptionType() != CChoreoEvent::CC_MASTER ) { continue; } char actorname[ 512 ]; if ( e->GetActor() ) { Q_strncpy( actorname, e->GetActor()->GetName(), sizeof( actorname ) ); _strupr( actorname ); } else { Q_strncpy( actorname, "(NULL ACTOR)", sizeof( actorname ) ); } logprint( "script.txt", "\t\t\t%s\n", actorname); // Now try to find a better bit of text char wavname[ 512 ]; wavname[ 0 ] = 0; char sentence_text[ 1024 ]; sentence_text[ 0 ] = 0; int soundindex = g_pSoundEmitterSystem->GetSoundIndex( e->GetParameters() ); if ( soundindex != -1 ) { CSoundParametersInternal *internal = g_pSoundEmitterSystem->InternalGetParametersForSound( soundindex ); if ( internal && internal->NumSoundNames() > 0 ) { CUtlSymbol &symwave = internal->GetSoundNames()[ 0 ].symbol; char const *pname = g_pSoundEmitterSystem->GetWaveName( symwave ); if ( pname && ( Q_stristr( pname, "vo/" ) || Q_stristr( pname, "vo\\" ) || Q_stristr( pname, "combined" ) ) ) { Q_strncpy( wavname, pname, sizeof( wavname ) ); // Convert to regular text logprint( "script.txt", "\t\t\t\twav(%s)\n", wavname ); CSentence sentence; if ( LoadSentenceFromWavFile( va( "sound/%s", PSkipSoundChars( wavname ) ), sentence ) ) { if ( Q_strlen( sentence.GetText() ) > 0 ) { Q_snprintf( sentence_text, sizeof( sentence_text ), "%s", sentence.GetText() ); cleanquotes( sentence_text ); } } } } } char tok[ 256 ]; Q_strncpy( tok, e->GetParameters(), sizeof( tok ) ); char ansi[ 2048 ]; ansi[ 0 ] = 0; wchar_t *str = g_pVGuiLocalize->Find( tok ); if ( !str ) { logprint( "script.txt", "\t\tMissing token '%s' event '%s'\n\n", tok, e->GetName() ); Q_snprintf( ansi, sizeof( ansi ), "missing '%s' for '%s'", tok, e->GetName() ); } else { if ( !wcsncmp( str, L"!!!", wcslen( L"!!!" ) ) ) { logprint( "script.txt", "\t\t'%s': event '%s'\n\n", tok, e->GetName() ); Q_snprintf( ansi, sizeof( ansi ), "!!! '%s' for '%s'", tok, e->GetName() ); } else { g_pVGuiLocalize->ConvertUnicodeToANSI( str, ansi, sizeof( ansi ) ); // Convert to regular text logprint( "script.txt", "\t\t\t\tcc_token(%s)\n\n\t\t\"%s\"\n\n", tok, ansi ); } } // Now spit out the CSV version... RemoveCommas( actorname ); RemoveCommas( tok ); RemoveCommas( ansi ); char mapname[ 512 ]; mapname[ 0 ] = 0; int idx = g_FirstMapForVCD.Find( vcdname ); if ( idx != g_FirstMapForVCD.InvalidIndex() ) { Q_strncpy( mapname, g_Analysis.symbols.String( g_FirstMapForVCD[ idx ] ), sizeof( mapname ) ); } static unsigned int sortindex = 0; logprint( "script.csv", "%u,%s,%s,%s,%6.3f,%s,%s,\"%s\",\"%s\"\n", sortindex++, mapname, vcdname, actorname, e->GetStartTime(), tok, wavname, ansi, sentence_text ); } } void CheckLocalizationEntries( CUtlVector< CUtlSymbol >& vcdfiles, CUtlRBTree< CUtlSymbol, int >& referencedcaptionwaves ) { int disabledcount = 0; int validcount = 0; int missingcount = 0; int wavfile = 0; int gamedirskip = Q_strlen( gamedir ); if ( build_script ) { g_pFullFileSystem->RemoveFile( "script.txt", "GAME" ); g_pFullFileSystem->RemoveFile( "script.csv", "GAME" ); } int c = vcdfiles.Count(); for ( int i = 0; i < c; ++i ) { CUtlSymbol& vcdname = vcdfiles[ i ]; CUtlRBTree< CChoreoEvent *, int > sortedSpeakEvents( 0, 0, EventStartTimeLessFunc ); // Load the .vcd char fullname[ 512 ]; Q_snprintf( fullname, sizeof( fullname ), "%s", g_Analysis.symbols.String( vcdname ) ); LoadScriptFile( fullname ); CChoreoScene *scene = ChoreoLoadScene( fullname, NULL, &g_TokenProcessor, Con_Printf ); if ( !scene ) { vprint( 0, "Warning: Unable to load %s\n", fullname ); continue; } // Now iterate the events looking for speak events int numevents = scene->GetNumEvents(); for ( int j = 0; j < numevents; j++ ) { CChoreoEvent *e = scene->GetEvent( j ); if ( e->GetType() != CChoreoEvent::SPEAK ) continue; if ( e->GetCloseCaptionType() == CChoreoEvent::CC_DISABLED ) { ++disabledcount; continue; } if ( build_script ) { if ( sortedSpeakEvents.Find( e ) == sortedSpeakEvents.InvalidIndex() ) { sortedSpeakEvents.Insert( e ); } } char tok[ 256 ]; for ( int pass = 0; pass <= 1; ++pass ) { bool iscombined = false; if ( pass == 0 ) { Q_strncpy( tok, e->GetParameters(), sizeof( tok ) ); } else { if ( e->GetCloseCaptionType() != CChoreoEvent::CC_MASTER ) continue; if ( e->GetNumSlaves() <= 0 ) continue; if ( !e->GetPlaybackCloseCaptionToken( tok, sizeof( tok ) ) ) { ++missingcount; continue;; } iscombined = true; } // Look it up wchar_t *str = g_pVGuiLocalize->Find( tok ); if ( !str ) { char fn[ 256 ]; //Q_FileBase( g_Analysis.symbols.String( vcdname ), fn, sizeof( fn ) ); Q_strncpy( fn, &g_Analysis.symbols.String( vcdname )[ gamedirskip ], sizeof( fn ) ); if ( Q_stristr( tok, ".wav" ) ) { if ( verbose ) { if ( !regenerate_quiet ) { vprint( 0, "(OBSOLETE???)missing cc token '%s' (!.wav file): vcd (%s), event (%s)\n", tok, fn, e->GetName() ); } } ++wavfile; } else { if ( !regenerate_quiet ) { vprint( 0, "missing %s cc token '%s': vcd (%s), event (%s)\n", pass == 0 ? "normal" : "combined", tok, fn, e->GetName() ); } // Add the "!!!entry" to a temp file if ( verbose ) { char suggested[ 4096 ]; Q_snprintf( suggested, sizeof( suggested ), "!!!%s", tok ); int soundindex = g_pSoundEmitterSystem->GetSoundIndex( tok ); if ( soundindex != -1 ) { // Now try to find a better bit of text CSoundParametersInternal *internal = g_pSoundEmitterSystem->InternalGetParametersForSound( soundindex ); if ( internal && internal->NumSoundNames() > 0 ) { CUtlSymbol &symwave = internal->GetSoundNames()[ 0 ].symbol; char const *wavname = g_pSoundEmitterSystem->GetWaveName( symwave ); if ( wavname && ( Q_stristr( wavname, "vo/" ) || Q_stristr( wavname, "vo\\" ) ) ) { CSentence sentence; if ( LoadSentenceFromWavFile( va( "sound/%s", PSkipSoundChars( wavname ) ), sentence ) ) { if ( Q_strlen( sentence.GetText() ) > 0 ) { Q_snprintf( suggested, sizeof( suggested ), "%s", sentence.GetText() ); cleanquotes( suggested ); } } } } } logprint( "missing.txt", "\t\"%s\"\t\t\"%s\"\n", tok, suggested ); } ++missingcount; } } else { if ( verbose ) { if ( !wcsncmp( str, L"!!!", wcslen( L"!!!" ) ) ) { if ( !regenerate_quiet ) { vprint( 0, "Autogenerated closecaption token '%s' not edited\n", tok ); } } } ++validcount; } // Verify checksum if ( iscombined ) { ValidateCombinedSoundCheckSum( e, referencedcaptionwaves ); } } } SpewScript( fullname, sortedSpeakEvents ); sortedSpeakEvents.RemoveAll(); delete scene; } int total = validcount + missingcount + wavfile + disabledcount; if ( total != 0 ) { vprint( 0, "\n%.2f %%%% invalid (%i valid, %i missing, %i wavfile(OBSOLETE), %i disabled - total %i)\n", 100.0f * (float)missingcount / (float)total, validcount, missingcount, wavfile, disabledcount, total ); } } void CheckForOrphanedCombinedWavs( CUtlVector< CUtlSymbol >& diskwaves, CUtlRBTree< CUtlSymbol, int >& captionsused ) { if ( g_pFullFileSystem->FileExists( "orphaned.bat", "GAME" ) ) { g_pFullFileSystem->RemoveFile( "orphaned.bat", "GAME" ); } int orphans = 0; int c = diskwaves.Count(); for ( int i = 0; i < c; ++i ) { CUtlSymbol &sym = diskwaves[ i ]; if ( captionsused.Find( sym ) != captionsused.InvalidIndex() ) continue; char fn[ 256 ]; Q_strncpy( fn, g_Analysis.symbols.String( sym ), sizeof( fn ) ); vprint( 1, "Orphaned wav file '%s'\n", fn ); logprint( "orphaned.bat", "del \"%s\" /f\n", fn ); ++orphans; } if ( orphans != 0 ) { vprint( 0, "\n%.2f %%%% (%i/%i), orphaned combined .wav files in sound/combined/... folder\n", 100.0f * (float)orphans / (float)c, orphans, c ); vprint( 0, "created orphaned.bat file\n" ); } else { vprint( 0, "\nNo orphaned files found among %d possible disk waves\n", c ); } } float GetWaveDuration( char const *wavname ) { if ( !g_pFullFileSystem->FileExists( wavname ) ) { return 0.0f; } CAudioSource *wave = sound->LoadSound( wavname ); if ( !wave ) { //vprint( 0, "unable to load %s\n", wavname ); return 0.0f; } CAudioMixer *pMixer = wave->CreateMixer(); if ( !pMixer ) { vprint( 0, "unable to create mixer for %s\n", wavname ); delete wave; return 0.0f; } float duration = wave->GetRunningLength(); return duration; } void GetWaveSentence( char const *wavname, CSentence& sentence ) { sentence.Reset(); if ( !g_pFullFileSystem->FileExists( wavname ) ) { return; } CAudioSource *wave = sound->LoadSound( wavname ); if ( !wave ) { //vprint( 0, "unable to load %s\n", wavname ); return; } sentence = *wave->GetSentence(); } void ValidateForeignLanguageWaves( char const *language, CUtlVector< CUtlSymbol >& combinedwavfiles ) { // Need to compute the gamedir to the specified language char langdir[ 512 ]; char strippedgamedir[ 512 ]; Q_strncpy( langdir, gamedir, sizeof( langdir ) ); Q_StripTrailingSlash( langdir ); Q_strncpy( strippedgamedir, langdir, sizeof( strippedgamedir ) ); Q_strcat( langdir, "_", sizeof(langdir) ); Q_strcat( langdir, language, sizeof(langdir) ); int skipchars = Q_strlen( strippedgamedir ); // Need to add this to the file system int missing = 0; int outdated = 0; int c = combinedwavfiles.Count(); for ( int i = 0; i < c; ++i ) { CUtlSymbol& sym = combinedwavfiles[ i ]; char wavname[ 512 ]; Q_strncpy( wavname, g_Analysis.symbols.String( sym ), sizeof( wavname ) ); // Now get language specific wav name char localizedwavename[ 512 ]; Q_snprintf( localizedwavename, sizeof( localizedwavename ), "%s%s", langdir, &wavname[ skipchars ] ); float duration_english = GetWaveDuration( wavname ); if ( !duration_english ) { continue; } // Now see if the localized file exists float duration_localized = GetWaveDuration( localizedwavename ); if ( !duration_localized ) { ++missing; vprint( 0, "Missing localized file %s\n", localizedwavename ); continue; } CSentence sentence_english; GetWaveSentence( wavname, sentence_english ); CSentence sentence_localized; GetWaveSentence( localizedwavename, sentence_localized ); if ( sentence_english.GetText() && sentence_english.GetText()[0] ) { if ( !sentence_localized.GetText() || !sentence_localized.GetText()[0] ) { vprint( 0, "--> Localized combined file for '%s' doesn't have sentence data '%s'\n", language, localizedwavename ); } else if ( !Q_stricmp( sentence_english.GetText(), sentence_localized.GetText()) ) { vprint( 0, "--> Localized combined file for '%s' still using english phoneme and text data '%s'\n", language, localizedwavename ); } } if ( fabs( duration_localized - duration_english ) > SOUND_DURATION_TOLERANCE ) { ++outdated; vprint( 0, "--> Mismatched localized file %s (english %.2f s./%s %.2f s.)\n", localizedwavename, duration_english, language, duration_localized ); continue; } } if ( c != 0 ) { vprint( 0, "%.2f %%%% missing(%i)+outdated(%i)/total(%i), combined .wav files in %s closecaption/ folder\n", 100.0f * (float)(missing + outdated ) / (float)c, missing, outdated, c, language ); } } void BuildReverseSoundLookup( CUtlDict< CUtlSymbol, int >& wavtosound ) { // Build a dictionary of wav names to sound names int c = g_pSoundEmitterSystem->GetSoundCount(); for ( int i = 0; i < c; ++i ) { char const *soundname = g_pSoundEmitterSystem->GetSoundName( i ); CUtlSymbol soundSymbol = g_Analysis.symbols.AddString( soundname ); CSoundParametersInternal* params = g_pSoundEmitterSystem->InternalGetParametersForSound( i ); if ( soundname && params ) { int soundcount = params->NumSoundNames(); for ( int j = 0; j < soundcount; ++j ) { SoundFile& sf = params->GetSoundNames()[ j ]; char const *pwavname = g_pSoundEmitterSystem->GetWaveName( sf.symbol ); char fixed[ 512 ]; Q_strncpy( fixed, PSkipSoundChars( pwavname ), sizeof( fixed ) ); _strlwr( fixed ); Q_FixSlashes( fixed ); int curidx = wavtosound.Find( fixed ); if ( curidx == wavtosound.InvalidIndex() ) { wavtosound.Insert( fixed, soundSymbol ); //vprint( 0, "entry %s == %s\n", fixed, soundname ); } } } } vprint( 0, "Reverse lookup has %i entries from %i available sounds\n", wavtosound.Count(), c ); } #define UNK_SOUND_ENTRY "" char const *FindSoundEntry( CUtlDict< CUtlSymbol, int >& wavtosound, char const *wavname ) { char fixed[ 512 ]; Q_strncpy( fixed, PSkipSoundChars( wavname ), sizeof( fixed ) ); _strlwr( fixed ); Q_FixSlashes( fixed ); int idx = wavtosound.Find( fixed ); if ( idx != wavtosound.InvalidIndex() ) { CUtlSymbol snd = wavtosound[ idx ]; return g_Analysis.symbols.String( snd ); } return UNK_SOUND_ENTRY; } void CheckWaveFile( CUtlDict< CUtlSymbol, int >& wavtosound, char const *wavname ) { // vprint( 0, "%s\n", wavname ); char const *soundname = FindSoundEntry( wavtosound, wavname ); char ansi[ 512 ]; ansi[ 0 ] = 0; CSentence sentence; if ( LoadSentenceFromWavFile( va( "sound/%s", PSkipSoundChars( wavname ) ), sentence ) ) { if ( Q_strlen( sentence.GetText() ) > 0 ) { Q_snprintf( ansi, sizeof( ansi ), "%s", sentence.GetText() ); cleanquotes( ansi ); } } // Now look up cc token wchar_t *text = g_pVGuiLocalize->Find( soundname ); char caption[ 1024 ]; Q_strncpy( caption, "!!!", sizeof( caption ) ); if ( text ) { g_pVGuiLocalize->ConvertUnicodeToANSI( text, caption, sizeof( caption ) ); } else { if ( !Q_stricmp( soundname, UNK_SOUND_ENTRY ) ) { Q_snprintf( caption, sizeof( caption ), "!!!%s", soundname ); } } logprint( "wavcheck.csv", "\"%s\",\"%s\",\"%s\",\"%s\"\n", wavname, soundname, caption, ansi ); } void WavCheck( CUtlVector< CUtlSymbol >& wavfiles ) { g_pFullFileSystem->RemoveFile( "wavcheck.csv", "GAME" ); vprint( 0, "Building reverse lookup\n" ); logprint( "wavcheck.csv", "\"%s\",\"%s\",\"%s\",\"%s\"\n", "WaveName", "Sound Script Name", "Close Caption", "Phoneme Data String" ); CUtlDict< CUtlSymbol, int > wavtosound; BuildReverseSoundLookup( wavtosound ); vprint( 0, "Performing wavcheck\n" ); int c = wavfiles.Count(); int offset = Q_strlen( gamedir ) + Q_strlen( "sound/" ); for ( int i = 0; i < c; ++i ) { char const *wavname = g_Analysis.symbols.String( wavfiles[ i ] ); CheckWaveFile( wavtosound, wavname + offset ); if ( !(i % 100 ) ) { vprint( 0, "Finished %i/%i\n", i, c ); } } } void BuildWavFileToFullPathLookup( CUtlVector< CUtlSymbol >& wavfile, CUtlDict< int, int >& wavtofullpath ) { int c = wavfile.Count(); for ( int i = 0; i < c; ++i ) { CUtlSymbol &sym = wavfile[ i ]; char shortname[ 512 ]; Q_FileBase( g_Analysis.symbols.String( sym ), shortname, sizeof( shortname ) ); Q_SetExtension( shortname, ".wav", sizeof( shortname ) ); Q_FixSlashes( shortname ); Q_strlower( shortname ); int idx = wavtofullpath.Find( shortname ); if ( idx == wavtofullpath.InvalidIndex() ) { wavtofullpath.Insert( shortname, i ); } } } static void COM_CreatePath (const char *path) { char temppath[512]; Q_strncpy( temppath, path, sizeof(temppath) ); for (char *ofs = temppath+1 ; *ofs ; ofs++) { if (*ofs == '/' || *ofs == '\\') { // create the directory char old = *ofs; *ofs = 0; mkdir (temppath); *ofs = old; } } } void MakeBatchFile( CUtlVector< CUtlSymbol >& wavfiles, char const *pchFromdir, char const *pchTodir ) { g_pFullFileSystem->RemoveFile( "copywaves.bat", "GAME" ); vprint( 0, "Building reverse lookup\n" ); CUtlDict< int, int > wavtofullpath; BuildWavFileToFullPathLookup( wavfiles, wavtofullpath ); CUtlVector< CUtlSymbol > files; BuildFileList( files, pchFromdir, ".wav" ); int gamedirskip = Q_strlen( gamedir ) + Q_strlen( "sound//" ); int c = files.Count(); for ( int i = 0; i < c; ++i ) { char const *sname = g_Analysis.symbols.String( files[ i ] ); if ( !sname ) continue; char shortname[ 512 ]; Q_strncpy( shortname, sname, sizeof( shortname ) ); char fn[ 512 ]; Q_FileBase( shortname, fn, sizeof( fn ) ); Q_SetExtension( fn, ".wav", sizeof( fn ) ); Q_strlower( fn ); Q_FixSlashes( fn ); int slot = wavtofullpath.Find( fn ); if ( slot == wavtofullpath.InvalidIndex() ) { vprint( 0, "Couldn't find slot for '%s'\n", sname ); continue; } char fullname[ 512 ]; Q_snprintf( fullname, sizeof( fullname ), "%s/%s", pchTodir, &g_Analysis.symbols.String( wavfiles[ wavtofullpath[ slot ] ] )[ gamedirskip ] ); Q_strlower( fullname ); Q_FixSlashes( fullname ); //logprint( "copywaves.bat", "xcopy \"%s\" \"%s\"\n", //shortname, //fullname ); COM_CreatePath( fullname ); CopyFile( shortname, fullname, TRUE ); } } //----------------------------------------------------------------------------- // Purpose: // Input : store - //----------------------------------------------------------------------------- void StoreValveDataChunk( CSentence& sentence, IterateOutputRIFF& store ) { // Buffer and dump data CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); sentence.SaveToBuffer( buf ); // Copy into store store.ChunkWriteData( buf.Base(), buf.TellPut() ); } void SaveWave( char const *filename, CSentence& s ) { char infile[ 512 ]; Q_strncpy( infile, filename, sizeof( infile ) ); Q_SetExtension( infile, ".tmp", sizeof( infile ) ); // Rename infile MoveFile( filename, infile ); SetFileAttributes( filename, FILE_ATTRIBUTE_NORMAL ); { InFileRIFF riff( infile, io_in ); Assert( riff.RIFFName() == RIFF_WAVE ); // set up the iterator for the whole file (root RIFF is a chunk) IterateRIFF walk( riff, riff.RIFFSize() ); OutFileRIFF riffout( filename, io_out ); IterateOutputRIFF store( riffout ); bool wordtrackwritten = false; // Walk input chunks and copy to output while ( walk.ChunkAvailable() ) { unsigned int originalPos = store.ChunkGetPosition(); store.ChunkStart( walk.ChunkName() ); bool skipchunk = false; switch ( walk.ChunkName() ) { case WAVE_VALVEDATA: // Overwrite data StoreValveDataChunk( s, store ); wordtrackwritten = true; break; default: store.CopyChunkData( walk ); break; } store.ChunkFinish(); if ( skipchunk ) { store.ChunkSetPosition( originalPos ); } walk.ChunkNext(); } if ( !wordtrackwritten ) { store.ChunkStart( WAVE_VALVEDATA ); StoreValveDataChunk( s, store ); store.ChunkFinish(); } } SetFileAttributes( infile, FILE_ATTRIBUTE_NORMAL ); DeleteFile( infile ); } void ExtractPhonemesForWave( IPhonemeExtractor *extractor, char const *wavname ) { char formatbuffer[ 1024 ]; int formatsize = sizeof( formatbuffer ); int dataSize = 0; CSentence sentence; if ( !LoadSentenceFromWavFile( wavname, sentence, formatbuffer, &formatsize, &dataSize ) ) { vprint( 0, " skip '%s' missing\n", wavname ); return; } if ( !forceextract && sentence.m_Words.Count() > 0 ) { vprint( 0, " skip '%s', already has phonemes\n", wavname ); return; } if ( forceextract ) { sentence.Reset(); } if ( formatsize == 0 ) { vprint( 0, " skip '%s', not WAVE_FMT parsed\n", wavname ); return; } const WAVEFORMATEX *pHeader = (const WAVEFORMATEX *)formatbuffer; int format = pHeader->wFormatTag; int bits = pHeader->wBitsPerSample; int rate = pHeader->nSamplesPerSec; int channels = pHeader->nChannels; int sampleSize = (bits * channels) / 8; // this can never be zero -- other functions divide by this. // This should never happen, but avoid crashing if ( sampleSize <= 0 ) sampleSize = 1; int sampleCount = 0; float truesamplesize = sampleSize; if ( format == WAVE_FORMAT_ADPCM ) { sampleSize = 1; ADPCMWAVEFORMAT *pFormat = (ADPCMWAVEFORMAT *)formatbuffer; int blockSize = ((pFormat->wSamplesPerBlock - 2) * pFormat->wfx.nChannels ) / 2; blockSize += 7 * pFormat->wfx.nChannels; int blockCount = sampleCount / blockSize; int blockRem = sampleCount % blockSize; // total samples in complete blocks sampleCount = blockCount * pFormat->wSamplesPerBlock; // add remaining in a short block if ( blockRem ) { sampleCount += pFormat->wSamplesPerBlock - (((blockSize - blockRem) * 2) / channels); } truesamplesize = 0.5f; } else { sampleCount = dataSize / sampleSize; } // Do extraction // Current set of tags CSentence outsentence; char filename[ 512 ]; Q_snprintf( filename, sizeof( filename ), "%s", wavname ); int result = extractor->Extract( filename, dataSize, // (int)( m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate() * m_pWaveFile->TrueSampleSize() ), Msg, sentence, outsentence ); if ( result != SR_RESULT_SUCCESS ) { vprint( 0, " failed to analyze '%s', skipping\n", wavname ); return; } float bytespersecond = rate * truesamplesize; // Now convert byte offsets to times int i; for ( i = 0; i < outsentence.m_Words.Size(); i++ ) { CWordTag *tag = outsentence.m_Words[ i ]; Assert( tag ); if ( !tag ) continue; tag->m_flStartTime = ( float )(tag->m_uiStartByte ) / bytespersecond; tag->m_flEndTime = ( float )(tag->m_uiEndByte ) / bytespersecond; for ( int j = 0; j < tag->m_Phonemes.Size(); j++ ) { CPhonemeTag *ptag = tag->m_Phonemes[ j ]; Assert( ptag ); if ( !ptag ) continue; ptag->SetStartTime( ( float )(ptag->m_uiStartByte ) / bytespersecond ); ptag->SetEndTime( ( float )(ptag->m_uiEndByte ) / bytespersecond ); } } sentence = outsentence; outsentence.Reset(); // Resave it SaveWave( filename, sentence ); } struct Extractor { PE_APITYPE apitype; CSysModule *module; IPhonemeExtractor *extractor; }; CUtlVector< Extractor > g_Extractors; void UnloadPhonemeConverters() { int c = g_Extractors.Count(); for ( int i = c - 1; i >= 0; i-- ) { Extractor *e = &g_Extractors[ i ]; g_pFullFileSystem->UnloadModule( e->module ); } g_Extractors.RemoveAll(); } int LoadPhonemeExtractors() { // Enumerate modules under bin folder of exe FileFindHandle_t findHandle; const char *pFilename = g_pFullFileSystem->FindFirstEx( "phonemeextractors/*.dll", "EXECUTABLE_PATH", &findHandle ); int useextractor = -1; while ( pFilename ) { char fullpath[ 512 ]; Q_snprintf( fullpath, sizeof( fullpath ), "phonemeextractors/%s", pFilename ); pFilename = g_pFullFileSystem->FindNext( findHandle ); Con_Printf( "Loading extractor from %s\n", fullpath ); Extractor e; e.module = Sys_LoadModule( fullpath ); if ( !e.module ) { Warning( "Unable to Sys_LoadModule %s\n", fullpath ); continue; } CreateInterfaceFn factory = Sys_GetFactory( e.module ); if ( !factory ) { Warning( "Unable to get factory from %s\n", fullpath ); continue; } e.extractor = ( IPhonemeExtractor * )factory( VPHONEME_EXTRACTOR_INTERFACE, NULL ); if ( !e.extractor ) { Warning( "Unable to get IPhonemeExtractor interface version %s from %s\n", VPHONEME_EXTRACTOR_INTERFACE, fullpath ); continue; } e.apitype = e.extractor->GetAPIType(); if ( e.apitype == SPEECH_API_LIPSINC ) { useextractor = g_Extractors.Count(); } g_Extractors.AddToTail( e ); } g_pFullFileSystem->FindClose( findHandle ); return useextractor; } void ExtractPhonemes( CUtlVector< CUtlSymbol >& wavfiles ) { int index = LoadPhonemeExtractors(); if ( index == -1 ) return; if ( index == 0 ) { vprint( 0, "Couldn't find suitable extractor\n" ); return; } IPhonemeExtractor *extractor = g_Extractors[ index ].extractor; Assert( extractor ); vprint( 0, "Using %s\n", extractor->GetName() ); int c = wavfiles.Count(); vprint( 0, "Performing '%i' extractions (might take a while...)\n", c ); for ( int i = 0; i < c; ++i ) { char const *wavname = g_Analysis.symbols.String( wavfiles[ i ] ); ExtractPhonemesForWave( extractor, wavname ); if ( !(i % 50 ) ) { vprint( 0, "Finished %i/%i\n", i, c ); } } UnloadPhonemeConverters(); } void CheckWavForLoops( char const *wavname ) { InFileRIFF riff( wavname, io_in ); // UNDONE: Don't use printf to handle errors if ( riff.RIFFName() != RIFF_WAVE ) { return; } // set up the iterator for the whole file (root RIFF is a chunk) IterateRIFF walk( riff, riff.RIFFSize() ); while ( walk.ChunkAvailable( ) ) { switch( walk.ChunkName() ) { case WAVE_CUE: vprint( 0, "'%s' has a CUE chunk\n", wavname ); return; default: break; } walk.ChunkNext(); } } void CheckForLoops( CUtlVector< CUtlSymbol >& wavfiles ) { int c = wavfiles.Count(); vprint( 0, "Performing '%i' extractions (might take a while...)\n", c ); for ( int i = 0; i < c; ++i ) { char const *wavname = g_Analysis.symbols.String( wavfiles[ i ] ); CheckWavForLoops( wavname ); if ( !(i % 50 ) ) { vprint( 0, "Finished %i/%i\n", i, c ); } } } #define MAX_LOCALIZED_CHARS 2048 //----------------------------------------------------------------------------- // Purpose: converts an unicode string to an english string //----------------------------------------------------------------------------- int ConvertUnicodeToANSI(const wchar_t *unicode, char *ansi, int ansiBufferSize) { int result = ::WideCharToMultiByte(CP_UTF8, 0, unicode, -1, ansi, ansiBufferSize, NULL, NULL); ansi[ansiBufferSize - 1] = 0; return result; } struct OrderedCaption_t { OrderedCaption_t() : sym( UTL_INVAL_SYMBOL ), commands( NULL ), english( NULL ), blankenglish( false ) { } OrderedCaption_t( const OrderedCaption_t& src ) { sym = src.sym; if ( src.commands ) { int len = wcslen( src.commands ) + 1; commands = new wchar_t[ len ]; wcscpy( commands, src.commands ); } else { commands = NULL; } if ( src.english ) { int len = wcslen( src.english ) + 1; english = new wchar_t[ len ]; wcscpy( english, src.english ); } else { english = NULL; } blankenglish = src.blankenglish; } ~OrderedCaption_t() { delete[] commands; delete[] english; } CUtlSymbol sym; wchar_t *commands; // any stuff at the beginning of the US captions wchar_t *english; bool blankenglish; }; bool SplitCommand( wchar_t const **ppIn, wchar_t *cmd, wchar_t *args ) { const wchar_t *in = *ppIn; const wchar_t *oldin = in; if ( in[0] != L'<' ) { *ppIn += ( oldin - in ); return false; } args[ 0 ] = 0; cmd[ 0 ]= 0; wchar_t *out = cmd; in++; while ( *in != L'\0' && *in != L':' && *in != L'>' && !isspace( *in ) ) { *out++ = *in++; } *out = L'\0'; if ( *in != L':' ) { *ppIn += ( in - oldin ); return true; } in++; out = args; while ( *in != L'\0' && *in != L'>' ) { *out++ = *in++; } *out = L'\0'; //if ( *in == L'>' ) // in++; *ppIn += ( in - oldin ); return true; } wchar_t *GetStartupCommands( const wchar_t *str ) { const wchar_t *curpos = str; for ( ; curpos && *curpos != L'\0'; ++curpos ) { wchar_t cmd[ 256 ]; wchar_t args[ 256 ]; if ( SplitCommand( &curpos, cmd, args ) ) { continue; } // Got to first non-command character break; } if ( curpos - str >= 1 ) { int len = curpos - str; wchar_t *cmds = new wchar_t[ len + 1 ]; wcsncpy( cmds, str, len ); cmds[ len ] = L'\0'; return cmds; } return NULL; } wchar_t *CopyUnicode( const wchar_t *in ) { int len = wcslen( in ) + 1; wchar_t *out = new wchar_t[ len ]; wcsncpy( out, in, len ); out[ len - 1 ] = L'\0'; return out; } void BuildOrderedCaptionList( CUtlVector< OrderedCaption_t >& list ) { // parse out the file FileHandle_t file = g_pFullFileSystem->Open( "resource/closecaption_english.txt", "rb"); if ( file == FILESYSTEM_INVALID_HANDLE ) { // assert(!("CLocalizedStringTable::AddFile() failed to load file")); return; } // read into a memory block int fileSize = g_pFullFileSystem->Size(file) ; wchar_t *memBlock = (wchar_t *)malloc(fileSize + sizeof(wchar_t)); wchar_t *data = memBlock; g_pFullFileSystem->Read(memBlock, fileSize, file); // null-terminate the stream memBlock[fileSize / sizeof(wchar_t)] = 0x0000; // check the first character, make sure this a little-endian unicode file if (data[0] != 0xFEFF) { g_pFullFileSystem->Close(file); free(memBlock); return; } data++; // parse out a token at a time enum states_e { STATE_BASE, // looking for base settings STATE_TOKENS, // reading in unicode tokens }; bool bQuoted; bool bEnglishFile = true; states_e state = STATE_BASE; while (1) { // read the key and the value wchar_t keytoken[128]; data = ReadUnicodeToken(data, keytoken, 128, bQuoted); if (!keytoken[0]) break; // we've hit the null terminator // convert the token to a string char key[128]; ConvertUnicodeToANSI(keytoken, key, sizeof(key)); // if we have a C++ style comment, read to end of line and continue if (!strnicmp(key, "//", 2)) { data = ReadToEndOfLine(data); continue; } wchar_t valuetoken[ MAX_LOCALIZED_CHARS ]; data = ReadUnicodeToken(data, valuetoken, MAX_LOCALIZED_CHARS, bQuoted); if (!valuetoken[0] && !bQuoted) break; // we've hit the null terminator if (state == STATE_BASE) { if (!stricmp(key, "Language")) { // copy out our language setting /* char value[MAX_LOCALIZED_CHARS]; ConvertUnicodeToANSI(valuetoken, value, sizeof(value)); strncpy(m_szLanguage, value, sizeof(m_szLanguage) - 1); */ } else if (!stricmp(key, "Tokens")) { state = STATE_TOKENS; } else if (!stricmp(key, "}")) { // we've hit the end break; } } else if (state == STATE_TOKENS) { if (!stricmp(key, "}")) { // end of tokens state = STATE_BASE; } else { // skip our [english] beginnings (in non-english files) if ( (bEnglishFile) || (!bEnglishFile && strnicmp(key, "[english]", 9))) { // add the string to the table //AddString(key, valuetoken, NULL); CUtlSymbol sym = g_Analysis.symbols.AddString( key ); OrderedCaption_t cap; cap.sym = sym; cap.commands = GetStartupCommands( valuetoken ); cap.english = CopyUnicode( valuetoken ); cap.blankenglish = IsAllSpaces( valuetoken ); list.AddToTail( cap ); } } } } g_pFullFileSystem->Close(file); free(memBlock); vprint( 0, "Loaded %i captionnames from closecaption_english.txt\n", list.Count() ); } struct LookupData_t { LookupData_t() : unicode( 0 ), caption( 0 ) { } wchar_t *unicode; char *caption; }; void LoadImportData( char const *filename, CUtlDict< LookupData_t, int >& lookup ) { // parse out the file FileHandle_t file = g_pFullFileSystem->Open( filename, "rb"); if ( file == FILESYSTEM_INVALID_HANDLE ) { // assert(!("CLocalizedStringTable::AddFile() failed to load file")); return; } // read into a memory block int fileSize = g_pFullFileSystem->Size(file) ; wchar_t *memBlock = (wchar_t *)malloc(fileSize + sizeof(wchar_t)); wchar_t *data = memBlock; g_pFullFileSystem->Read(memBlock, fileSize, file); // null-terminate the stream memBlock[fileSize / sizeof(wchar_t)] = 0x0000; // check the first character, make sure this a little-endian unicode file if (data[0] != 0xFEFF) { g_pFullFileSystem->Close(file); free(memBlock); return; } data++; bool bQuoted; while (1) { // read the key and the value wchar_t keytoken[128]; data = ReadUnicodeTokenNoSpecial(data, keytoken, 128, bQuoted); if (!keytoken[0]) break; // we've hit the null terminator // convert the token to a string char key[128]; ConvertUnicodeToANSI(keytoken, key, sizeof(key)); // vprint( 0, "keyname %s\n", key ); // if we have a C++ style comment, read to end of line and continue if (!strnicmp(key, "//", 2)) { data = ReadToEndOfLine(data); continue; } wchar_t valuetoken[ MAX_LOCALIZED_CHARS ]; data = ReadUnicodeToken(data, valuetoken, MAX_LOCALIZED_CHARS, bQuoted); if (!valuetoken[0] && !bQuoted) break; // we've hit the null terminator wchar_t *vcopy = new wchar_t[ wcslen( valuetoken ) + 1 ]; wcscpy( vcopy, valuetoken ); LookupData_t ld; ld.unicode = vcopy; ld.caption = NULL; lookup.Insert( key, ld ); } g_pFullFileSystem->Close(file); free(memBlock); vprint( 0, "Loaded %i wav/captions from %s\n", lookup.Count(), filename ); } #define CAPTION_OUT_FILE "resource/closecaption_test.txt" //----------------------------------------------------------------------------- // Purpose: importfile is a unicode file contains pairs of .wav names and caption strings // we need to read in the closecaption_english.txt file // and then build a reverse lookup of .wav to caption name and build a new // closecaption_test.txt file based on the unicode caption strings // Input : *importfile - //----------------------------------------------------------------------------- void ImportCaptions( char const *pchImportfile ) { CUtlVector< OrderedCaption_t > captionlist; BuildOrderedCaptionList( captionlist ); CUtlDict< LookupData_t, int > newCaptions; LoadImportData( pchImportfile, newCaptions ); // Now build a .wav to caption name lookup CUtlDict< CUtlSymbol, int > wavtosound; BuildReverseSoundLookup( wavtosound ); CUtlDict< wchar_t *, int > captionToUnicode; // Now walk the import data, and try to figure out the caption name for each one int c = newCaptions.Count(); for ( int i = 0; i < c ; ++i ) { char const *wavname = newCaptions.GetElementName( i ); LookupData_t& data = newCaptions[ i ]; char fn[ 512 ]; Q_strncpy( fn, wavname, sizeof( fn ) ); Q_strlower( fn ); Q_FixSlashes( fn ); // See if we can find the wavname in the reverse lookup int idx = wavtosound.Find( fn ); if ( idx != wavtosound.InvalidIndex() ) { data.caption = strdup( g_Analysis.symbols.String( wavtosound[ idx ] ) ); captionToUnicode.Insert( data.caption, data.unicode ); } else { vprint( 0, "unable to find caption matching '%s'\n", wavname ); } } CUtlBuffer buf( 0, 0 ); c = captionlist.Count(); for ( int i = 0; i < c; ++i ) { char const *captionname = g_Analysis.symbols.String( captionlist[ i ].sym ); // Find it in the captionToUnicodeFolder int idx = captionToUnicode.Find( captionname ); if ( idx != captionToUnicode.InvalidIndex() ) { // Skip blank english entries if ( captionlist[ i ].blankenglish ) { vprint( 0, "skipping %s, english caption is blank\n", captionname ); continue; } wchar_t *u = captionToUnicode[ idx ]; wchar_t *prefix = captionlist[ i ].commands; wchar_t composed[ MAX_LOCALIZED_CHARS ]; int maxlen = ( sizeof( composed ) / sizeof( wchar_t ) ) - 1; if ( prefix ) { _snwprintf( composed, maxlen, L"%s%s", prefix, u ); } else { wcsncpy( composed, u, maxlen ); } composed[ maxlen ] = L'\0'; // Write to buffer WriteAsciiStringAsUnicode( buf, captionname, true ); WriteAsciiStringAsUnicode( buf, "\t", false ); WriteUnicodeString( buf, composed, true ); WriteAsciiStringAsUnicode( buf, "\r\n", false ); // Now write the "[ENGLISH]" entry char engcap[ 512 ]; Q_snprintf( engcap, sizeof( engcap ), "[english]%s", captionname ); WriteAsciiStringAsUnicode( buf, engcap, true ); WriteAsciiStringAsUnicode( buf, "\t", false ); WriteUnicodeString( buf, captionlist[ i ].english ? captionlist[ i ].english : L"???", true ); WriteAsciiStringAsUnicode( buf, "\r\n", false ); } else { vprint( 0, "no lookup for cc token '%s'\n", captionname ); } } // Now try and spit out a file like the cc english file, but with the new data FileHandle_t fh = g_pFullFileSystem->Open( CAPTION_OUT_FILE , "wb" ); if ( FILESYSTEM_INVALID_HANDLE != fh ) { g_pFullFileSystem->Write( buf.Base(), buf.TellPut(), fh ); g_pFullFileSystem->Close( fh ); } else { vprint( 0, "Unable to open %s for writing\n", CAPTION_OUT_FILE ); } // Cleanup memory c = newCaptions.Count(); for ( int i = 0 ; i < c; ++i ) { LookupData_t& data = newCaptions[ i ]; delete[] data.unicode; free( data.caption ); } newCaptions.Purge(); } bool IsWavFileDucked( char const *name ) { CSentence sentence; if ( LoadSentenceFromWavFile( name , sentence ) ) { return sentence.GetVoiceDuck(); } vprint( 0, "IsWavFileDucked: Missing .wav %s!!!\n", name ); return false; } void SetWavFileDucking( char const *name, bool ducking ) { CSentence sentence; if ( LoadSentenceFromWavFile( name , sentence ) ) { Assert( sentence.GetVoiceDuck() != ducking ); sentence.SetVoiceDuck( ducking ); // Save it back out SaveWave( name, sentence ); vprint( 1, "duck(%s): %s\n", ducking ? "true" : "false", name ); return; } vprint( 0, "SetWavFileDucking: Missing .wav %s!!!\n", name ); } void SyncDucking( CUtlVector< CUtlSymbol >& english, CUtlVector< CUtlSymbol >& localized ) { int i, c; CUtlRBTree< CUtlSymbol > englishducked( 0, 0, DefLessFunc( CUtlSymbol ) ); CUtlRBTree< CUtlSymbol > englishunducked( 0, 0, DefLessFunc( CUtlSymbol ) ); int fromoffset = Q_strlen( fromdir ) + 1; int tooffset = Q_strlen( todir ) + 1; c = english.Count(); for ( i = 0; i < c; ++i ) { CUtlSymbol& sym = english[ i ]; char fn[ 512 ]; Q_strncpy( fn, g_Analysis.symbols.String( sym ), sizeof( fn ) ); if ( !( i % 1000 ) ) { vprint( 1, "analyzed %i / %i (%.1f %%)\n", i, c, 100.0f * (float)i/(float)c ); } bool ducked = IsWavFileDucked( fn ); CUtlSymbol croppedSym = g_Analysis.symbols.AddString( &fn[ fromoffset ] ); if ( ducked ) { englishducked.Insert( croppedSym ); } else { englishunducked.Insert( croppedSym ); } } int updated = 0; // Now walk the localized tree and sync it to the english version c = localized.Count(); for ( i = 0; i < c; ++i ) { CUtlSymbol& sym = localized[ i ]; char fn[ 512 ]; Q_strncpy( fn, g_Analysis.symbols.String( sym ), sizeof( fn ) ); bool ducked = IsWavFileDucked( fn ); CUtlSymbol croppedSym = g_Analysis.symbols.AddString( &fn[ tooffset ] ); bool inenglishducked = englishducked.Find( croppedSym ) != englishducked.InvalidIndex() ? true : false; bool inenglishunducked = englishunducked.Find( croppedSym ) != englishunducked.InvalidIndex() ? true : false; if ( !( i % 100 ) ) { vprint( 1, "sync'd %i / %i (%.1f %%)\n", i, c, 100.0f * (float)i/(float)c ); } if ( ducked && inenglishducked ) continue; if ( !ducked && inenglishunducked ) continue; if ( !inenglishducked && !inenglishunducked ) { vprint( 0, "Warning: %s is in localized tree, missing from english tree!!\n", fn ); continue; } Assert( inenglishducked ^ inenglishunducked ); SetWavFileDucking( fn, inenglishducked ); ++updated; } vprint( 0, "finished, updated %i / %i (%.1f %%) localized .wavs\n", updated, c, 100.0f * (float)updated/(float)c ); } //----------------------------------------------------------------------------- // The application object //----------------------------------------------------------------------------- class CLocalizationCheckApp : public CTier3SteamApp { typedef CTier3SteamApp BaseClass; public: // Methods of IApplication virtual bool Create(); virtual bool PreInit( ); virtual int Main(); virtual void PostShutdown( ); virtual void Destroy() {} }; DEFINE_CONSOLE_STEAM_APPLICATION_OBJECT( CLocalizationCheckApp ); //----------------------------------------------------------------------------- // The application object //----------------------------------------------------------------------------- bool CLocalizationCheckApp::Create() { SpewOutputFunc( SpewFunc ); SpewActivate( "localization_check", 2 ); AppSystemInfo_t appSystems[] = { { "vgui2.dll", VGUI_IVGUI_INTERFACE_VERSION }, { "soundemittersystem.dll", SOUNDEMITTERSYSTEM_INTERFACE_VERSION }, { "", "" } // Required to terminate the list }; return AddSystems( appSystems ); } //----------------------------------------------------------------------------- // Init, shutdown //----------------------------------------------------------------------------- bool CLocalizationCheckApp::PreInit( ) { if ( !BaseClass::PreInit() ) return false; g_pFileSystem = filesystem = g_pFullFileSystem; if ( !g_pFullFileSystem || !g_pSoundEmitterSystem || !g_pVGuiLocalize ) { Error( "Unable to load required library interface!\n" ); return false; } char workingdir[ 256 ]; workingdir[0] = 0; Q_getwd( workingdir, sizeof( workingdir ) ); // If they didn't specify -game on the command line, use VPROJECT. if ( !SetupSearchPaths( workingdir, false, true ) ) { Warning( "Unable to set up the file system!\n" ); return false; } // work out of the root directory (same as the reslists) g_pFullFileSystem->AddSearchPath(".", "root"); return true; } void CLocalizationCheckApp::PostShutdown( ) { g_pFileSystem = filesystem = NULL; BaseClass::PostShutdown(); } //----------------------------------------------------------------------------- // Purpose: // Input : argc - // argv[] - // Output : int //----------------------------------------------------------------------------- int CLocalizationCheckApp::Main() { char language[ 256 ]; memset( language, 0, sizeof( language ) ); bool extractenglish = false; bool forceducking = false; int iArg = 1; int argc = CommandLine()->ParmCount(); for (; iArgGetParm( iArg ); if ( pArg[ 0 ] == '-' ) { switch( pArg[ 1 ] ) { case 'p': extractenglish = true; break; case 'v': verbose = true; break; case 'r': regenerate = true; break; case 'q': regenerate_quiet = true; break; case 'u': generate_usage = true; break; case 'b': nuke = true; break; case 's': checkscriptsounds = true; break; case 'c': build_cc = true; break; case 'x': build_script = true; break; case 'w': wavcheck = true; Q_strncpy( sounddir, CommandLine()->GetParm( iArg + 1 ), sizeof( sounddir ) ); iArg++; break; case 'e': extractphonemes = true; Q_strncpy( sounddir, CommandLine()->GetParm( iArg + 1 ), sizeof( sounddir ) ); iArg++; break; case 'l': if ( !Q_stricmp( &pArg[1], "loop" ) ) { checkforloops = true; Q_strncpy( sounddir, CommandLine()->GetParm( iArg + 1 ), sizeof( sounddir ) ); iArg++; } else { uselogfile = true; } break; case 'f': if ( !Q_stricmp( pArg, "-forceduck" )) { forceducking = true; break; } forceextract = true; break; case 'd': checkfordups = true; break; case 'i': { importcaptions = true; Q_strncpy( importfile, CommandLine()->GetParm( iArg + 1 ), sizeof( importfile ) ); iArg++; } break; case 'm': { makecopybatch = true; Q_strncpy( fromdir, CommandLine()->GetParm( iArg + 1 ), sizeof( fromdir ) ); Q_strncpy( todir, CommandLine()->GetParm( iArg + 2 ), sizeof( todir ) ); iArg += 2; } break; case 'a': { syncducking = true; Q_strncpy( fromdir, CommandLine()->GetParm( iArg + 1 ), sizeof( fromdir ) ); Q_strncpy( todir, CommandLine()->GetParm( iArg + 2 ), sizeof( todir ) ); iArg += 2; } break; default: printusage(); break; } } } if ( argc < 2 || (iArg != argc ) ) { PrintHeader(); printusage(); } Q_strncpy( language, CommandLine()->GetParm( argc - 1 ), sizeof( language ) ); // If it's english, turn off checks. if ( !Q_stricmp( language, "english" ) ) { language[ 0 ] = 0; } if ( !forceducking && !checkforloops && !syncducking && !extractphonemes && !importcaptions && language[0] != 0 ) { // See if it's a valid language int idx = CSentence::LanguageForName( language ); if ( idx == -1 ) { vprint( 0, "\nSkipping language check, '%s' is not a valid language\n", language ); vprint( 0, "Valid Language Names:\n" ); for ( int j = 0; j < CC_NUM_LANGUAGES; ++j ) { vprint( 2, "%s\n", CSentence::NameForLanguage( j ) ); } printusage(); } } CheckLogFile(); PrintHeader(); vprint( 0, " Looking for localization inconsistencies...\n" ); if ( !checkforloops&& !syncducking && !extractphonemes && !importcaptions && language[0] != 0 ) { vprint( 0, "\nLanguage: %s\n", language ); } vprint( 0, "Initializing stub sound system\n" ); sound->Init(); vprint( 0, "Initializing localization database system\n" ); // Always start with english g_pVGuiLocalize->AddFile( "resource/closecaption_english.txt" ); // Todo add language specific file Q_FixSlashes( gamedir ); Q_strlower( gamedir ); char vcddir[ 512 ]; Q_snprintf( vcddir, sizeof( vcddir ), "%sscenes", gamedir ); char ccdir[ 512 ]; Q_snprintf( ccdir, sizeof( ccdir ), "%ssound/combined", gamedir ); vprint( 0, "game dir %s\nvcd dir %s\n\n", gamedir, vcddir ); Q_StripTrailingSlash( sounddir ); Q_StripTrailingSlash( vcddir ); // //ProcessMaterialsDirectory( vmtdir ); vprint( 0, "Initializing sound emitter system\n" ); g_pSoundEmitterSystem->ModInit(); vprint( 0, "Loaded %i sounds\n", g_pSoundEmitterSystem->GetSoundCount() ); if ( forceducking ) { CUtlVector< CUtlSymbol > wavefiles; char workingdir[ 256 ]; workingdir[0] = 0; Q_getwd( workingdir, sizeof( workingdir ) ); BuildFileList( wavefiles, workingdir, ".wav" ); vprint( 0, "forcing ducking on %i .wav files in %s\n\n", wavefiles.Count(), workingdir ); for ( int i = 0; i < wavefiles.Count(); i++ ) { CUtlSymbol& sym = wavefiles[ i ]; char fn[ 512 ]; Q_strncpy( fn, g_Analysis.symbols.String( sym ), sizeof( fn ) ); SetWavFileDucking( fn, true ); } } else { vprint( 0, "Building list of .vcd files\n" ); CUtlVector< CUtlSymbol > vcdfiles; BuildFileList( vcdfiles, vcddir, ".vcd" ); vprint( 0, "found %i .vcd files\n\n", vcdfiles.Count() ); if ( extractenglish && !language[0] ) { vprint( 0, "extractenglish: pulling raw english txt from file\n" ); ExtractEnglish(); } else if ( wavcheck ) { vprint( 0, "wavcheck: building list of known .wav files\n" ); CUtlVector< CUtlSymbol > wavfiles; BuildFileList( wavfiles, va( "%ssound/%s", gamedir, sounddir ), ".wav" ); vprint( 0, "found %i .wav files\n\n", wavfiles.Count() ); WavCheck( wavfiles ); } else if ( makecopybatch ) { vprint( 0, "makecopybatch: building list of known .wav files\n" ); CUtlVector< CUtlSymbol > wavfiles; BuildFileList( wavfiles, va( "%ssound/%s", gamedir, sounddir ), ".wav" ); vprint( 0, "found %i .wav files\n\n", wavfiles.Count() ); MakeBatchFile( wavfiles, fromdir, todir ); } else if ( extractphonemes ) { vprint( 0, "extractphonemes: building list of known .wav files\n" ); CUtlVector< CUtlSymbol > wavfiles; BuildFileList( wavfiles, sounddir, ".wav" ); vprint( 0, "found %i .wav files in %s (and subdirs)\n\n", wavfiles.Count(), sounddir ); ExtractPhonemes( wavfiles ); } else if ( checkforloops ) { vprint( 0, "checkforloops: building list of known .wav files\n" ); CUtlVector< CUtlSymbol > wavfiles; BuildFileList( wavfiles, sounddir, ".wav" ); vprint( 0, "found %i .wav files in %s (and subdirs)\n\n", wavfiles.Count(), sounddir ); CheckForLoops( wavfiles ); } else if ( importcaptions ) { vprint( 0, "importcaptions: importing captions from '%s'\n", importfile ); ImportCaptions( importfile ); } else if ( checkfordups ) { vprint( 0, "checkfordups: checking for duplicate captions\n" ); CheckDuplcatedText(); } else if ( syncducking ) { vprint( 0, "syncducking: building list of known .wav files in\n %s\n %s\n", fromdir, todir ); CUtlVector< CUtlSymbol > englishfiles; BuildFileList( englishfiles, fromdir, ".wav" ); vprint( 0, "found %i .wav files in %s (and subdirs)\n\n", englishfiles.Count(), fromdir ); CUtlVector< CUtlSymbol > localized_files; BuildFileList( localized_files, todir, ".wav" ); vprint( 0, "found %i .wav files in %s (and subdirs)\n\n", localized_files.Count(), todir ); SyncDucking( englishfiles, localized_files ); } else { CUtlVector< CUtlSymbol > vcdsinreslist; BuildVCDAndMapNameListsFromReslists( vcdsinreslist ); // Check for missing localization data for all speak events not marked CC_DISABLED CUtlRBTree< CUtlSymbol, int > referencedcaptionwaves( 0, 0, SymbolLessFunc ); vprint( 0, "\nValidating close caption tokens and combined .wav file checksums\n\n" ); CheckLocalizationEntries( vcdfiles, referencedcaptionwaves ); vprint( 0, "\nChecking for orphaned combined .wav files\n\n" ); CUtlVector< CUtlSymbol > combinedwavfiles; BuildFileList( combinedwavfiles, ccdir, ".wav" ); CheckForOrphanedCombinedWavs( combinedwavfiles, referencedcaptionwaves ); if ( language[0] != 0 ) { vprint( 0, "\nChecking for missing or out of date localized combined .wav files\n\n" ); ValidateForeignLanguageWaves( language, combinedwavfiles ); } if ( generate_usage ) { // Figure out which .vcds are unused CheckUnusedVcds( vcdsinreslist, vcdfiles ); } if ( checkscriptsounds ) { CheckUnusedSounds(); } } } vprint( 0, "\nCleaning up...\n" ); g_pSoundEmitterSystem->ModShutdown(); // Unload localization system g_pVGuiLocalize->RemoveAll(); sound->Shutdown(); FileSystem_Term(); g_Analysis.symbols.RemoveAll(); return 0; }