//========= Copyright � 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "host.h" #include "sysexternal.h" #include "networkstringtable.h" #include "utlbuffer.h" #include "bitbuf.h" #include "netmessages.h" #include "net.h" #include "filesystem_engine.h" #include "baseclient.h" #include "vprof.h" #include "tier2/utlstreambuffer.h" #include "checksum_engine.h" #include "MapReslistGenerator.h" #include "lzma/lzma.h" #include "../utils/common/bsplib.h" #include "ibsppack.h" #include "tier0/icommandline.h" #include "tier1/lzmaDecoder.h" #include "server.h" #include "eiface.h" #include "cdll_engine_int.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar sv_dumpstringtables( "sv_dumpstringtables", "0", FCVAR_CHEAT ); #define BSPPACK_STRINGTABLE_DICTIONARY "stringtable_dictionary.dct" #define BSPPACK_STRINGTABLE_DICTIONARY_FALLBACK "stringtable_dictionary_fallback.dct" // These are automatically added by vbsp. Should be removed when adding the real files here // These must match what is used in utils\vbsp\vbsp.cpp!!! #define BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE "stringtable_dictionary_xbox.dct" #define BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE_FALLBACK "stringtable_dictionary_fallback_xbox.dct" #define RESLISTS_FOLDER "reslists" #define RESLISTS_FOLDER_X360 "reslists_xbox" #define SUBSTRING_BITS 5 struct StringHistoryEntry { char string[ (1<& history, char const *newstring, int& substringsize ) { int bestindex = -1; int bestcount = 0; int c = history.Count(); for ( int i = 0; i < c; i++ ) { char const *prev = history[ i ].string; int similar = CountSimilarCharacters( prev, newstring ); if ( similar < 3 ) continue; if ( similar > bestcount ) { bestcount = similar; bestindex = i; } } substringsize = bestcount; return bestindex; } static ConVar stringtable_usedictionaries( "stringtable_usedictionaries", #if defined( PORTAL2 ) "0", // Don't use dictionaries on portal2, its only two player! Just send them. #else "1", // On CS:GO we disable stringtable dictionaries for community servers for maps to be downloadable in code (see: CNetworkStringTable::WriteUpdate) #endif // PORTAL2 0, "Use dictionaries for string table networking\n" ); static ConVar stringtable_alwaysrebuilddictionaries( "stringtable_alwaysrebuilddictionaries", "0", 0, "Rebuild dictionary file on every level load\n" ); static ConVar stringtable_showsizes( "stringtable_showsizes", "0", 0, "Show sizes of string tables when building for signon\n" ); class CNetworkStringTableDictionaryManager: public INetworkStringTableDictionaryMananger { public: CNetworkStringTableDictionaryManager(); // INetworkStringTableDictionaryMananger virtual bool OnLevelLoadStart( char const *pchMapName, CRC32_t *pStringTableCRC ); virtual void OnBSPFullyUnloaded(); virtual CRC32_t GetCRC() { return m_CRC; } // Returns -1 if string can't be found in db int Find( char const *pchString ) const; char const *Lookup( int index ) const; int GetEncodeBits() const; bool ShouldRecreateDictionary( char const *pchMapName ); bool IsValid() const; void ProcessBuffer( CUtlBuffer &buf ); void SetLoadedFallbacks( bool bLoadedFromFallbacks ); bool WriteDictionaryToBSP( char const *pchMapName, CUtlBuffer &buf, bool bCreatingFor360 ); void CacheNewStringTableForWriteToBSPOnLevelShutdown( char const *pchMapName, CUtlBuffer &buf, bool bCreatingFor360 ); private: void LoadMapStrings( char const *pchMapName, bool bServer ); void Clear(); bool LoadDictionaryFile( CUtlBuffer &buf, char const *pchMapName ); CRC32_t HashStringCaselessIgnoreSlashes( char const *pString ) const { if ( !pString ) { pString = ""; } int len = Q_strlen( pString ) + 1; char *name = (char *)stackalloc( len ); Q_strncpy( name, pString, len ); Q_FixSlashes( name ); Q_strlower( name ); return CRC32_ProcessSingleBuffer( (const void *)name, len ); } CUtlString m_sCurrentMap; // NOTE NOTE: These need to be 'persistent' objects since our code stores off raw ptrs to these strings when // precaching, etc. Can't be constructed on stack buffer or with va( "%s", xxx ), etc.!!!!! // Otherwise we could use FileNameHandle_t type stuff. // Raw strings in order from the data file CUtlVector< CUtlString > m_Strings; // Mapping of string back to index in CUtlVector CUtlMap< CRC32_t, int > m_StringHashToIndex; CRC32_t m_CRC; int m_nEncodeBits; bool m_bForceRebuildDictionaries; bool m_bLoadedFallbacks; struct CStringTableDictionaryCache { CStringTableDictionaryCache() { Reset(); } void Reset() { m_bActive = false; m_sBSPName = ""; m_bCreatingForX360 = false; m_Buffer.Purge(); } bool m_bActive; CUtlString m_sBSPName; CUtlBuffer m_Buffer; bool m_bCreatingForX360; }; CStringTableDictionaryCache m_BuildStringTableDictionaryCache; }; static CNetworkStringTableDictionaryManager g_StringTableDictionary; // Expose to rest of engine INetworkStringTableDictionaryMananger *g_pStringTableDictionary = &g_StringTableDictionary; CNetworkStringTableDictionaryManager::CNetworkStringTableDictionaryManager() : m_StringHashToIndex( 0, 0, DefLessFunc( CRC32_t ) ), m_nEncodeBits( 1 ), m_bForceRebuildDictionaries( false ), m_bLoadedFallbacks( false ), m_CRC( 0 ) { } void CNetworkStringTableDictionaryManager::Clear() { m_StringHashToIndex.Purge(); m_Strings.Purge(); m_CRC = 0; m_nEncodeBits = 1; } bool CNetworkStringTableDictionaryManager::IsValid() const { return m_Strings.Count() > 0; } int CNetworkStringTableDictionaryManager::GetEncodeBits() const { return m_nEncodeBits; } void CNetworkStringTableDictionaryManager::SetLoadedFallbacks( bool bLoadedFromFallbacks ) { m_bLoadedFallbacks = bLoadedFromFallbacks; } bool CNetworkStringTableDictionaryManager::OnLevelLoadStart( char const *pchMapName, CRC32_t *pStringTableCRC ) { m_BuildStringTableDictionaryCache.Reset(); // INFESTED_DLL - disable string dictionaries for Alien Swarm random maps. TODO: Move a check for this into the game interface static char gamedir[MAX_OSPATH]; Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) ); if ( !Q_stricmp( gamedir, "infested" ) ) { return true; } m_bForceRebuildDictionaries = stringtable_alwaysrebuilddictionaries.GetBool() || CommandLine()->FindParm( "-stringtables" ); if ( pchMapName ) LoadMapStrings( pchMapName, pStringTableCRC == NULL ); else return true; // assume that stringtables will match since we will download the map later if ( pStringTableCRC ) return ( *pStringTableCRC == m_CRC ); return true; } bool CNetworkStringTableDictionaryManager::ShouldRecreateDictionary( char const *pchMapName ) { // INFESTED_DLL - disable string dictionaries for Alien Swarm random maps. TODO: Move a check for this into the game interface static char gamedir[MAX_OSPATH]; Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) ); if ( !Q_stricmp( gamedir, "infested" ) ) { return false; } if ( m_bForceRebuildDictionaries || // being forced with -stringtables or stringtable_alwaysrebuilddictionaries ( MapReslistGenerator().IsEnabled() && // true if -makereslists ( m_Strings.Count() == 0 || // True if we couldn't load the file up before... m_bLoadedFallbacks ) // True if we loaded fallback file instead ) ) { return m_Strings.Count() == 0 || // True if we couldn't load the file up before... m_bLoadedFallbacks || // True if we loaded fallback file instead MapReslistGenerator().IsEnabled(); // true if -makereslists } return false; } inline char const *GetStringTableDictionaryFileName( bool bIsFallback ) { if ( bIsFallback ) { if ( IsGameConsole() || NET_IsDedicatedForXbox() ) { return BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE_FALLBACK; } return BSPPACK_STRINGTABLE_DICTIONARY_FALLBACK; } if ( IsGameConsole() || NET_IsDedicatedForXbox() ) { return BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE; } return BSPPACK_STRINGTABLE_DICTIONARY; } bool CNetworkStringTableDictionaryManager::LoadDictionaryFile( CUtlBuffer &buf, char const *pchMapName ) { m_bLoadedFallbacks = false; if ( g_pFileSystem->ReadFile( GetStringTableDictionaryFileName( false ), "BSP", buf ) ) { return true; } // Try backup file if ( g_pFileSystem->ReadFile( GetStringTableDictionaryFileName( true ), "BSP", buf ) ) { Warning( "#######################################\n" ); Warning( "Map %s using default stringtable dictionary, don't ship this way!!!\n", pchMapName ); Warning( "Run with -stringtables on the command line or convar\n" ); Warning( "stringtable_alwaysrebuilddictionaries enabled to build the string table\n" ); Warning( "#######################################\n" ); m_bLoadedFallbacks = true; return true; } // Try fallback file char szFallback[ 256 ]; Q_snprintf( szFallback, sizeof( szFallback ), "reslists/%s.dict", pchMapName ); if ( g_pFileSystem->ReadFile( szFallback, "GAME", buf ) ) { Warning( "#######################################\n" ); Warning( "Map %s using fallback stringtable dictionary, don't ship this way!!!\n", pchMapName ); Warning( "Run with -stringtables on the command line or convar\n" ); Warning( "stringtable_alwaysrebuilddictionaries enabled to build the string table\n" ); Warning( "#######################################\n" ); m_bLoadedFallbacks = true; return true; } Warning( "#######################################\n" ); Warning( "Map %s missing stringtable dictionary, don't ship this way!!!\n", pchMapName ); Warning( "Run with -stringtables on the command line or convar\n" ); Warning( "stringtable_alwaysrebuilddictionaries enabled to build the string table\n" ); Warning( "#######################################\n" ); return false; } void CNetworkStringTableDictionaryManager::ProcessBuffer( CUtlBuffer &buf ) { Clear(); m_CRC = CRC32_ProcessSingleBuffer( buf.Base(), buf.TellMaxPut() ); while ( buf.GetBytesRemaining() > 0 ) { char line[ MAX_PATH ]; buf.GetString( line, sizeof( line ) ); if ( !line[ 0 ] ) continue; MEM_ALLOC_CREDIT(); CUtlString str; str = line; int index = m_Strings.AddToTail( str ); CRC32_t hash = HashStringCaselessIgnoreSlashes( str ); m_StringHashToIndex.Insert( hash, index ); } m_nEncodeBits = Q_log2( m_Strings.Count() ) + 1; } void CNetworkStringTableDictionaryManager::LoadMapStrings( char const *pchMapName, bool bServer ) { if ( m_sCurrentMap == pchMapName ) return; m_sCurrentMap = pchMapName; Clear(); // On the client we need to add the bsp to the file search path before loading the dictionary file... // It's added on the server in CGameServer::SpawnServer char szModelName[MAX_PATH]; char szNameOnDisk[MAX_PATH]; Q_snprintf( szModelName, sizeof( szModelName ), "maps/%s.bsp", pchMapName ); GetMapPathNameOnDisk( szNameOnDisk, szModelName, sizeof( szNameOnDisk ) ); if ( !bServer ) { // Add to file system g_pFileSystem->AddSearchPath( szNameOnDisk, "GAME", PATH_ADD_TO_HEAD ); // Force reload all materials since BSP has changed // TODO: modelloader->UnloadUnreferencedModels(); materials->ReloadMaterials(); } if ( stringtable_usedictionaries.GetBool() ) { // Load the data file CUtlBuffer buf; if ( LoadDictionaryFile( buf, pchMapName ) ) { // LZMA decompress it if needed CLZMA lzma; if ( lzma.IsCompressed( (byte *)buf.Base() ) ) { unsigned int decompressedSize = lzma.GetActualSize( (byte *)buf.Base() ); byte *work = new byte[ decompressedSize ]; int outputLength = lzma.Uncompress( (byte *)buf.Base(), work ); buf.Clear(); buf.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); buf.Put( work, outputLength ); delete[] work; } ProcessBuffer( buf ); } } } int CNetworkStringTableDictionaryManager::Find( char const *pchString ) const { CRC32_t hash = HashStringCaselessIgnoreSlashes( pchString ); // Note that a string can contain subparts that together both exist as a valid filename, but they may be from separate paths int idx = m_StringHashToIndex.Find( hash ); if ( idx == m_StringHashToIndex.InvalidIndex() ) return -1; return m_StringHashToIndex[ idx ]; } char const *CNetworkStringTableDictionaryManager::Lookup( int index ) const { if ( index < 0 || index >= m_Strings.Count() ) { Assert( 0 ); return ""; } const CUtlString &string = m_Strings[ index ]; return string.String(); } void CNetworkStringTableDictionaryManager::OnBSPFullyUnloaded() { if ( !m_BuildStringTableDictionaryCache.m_bActive ) return; WriteDictionaryToBSP( m_BuildStringTableDictionaryCache.m_sBSPName, m_BuildStringTableDictionaryCache.m_Buffer, m_BuildStringTableDictionaryCache.m_bCreatingForX360 ); m_BuildStringTableDictionaryCache.Reset(); } bool CNetworkStringTableDictionaryManager::WriteDictionaryToBSP( char const *pchMapName, CUtlBuffer &buf, bool bCreatingFor360 ) { char mapPath[ MAX_PATH ]; Q_snprintf( mapPath, sizeof( mapPath ), "maps/%s.bsp", pchMapName ); // We shouldn't ever fail here since we don't queue this up if it might fail // Make sure that the file is writable before building stringtable dictionary. if ( !g_pFileSystem->IsFileWritable( mapPath, "GAME" ) ) { return false; } // load the bsppack dll IBSPPack *iBSPPack = NULL; CSysModule *pModule = FileSystem_LoadModule( "bsppack" ); if ( pModule ) { CreateInterfaceFn factory = Sys_GetFactory( pModule ); if ( factory ) { iBSPPack = ( IBSPPack * )factory( IBSPPACK_VERSION_STRING, NULL ); } } if( !iBSPPack ) { if ( pModule ) { Sys_UnloadModule( pModule ); } ConMsg( "Can't load bsppack.dll\n" ); return false; } iBSPPack->LoadBSPFile( g_pFileSystem, mapPath ); char const *relative = bCreatingFor360 ? BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE : BSPPACK_STRINGTABLE_DICTIONARY; // Remove the fallback that VBSP adds by default iBSPPack->RemoveFileFromPack( bCreatingFor360 ? BSPPACK_STRINGTABLE_DICTIONARY_GAMECONSOLE_FALLBACK : BSPPACK_STRINGTABLE_DICTIONARY_FALLBACK ); // Now add in the up to date information iBSPPack->AddBufferToPack( relative, buf.Base(), buf.TellPut(), false ); iBSPPack->WriteBSPFile( mapPath ); iBSPPack->ClearPackFile(); FileSystem_UnloadModule( pModule ); Msg( "Updated stringtable dictionary saved to %s\n", mapPath ); return true; } void CNetworkStringTableDictionaryManager::CacheNewStringTableForWriteToBSPOnLevelShutdown( char const *pchMapName, CUtlBuffer &buf, bool bCreatingFor360 ) { m_BuildStringTableDictionaryCache.m_bActive = true; m_BuildStringTableDictionaryCache.m_sBSPName = pchMapName; m_BuildStringTableDictionaryCache.m_Buffer.Purge(); m_BuildStringTableDictionaryCache.m_Buffer.Put( buf.Base(), buf.TellPut() ); m_BuildStringTableDictionaryCache.m_bCreatingForX360 = bCreatingFor360; } //----------------------------------------------------------------------------- // Implementation for general purpose strings //----------------------------------------------------------------------------- class CNetworkStringDict : public INetworkStringDict { public: CNetworkStringDict( bool bUseDictionary ) : m_bUseDictionary( bUseDictionary ), m_Items( 0, 0, CTableItem::Less ) { } virtual ~CNetworkStringDict() { Purge(); } unsigned int Count() { return m_Items.Count(); } void Purge() { m_Items.Purge(); } const char *String( int index ) { return m_Items.Key( index ).GetName(); } bool IsValidIndex( int index ) { return m_Items.IsValidIndex( index ); } int Insert( const char *pString ) { CTableItem item; item.SetName( m_bUseDictionary, pString ); return m_Items.Insert( item ); } int Find( const char *pString ) { CTableItem search; search.SetName( false, pString ); int iResult = m_Items.Find( search ); if ( iResult == m_Items.InvalidIndex() ) { return -1; } return iResult; } CNetworkStringTableItem &Element( int index ) { return m_Items.Element( index ); } const CNetworkStringTableItem &Element( int index ) const { return m_Items.Element( index ); } virtual void UpdateDictionary( int index ) { if ( !m_bUseDictionary ) return; CTableItem &item = m_Items.Key( index ); item.Update(); } virtual int DictionaryIndex( int index ) { if ( !m_bUseDictionary ) return -1; CTableItem &item = m_Items.Key( index ); return item.GetDictionaryIndex(); } private: bool m_bUseDictionary; // We use this type of item to avoid having two copies of the strings in memory -- // either we have a dictionary slot and point to that, or we have a m_Name CUtlString that gets // wiped between levels class CTableItem { public: CTableItem() : m_DictionaryIndex( -1 ), m_StringHash( 0u ) { } char const *GetName() const { return m_Name.String(); } void Update() { m_DictionaryIndex = g_StringTableDictionary.Find( m_Name.String() ); ComputeHash(); } int GetDictionaryIndex() const { return m_DictionaryIndex; } void SetName( bool bUseDictionary, char const *pString ) { m_Name = pString; m_DictionaryIndex = bUseDictionary ? g_StringTableDictionary.Find( pString ) : -1; ComputeHash(); } static bool Less( const CTableItem &lhs, const CTableItem &rhs ) { return lhs.m_StringHash < rhs.m_StringHash; } private: void ComputeHash() { char const *pName = GetName(); int len = Q_strlen( pName ) + 1; char *name = (char *)stackalloc( len ); Q_strncpy( name, pName, len ); Q_FixSlashes( name ); Q_strlower( name ); m_StringHash = CRC32_ProcessSingleBuffer( (const void *)name, len ); } int m_DictionaryIndex; CUtlString m_Name; CRC32_t m_StringHash; }; CUtlMap< CTableItem, CNetworkStringTableItem > m_Items; }; void CNetworkStringTable::CheckDictionary( int stringNumber ) { m_pItems->UpdateDictionary( stringNumber ); } //----------------------------------------------------------------------------- // Purpose: // Input : id - // *tableName - // maxentries - //----------------------------------------------------------------------------- CNetworkStringTable::CNetworkStringTable( TABLEID id, const char *tableName, int maxentries, int userdatafixedsize, int userdatanetworkbits, int flags ) : m_bAllowClientSideAddString( false ), m_pItemsClientSide( NULL ), m_nFlags( flags ) { if ( maxentries < 0 || userdatafixedsize < 0 || userdatanetworkbits < 0 ) { Host_Error( "String tables negative constructor unsupported %i %i %i\n", maxentries, userdatafixedsize, userdatanetworkbits ); } m_id = id; int len = strlen( tableName ) + 1; m_pszTableName = new char[ len ]; Assert( m_pszTableName ); Assert( tableName ); Q_strncpy( m_pszTableName, tableName, len ); m_changeFunc = NULL; m_pObject = NULL; m_nTickCount = 0; for ( int i = 0; i < MIRROR_TABLE_MAX_COUNT; ++i ) m_pMirrorTable[ i ] = NULL; m_nLastChangedTick = 0; m_bChangeHistoryEnabled = false; m_bLocked = false; m_nMaxEntries = maxentries; m_nEntryBits = Q_log2( m_nMaxEntries ); m_bUserDataFixedSize = userdatafixedsize != 0; m_nUserDataSize = userdatafixedsize; m_nUserDataSizeBits = userdatanetworkbits; if ( m_nUserDataSizeBits > CNetworkStringTableItem::MAX_USERDATA_BITS ) { Host_Error( "String tables user data bits restricted to %i bits, requested %i is too large\n", CNetworkStringTableItem::MAX_USERDATA_BITS, m_nUserDataSizeBits ); } if ( m_nUserDataSize > CNetworkStringTableItem::MAX_USERDATA_SIZE ) { Host_Error( "String tables user data size restricted to %i bytes, requested %i is too large\n", CNetworkStringTableItem::MAX_USERDATA_SIZE, m_nUserDataSize ); } // Make sure maxentries is power of 2 if ( ( 1 << m_nEntryBits ) != maxentries ) { Host_Error( "String tables must be powers of two in size!, %i is not a power of 2 [%s]\n", maxentries, tableName ); } m_pItems = new CNetworkStringDict( m_nFlags & NSF_DICTIONARY_ENABLED ); } void CNetworkStringTable::SetAllowClientSideAddString( bool state ) { if ( state == m_bAllowClientSideAddString ) return; m_bAllowClientSideAddString = state; if ( m_pItemsClientSide ) { delete m_pItemsClientSide; m_pItemsClientSide = NULL; } if ( m_bAllowClientSideAddString ) { m_pItemsClientSide = new CNetworkStringDict( NSF_NONE ); m_pItemsClientSide->Insert( "___clientsideitemsplaceholder0___" ); // 0 slot can't be used m_pItemsClientSide->Insert( "___clientsideitemsplaceholder1___" ); // -1 can't be used since it looks like the "invalid" index from other string lookups } } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNetworkStringTable::IsUserDataFixedSize() const { return m_bUserDataFixedSize; } bool CNetworkStringTable::IsUsingDictionary() const { return m_nFlags & NSF_DICTIONARY_ENABLED; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNetworkStringTable::GetUserDataSize() const { return m_nUserDataSize; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNetworkStringTable::GetUserDataSizeBits() const { return m_nUserDataSizeBits; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CNetworkStringTable::~CNetworkStringTable( void ) { delete[] m_pszTableName; delete m_pItems; delete m_pItemsClientSide; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTable::DeleteAllStrings( void ) { delete m_pItems; m_pItems = new CNetworkStringDict( m_nFlags & NSF_DICTIONARY_ENABLED ); if ( m_pItemsClientSide ) { delete m_pItemsClientSide; m_pItemsClientSide = new CNetworkStringDict( NSF_NONE ); m_pItemsClientSide->Insert( "___clientsideitemsplaceholder0___" ); // 0 slot can't be used m_pItemsClientSide->Insert( "___clientsideitemsplaceholder1___" ); // -1 can't be used since it looks like the "invalid" index from other string lookups } } //----------------------------------------------------------------------------- // Purpose: // Input : i - // Output : CNetworkStringTableItem //----------------------------------------------------------------------------- CNetworkStringTableItem *CNetworkStringTable::GetItem( int i ) { if ( i >= 0 ) { return &m_pItems->Element( i ); } Assert( m_pItemsClientSide ); return &m_pItemsClientSide->Element( -i ); } //----------------------------------------------------------------------------- // Purpose: Returns the table identifier // Output : TABLEID //----------------------------------------------------------------------------- TABLEID CNetworkStringTable::GetTableId( void ) const { return m_id; } //----------------------------------------------------------------------------- // Purpose: Returns the max size of the table // Output : int //----------------------------------------------------------------------------- int CNetworkStringTable::GetMaxStrings( void ) const { return m_nMaxEntries; } //----------------------------------------------------------------------------- // Purpose: Returns a table, by name // Output : const char //----------------------------------------------------------------------------- const char *CNetworkStringTable::GetTableName( void ) const { return m_pszTableName; } //----------------------------------------------------------------------------- // Purpose: Returns the number of bits needed to encode an entry index // Output : int //----------------------------------------------------------------------------- int CNetworkStringTable::GetEntryBits( void ) const { return m_nEntryBits; } void CNetworkStringTable::SetTick(int tick_count) { Assert( tick_count >= m_nTickCount ); m_nTickCount = tick_count; } bool CNetworkStringTable::Lock( bool bLock ) { bool bState = m_bLocked; m_bLocked = bLock; return bState; } pfnStringChanged CNetworkStringTable::GetCallback() { return m_changeFunc; } #ifndef SHARED_NET_STRING_TABLES //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTable::EnableRollback() { // stringtable must be empty Assert( m_pItems->Count() == 0); m_bChangeHistoryEnabled = true; } void CNetworkStringTable::SetMirrorTable(uint nIndex, INetworkStringTable *table) { Assert( nIndex < MIRROR_TABLE_MAX_COUNT ); m_pMirrorTable[ nIndex ] = table; } void CNetworkStringTable::RestoreTick(int tick) { // TODO optimize this, most of the time the tables doens't really change m_nLastChangedTick = 0; int count = m_pItems->Count(); for ( int i = 0; i < count; i++ ) { // restore tick in all entries int tickChanged = m_pItems->Element( i ).RestoreTick( tick ); if ( tickChanged > m_nLastChangedTick ) m_nLastChangedTick = tickChanged; } } //----------------------------------------------------------------------------- // Purpose: updates the mirror table, if set one // Output : return true if some entries were updates //----------------------------------------------------------------------------- void CNetworkStringTable::UpdateMirrorTable( int tick_ack ) { for ( int nMirrorTableIndex = 0; nMirrorTableIndex < MIRROR_TABLE_MAX_COUNT; ++nMirrorTableIndex ) { if ( !m_pMirrorTable[ nMirrorTableIndex ] ) continue; m_pMirrorTable[ nMirrorTableIndex ]->SetTick( m_nTickCount ); // use same tick int count = m_pItems->Count(); for ( int i = 0; i < count; i++ ) { CNetworkStringTableItem *p = &m_pItems->Element( i ); // mirror is up to date if ( p->GetTickChanged() <= tick_ack ) continue; const void *pUserData = p->GetUserData(); int nBytes = p->GetUserDataLength(); if ( !nBytes || !pUserData ) { nBytes = 0; pUserData = NULL; } // Check if we are updating an old entry or adding a new one if ( i < m_pMirrorTable[ nMirrorTableIndex ]->GetNumStrings() ) { m_pMirrorTable[ nMirrorTableIndex ]->SetStringUserData( i, nBytes, pUserData ); } else { // Grow the table (entryindex must be the next empty slot) Assert( i == m_pMirrorTable[ nMirrorTableIndex ]->GetNumStrings() ); char const *pName = m_pItems->String( i ); m_pMirrorTable[ nMirrorTableIndex ]->AddString( true, pName, nBytes, pUserData ); } } } } int CNetworkStringTable::WriteUpdate( CBaseClient *client, bf_write &buf, int tick_ack ) const { CUtlVector< StringHistoryEntry > history; int entriesUpdated = 0; int lastEntry = -1; int lastDictionaryIndex = -1; int nDictionaryEncodeBits = g_StringTableDictionary.GetEncodeBits(); bool bUseDictionaries = IsUsingDictionary(); bool bEncodeUsingDictionaries = bUseDictionaries && stringtable_usedictionaries.GetBool() && g_StringTableDictionary.IsValid(); // INFESTED_DLL - disable string dictionaries for Alien Swarm random maps. TODO: Move a check for this into the game interface static char gamedir[MAX_OSPATH]; Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) ); if ( !Q_stricmp( gamedir, "infested" ) ) { bEncodeUsingDictionaries = false; } // CSGO_DLL - disable string dictionaries for CS:GO community servers to allow for map downloading, // keep it enabled on Valve official servers since clients always have all maps or on listen servers if ( bEncodeUsingDictionaries && !Q_stricmp( gamedir, "csgo" ) ) { bEncodeUsingDictionaries = IsGameConsole(); } int count = m_pItems->Count(); int nDictionaryCount = 0; buf.WriteOneBit( bEncodeUsingDictionaries ? 1 : 0 ); for ( int i = 0; i < count; i++ ) { CNetworkStringTableItem *p = &m_pItems->Element( i ); // Client is up to date if ( p->GetTickChanged() <= tick_ack ) continue; int nStartBit = buf.GetNumBitsWritten(); // Write Entry index if ( (lastEntry+1) == i ) { buf.WriteOneBit( 1 ); } else { buf.WriteOneBit( 0 ); buf.WriteUBitLong( i, m_nEntryBits ); } // check if string can use older string as base eg "models/weapons/gun1" & "models/weapons/gun2" char const *pEntry = m_pItems->String( i ); if ( p->GetTickCreated() >= tick_ack ) { // this item has just been created, send string itself buf.WriteOneBit( 1 ); int nCurrentDictionaryIndex = m_pItems->DictionaryIndex( i ); if ( bEncodeUsingDictionaries && nCurrentDictionaryIndex != -1 ) { ++nDictionaryCount; buf.WriteOneBit( 1 ); if ( ( lastDictionaryIndex +1 ) == nCurrentDictionaryIndex ) { buf.WriteOneBit( 1 ); } else { buf.WriteOneBit( 0 ); buf.WriteUBitLong( nCurrentDictionaryIndex, nDictionaryEncodeBits ); } lastDictionaryIndex = nCurrentDictionaryIndex; } else { if ( bEncodeUsingDictionaries ) { buf.WriteOneBit( 0 ); } int substringsize = 0; int bestprevious = GetBestPreviousString( history, pEntry, substringsize ); if ( bestprevious != -1 ) { buf.WriteOneBit( 1 ); buf.WriteUBitLong( bestprevious, 5 ); // history never has more than 32 entries buf.WriteUBitLong( substringsize, SUBSTRING_BITS ); buf.WriteString( pEntry + substringsize ); } else { buf.WriteOneBit( 0 ); buf.WriteString( pEntry ); } } } else { buf.WriteOneBit( 0 ); } // Write the item's user data. int len; const void *pUserData = GetStringUserData( i, &len ); if ( pUserData && len > 0 ) { buf.WriteOneBit( 1 ); if ( IsUserDataFixedSize() ) { // Don't have to send length, it was sent as part of the table definition buf.WriteBits( pUserData, GetUserDataSizeBits() ); } else { buf.WriteUBitLong( len, CNetworkStringTableItem::MAX_USERDATA_BITS ); buf.WriteBits( pUserData, len*8 ); } } else { buf.WriteOneBit( 0 ); } // limit string history to 32 entries if ( history.Count() > 31 ) { history.Remove( 0 ); } // add string to string history StringHistoryEntry she; Q_strncpy( she.string, pEntry, sizeof( she.string ) ); history.AddToTail( she ); entriesUpdated++; lastEntry = i; if ( client && client->IsTracing() ) { int nBits = buf.GetNumBitsWritten() - nStartBit; client->TraceNetworkMsg( nBits, " [%s] %d:%s ", GetTableName(), i, GetString( i ) ); } } // If writing the baseline, and using dictionaries, and less than 90% of strings are in the dictionaries, // then we need to consider rebuild the dictionaries if ( tick_ack == -1 && count > 20 && bEncodeUsingDictionaries && nDictionaryCount < 0.9f * count ) { if ( IsGameConsole() ) { Warning( "String Table dictionary for %s should be rebuilt, only found %d of %d strings in dictionary\n", GetTableName(), nDictionaryCount, count ); } } return entriesUpdated; } //----------------------------------------------------------------------------- // Purpose: Parse string update //----------------------------------------------------------------------------- void CNetworkStringTable::ParseUpdate( bf_read &buf, int entries ) { int lastEntry = -1; int lastDictionaryIndex = -1; int nDictionaryEncodeBits = g_StringTableDictionary.GetEncodeBits(); // bool bUseDictionaries = IsUsingDictionary(); bool bEncodeUsingDictionaries = buf.ReadOneBit() ? true : false; CUtlVector< StringHistoryEntry > history; for (int i=0; i= GetMaxStrings() ) { Host_Error( "Server sent bogus string index %i for table %s\n", entryIndex, GetTableName() ); } const char *pEntry = NULL; char entry[ 1024 ]; char substr[ 1024 ]; if ( buf.ReadOneBit() ) { // It's using dictionary if ( bEncodeUsingDictionaries && buf.ReadOneBit() ) { // It's sequential, no need to encode full dictionary index if ( buf.ReadOneBit() ) { lastDictionaryIndex++; } else { lastDictionaryIndex = buf.ReadUBitLong( nDictionaryEncodeBits ); } char const *lookup = g_StringTableDictionary.Lookup( lastDictionaryIndex ); Q_strncpy( entry, lookup, sizeof( entry ) ); } else { bool substringcheck = buf.ReadOneBit() ? true : false; if ( substringcheck ) { int index = buf.ReadUBitLong( 5 ); int bytestocopy = buf.ReadUBitLong( SUBSTRING_BITS ); Q_strncpy( entry, history[ index ].string, bytestocopy + 1 ); buf.ReadString( substr, sizeof(substr) ); Q_strncat( entry, substr, sizeof(entry), COPY_ALL_CHARACTERS ); } else { buf.ReadString( entry, sizeof( entry ) ); } } pEntry = entry; } // Read in the user data. unsigned char tempbuf[ CNetworkStringTableItem::MAX_USERDATA_SIZE ]; memset( tempbuf, 0, sizeof( tempbuf ) ); const void *pUserData = NULL; int nBytes = 0; if ( buf.ReadOneBit() ) { if ( IsUserDataFixedSize() ) { // Don't need to read length, it's fixed length and the length was networked down already. nBytes = GetUserDataSize(); Assert( nBytes > 0 ); tempbuf[nBytes-1] = 0; // be safe, clear last byte buf.ReadBits( tempbuf, GetUserDataSizeBits() ); } else { nBytes = buf.ReadUBitLong( CNetworkStringTableItem::MAX_USERDATA_BITS ); ErrorIfNot( nBytes <= sizeof( tempbuf ), ("CNetworkStringTableClient::ParseUpdate: message too large (%d bytes).", nBytes) ); buf.ReadBytes( tempbuf, nBytes ); } pUserData = tempbuf; } // Check if we are updating an old entry or adding a new one if ( entryIndex < GetNumStrings() ) { SetStringUserData( entryIndex, nBytes, pUserData ); #ifdef _DEBUG if ( pEntry ) { Assert( !Q_strcmp( pEntry, GetString( entryIndex ) ) ); // make sure string didn't change } #endif pEntry = GetString( entryIndex ); // string didn't change } else { // Grow the table (entryindex must be the next empty slot) Assert( (entryIndex == GetNumStrings()) && (pEntry != NULL) ); if ( pEntry == NULL ) { Msg("CNetworkStringTable::ParseUpdate: NULL pEntry, table %s, index %i\n", GetTableName(), entryIndex ); pEntry = "";// avoid crash because of NULL strings } AddString( true, pEntry, nBytes, pUserData ); } if ( history.Count() > 31 ) { history.Remove( 0 ); } StringHistoryEntry she; Q_strncpy( she.string, pEntry, sizeof( she.string ) ); history.AddToTail( she ); } } void CNetworkStringTable::CopyStringTable(CNetworkStringTable * table) { Assert (m_pItems->Count() == 0); // table must be empty before coping for ( unsigned int i = 0; i < table->m_pItems->Count() ; ++i ) { CNetworkStringTableItem *item = &table->m_pItems->Element( i ); m_nTickCount = item->m_nTickChanged; AddString( true, table->GetString( i ), item->m_nUserDataLength, item->m_pUserData ); } } #endif void CNetworkStringTable::TriggerCallbacks( int tick_ack ) { if ( m_changeFunc == NULL ) return; COM_TimestampedLog( "Change(%s):Start", GetTableName() ); int count = m_pItems->Count(); for ( int i = 0; i < count; i++ ) { CNetworkStringTableItem *pItem = &m_pItems->Element( i ); // mirror is up to date if ( pItem->GetTickChanged() <= tick_ack ) continue; int userDataSize; const void *pUserData = pItem->GetUserData( &userDataSize ); // fire the callback function ( *m_changeFunc )( m_pObject, this, i, GetString( i ), pUserData ); } COM_TimestampedLog( "Change(%s):End", GetTableName() ); } //----------------------------------------------------------------------------- // Purpose: // Input : changeFunc - //----------------------------------------------------------------------------- void CNetworkStringTable::SetStringChangedCallback( void *object, pfnStringChanged changeFunc ) { m_changeFunc = changeFunc; m_pObject = object; } //----------------------------------------------------------------------------- // Purpose: // Input : *client - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNetworkStringTable::ChangedSinceTick( int tick ) const { return ( m_nLastChangedTick > tick ); } //----------------------------------------------------------------------------- // Purpose: // Input : *value - // Output : int //----------------------------------------------------------------------------- int CNetworkStringTable::AddString( bool bIsServer, const char *string, int length /*= -1*/, const void *userdata /*= NULL*/ ) { bool bHasChanged; CNetworkStringTableItem *item; if ( !string ) { Assert( string ); ConMsg( "Warning: Can't add NULL string to table %s\n", GetTableName() ); return INVALID_STRING_INDEX; } #ifdef _DEBUG if ( m_bLocked ) { DevMsg("Warning! CNetworkStringTable::AddString: adding '%s' while locked.\n", string ); } #endif int i = m_pItems->Find( string ); if ( !bIsServer ) { if ( m_pItems->IsValidIndex( i ) && !m_pItemsClientSide ) { bIsServer = true; } else if ( !m_pItemsClientSide ) { // NOTE: YWB 9/11/2008 // This case is probably a bug the since the server "owns" the state of the string table and if the client adds // some extra value in and then the server comes along and uses that slot, then all hell breaks loose (probably). // SetAllowClientSideAddString was added to allow for client side only precaching to be in a separate chunk of the table -- it should be used in this case. // TODO: Make this a Sys_Error? DevMsg( "CNetworkStringTable::AddString: client added string which server didn't put into table (consider SetAllowClientSideAddString?): %s %s\n", GetTableName(), string ); } } if ( !bIsServer && m_pItemsClientSide ) { i = m_pItemsClientSide->Find( string ); if ( !m_pItemsClientSide->IsValidIndex( i ) ) { // not in list yet, create it now if ( m_pItemsClientSide->Count() >= (unsigned int)GetMaxStrings() ) { // Too many strings, FIXME: Print warning message ConMsg( "Warning: Table %s is full, can't add %s\n", GetTableName(), string ); return INVALID_STRING_INDEX; } // create new item { MEM_ALLOC_CREDIT(); i = m_pItemsClientSide->Insert( string ); } item = &m_pItemsClientSide->Element( i ); // set changed ticks item->m_nTickChanged = m_nTickCount; #ifndef SHARED_NET_STRING_TABLES item->m_nTickCreated = m_nTickCount; if ( m_bChangeHistoryEnabled ) { item->EnableChangeHistory(); } #endif bHasChanged = true; } else { item = &m_pItemsClientSide->Element( i ); // item already exists bHasChanged = false; // not changed yet } if ( length > -1 ) { if ( item->SetUserData( m_nTickCount, length, userdata ) ) { bHasChanged = true; } } if ( bHasChanged && !m_bChangeHistoryEnabled ) { DataChanged( -i, item ); } // Negate i for returning to client i = -i; } else { // See if it's already there if ( !m_pItems->IsValidIndex( i ) ) { // not in list yet, create it now if ( m_pItems->Count() >= (unsigned int)GetMaxStrings() ) { // Too many strings, FIXME: Print warning message ConMsg( "Warning: Table %s is full, can't add %s\n", GetTableName(), string ); return INVALID_STRING_INDEX; } // create new item { MEM_ALLOC_CREDIT(); i = m_pItems->Insert( string ); } item = &m_pItems->Element( i ); // set changed ticks item->m_nTickChanged = m_nTickCount; #ifndef SHARED_NET_STRING_TABLES item->m_nTickCreated = m_nTickCount; if ( m_bChangeHistoryEnabled ) { item->EnableChangeHistory(); } #endif bHasChanged = true; } else { item = &m_pItems->Element( i ); // item already exists bHasChanged = false; // not changed yet } if ( length > -1 ) { if ( item->SetUserData( m_nTickCount, length, userdata ) ) { bHasChanged = true; } } if ( bHasChanged && !m_bChangeHistoryEnabled ) { DataChanged( i, item ); } } return i; } //----------------------------------------------------------------------------- // Purpose: // Input : stringNumber - // Output : const char //----------------------------------------------------------------------------- const char *CNetworkStringTable::GetString( int stringNumber ) const { INetworkStringDict *dict = m_pItems; if ( m_pItemsClientSide && stringNumber < -1 ) { dict = m_pItemsClientSide; stringNumber = -stringNumber; } Assert( dict->IsValidIndex( stringNumber ) ); if ( dict->IsValidIndex( stringNumber ) ) { return dict->String( stringNumber ); } return NULL; } //----------------------------------------------------------------------------- // Purpose: // Input : stringNumber - // length - // *userdata - //----------------------------------------------------------------------------- void CNetworkStringTable::SetStringUserData( int stringNumber, int length /*=0*/, const void *userdata /*= 0*/ ) { #ifdef _DEBUG if ( m_bLocked ) { DevMsg("Warning! CNetworkStringTable::SetStringUserData: changing entry %i while locked.\n", stringNumber ); } #endif INetworkStringDict *dict = m_pItems; int saveStringNumber = stringNumber; if ( m_pItemsClientSide && stringNumber < -1 ) { dict = m_pItemsClientSide; stringNumber = -stringNumber; } Assert( (length == 0 && userdata == NULL) || ( length > 0 && userdata != NULL) ); Assert( dict->IsValidIndex( stringNumber ) ); CNetworkStringTableItem *p = &dict->Element( stringNumber ); Assert( p ); if ( p->SetUserData( m_nTickCount, length, userdata ) ) { // Mark changed DataChanged( saveStringNumber, p ); } } //----------------------------------------------------------------------------- // Purpose: // Input : *item - //----------------------------------------------------------------------------- void CNetworkStringTable::DataChanged( int stringNumber, CNetworkStringTableItem *item ) { Assert( item ); if ( !item ) return; // Mark table as changed m_nLastChangedTick = m_nTickCount; // Invoke callback if one was installed #ifndef SHARED_NET_STRING_TABLES // but not if client & server share the same containers, we trigger that later if ( m_changeFunc != NULL ) { int userDataSize; const void *pUserData = item->GetUserData( &userDataSize ); ( *m_changeFunc )( m_pObject, this, stringNumber, GetString( stringNumber ), pUserData ); } #endif } #ifndef SHARED_NET_STRING_TABLES void CNetworkStringTable::WriteStringTable( bf_write& buf ) { int numstrings = m_pItems->Count(); buf.WriteWord( numstrings ); for ( int i = 0 ; i < numstrings; i++ ) { buf.WriteString( GetString( i ) ); int userDataSize; const void *pUserData = GetStringUserData( i, &userDataSize ); if ( userDataSize > 0 ) { buf.WriteOneBit( 1 ); buf.WriteWord( (short)userDataSize ); buf.WriteBytes( pUserData, userDataSize ); } else { buf.WriteOneBit( 0 ); } } if ( m_pItemsClientSide ) { buf.WriteOneBit( 1 ); numstrings = m_pItemsClientSide->Count(); buf.WriteWord( numstrings ); for ( int i = 0 ; i < numstrings; i++ ) { buf.WriteString( m_pItemsClientSide->String( i ) ); int userDataSize; const void *pUserData = m_pItemsClientSide->Element( i ).GetUserData( &userDataSize ); if ( userDataSize > 0 ) { buf.WriteOneBit( 1 ); buf.WriteWord( (short)userDataSize ); buf.WriteBytes( pUserData, userDataSize ); } else { buf.WriteOneBit( 0 ); } } } else { buf.WriteOneBit( 0 ); } } bool CNetworkStringTable::ReadStringTable( bf_read& buf ) { DeleteAllStrings(); int numstrings = buf.ReadWord(); for ( int i = 0 ; i < numstrings; i++ ) { char stringname[4096]; buf.ReadString( stringname, sizeof( stringname ) ); Assert( V_strlen( stringname ) < 100 ); if ( buf.ReadOneBit() == 1 ) { int userDataSize = (int)buf.ReadWord(); Assert( userDataSize > 0 ); byte *data = new byte[ userDataSize + 4 ]; Assert( data ); buf.ReadBytes( data, userDataSize ); AddString( true, stringname, userDataSize, data ); delete[] data; Assert( buf.GetNumBytesLeft() > 10 ); } else { AddString( true, stringname ); } } // Client side stuff if ( buf.ReadOneBit() == 1 ) { int numstrings = buf.ReadWord(); for ( int i = 0 ; i < numstrings; i++ ) { char stringname[4096]; buf.ReadString( stringname, sizeof( stringname ) ); if ( buf.ReadOneBit() == 1 ) { int userDataSize = (int)buf.ReadWord(); Assert( userDataSize > 0 ); byte *data = new byte[ userDataSize + 4 ]; Assert( data ); buf.ReadBytes( data, userDataSize ); if ( i >= 2 ) { AddString( false, stringname, userDataSize, data ); } delete[] data; } else { if ( i >= 2 ) { AddString( false, stringname ); } } } } return true; } #endif //----------------------------------------------------------------------------- // Purpose: // Input : stringNumber - // length - // Output : const void //----------------------------------------------------------------------------- const void *CNetworkStringTable::GetStringUserData( int stringNumber, int *length ) const { INetworkStringDict *dict = m_pItems; if ( m_pItemsClientSide && stringNumber < -1 ) { dict = m_pItemsClientSide; stringNumber = -stringNumber; } CNetworkStringTableItem *p; Assert( dict->IsValidIndex( stringNumber ) ); p = &dict->Element( stringNumber ); Assert( p ); return p->GetUserData( length ); } //----------------------------------------------------------------------------- // Purpose: // Output : int //----------------------------------------------------------------------------- int CNetworkStringTable::GetNumStrings( void ) const { return m_pItems->Count(); } //----------------------------------------------------------------------------- // Purpose: // Input : stringTable - // *string - // Output : int //----------------------------------------------------------------------------- int CNetworkStringTable::FindStringIndex( char const *string ) { int i = m_pItems->Find( string ); if ( m_pItems->IsValidIndex( i ) ) return i; if ( m_pItemsClientSide ) { i = m_pItemsClientSide->Find( string ); if ( m_pItemsClientSide->IsValidIndex( i ) ) return -i; } return INVALID_STRING_INDEX; } void CNetworkStringTable::UpdateDictionaryString( int stringNumber ) { if ( !IsUsingDictionary() ) { return; } // Client side only items don't need to deal with dictionary if ( m_pItemsClientSide && stringNumber < -1 ) { return; } CheckDictionary( stringNumber ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTable::Dump( void ) { ConMsg( "Table %s\n", GetTableName() ); ConMsg( " %i/%i items\n", GetNumStrings(), GetMaxStrings() ); for ( int i = 0; i < GetNumStrings() ; i++ ) { if ( IsUsingDictionary() ) { int nCurrentDictionaryIndex = m_pItems->DictionaryIndex( i ); if ( nCurrentDictionaryIndex != -1 ) { ConMsg( "d(%05d) %i : %s\n", nCurrentDictionaryIndex, i, m_pItems->String( i ) ); } else { ConMsg( " %i : %s\n", i, m_pItems->String( i ) ); } } else { ConMsg( " %i : %s\n", i, m_pItems->String( i ) ); } } if ( m_pItemsClientSide ) { for ( int i = 0; i < (int)m_pItemsClientSide->Count() ; i++ ) { ConMsg( " (c)%i : %s\n", i, m_pItemsClientSide->String( i ) ); } } ConMsg( "\n" ); } #ifndef SHARED_NET_STRING_TABLES static ConVar sv_temp_baseline_string_table_buffer_size( "sv_temp_baseline_string_table_buffer_size", "131072", 0, "Buffer size for writing string table baselines" ); bool CNetworkStringTable::WriteBaselines( CSVCMsg_CreateStringTable_t &msg ) { msg.Clear(); // allocate the temp buffer for the packet ents msg.mutable_string_data()->resize( sv_temp_baseline_string_table_buffer_size.GetInt() ); bf_write string_data_buf( &(*msg.mutable_string_data())[0], msg.string_data().size() ); msg.set_flags( m_nFlags ); msg.set_name( GetTableName() ); msg.set_max_entries( GetMaxStrings() ); msg.set_num_entries( GetNumStrings() ); msg.set_user_data_fixed_size( IsUserDataFixedSize() ); msg.set_user_data_size( GetUserDataSize() ); msg.set_user_data_size_bits( GetUserDataSizeBits() ); // tick = -1 ensures that all entries are updated = baseline int entries = WriteUpdate( NULL, string_data_buf, -1 ); // resize the buffer to the actual byte size msg.mutable_string_data()->resize( Bits2Bytes( string_data_buf.GetNumBitsWritten() ) ); return entries == msg.num_entries(); } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CNetworkStringTableContainer::CNetworkStringTableContainer( void ) { m_bAllowCreation = false; m_bLocked = true; m_nTickCount = 0; m_bEnableRollback = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CNetworkStringTableContainer::~CNetworkStringTableContainer( void ) { RemoveAllTables(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTableContainer::AllowCreation( bool state ) { m_bAllowCreation = state; } bool CNetworkStringTableContainer::Lock( bool bLock ) { bool oldLock = m_bLocked; m_bLocked = bLock; // Determine if an update is needed for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); table->Lock( bLock ); } return oldLock; } void CNetworkStringTableContainer::SetAllowClientSideAddString( INetworkStringTable *table, bool bAllowClientSideAddString ) { for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *t = (CNetworkStringTable*) GetTable( i ); if ( t == table ) { t->SetAllowClientSideAddString( bAllowClientSideAddString ); return; } } } //----------------------------------------------------------------------------- // Purpose: // Input : *tableName - // maxentries - // Output : TABLEID //----------------------------------------------------------------------------- INetworkStringTable *CNetworkStringTableContainer::CreateStringTable( const char *tableName, int maxentries, int userdatafixedsize /*= 0*/, int userdatanetworkbits /*= 0*/, int flags /*= NSF_NONE*/ ) { if ( !m_bAllowCreation ) { Sys_Error( "Tried to create string table '%s' at wrong time\n", tableName ); return NULL; } CNetworkStringTable *pTable = (CNetworkStringTable*) FindTable( tableName ); if ( pTable != NULL ) { Sys_Error( "Tried to create string table '%s' twice\n", tableName ); return NULL; } if ( m_Tables.Count() >= MAX_TABLES ) { Sys_Error( "Only %i string tables allowed, can't create'%s'", MAX_TABLES, tableName); return NULL; } TABLEID id = m_Tables.Count(); pTable = new CNetworkStringTable( id, tableName, maxentries, userdatafixedsize, userdatanetworkbits, flags ); Assert( pTable ); #ifndef SHARED_NET_STRING_TABLES if ( m_bEnableRollback ) { pTable->EnableRollback(); } #endif pTable->SetTick( m_nTickCount ); m_Tables.AddToTail( pTable ); return pTable; } //----------------------------------------------------------------------------- // Purpose: // Input : *tableName - //----------------------------------------------------------------------------- INetworkStringTable *CNetworkStringTableContainer::FindTable( const char *tableName ) const { for ( int i = 0; i < m_Tables.Count(); i++ ) { if ( !Q_stricmp( tableName, m_Tables[ i ]->GetTableName() ) ) return m_Tables[i]; } return NULL; } //----------------------------------------------------------------------------- // Purpose: // Input : stringTable - // Output : CNetworkStringTableServer //----------------------------------------------------------------------------- INetworkStringTable *CNetworkStringTableContainer::GetTable( TABLEID stringTable ) const { if ( stringTable < 0 || stringTable >= m_Tables.Count() ) return NULL; return m_Tables[ stringTable ]; } int CNetworkStringTableContainer::GetNumTables( void ) const { return m_Tables.Count(); } #ifndef SHARED_NET_STRING_TABLES //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTableContainer::WriteBaselines( char const *pchMapName, bf_write &buf ) { if ( g_StringTableDictionary.ShouldRecreateDictionary( pchMapName ) ) { // Creates dictionary, will write it out after level is exited CreateDictionary( pchMapName ); } CSVCMsg_CreateStringTable_t msg; for ( int i = 0 ; i < m_Tables.Count() ; i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); int before = buf.GetNumBytesWritten(); if ( !table->WriteBaselines( msg ) ) { Host_Error( "Index error writing string table baseline %s\n", table->GetTableName() ); } if ( !msg.WriteToBuffer( buf ) ) { Host_Error( "Overflow error writing string table baseline %s\n", table->GetTableName() ); } int after = buf.GetNumBytesWritten(); if ( sv_dumpstringtables.GetBool() ) { DevMsg( "CNetworkStringTableContainer::WriteBaselines wrote %d bytes for table %s [space remaining %d bytes]\n", after - before, table->GetTableName(), buf.GetNumBytesLeft() ); } } } void CNetworkStringTableContainer::WriteStringTables( bf_write& buf ) { int numTables = m_Tables.Count(); buf.WriteByte( numTables ); for ( int i = 0; i < numTables; i++ ) { CNetworkStringTable *table = m_Tables[ i ]; buf.WriteString( table->GetTableName() ); table->WriteStringTable( buf ); } } bool CNetworkStringTableContainer::ReadStringTables( bf_read& buf ) { int numTables = buf.ReadByte(); for ( int i = 0 ; i < numTables; i++ ) { char tablename[ 256 ]; buf.ReadString( tablename, sizeof( tablename ) ); // Find this table by name CNetworkStringTable *table = (CNetworkStringTable*)FindTable( tablename ); Assert( table ); // Now read the data for the table if ( !table->ReadStringTable( buf ) ) { Host_Error( "Error reading string table %s\n", tablename ); } } return true; } //----------------------------------------------------------------------------- // Purpose: // Input : *cl - // *msg - //----------------------------------------------------------------------------- void CNetworkStringTableContainer::WriteUpdateMessage( CBaseClient *client, int tick_ack, bf_write &buf ) { VPROF_BUDGET( "CNetworkStringTableContainer::WriteUpdateMessage", VPROF_BUDGETGROUP_OTHER_NETWORKING ); //a working buffer to build our string tables into. Note that this is on the stack, so sanity check that the size doesn't get too large (hence the compile assert below). //If it does, a heap allocated solution will be needed uint8 StringTableBuff[ NET_MAX_PAYLOAD ]; COMPILE_TIME_ASSERT( sizeof( StringTableBuff ) < 300 * 1024 ); // Determine if an update is needed for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); if ( !table ) continue; if ( !table->ChangedSinceTick( tick_ack ) ) continue; CSVCMsg_UpdateStringTable_t msg; //setup a writer for the bits that go to our temporary buffer so we can assign it over later bf_write string_data_buf( StringTableBuff, sizeof( StringTableBuff ) ); msg.set_table_id( table->GetTableId() ); msg.set_num_changed_entries( table->WriteUpdate( client, string_data_buf, tick_ack ) ); //handle the situation where the data may have been truncated if( string_data_buf.IsOverflowed() ) return; Assert( msg.num_changed_entries() > 0 ); // don't send unnecessary empty updates //copy over the data we wrote into the actual message msg.mutable_string_data()->assign( StringTableBuff, StringTableBuff + Bits2Bytes( string_data_buf.GetNumBitsWritten() ) ); if ( !msg.WriteToBuffer( buf ) ) return; if ( client && client->IsTracing() ) { client->TraceNetworkData( buf, "StringTable %s", table->GetTableName() ); } } } //----------------------------------------------------------------------------- // Purpose: // Input : *cl - // *msg - //----------------------------------------------------------------------------- void CNetworkStringTableContainer::DirectUpdate( int tick_ack ) { VPROF_BUDGET( "CNetworkStringTableContainer::DirectUpdate", VPROF_BUDGETGROUP_OTHER_NETWORKING ); // Determine if an update is needed for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); Assert( table ); if ( !table->ChangedSinceTick( tick_ack ) ) continue; table->UpdateMirrorTable( tick_ack ); } } void CNetworkStringTableContainer::EnableRollback( bool bState ) { // we can't dis/enable rollback if we already created tabled Assert( m_Tables.Count() == 0 ); m_bEnableRollback = bState; } void CNetworkStringTableContainer::RestoreTick( int tick ) { for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); Assert( table ); table->RestoreTick( tick ); } } #endif void CNetworkStringTableContainer::TriggerCallbacks( int tick_ack ) { // Determine if an update is needed for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); Assert( table ); if ( !table->ChangedSinceTick( tick_ack ) ) continue; table->TriggerCallbacks( tick_ack ); } } void CNetworkStringTableContainer::SetTick( int tick_count) { // Assert( tick_count > 0 ); m_nTickCount = tick_count; // Determine if an update is needed for ( int i = 0; i < m_Tables.Count(); i++ ) { CNetworkStringTable *table = (CNetworkStringTable*) GetTable( i ); Assert( table ); table->SetTick( tick_count ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTableContainer::RemoveAllTables( void ) { while ( m_Tables.Count() > 0 ) { CNetworkStringTable *table = m_Tables[ 0 ]; m_Tables.Remove( 0 ); delete table; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNetworkStringTableContainer::Dump( void ) { for ( int i = 0; i < m_Tables.Count(); i++ ) { m_Tables[ i ]->Dump(); } } void CNetworkStringTableContainer::CreateDictionary( char const *pchMapName ) { // Don't do this on Game Consoles!!! if ( IsGameConsole() ) { Warning( "Map %s missing GameConsole stringtable dictionary!!!\n", pchMapName ); return; } char mapPath[ MAX_PATH ]; Q_snprintf( mapPath, sizeof( mapPath ), "maps/%s.bsp", pchMapName ); // Make sure that the file is writable before building stringtable dictionary. if( !g_pFileSystem->IsFileWritable( mapPath, "GAME" ) ) { Warning( "#####################################################################################\n" ); Warning( "Can't recreate dictionary for %s, file must be writable!!!\n", mapPath ); Warning( "#####################################################################################\n" ); return; } Msg( "Creating dictionary %s\n", pchMapName ); // Create dictionary CUtlBuffer buf; for ( int i = 0; i < m_Tables.Count(); ++i ) { CNetworkStringTable *table = m_Tables[ i ]; if ( !table->IsUsingDictionary() ) continue; int nNumStrings = table->GetNumStrings(); for ( int j = 0; j < nNumStrings; ++j ) { char const *str = table->GetString( j ); // Skip empty strings (slot 0 is sometimes encoded as "") if ( !*str ) continue; buf.PutString( str ); } } g_StringTableDictionary.CacheNewStringTableForWriteToBSPOnLevelShutdown( pchMapName, buf, MapReslistGenerator().IsCreatingForXbox() ); } void CNetworkStringTableContainer::UpdateDictionaryStrings() { for ( int i = 0; i < m_Tables.Count(); ++i ) { CNetworkStringTable *table = m_Tables[ i ]; if ( !table->IsUsingDictionary() ) continue; int nNumStrings = table->GetNumStrings(); for ( int j = 0; j < nNumStrings; ++j ) { table->UpdateDictionaryString( j ); } } }