diff options
Diffstat (limited to 'game/client/hud_closecaption.cpp')
| -rw-r--r-- | game/client/hud_closecaption.cpp | 2912 |
1 files changed, 2912 insertions, 0 deletions
diff --git a/game/client/hud_closecaption.cpp b/game/client/hud_closecaption.cpp new file mode 100644 index 0000000..b0598f2 --- /dev/null +++ b/game/client/hud_closecaption.cpp @@ -0,0 +1,2912 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" +#include <ctype.h> +#include "sentence.h" +#include "hud_closecaption.h" +#include "tier1/strtools.h" +#include <vgui_controls/Controls.h> +#include <vgui/IVGui.h> +#include <vgui/ISurface.h> +#include <vgui/IScheme.h> +#include <vgui/ILocalize.h> +#include "iclientmode.h" +#include "hud_macros.h" +#include "checksum_crc.h" +#include "filesystem.h" +#include "datacache/idatacache.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define CC_INSET 12 + +extern ISoundEmitterSystemBase *soundemitterbase; + +// Marked as FCVAR_USERINFO so that the server can cull CC messages before networking them down to us!!! +ConVar closecaption( "closecaption", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX | FCVAR_USERINFO, "Enable close captioning." ); +extern ConVar cc_lang; +static ConVar cc_linger_time( "cc_linger_time", "1.0", FCVAR_ARCHIVE, "Close caption linger time." ); +static ConVar cc_predisplay_time( "cc_predisplay_time", "0.25", FCVAR_ARCHIVE, "Close caption delay before showing caption." ); +static ConVar cc_captiontrace( "cc_captiontrace", "1", 0, "Show missing closecaptions (0 = no, 1 = devconsole, 2 = show in hud)" ); +static ConVar cc_subtitles( "cc_subtitles", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "If set, don't show sound effect captions, just voice overs (i.e., won't help hearing impaired players)." ); +ConVar english( "english", "1", FCVAR_USERINFO, "If set to 1, running the english language set of assets." ); +static ConVar cc_smallfontlength( "cc_smallfontlength", "300", 0, "If text stream is this long, force usage of small font size." ); + +#define MAX_CAPTION_CHARACTERS 4096 + +#define CAPTION_PAN_FADE_TIME 0.5 // The time it takes for a line to fade while panning over a large entry +#define CAPTION_PAN_SLIDE_TIME 0.5 // The time it takes for a line to slide on while panning over a large entry + + +// A work unit is a pre-processed chunk of CC text to display +// Any state changes (font/color/etc) cause a new work unit to be precomputed +// Moving onto a new line also causes a new Work Unit +// The width and height are stored so that layout can be quickly recomputed each frame +class CCloseCaptionWorkUnit +{ +public: + CCloseCaptionWorkUnit(); + ~CCloseCaptionWorkUnit(); + + void SetWidth( int w ); + int GetWidth() const; + + void SetHeight( int h ); + int GetHeight() const; + + void SetPos( int x, int y ); + void GetPos( int& x, int &y ) const; + + void SetFadeStart( float flTime ); + float GetFadeStart( void ) const; + + void SetBold( bool bold ); + bool GetBold() const; + + void SetItalic( bool ital ); + bool GetItalic() const; + + void SetStream( const wchar_t *stream ); + const wchar_t *GetStream() const; + + void SetColor( Color& clr ); + Color GetColor() const; + + vgui::HFont GetFont() const + { + return m_hFont; + } + + void SetFont( vgui::HFont fnt ) + { + m_hFont = fnt; + } + + void Dump() + { + char buf[ 2048 ]; + g_pVGuiLocalize->ConvertUnicodeToANSI( GetStream(), buf, sizeof( buf ) ); + + Msg( "x = %i, y = %i, w = %i h = %i text %s\n", m_nX, m_nY, m_nWidth, m_nHeight, buf ); + } + +private: + + int m_nX; + int m_nY; + int m_nWidth; + int m_nHeight; + float m_flFadeStartTime; + + bool m_bBold; + bool m_bItalic; + wchar_t *m_pszStream; + vgui::HFont m_hFont; + Color m_Color; +}; + +CCloseCaptionWorkUnit::CCloseCaptionWorkUnit() : + m_nWidth(0), + m_nHeight(0), + m_bBold(false), + m_bItalic(false), + m_pszStream(0), + m_Color( Color( 255, 255, 255, 255 ) ), + m_hFont( 0 ), + m_flFadeStartTime(0) +{ +} + +CCloseCaptionWorkUnit::~CCloseCaptionWorkUnit() +{ + delete[] m_pszStream; + m_pszStream = NULL; +} + +void CCloseCaptionWorkUnit::SetWidth( int w ) +{ + m_nWidth = w; +} + +int CCloseCaptionWorkUnit::GetWidth() const +{ + return m_nWidth; +} + +void CCloseCaptionWorkUnit::SetHeight( int h ) +{ + m_nHeight = h; +} + +int CCloseCaptionWorkUnit::GetHeight() const +{ + return m_nHeight; +} + +void CCloseCaptionWorkUnit::SetPos( int x, int y ) +{ + m_nX = x; + m_nY = y; +} + +void CCloseCaptionWorkUnit::GetPos( int& x, int &y ) const +{ + x = m_nX; + y = m_nY; +} + +void CCloseCaptionWorkUnit::SetFadeStart( float flTime ) +{ + m_flFadeStartTime = flTime; +} + +float CCloseCaptionWorkUnit::GetFadeStart( void ) const +{ + return m_flFadeStartTime; +} + +void CCloseCaptionWorkUnit::SetBold( bool bold ) +{ + m_bBold = bold; +} + +bool CCloseCaptionWorkUnit::GetBold() const +{ + return m_bBold; +} + +void CCloseCaptionWorkUnit::SetItalic( bool ital ) +{ + m_bItalic = ital; +} + +bool CCloseCaptionWorkUnit::GetItalic() const +{ + return m_bItalic; +} + +void CCloseCaptionWorkUnit::SetStream( const wchar_t *stream ) +{ + delete[] m_pszStream; + m_pszStream = NULL; + + int len = wcslen( stream ); + Assert( len < 4096 ); + m_pszStream = new wchar_t[ len + 1 ]; + wcsncpy( m_pszStream, stream, len ); + m_pszStream[ len ] = L'\0'; +} + +const wchar_t *CCloseCaptionWorkUnit::GetStream() const +{ + return m_pszStream ? m_pszStream : L""; +} + +void CCloseCaptionWorkUnit::SetColor( Color& clr ) +{ + m_Color = clr; +} + +Color CCloseCaptionWorkUnit::GetColor() const +{ + return m_Color; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CCloseCaptionItem +{ +public: + CCloseCaptionItem( + const wchar_t *stream, + float timetolive, + float addedtime, + float predisplay, + bool valid, + bool fromplayer + ) : + m_flTimeToLive( 0.0f ), + m_flAddedTime( addedtime ), + m_bValid( false ), + m_nTotalWidth( 0 ), + m_nTotalHeight( 0 ), + m_bSizeComputed( false ), + m_bFromPlayer( fromplayer ) + + { + SetStream( stream ); + SetTimeToLive( timetolive ); + SetInitialLifeSpan( timetolive ); + SetPreDisplayTime( cc_predisplay_time.GetFloat() + predisplay ); + m_bValid = valid; + } + + CCloseCaptionItem( const CCloseCaptionItem& src ) + { + SetStream( src.m_szStream ); + m_flTimeToLive = src.m_flTimeToLive; + m_bValid = src.m_bValid; + m_bFromPlayer = src.m_bFromPlayer; + m_flAddedTime = src.m_flAddedTime; + } + + ~CCloseCaptionItem( void ) + { + while ( m_Work.Count() > 0 ) + { + CCloseCaptionWorkUnit *unit = m_Work[ 0 ]; + m_Work.Remove( 0 ); + delete unit; + } + + } + + void SetStream( const wchar_t *stream) + { + wcsncpy( m_szStream, stream, sizeof( m_szStream ) / sizeof( wchar_t ) ); + } + + const wchar_t *GetStream() const + { + return m_szStream; + } + + void SetTimeToLive( float ttl ) + { + m_flTimeToLive = ttl; + } + + float GetTimeToLive( void ) const + { + return m_flTimeToLive; + } + + void SetInitialLifeSpan( float t ) + { + m_flInitialLifeSpan = t; + } + + float GetInitialLifeSpan() const + { + return m_flInitialLifeSpan; + } + + bool IsValid() const + { + return m_bValid; + } + + void SetHeight( int h ) + { + m_nTotalHeight = h; + } + int GetHeight() const + { + return m_nTotalHeight; + } + void SetWidth( int w ) + { + m_nTotalWidth = w; + } + int GetWidth() const + { + return m_nTotalWidth; + } + + void AddWork( CCloseCaptionWorkUnit *unit ) + { + m_Work.AddToTail( unit ); + } + + int GetNumWorkUnits() const + { + return m_Work.Count(); + } + + CCloseCaptionWorkUnit *GetWorkUnit( int index ) + { + Assert( index >= 0 && index < m_Work.Count() ); + + return m_Work[ index ]; + } + + void SetSizeComputed( bool computed ) + { + m_bSizeComputed = computed; + } + + bool GetSizeComputed() const + { + return m_bSizeComputed; + } + + void SetPreDisplayTime( float t ) + { + m_flPreDisplayTime = t; + } + + float GetPreDisplayTime() const + { + return m_flPreDisplayTime; + } + + float GetAlpha( float fadeintimehidden, float fadeintime, float fadeouttime ) + { + float time_since_start = m_flInitialLifeSpan - m_flTimeToLive; + float time_until_end = m_flTimeToLive; + + float totalfadeintime = fadeintimehidden + fadeintime; + + if ( totalfadeintime > 0.001f && + time_since_start < totalfadeintime ) + { + if ( time_since_start >= fadeintimehidden ) + { + float f = 1.0f; + if ( fadeintime > 0.001f ) + { + f = ( time_since_start - fadeintimehidden ) / fadeintime; + } + f = clamp( f, 0.0f, 1.0f ); + return f; + } + + return 0.0f; + } + + if ( fadeouttime > 0.001f && + time_until_end < fadeouttime ) + { + float f = time_until_end / fadeouttime; + f = clamp( f, 0.0f, 1.0f ); + return f; + } + + return 1.0f; + } + + float GetAddedTime() const + { + return m_flAddedTime; + } + + void SetAddedTime( float addt ) + { + m_flAddedTime = addt; + } + + bool IsFromPlayer() const + { + return m_bFromPlayer; + } + +private: + wchar_t m_szStream[ MAX_CAPTION_CHARACTERS ]; + + float m_flPreDisplayTime; + float m_flTimeToLive; + float m_flInitialLifeSpan; + float m_flAddedTime; + bool m_bValid; + int m_nTotalWidth; + int m_nTotalHeight; + + bool m_bSizeComputed; + bool m_bFromPlayer; + + CUtlVector< CCloseCaptionWorkUnit * > m_Work; + +}; + +struct VisibleStreamItem +{ + int height; + int width; + CCloseCaptionItem *item; +}; + +//----------------------------------------------------------------------------- +// Purpose: The only resource manager parameter we currently care about is the name +// of the .vcd to cache into memory +//----------------------------------------------------------------------------- +struct asynccaptionparams_t +{ + const char *dbfile; + int fileindex; + int blocktoload; + int blockoffset; + int blocksize; +}; + +// 16K of cache for close caption data +#define MAX_ASYNCCAPTION_MEMORY_CACHE (int)( 64.0 * 1024.0f ) + +void CaptionAsyncLoaderCallback( const FileAsyncRequest_t &request, int numReadBytes, FSAsyncStatus_t asyncStatus ); + +struct AsyncCaptionData_t +{ + int m_nBlockNum; + byte *m_pBlockData; + int m_nFileIndex; + int m_nBlockSize; + + bool m_bLoadPending : 1; + bool m_bLoadCompleted : 1; + + FSAsyncControl_t m_hAsyncControl; + + AsyncCaptionData_t() : + m_nBlockNum( -1 ), + m_pBlockData( 0 ), + m_nFileIndex( -1 ), + m_nBlockSize( 0 ), + m_bLoadPending( false ), + m_bLoadCompleted( false ), + m_hAsyncControl( NULL ) + { + } + + // APIS required by CDataManager + void DestroyResource() + { + if ( m_bLoadPending && !m_bLoadCompleted ) + { + filesystem->AsyncFinish( m_hAsyncControl, true ); + } + filesystem->AsyncRelease( m_hAsyncControl ); + + WipeData(); + delete this; + } + + void ReleaseData() + { + filesystem->AsyncRelease( m_hAsyncControl ); + m_hAsyncControl = 0; + WipeData(); + m_bLoadCompleted = false; + Assert( !m_bLoadPending ); + } + + void WipeData() + { + delete[] m_pBlockData; + m_pBlockData = NULL; + } + + AsyncCaptionData_t *GetData() + { + return this; + } + unsigned int Size() + { + return sizeof( *this ) + m_nBlockSize; + } + + void AsyncLoad( const char *fileName, int blockOffset ) + { + // Already pending + Assert ( !m_hAsyncControl ); + + // async load the file + FileAsyncRequest_t fileRequest; + fileRequest.pContext = (void *)this; + fileRequest.pfnCallback = ::CaptionAsyncLoaderCallback; + fileRequest.pData = m_pBlockData; + fileRequest.pszFilename = fileName; + fileRequest.nOffset = blockOffset; + fileRequest.flags = 0; + fileRequest.nBytes = m_nBlockSize; + fileRequest.priority = -1; + fileRequest.pszPathID = "GAME"; + + // queue for async load + MEM_ALLOC_CREDIT(); + filesystem->AsyncRead( fileRequest, &m_hAsyncControl ); + } + + // you must implement these static functions for the ResourceManager + // ----------------------------------------------------------- + static AsyncCaptionData_t *CreateResource( const asynccaptionparams_t ¶ms ) + { + AsyncCaptionData_t *data = new AsyncCaptionData_t; + data->m_nBlockNum = params.blocktoload; + data->m_nFileIndex = params.fileindex; + data->m_nBlockSize = params.blocksize; + data->m_pBlockData = new byte[ data->m_nBlockSize ]; + return data; + } + + static unsigned int EstimatedSize( const asynccaptionparams_t ¶ms ) + { + // The block size is assumed to be 4K + return ( sizeof( AsyncCaptionData_t ) + params.blocksize ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: This manages the instanced scene memory handles. We essentially grow a handle list by scene filename where +// the handle is a pointer to a AsyncCaptionData_t defined above. If the resource manager uncaches the handle, we reload the +// .vcd from disk. Precaching a .vcd calls into FindOrAddBlock which moves the .vcd to the head of the LRU if it's in memory +// or it reloads it from disk otherwise. +//----------------------------------------------------------------------------- +class CAsyncCaptionResourceManager : public CAutoGameSystem, public CManagedDataCacheClient< AsyncCaptionData_t, asynccaptionparams_t > +{ +public: + CAsyncCaptionResourceManager() : CAutoGameSystem( "CAsyncCaptionResourceManager" ) + { + } + + void SetDbInfo( const CUtlVector< AsyncCaption_t > & info ) + { + m_Db = info; + } + + virtual bool Init() + { + CCacheClientBaseClass::Init( datacache, "Captions", MAX_ASYNCCAPTION_MEMORY_CACHE ); + return true; + } + virtual void Shutdown() + { + Clear(); + CCacheClientBaseClass::Shutdown(); + } + + //----------------------------------------------------------------------------- + // Purpose: Spew a cache summary to the console + //----------------------------------------------------------------------------- + void SpewMemoryUsage() + { + GetCacheSection()->OutputReport(); + + DataCacheStatus_t status; + DataCacheLimits_t limits; + GetCacheSection()->GetStatus( &status, &limits ); + int bytesUsed = status.nBytes; + int bytesTotal = limits.nMaxBytes; + + float percent = 100.0f * (float)bytesUsed / (float)bytesTotal; + + int count = 0; + for ( int i = 0; i < m_Db.Count(); ++i ) + { + count += m_Db[ i ].m_RequestedBlocks.Count(); + } + + DevMsg( "CAsyncCaptionResourceManager: %i blocks total %s, %.2f %% of capacity\n", count, Q_pretifymem( bytesUsed, 2 ), percent ); + } + + virtual void LevelInitPostEntity() + { + } + + void CaptionAsyncLoaderCallback( const FileAsyncRequest_t &request, int numReadBytes, FSAsyncStatus_t asyncStatus ) + { + // get our preserved data + AsyncCaptionData_t *pData = ( AsyncCaptionData_t * )request.pContext; + + Assert( pData ); + + // mark as completed in single atomic operation + pData->m_bLoadCompleted = true; + } + + int ComputeBlockOffset( int fileIndex, int blockNum ) + { + return m_Db[ fileIndex ].m_Header.dataoffset + blockNum * m_Db[ fileIndex ].m_Header.blocksize; + } + + void GetBlockInfo( int fileIndex, int blockNum, bool& entry, bool& pending, bool& loaded ) + { + pending = false; + loaded = false; + AsyncCaption_t::BlockInfo_t search; + search.fileindex = fileIndex; + search.blocknum = blockNum; + + CUtlRBTree< AsyncCaption_t::BlockInfo_t, unsigned short >& requested = m_Db[ fileIndex ].m_RequestedBlocks; + + int idx = requested.Find( search ); + if ( idx == requested.InvalidIndex() ) + { + entry = false; + return; + } + entry = true; + + DataCacheHandle_t handle = requested[ idx ].handle; + AsyncCaptionData_t *pCaptionData = CacheLock( handle ); + if ( pCaptionData ) + { + if ( pCaptionData->m_bLoadPending ) + { + pending = true; + } + else if ( pCaptionData->m_bLoadCompleted ) + { + loaded = true; + } + CacheUnlock( handle ); + } + } + + // Either commences async loading or polls for async loading once per frame to wait for it to complete... + void PollForAsyncLoading( CHudCloseCaption *hudCloseCaption, int dbFileIndex, int blockNum ) + { + const char *dbname = m_Db[ dbFileIndex ].m_DataBaseFile.String(); + + CUtlRBTree< AsyncCaption_t::BlockInfo_t, unsigned short >& requested = m_Db[ dbFileIndex ].m_RequestedBlocks; + + int idx = FindOrAddBlock( dbFileIndex, blockNum ); + if ( idx == requested.InvalidIndex() ) + { + Assert( 0 ); + return; + } + + DataCacheHandle_t handle = requested[ idx ].handle; + + AsyncCaptionData_t *pCaptionData = CacheLock( handle ); + if ( !pCaptionData ) + { + // Try and reload it + char fn[ 256 ]; + Q_strncpy( fn, dbname, sizeof( fn ) ); + Q_FixSlashes( fn ); + + asynccaptionparams_t params; + params.dbfile = fn; + params.blocktoload = blockNum; + params.blocksize = m_Db[ dbFileIndex ].m_Header.blocksize; + params.blockoffset = ComputeBlockOffset( dbFileIndex, blockNum ); + params.fileindex = dbFileIndex; + + handle = requested[ idx ].handle = CacheCreate( params ); + pCaptionData = CacheLock( handle ); + if ( !pCaptionData ) + { + Assert( pCaptionData ); + return; + } + } + + if ( pCaptionData->m_bLoadCompleted ) + { + pCaptionData->m_bLoadPending = false; + // Copy in data at this point + Assert( hudCloseCaption ); + if ( hudCloseCaption ) + { + hudCloseCaption->OnFinishAsyncLoad( requested[ idx ].fileindex, requested[ idx ].blocknum, pCaptionData ); + } + + // This finalizes the load (unlocks the handle) + GetCacheSection()->BreakLock( handle ); + return; + } + + if ( pCaptionData->m_bLoadPending ) + { + CacheUnlock( handle ); + return; + } + + // Commence load (locks handle for entire async load) (unlocked above) + pCaptionData->m_bLoadPending = true; + pCaptionData->AsyncLoad( dbname, ComputeBlockOffset( dbFileIndex, blockNum ) ); + } + + //----------------------------------------------------------------------------- + // Purpose: Touch the cache or load the scene into the cache for the first time + // Input : *filename - + //----------------------------------------------------------------------------- + int FindOrAddBlock( int dbFileIndex, int blockNum ) + { + const char *dbname = m_Db[ dbFileIndex ].m_DataBaseFile.String(); + + CUtlRBTree< AsyncCaption_t::BlockInfo_t, unsigned short >& requested = m_Db[ dbFileIndex ].m_RequestedBlocks; + + AsyncCaption_t::BlockInfo_t search; + search.blocknum = blockNum; + search.fileindex = dbFileIndex; + + int idx = requested.Find( search ); + if ( idx != requested.InvalidIndex() ) + { + // Move it to head of LRU + CacheTouch( requested[ idx ].handle ); + return idx; + } + + char fn[ 256 ]; + Q_strncpy( fn, dbname, sizeof( fn ) ); + Q_FixSlashes( fn ); + + asynccaptionparams_t params; + params.dbfile = fn; + params.blocktoload = blockNum; + params.blockoffset = ComputeBlockOffset( dbFileIndex, blockNum ); + params.blocksize = m_Db[ dbFileIndex ].m_Header.blocksize; + params.fileindex = dbFileIndex; + + memhandle_t handle = CacheCreate( params ); + + AsyncCaption_t::BlockInfo_t info; + info.fileindex = dbFileIndex; + info.blocknum = blockNum; + info.handle = handle; + + // Add scene filename to dictionary + idx = requested.Insert( info ); + return idx; + } + + void Flush() + { + CacheFlush(); + } + + void Clear() + { + for ( int file = 0; file < m_Db.Count(); ++file ) + { + CUtlRBTree< AsyncCaption_t::BlockInfo_t, unsigned short >& requested = m_Db[ file ].m_RequestedBlocks; + + int c = requested.Count(); + for ( int i = 0; i < c; ++i ) + { + memhandle_t dat = requested[ i ].handle; + CacheRemove( dat ); + } + + requested.RemoveAll(); + } + } + +private: + + CUtlVector< AsyncCaption_t > m_Db; +}; + +CAsyncCaptionResourceManager g_AsyncCaptionResourceManager; + +void CaptionAsyncLoaderCallback( const FileAsyncRequest_t &request, int numReadBytes, FSAsyncStatus_t asyncStatus ) +{ + g_AsyncCaptionResourceManager.CaptionAsyncLoaderCallback( request, numReadBytes, asyncStatus ); +} + +DECLARE_HUDELEMENT( CHudCloseCaption ); + +DECLARE_HUD_MESSAGE( CHudCloseCaption, CloseCaption ); + +CHudCloseCaption::CHudCloseCaption( const char *pElementName ) + : CHudElement( pElementName ), + vgui::Panel( NULL, "HudCloseCaption" ), + m_CloseCaptionRepeats( 0, 0, CaptionTokenLessFunc ), + m_CurrentLanguage( UTL_INVAL_SYMBOL ), + m_bPaintDebugInfo( false ) +{ + vgui::Panel *pParent = g_pClientMode->GetViewport(); + SetParent( pParent ); + + m_nGoalHeight = 0; + m_nCurrentHeight = 0; + m_flGoalAlpha = 1.0f; + m_flCurrentAlpha = 1.0f; + + m_flGoalHeightStartTime = 0; + m_flGoalHeightFinishTime = 0; + + m_bLocked = false; + m_bVisibleDueToDirect = false; + + SetPaintBorderEnabled( false ); + SetPaintBackgroundEnabled( false ); + + vgui::ivgui()->AddTickSignal( GetVPanel(), 0 ); + + if ( !IsX360() ) + { + g_pVGuiLocalize->AddFile( "resource/closecaption_%language%.txt", "GAME", true ); + } + + HOOK_HUD_MESSAGE( CHudCloseCaption, CloseCaption ); + + char uilanguage[ 64 ]; + uilanguage[0] = 0; + engine->GetUILanguage( uilanguage, sizeof( uilanguage ) ); + + if ( !Q_stricmp( uilanguage, "english" ) ) + { + english.SetValue( 1 ); + } + else + { + english.SetValue( 0 ); + } + + char dbfile [ 512 ]; + Q_snprintf( dbfile, sizeof( dbfile ), "resource/closecaption_%s.dat", uilanguage ); + InitCaptionDictionary( dbfile ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CHudCloseCaption::~CHudCloseCaption() +{ + m_CloseCaptionRepeats.RemoveAll(); + + ClearAsyncWork(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *newmap - +//----------------------------------------------------------------------------- +void CHudCloseCaption::LevelInit( void ) +{ + CreateFonts(); + // Reset repeat counters per level + m_CloseCaptionRepeats.RemoveAll(); + + // Wipe any stale pending work items... + ClearAsyncWork(); +} + +static ConVar cc_minvisibleitems( "cc_minvisibleitems", "1", 0, "Minimum number of caption items to show." ); + +void CHudCloseCaption::TogglePaintDebug() +{ + m_bPaintDebugInfo = !m_bPaintDebugInfo; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHudCloseCaption::Paint( void ) +{ + int w, h; + GetSize( w, h ); + + if ( m_bPaintDebugInfo ) + { + int blockWide = 350; + int startx = 50; + + int y = 0; + int size = 8; + int sizewithgap = size + 1; + + for ( int a = 0; a < m_AsyncCaptions.Count(); ++a ) + { + int x = startx; + + int c = m_AsyncCaptions[ a ].m_Header.numblocks; + for ( int i = 0 ; i < c; ++i ) + { + bool entry, pending, loaded; + g_AsyncCaptionResourceManager.GetBlockInfo( a, i, entry, pending, loaded ); + + if ( !entry ) + { + vgui::surface()->DrawSetColor( Color( 0, 0, 0, 127 ) ); + } + else if ( pending ) + { + vgui::surface()->DrawSetColor( Color( 0, 0, 255, 127 ) ); + } + else if ( loaded ) + { + vgui::surface()->DrawSetColor( Color( 0, 255, 0, 127 ) ); + } + else + { + vgui::surface()->DrawSetColor( Color( 255, 255, 0, 127 ) ); + } + + vgui::surface()->DrawFilledRect( x, y, x + size, y + size ); + x += sizewithgap; + if ( x >= startx + blockWide ) + { + x = startx; + y += sizewithgap; + } + } + + y += sizewithgap; + } + } + + wrect_t rcOutput; + rcOutput.left = 0; + rcOutput.right = w; + rcOutput.bottom = h; + rcOutput.top = m_nTopOffset; + + wrect_t rcText = rcOutput; + + int avail_width = rcText.right - rcText.left - 2 * CC_INSET; + int avail_height = rcText.bottom - rcText.top - 2 * CC_INSET; + + int totalheight = 0; + int i; + CUtlVector< VisibleStreamItem > visibleitems; + int c = m_Items.Count(); + int maxwidth = 0; + + for ( i = 0; i < c; i++ ) + { + CCloseCaptionItem *item = m_Items[ i ]; + + // Not ready for display yet. + if ( item->GetPreDisplayTime() > 0.0f ) + { + continue; + } + + if ( !item->GetSizeComputed() ) + { + ComputeStreamWork( avail_width, item ); + } + + int itemwidth = item->GetWidth(); + int itemheight = item->GetHeight(); + + totalheight += itemheight; + if ( itemwidth > maxwidth ) + { + maxwidth = itemwidth; + } + + VisibleStreamItem si; + si.height = itemheight; + si.width = itemwidth; + si.item = item; + + visibleitems.AddToTail( si ); + + // Start popping really old items off the stack if we run out of space + while ( itemheight <= avail_height && + totalheight > avail_height && + visibleitems.Count() > cc_minvisibleitems.GetInt() ) + { + VisibleStreamItem & pop = visibleitems[ 0 ]; + totalheight -= pop.height; + + // And make it die right away... + pop.item->SetTimeToLive( 0.0f ); + + visibleitems.Remove( 0 ); + } + } + + float desiredAlpha = visibleitems.Count() >= 1 ? 1.0f : 0.0f; + + // Always return at least one line height for drawing the surrounding box + totalheight = MAX( totalheight, m_nLineHeight ); + + // Trigger box growing + if ( totalheight != m_nGoalHeight ) + { + m_nGoalHeight = totalheight; + m_flGoalHeightStartTime = gpGlobals->curtime; + m_flGoalHeightFinishTime = gpGlobals->curtime + m_flGrowTime; + } + if ( desiredAlpha != m_flGoalAlpha ) + { + m_flGoalAlpha = desiredAlpha; + m_flGoalHeightStartTime = gpGlobals->curtime; + m_flGoalHeightFinishTime = gpGlobals->curtime + m_flGrowTime; + } + + // If shrunk to zero and faded out, nothing left to do + if ( !visibleitems.Count() && + m_nGoalHeight == m_nCurrentHeight && + m_flGoalAlpha == m_flCurrentAlpha ) + { + m_flGoalHeightStartTime = 0; + m_flGoalHeightFinishTime = 0; + return; + } + + bool growingDown = false; + + // Continue growth? + if ( m_flGoalHeightFinishTime && + m_flGoalHeightStartTime && + m_flGoalHeightFinishTime > m_flGoalHeightStartTime ) + { + float togo = m_nGoalHeight - m_nCurrentHeight; + float alphatogo = m_flGoalAlpha - m_flCurrentAlpha; + + growingDown = togo < 0.0f ? true : false; + + float dt = m_flGoalHeightFinishTime - m_flGoalHeightStartTime; + float frac = ( gpGlobals->curtime - m_flGoalHeightStartTime ) / dt; + frac = clamp( frac, 0.0f, 1.0f ); + int newHeight = m_nCurrentHeight + (int)( frac * togo ); + m_nCurrentHeight = newHeight; + float newAlpha = m_flCurrentAlpha + frac * alphatogo; + m_flCurrentAlpha = clamp( newAlpha, 0.0f, 1.0f ); + } + else + { + m_nCurrentHeight = m_nGoalHeight; + m_flCurrentAlpha = m_flGoalAlpha; + } + + rcText.top = rcText.bottom - m_nCurrentHeight - 2 * CC_INSET; + + Color bgColor = GetBgColor(); + bgColor[3] = m_flBackgroundAlpha; + DrawBox( rcText.left, MAX(rcText.top,0), rcText.right - rcText.left, rcText.bottom - MAX(rcText.top,0), bgColor, m_flCurrentAlpha ); + + if ( !visibleitems.Count() ) + { + return; + } + + rcText.left += CC_INSET; + rcText.right -= CC_INSET; + + int textHeight = m_nCurrentHeight; + if ( growingDown ) + { + // If growing downward, keep the text locked to the bottom of the window instead of anchored to the top + textHeight = totalheight; + } + + rcText.top = rcText.bottom - textHeight - CC_INSET; + + // Now draw them + c = visibleitems.Count(); + for ( i = 0; i < c; i++ ) + { + VisibleStreamItem *si = &visibleitems[ i ]; + + // If the oldest/top item was created with additional time, we can remove that now + if ( i == 0 ) + { + if ( si->item->GetAddedTime() > 0.0f ) + { + float ttl = si->item->GetTimeToLive(); + ttl -= si->item->GetAddedTime(); + ttl = MAX( 0.0f, ttl ); + si->item->SetTimeToLive( ttl ); + si->item->SetAddedTime( 0.0f ); + } + } + + int height = si->height; + CCloseCaptionItem *item = si->item; + + int iFadeLine = -1; + float flFadeLineAlpha = 1.0; + + // If the height is greater than the total height of the element, + // we need to slowly pan over this item. + if ( height > avail_height ) + { + // Figure out how many lines we'll need to move to see the whole caption + int units = item->GetNumWorkUnits(); + int iTotalMove = 0; + for ( int j = 0 ; j < units; j++ ) + { + CCloseCaptionWorkUnit *wu = item->GetWorkUnit( j ); + iTotalMove += wu->GetHeight(); + if ( iTotalMove >= (height - avail_height) ) + { + units = j+1; + break; + } + } + + // Figure out the delta between each point where we move the line + float flMoveDelta = item->GetInitialLifeSpan() / (float)units; + float flCurMove = item->GetInitialLifeSpan() - item->GetTimeToLive(); + int iHeightToMove = 0; + + int iLinesToMove = clamp( Floor2Int( flCurMove / flMoveDelta ), 0, units ); + if ( iLinesToMove ) + { + int iCurrentLineHeight = 0; + for ( int j = 0 ; j < iLinesToMove; j++ ) + { + iHeightToMove = iCurrentLineHeight; + + CCloseCaptionWorkUnit *wu = item->GetWorkUnit( j ); + iCurrentLineHeight += wu->GetHeight(); + } + + // Slide to the desired distance, once the fade is done + float flTimePostMove = flCurMove - (flMoveDelta * iLinesToMove); + if ( flTimePostMove < CAPTION_PAN_FADE_TIME ) + { + iFadeLine = iLinesToMove-1; + + // It's time to fade out the top line. If it hasn't started fading yet, start it. + CCloseCaptionWorkUnit *wu = item->GetWorkUnit(iFadeLine); + if ( wu->GetFadeStart() == 0 ) + { + wu->SetFadeStart( gpGlobals->curtime ); + } + + // Fade out quickly + float flFadeTime = (gpGlobals->curtime - wu->GetFadeStart()) / CAPTION_PAN_FADE_TIME; + flFadeLineAlpha = clamp( 1.0f - flFadeTime, 0.f, 1.f ); + } + else if ( flTimePostMove < (CAPTION_PAN_FADE_TIME+CAPTION_PAN_SLIDE_TIME) ) + { + flTimePostMove -= CAPTION_PAN_FADE_TIME; + float flSlideTime = clamp( flTimePostMove / 0.25f, 0.f, 1.f ); + iHeightToMove += ceil((iCurrentLineHeight - iHeightToMove) * flSlideTime); + } + else + { + iHeightToMove = iCurrentLineHeight; + } + } + + // Minor adjustment to center the caption text within the window. + rcText.top = -iHeightToMove + 2; + } + + rcText.bottom = rcText.top + height; + + wrect_t rcOut = rcText; + + rcOut.right = rcOut.left + si->width + 6; + + DrawStream( rcOut, rcOutput, item, iFadeLine, flFadeLineAlpha ); + + rcText.top += height; + rcText.bottom += height; + + if ( rcText.top >= rcOutput.bottom ) + break; + } +} + +void CHudCloseCaption::OnTick( void ) +{ + // See if any async work has completed + ProcessAsyncWork(); + + + float dt = gpGlobals->frametime; + + int c = m_Items.Count(); + int i; + + if ( m_bVisibleDueToDirect ) + { + SetVisible( true ); + if ( !c ) + { + // Don't clear our force visible if we're waiting for the caption to load + if ( m_AsyncWork.Count() == 0 ) + { + m_bVisibleDueToDirect = false; + } + } + } + else + { + SetVisible( closecaption.GetBool() ); + } + + // Pass one decay all timers + for ( i = 0 ; i < c ; ++i ) + { + CCloseCaptionItem *item = m_Items[ i ]; + + float predisplay = item->GetPreDisplayTime(); + if ( predisplay > 0.0f ) + { + predisplay -= dt; + predisplay = MAX( 0.0f, predisplay ); + item->SetPreDisplayTime( predisplay ); + } + else + { + // remove time from actual playback + float ttl = item->GetTimeToLive(); + ttl -= dt; + ttl = MAX( 0.0f, ttl ); + item->SetTimeToLive( ttl ); + } + } + + // Pass two, remove from head until we get to first item with time remaining + bool foundfirstnondeletion = false; + for ( i = 0 ; i < c ; ++i ) + { + CCloseCaptionItem *item = m_Items[ i ]; + + // Skip items not yet showing... + float predisplay = item->GetPreDisplayTime(); + if ( predisplay > 0.0f ) + { + continue; + } + + float ttl = item->GetTimeToLive(); + if ( ttl > 0.0f ) + { + foundfirstnondeletion = true; + continue; + } + + // Skip the remainder of the items after we find the first/oldest active item + if ( foundfirstnondeletion ) + { + continue; + } + + delete item; + m_Items.Remove( i ); + --i; + --c; + } +} + +void CHudCloseCaption::Reset( void ) +{ + while ( m_Items.Count() > 0 ) + { + CCloseCaptionItem *i = m_Items[ 0 ]; + delete i; + m_Items.Remove( 0 ); + } + + ClearAsyncWork(); + Unlock(); +} + +bool CHudCloseCaption::SplitCommand( wchar_t const **ppIn, wchar_t *cmd, wchar_t *args ) const +{ + 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; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *stream - +// *findcmd - +// value - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CHudCloseCaption::GetFloatCommandValue( const wchar_t *stream, const wchar_t *findcmd, float& value ) const +{ + const wchar_t *curpos = stream; + + for ( ; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, findcmd ) ) + { + value = (float)wcstod( args, NULL ); + return true; + } + continue; + } + } + + return false; +} + + +bool CHudCloseCaption::StreamHasCommand( const wchar_t *stream, const wchar_t *findcmd ) const +{ + const wchar_t *curpos = stream; + + for ( ; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, findcmd ) ) + { + return true; + } + continue; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: It's blank or only comprised of whitespace/space characters... +// Input : *stream - +// Output : static bool +//----------------------------------------------------------------------------- +static bool IsAllSpaces( const wchar_t *stream ) +{ + const wchar_t *p = stream; + while ( *p != L'\0' ) + { + if ( !iswspace( *p ) ) + return false; + + p++; + } + + return true; +} + +bool CHudCloseCaption::StreamHasCommand( const wchar_t *stream, const wchar_t *search ) +{ + for ( const wchar_t *curpos = stream; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, search ) ) + { + return true; + } + } + } + return false; +} + +void CHudCloseCaption::Process( const wchar_t *stream, float duration, const char *tokenstream, bool fromplayer, bool direct ) +{ + if ( !direct ) + { + if ( !closecaption.GetBool() ) + { + Reset(); + return; + } + + // If we're locked, ignore all closecaption commands + if ( m_bLocked ) + return; + } + + // Nothing to do... + if ( IsAllSpaces( stream) ) + { + return; + } + + // If subtitling, don't show sfx captions at all + if ( cc_subtitles.GetBool() && StreamHasCommand( stream, L"sfx" ) ) + { + return; + } + + bool valid = true; + if ( !wcsncmp( stream, L"!!!", wcslen( L"!!!" ) ) ) + { + // It's in the text file, but hasn't been translated... + valid = false; + } + + if ( !wcsncmp( stream, L"-->", wcslen( L"-->" ) ) ) + { + // It's in the text file, but hasn't been translated... + valid = false; + + if ( cc_captiontrace.GetInt() < 2 ) + { + if ( cc_captiontrace.GetInt() == 1 ) + { + Msg( "Missing caption for '%s'\n", tokenstream ); + } + + return; + } + } + + float lifespan = duration + cc_linger_time.GetFloat(); + + float addedlife = 0.0f; + + if ( m_Items.Count() > 0 ) + { + // Get the remaining life span of the last item + CCloseCaptionItem *final = m_Items[ m_Items.Count() - 1 ]; + float prevlife = final->GetTimeToLive(); + + if ( prevlife > lifespan ) + { + addedlife = prevlife - lifespan; + } + + lifespan = MAX( lifespan, prevlife ); + } + + float delay = 0.0f; + float override_duration = 0.0f; + + wchar_t phrase[ MAX_CAPTION_CHARACTERS ]; + wchar_t *out = phrase; + + for ( const wchar_t *curpos = stream; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + const wchar_t *prevpos = curpos; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, L"delay" ) ) + { + + // End current phrase + *out = L'\0'; + + if ( wcslen( phrase ) > 0 ) + { + CCloseCaptionItem *item = new CCloseCaptionItem( phrase, lifespan, addedlife, delay, valid, fromplayer ); + m_Items.AddToTail( item ); + if ( StreamHasCommand( phrase, L"sfx" ) ) + { + // SFX show up instantly. + item->SetPreDisplayTime( 0.0f ); + } + + if ( GetFloatCommandValue( phrase, L"len", override_duration ) ) + { + item->SetTimeToLive( override_duration ); + } + } + + // Start new phrase + out = phrase; + + // Delay must be positive + delay = MAX( 0.0f, (float)wcstod( args, NULL ) ); + + continue; + } + + int copychars = curpos - prevpos; + while ( --copychars >= 0 ) + { + *out++ = *prevpos++; + } + } + + *out++ = *curpos; + } + + // End final phrase, if any + *out = L'\0'; + if ( wcslen( phrase ) > 0 ) + { + CCloseCaptionItem *item = new CCloseCaptionItem( phrase, lifespan, addedlife, delay, valid, fromplayer ); + m_Items.AddToTail( item ); + + if ( StreamHasCommand( phrase, L"sfx" ) ) + { + // SFX show up instantly. + item->SetPreDisplayTime( 0.0f ); + } + + if ( GetFloatCommandValue( phrase, L"len", override_duration ) ) + { + item->SetTimeToLive( override_duration ); + item->SetInitialLifeSpan( override_duration ); + } + } +} + +void CHudCloseCaption::CreateFonts( void ) +{ + vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() ); + + m_hFonts[CCFONT_NORMAL] = pScheme->GetFont( "CloseCaption_Normal" ); + + if ( IsPC() ) + { + m_hFonts[CCFONT_BOLD] = pScheme->GetFont( "CloseCaption_Bold" ); + m_hFonts[CCFONT_ITALIC] = pScheme->GetFont( "CloseCaption_Italic" ); + m_hFonts[CCFONT_ITALICBOLD] = pScheme->GetFont( "CloseCaption_BoldItalic" ); + } + else + { + m_hFonts[CCFONT_SMALL] = pScheme->GetFont( "CloseCaption_Small" ); + } + + m_nLineHeight = MAX( 6, vgui::surface()->GetFontTall( m_hFonts[ CCFONT_NORMAL ] ) ); +} + +struct WorkUnitParams +{ + WorkUnitParams() + { + Q_memset( stream, 0, sizeof( stream ) ); + out = stream; + x = 0; + y = 0; + width = 0; + bold = italic = false; + clr = Color( 255, 255, 255, 255 ); + newline = false; + font = 0; + } + + ~WorkUnitParams() + { + } + + void Finalize( int lineheight ) + { + *out = L'\0'; + } + + void Next( int lineheight ) + { + // Restart output + Q_memset( stream, 0, sizeof( stream ) ); + out = stream; + + x += width; + + width = 0; + // Leave bold, italic and color alone!!! + if ( newline ) + { + newline = false; + x = 0; + y += lineheight; + } + } + + int GetFontNumber() + { + return CHudCloseCaption::GetFontNumber( bold, italic ); + } + + wchar_t stream[ MAX_CAPTION_CHARACTERS ]; + wchar_t *out; + + int x; + int y; + int width; + bool bold; + bool italic; + Color clr; + bool newline; + vgui::HFont font; +}; + +void CHudCloseCaption::AddWorkUnit( CCloseCaptionItem *item, + WorkUnitParams& params ) +{ + params.Finalize( vgui::surface()->GetFontTall( params.font ) ); + +#ifdef WIN32 + if ( wcslen( params.stream ) > 0 ) +#else + // params.stream is still in ucs2 format here so just do a basic zero compare for length or just space + if ( ((uint16 *)params.stream)[0] != 0 && ((uint16 *)params.stream)[0] != 32 ) +#endif + { + CCloseCaptionWorkUnit *wu = new CCloseCaptionWorkUnit(); + + wu->SetStream( params.stream ); + wu->SetColor( params.clr ); + wu->SetBold( params.bold ); + wu->SetItalic( params.italic ); + wu->SetWidth( params.width ); + wu->SetHeight( vgui::surface()->GetFontTall( params.font ) ); + wu->SetPos( params.x, params.y ); + wu->SetFont( params.font ); + wu->SetFadeStart( 0 ); + + int curheight = item->GetHeight(); + int curwidth = item->GetWidth(); + + curheight = MAX( curheight, params.y + wu->GetHeight() ); + curwidth = MAX( curwidth, params.x + params.width ); + + item->SetHeight( curheight ); + item->SetWidth( curwidth ); + + // Add it + item->AddWork( wu ); + + params.Next( vgui::surface()->GetFontTall( params.font ) ); + } +} + +void CHudCloseCaption::ComputeStreamWork( int available_width, CCloseCaptionItem *item ) +{ + // Start with a clean param block + WorkUnitParams params; + + const wchar_t *curpos = item->GetStream(); + int streamlen = wcslen( curpos ); + CUtlVector< Color > colorStack; + + const wchar_t *most_recent_space = NULL; + int most_recent_space_w = -1; + + for ( ; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, L"cr" ) ) + { + params.newline = true; + AddWorkUnit( item, params); + } + else if ( !wcscmp( cmd, L"clr" ) ) + { + AddWorkUnit( item, params ); + + if ( args[0] == 0 && colorStack.Count()>= 2) + { + colorStack.Remove( colorStack.Count() - 1 ); + params.clr = colorStack[ colorStack.Count() - 1 ]; + } + else + { + int r = 0, g = 0, b = 0; + Color newcolor; + if ( 3 == swscanf( args, L"%i,%i,%i", &r, &g, &b ) ) + { + newcolor = Color( r, g, b, 255 ); + colorStack.AddToTail( newcolor ); + params.clr = colorStack[ colorStack.Count() - 1 ]; + } + } + } + else if ( !wcscmp( cmd, L"playerclr" ) ) + { + AddWorkUnit( item, params ); + + if ( args[0] == 0 && colorStack.Count()>= 2) + { + colorStack.Remove( colorStack.Count() - 1 ); + params.clr = colorStack[ colorStack.Count() - 1 ]; + } + else + { + // player and npc color selector + // e.g.,. 255,255,255:200,200,200 + int pr = 0, pg = 0, pb = 0, nr = 0, ng = 0, nb = 0; + Color newcolor; + if ( 6 == swscanf( args, L"%i,%i,%i:%i,%i,%i", &pr, &pg, &pb, &nr, &ng, &nb ) ) + { + newcolor = item->IsFromPlayer() ? Color( pr, pg, pb, 255 ) : Color( nr, ng, nb, 255 ); + colorStack.AddToTail( newcolor ); + params.clr = colorStack[ colorStack.Count() - 1 ]; + } + } + } + else if ( !wcscmp( cmd, L"I" ) ) + { + AddWorkUnit( item, params ); + params.italic = !params.italic; + } + else if ( !wcscmp( cmd, L"B" ) ) + { + AddWorkUnit( item, params ); + params.bold = !params.bold; + } + + continue; + } + + int font; + if ( IsPC() ) + { + font = params.GetFontNumber(); + } + else + { + font = streamlen >= cc_smallfontlength.GetInt() ? CCFONT_SMALL : CCFONT_NORMAL; + } + vgui::HFont useF = m_hFonts[font]; + params.font = useF; + + int w, h; + + wchar_t sz[2]; + sz[ 0 ] = *curpos; + sz[ 1 ] = L'\0'; + vgui::surface()->GetTextSize( useF, sz, w, h ); + + if ( ( params.x + params.width ) + w > available_width ) + { + if ( most_recent_space && curpos >= most_recent_space + 1 ) + { + // Roll back to previous space character if there is one... + int goback = curpos - most_recent_space - 1; + params.out -= ( goback + 1 ); + params.width = most_recent_space_w; + + wchar_t *extra = new wchar_t[ goback + 1 ]; + wcsncpy( extra, most_recent_space + 1, goback ); + extra[ goback ] = L'\0'; + + params.newline = true; + AddWorkUnit( item, params ); + + wcsncpy( params.out, extra, goback ); + params.out += goback; + int textw, texth; + vgui::surface()->GetTextSize( useF, extra, textw, texth ); + + params.width = textw; + + delete[] extra; + + most_recent_space = NULL; + most_recent_space_w = -1; + } + else + { + params.newline = true; + AddWorkUnit( item, params ); + } + } + *params.out++ = *curpos; + params.width += w; + + if ( iswspace( *curpos ) ) + { + most_recent_space = curpos; + most_recent_space_w = params.width; + } + } + + // Add the final unit. + params.newline = true; + AddWorkUnit( item, params ); + + item->SetSizeComputed( true ); + + // DumpWork( item ); +} + +void CHudCloseCaption:: DumpWork( CCloseCaptionItem *item ) +{ + int c = item->GetNumWorkUnits(); + for ( int i = 0 ; i < c; ++i ) + { + CCloseCaptionWorkUnit *wu = item->GetWorkUnit( i ); + wu->Dump(); + } +} + +void CHudCloseCaption::DrawStream( wrect_t &rcText, wrect_t &rcWindow, CCloseCaptionItem *item, int iFadeLine, float flFadeLineAlpha ) +{ + int c = item->GetNumWorkUnits(); + + wrect_t rcOut; + + float alpha = item->GetAlpha( m_flItemHiddenTime, m_flItemFadeInTime, m_flItemFadeOutTime ); + + for ( int i = 0 ; i < c; ++i ) + { + int x = 0; + int y = 0; + + CCloseCaptionWorkUnit *wu = item->GetWorkUnit( i ); + + vgui::HFont useF = wu->GetFont(); + + wu->GetPos( x, y ); + + rcOut.left = rcText.left + x + 3; + rcOut.right = rcOut.left + wu->GetWidth(); + rcOut.top = rcText.top + y; + rcOut.bottom = rcOut.top + wu->GetHeight(); + + // Adjust alpha to handle fade in/out at the top & bottom of the element. + // Used for single commentary entries that are too big to fit into the element. + float flLineAlpha = alpha; + if ( i == iFadeLine ) + { + flLineAlpha *= flFadeLineAlpha; + } + else if ( rcOut.top < rcWindow.top ) + { + // We're off the top of the element, so don't draw + continue; + } + else if ( rcOut.top > rcWindow.bottom ) + { + float flFadeHeight = (float)wu->GetHeight() * 0.25; + float flDist = (float)(rcOut.top - rcWindow.bottom) / flFadeHeight; + flDist = Bias( flDist, 0.2 ); + if ( flDist > 1 ) + continue; + + flLineAlpha *= 1.0 - flDist; + } + + Color useColor = wu->GetColor(); + + useColor[ 3 ] *= flLineAlpha; + + if ( !item->IsValid() ) + { + useColor = Color( 255, 255, 255, 255 * flLineAlpha ); + rcOut.right += 2; + vgui::surface()->DrawSetColor( Color( 100, 100, 40, 255 * flLineAlpha ) ); + vgui::surface()->DrawFilledRect( rcOut.left, rcOut.top, rcOut.right, rcOut.bottom ); + } + + vgui::surface()->DrawSetTextFont( useF ); + vgui::surface()->DrawSetTextPos( rcOut.left, rcOut.top ); + vgui::surface()->DrawSetTextColor( useColor ); + vgui::surface()->DrawPrintText( wu->GetStream(), wcslen( wu->GetStream() ) ); + } +} + +bool CHudCloseCaption::GetNoRepeatValue( const wchar_t *caption, float &retval ) +{ + retval = 0.0f; + const wchar_t *curpos = caption; + + for ( ; curpos && *curpos != L'\0'; ++curpos ) + { + wchar_t cmd[ 256 ]; + wchar_t args[ 256 ]; + + if ( SplitCommand( &curpos, cmd, args ) ) + { + if ( !wcscmp( cmd, L"norepeat" ) ) + { + retval = (float)wcstod( args, NULL ); + return true; + } + continue; + } + } + return false; +} + +bool CHudCloseCaption::CaptionTokenLessFunc( const CaptionRepeat &lhs, const CaptionRepeat &rhs ) +{ + return ( lhs.m_nTokenIndex < rhs.m_nTokenIndex ); +} + +static bool CaptionTrace( const char *token ) +{ + static CUtlSymbolTable s_MissingCloseCaptions; + + // Make sure we only show the message once + if ( UTL_INVAL_SYMBOL == s_MissingCloseCaptions.Find( token ) ) + { + s_MissingCloseCaptions.AddString( token ); + return true; + } + + return false; +} + +static ConVar cc_sentencecaptionnorepeat( "cc_sentencecaptionnorepeat", "4", 0, "How often a sentence can repeat." ); + +int CRCString( const char *str ) +{ + int len = Q_strlen( str ); + CRC32_t crc; + CRC32_Init( &crc ); + CRC32_ProcessBuffer( &crc, str, len ); + CRC32_Final( &crc ); + + return ( int )crc; +} + +class CAsyncCaption +{ +public: + CAsyncCaption() : + m_flDuration( 0.0f ), + m_bIsStream( false ), + m_bFromPlayer( false ) + { + } + + ~CAsyncCaption() + { + int c = m_Tokens.Count(); + for ( int i = 0; i < c; ++i ) + { + delete m_Tokens[ i ]; + } + m_Tokens.Purge(); + } + + void StartRequesting( CHudCloseCaption *hudCloseCaption, CUtlVector< AsyncCaption_t >& directories ) + { + // Issue pending async requests for each token in string + int c = m_Tokens.Count(); + for ( int i = 0; i < c; ++i ) + { + caption_t *caption = m_Tokens[ i ]; + Assert( !caption->stream ); + Assert( caption->dirindex >= 0 ); + + CaptionLookup_t& entry = directories[ caption->fileindex ].m_CaptionDirectory[ caption->dirindex ]; + + // Request this block, and if it's there, it'll call OnDataLoaded immediately + g_AsyncCaptionResourceManager.PollForAsyncLoading( hudCloseCaption, caption->fileindex, entry.blockNum ); + } + } + + void OnDataArrived( CUtlVector< AsyncCaption_t >& directories, int nFileIndex, int nBlockNum, AsyncCaptionData_t *pData ) + { + int c = m_Tokens.Count(); + for ( int i = 0; i < c; ++i ) + { + caption_t *caption = m_Tokens[ i ]; + if ( caption->stream != NULL ) + continue; + + // Lookup the data + CaptionLookup_t &entry = directories[ nFileIndex ].m_CaptionDirectory[ caption->dirindex ]; + if ( entry.blockNum != nBlockNum ) + continue; + +#ifdef WIN32 + const wchar_t *pIn = ( const wchar_t *)&pData->m_pBlockData[ entry.offset ]; + caption->stream = new wchar_t[ entry.length >> 1 ]; + memcpy( (void *)caption->stream, pIn, entry.length ); +#else + // we persist to disk as ucs2 so convert back to real unicode here + caption->stream = new wchar_t[ entry.length ]; + V_UCS2ToUnicode( (ucs2 *)&pData->m_pBlockData[ entry.offset ], caption->stream, entry.length*sizeof(wchar_t) ); +#endif + } + } + + void ProcessAsyncWork( CHudCloseCaption *hudCloseCaption, CUtlVector< AsyncCaption_t >& directories ) + { + int c = m_Tokens.Count(); + for ( int i = 0; i < c; ++i ) + { + caption_t *caption = m_Tokens[ i ]; + if ( caption->stream != NULL ) + continue; + + CaptionLookup_t& entry = directories[ caption->fileindex].m_CaptionDirectory[ caption->dirindex ]; + + // Request this block, and if it's there, it'll call OnDataLoaded immediately + g_AsyncCaptionResourceManager.PollForAsyncLoading( hudCloseCaption, caption->fileindex, entry.blockNum ); + } + } + + bool GetStream( OUT_Z_BYTECAP(bufSizeInBytes) wchar_t *buf, int bufSizeInBytes ) + { + Assert( bufSizeInBytes >= sizeof(buf[0]) ); + buf[ 0 ] = L'\0'; + + int c = m_Tokens.Count(); + for ( int i = 0; i < c; ++i ) + { + caption_t *caption = m_Tokens[ i ]; + if ( caption->stream == NULL ) + { + return false; + } + } + + unsigned int curlen = 0; + unsigned int maxlen = bufSizeInBytes / sizeof( wchar_t ); + + // Compose full stream from tokens + for ( int i = 0; i < c; ++i ) + { + caption_t *caption = m_Tokens[ i ]; + int len = wcslen( caption->stream ) + 1; + if ( curlen + len >= maxlen ) + break; + + wcscat( buf, caption->stream ); + if ( i < c - 1 ) + { + wcscat( buf, L" " ); + } + + curlen += len; + } + + return true; + } + + bool IsStream() const + { + return m_bIsStream; + } + + void SetIsStream( bool state ) + { + m_bIsStream = state; + } + + void AddRandomToken( CUtlVector< AsyncCaption_t >& directories ) + { + int dc = directories.Count(); + int fileindex = RandomInt( 0, dc - 1 ); + + int c = directories[ fileindex ].m_CaptionDirectory.Count(); + int idx = RandomInt( 0, c - 1 ); + + caption_t *caption = new caption_t; + char foo[ 32 ]; + Q_snprintf( foo, sizeof( foo ), "%d", idx ); + caption->token = strdup( foo ); + caption->dirindex = idx; + caption->stream = NULL; + caption->fileindex = fileindex; + + m_Tokens.AddToTail( caption ); + } + + bool AddToken + ( + CUtlVector< AsyncCaption_t >& directories, + const char *token + ) + { + CaptionLookup_t search; + search.SetHash( token ); + + int idx = -1; + int i; + int dc = directories.Count(); + for ( i = 0; i < dc; ++i ) + { + idx = directories[ i ].m_CaptionDirectory.Find( search ); + if ( idx == directories[ i ].m_CaptionDirectory.InvalidIndex() ) + continue; + + break; + } + + if ( i >= dc || idx == -1 ) + return false; + + caption_t *caption = new caption_t; + caption->token = strdup( token ); + caption->dirindex = idx; + caption->stream = NULL; + caption->fileindex = i; + + m_Tokens.AddToTail( caption ); + return true; + } + + int Count() const + { + return m_Tokens.Count(); + } + + const char *GetToken( int index ) + { + return m_Tokens[ index ]->token; + } + + void GetOriginalStream( char *buf, size_t bufsize ) + { + buf[ 0 ] = 0; + int c = Count(); + for ( int i = 0 ; i < c; ++i ) + { + Q_strncat( buf, GetToken( i ), bufsize, COPY_ALL_CHARACTERS ); + if ( i != c - 1 ) + { + Q_strncat( buf, " ", bufsize, COPY_ALL_CHARACTERS ); + } + } + } + + void SetDuration( float t ) + { + m_flDuration = t; + } + + float GetDuration() + { + return m_flDuration; + } + + bool IsFromPlayer() + { + return m_bFromPlayer; + } + + void SetFromPlayer( bool state ) + { + m_bFromPlayer = state; + } + + bool IsDirect() + { + return m_bDirect; + } + + void SetDirect( bool state ) + { + m_bDirect = state; + } +private: + float m_flDuration; + bool m_bIsStream : 1; + bool m_bFromPlayer : 1; + bool m_bDirect : 1; + + struct caption_t + { + caption_t() : + token( 0 ), + dirindex( -1 ), + fileindex( -1 ), + stream( 0 ) + { + } + + ~caption_t() + { + free( token ); + delete[] stream; + } + + void SetStream( const wchar_t *in ) + { + delete[] stream; + stream = 0; + if ( !in ) + return; + + int len = wcslen( in ); + stream = new wchar_t[ len + 1 ]; + wcsncpy( stream, in, len + 1 ); + } + + char *token; + int dirindex; + int fileindex; + wchar_t *stream; + }; + + CUtlVector< caption_t * > m_Tokens; +}; + +void CHudCloseCaption::ProcessAsyncWork() +{ + int i; + for( i = m_AsyncWork.Head(); i != m_AsyncWork.InvalidIndex(); i = m_AsyncWork.Next( i ) ) + { + // check for data arrival + CAsyncCaption *item = m_AsyncWork[ i ]; + item->ProcessAsyncWork( this, m_AsyncCaptions ); + } + // Now operate on any new data which arrived + for( i = m_AsyncWork.Head(); i != m_AsyncWork.InvalidIndex(); ) + { + int n = m_AsyncWork.Next( i ); + + CAsyncCaption *item = m_AsyncWork[ i ]; + wchar_t stream[ MAX_CAPTION_CHARACTERS ]; + + // If we get to the first item with pending async work, stop processing + if ( !item->GetStream( stream, sizeof( stream ) ) ) + { + break; + } + + if ( stream[ 0 ] != L'\0' ) + { + char original[ 512 ]; + item->GetOriginalStream( original, sizeof( original ) ); + + // Process it now + if ( item->IsStream() ) + { + _ProcessSentenceCaptionStream( item->Count(), original, stream ); + } + else + { + _ProcessCaption( stream, original, item->GetDuration(), item->IsFromPlayer(), item->IsDirect() ); + } + } + + m_AsyncWork.Remove( i ); + delete item; + + i = n; + } +} + +void CHudCloseCaption::ClearAsyncWork() +{ + for ( int i = m_AsyncWork.Head(); i != m_AsyncWork.InvalidIndex(); i = m_AsyncWork.Next( i ) ) + { + CAsyncCaption *item = m_AsyncWork[ i ]; + delete item; + } + m_AsyncWork.Purge(); +} + +extern void Hack_FixEscapeChars( char *str ); + +void CHudCloseCaption::ProcessCaptionDirect( const char *tokenname, float duration, bool fromplayer /* = false */ ) +{ + m_bVisibleDueToDirect = true; + + char token[ 512 ]; + Q_strncpy( token, tokenname, sizeof( token ) ); + if ( Q_strstr( token, "\\" ) ) + { + Hack_FixEscapeChars( token ); + } + + ProcessCaption( token, duration, fromplayer, true ); +} + +void CHudCloseCaption::PlayRandomCaption() +{ + CAsyncCaption *async = new CAsyncCaption; + async->SetIsStream( false ); + async->AddRandomToken( m_AsyncCaptions ); + async->SetDuration( RandomFloat( 1.0f, 3.0f ) ); + async->SetFromPlayer( RandomInt( 0, 1 ) == 0 ? true : false ); + async->StartRequesting( this, m_AsyncCaptions ); + m_AsyncWork.AddToTail( async ); +} + +bool CHudCloseCaption::AddAsyncWork( const char *tokenstream, bool bIsStream, float duration, bool fromplayer, bool direct /* = false */ ) +{ + bool bret = true; + + CAsyncCaption *async = new CAsyncCaption(); + async->SetIsStream( bIsStream ); + async->SetDirect( direct ); + if ( !bIsStream ) + { + bret = async->AddToken + ( + m_AsyncCaptions, + tokenstream + ); + } + else + { + // The first token from the stream is the name of the sentence + char tokenname[ 512 ]; + tokenname[ 0 ] = 0; + const char *p = tokenstream; + p = nexttoken( tokenname, p, ' ' ); + // p points to reset of sentence tokens, build up a unicode string from them... + while ( p && Q_strlen( tokenname ) > 0 ) + { + p = nexttoken( tokenname, p, ' ' ); + + if ( Q_strlen( tokenname ) == 0 ) + break; + + async->AddToken + ( + m_AsyncCaptions, + tokenname + ); + } + } + + m_AsyncWork.AddToTail( async ); + + async->SetDuration( duration ); + async->SetFromPlayer( fromplayer ); + // Do this last as the block might be resident already and this will finish immediately... + async->StartRequesting( this, m_AsyncCaptions ); + return bret; +} + + +void CHudCloseCaption::ProcessSentenceCaptionStream( const char *tokenstream ) +{ + float interval = cc_sentencecaptionnorepeat.GetFloat(); + interval = clamp( interval, 0.1f, 60.0f ); + + // The first token from the stream is the name of the sentence + char tokenname[ 512 ]; + + tokenname[ 0 ] = 0; + + const char *p = tokenstream; + + p = nexttoken( tokenname, p, ' ' ); + + if ( Q_strlen( tokenname ) > 0 ) + { + // Use it to check for "norepeat" rules + CaptionRepeat entry; + entry.m_nTokenIndex = CRCString( tokenname ); + + int idx = m_CloseCaptionRepeats.Find( entry ); + if ( m_CloseCaptionRepeats.InvalidIndex() == idx ) + { + entry.m_flLastEmitTime = gpGlobals->curtime; + entry.m_nLastEmitTick = gpGlobals->tickcount; + entry.m_flInterval = interval; + m_CloseCaptionRepeats.Insert( entry ); + } + else + { + entry = m_CloseCaptionRepeats[ idx ]; + if ( gpGlobals->curtime < ( entry.m_flLastEmitTime + entry.m_flInterval ) ) + { + return; + } + + entry.m_flLastEmitTime = gpGlobals->curtime; + entry.m_nLastEmitTick = gpGlobals->tickcount; + } + } + + AddAsyncWork( tokenstream, true, 0.0f, false ); +} + +void CHudCloseCaption::_ProcessSentenceCaptionStream( int wordCount, const char *tokenstream, const wchar_t *caption_full ) +{ + if ( wcslen( caption_full ) > 0 ) + { + Process( caption_full, ( wordCount + 1 ) * 0.75f, tokenstream, false /*never from player!*/ ); + } +} + +bool CHudCloseCaption::ProcessCaption( const char *tokenname, float duration, bool fromplayer /* = false */, bool direct /* = false */ ) +{ + return AddAsyncWork( tokenname, false, duration, fromplayer, direct ); +} + +void CHudCloseCaption::_ProcessCaption( const wchar_t *caption, const char *tokenname, float duration, bool fromplayer, bool direct ) +{ + // Get the string for the token + float interval = 0.0f; + bool hasnorepeat = GetNoRepeatValue( caption, interval ); + + CaptionRepeat entry; + entry.m_nTokenIndex = CRCString( tokenname ); + + int idx = m_CloseCaptionRepeats.Find( entry ); + if ( m_CloseCaptionRepeats.InvalidIndex() == idx ) + { + entry.m_flLastEmitTime = gpGlobals->curtime; + entry.m_nLastEmitTick = gpGlobals->tickcount; + entry.m_flInterval = interval; + m_CloseCaptionRepeats.Insert( entry ); + } + else + { + entry = m_CloseCaptionRepeats[ idx ]; + + // Interval of 0.0 means just don't double emit on same tick # + if ( entry.m_flInterval <= 0.0f ) + { + if ( gpGlobals->tickcount <= entry.m_nLastEmitTick ) + { + return; + } + } + else if ( hasnorepeat ) + { + if ( gpGlobals->curtime < ( entry.m_flLastEmitTime + entry.m_flInterval ) ) + { + return; + } + } + + entry.m_flLastEmitTime = gpGlobals->curtime; + entry.m_nLastEmitTick = gpGlobals->tickcount; + } + + Process( caption, duration, tokenname, fromplayer, direct ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pszName - +// iSize - +// *pbuf - +//----------------------------------------------------------------------------- +void CHudCloseCaption::MsgFunc_CloseCaption(bf_read &msg) +{ + char tokenname[ 512 ]; + msg.ReadString( tokenname, sizeof( tokenname ) ); + float duration = msg.ReadShort() * 0.1f; + byte flagbyte = msg.ReadByte(); + bool warnonmissing = flagbyte & CLOSE_CAPTION_WARNIFMISSING ? true : false; + bool fromplayer = flagbyte & CLOSE_CAPTION_FROMPLAYER ? true : false; + bool bIsMale = flagbyte & CLOSE_CAPTION_GENDER_MALE ? true : false; + bool bIsFemale = flagbyte & CLOSE_CAPTION_GENDER_FEMALE ? true : false; + + if ( warnonmissing && !IsX360() ) + { + wchar_t *pcheck = g_pVGuiLocalize->Find( tokenname ); + if ( !pcheck ) + { + Warning( "No caption found for '%s'\n", tokenname ); + } + } + + char szTestName[ 512 ]; + if ( bIsMale || bIsFemale ) + { + Q_snprintf( szTestName, sizeof( szTestName ), "%s_%s", tokenname, bIsMale ? "male" : "female" ); + // If the gender-ified version exists, use it, otherwise fall through and pass the non-gender string to the cc system for processing + if ( ProcessCaption( szTestName , duration, fromplayer ) ) + { + return; + } + } + + ProcessCaption( tokenname, duration, fromplayer ); +} + +int CHudCloseCaption::GetFontNumber( bool bold, bool italic ) +{ + if ( IsPC() && ( bold || italic ) ) + { + if( bold && italic ) + { + return CHudCloseCaption::CCFONT_ITALICBOLD; + } + + if ( bold ) + { + return CHudCloseCaption::CCFONT_BOLD; + } + + if ( italic ) + { + return CHudCloseCaption::CCFONT_ITALIC; + } + } + + return CHudCloseCaption::CCFONT_NORMAL; +} + +void CHudCloseCaption::Flush() +{ + g_AsyncCaptionResourceManager.Flush(); +} + +void CHudCloseCaption::InitCaptionDictionary( const char *dbfile ) +{ + if ( m_CurrentLanguage.IsValid() && !Q_stricmp( m_CurrentLanguage.String(), dbfile ) ) + return; + + m_CurrentLanguage = dbfile; + + m_AsyncCaptions.Purge(); + + g_AsyncCaptionResourceManager.Clear(); + + char searchPaths[4096]; + filesystem->GetSearchPath( "GAME", true, searchPaths, sizeof( searchPaths ) ); + + for ( char *path = strtok( searchPaths, ";" ); path; path = strtok( NULL, ";" ) ) + { + if ( IsX360() && ( filesystem->GetDVDMode() == DVDMODE_STRICT ) && !V_stristr( path, ".zip" ) ) + { + // only want zip paths + continue; + } + + char fullpath[MAX_PATH]; + Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", path, dbfile ); + Q_FixSlashes( fullpath ); + + if ( IsX360() ) + { + char fullpath360[MAX_PATH]; + UpdateOrCreateCaptionFile( fullpath, fullpath360, sizeof( fullpath360 ) ); + Q_strncpy( fullpath, fullpath360, sizeof( fullpath ) ); + } + + FileHandle_t fh = filesystem->Open( fullpath, "rb" ); + if ( FILESYSTEM_INVALID_HANDLE != fh ) + { + MEM_ALLOC_CREDIT(); + + CUtlBuffer dirbuffer; + + AsyncCaption_t& entry = m_AsyncCaptions[ m_AsyncCaptions.AddToTail() ]; + + // Read the header + filesystem->Read( &entry.m_Header, sizeof( entry.m_Header ), fh ); + if ( entry.m_Header.magic != COMPILED_CAPTION_FILEID ) + Error( "Invalid file id for %s\n", fullpath ); + if ( entry.m_Header.version != COMPILED_CAPTION_VERSION ) + Error( "Invalid file version for %s\n", fullpath ); + if ( entry.m_Header.directorysize < 0 || entry.m_Header.directorysize > 64 * 1024 ) + Error( "Invalid directory size %d for %s\n", entry.m_Header.directorysize, fullpath ); + //if ( entry.m_Header.blocksize != MAX_BLOCK_SIZE ) + // Error( "Invalid block size %d, expecting %d for %s\n", entry.m_Header.blocksize, MAX_BLOCK_SIZE, fullpath ); + + int directoryBytes = entry.m_Header.directorysize * sizeof( CaptionLookup_t ); + entry.m_CaptionDirectory.EnsureCapacity( entry.m_Header.directorysize ); + dirbuffer.EnsureCapacity( directoryBytes ); + + filesystem->Read( dirbuffer.Base(), directoryBytes, fh ); + filesystem->Close( fh ); + + entry.m_CaptionDirectory.CopyArray( (const CaptionLookup_t *)dirbuffer.PeekGet(), entry.m_Header.directorysize ); + entry.m_CaptionDirectory.RedoSort( true ); + + entry.m_DataBaseFile = fullpath; + } + } + + g_AsyncCaptionResourceManager.SetDbInfo( m_AsyncCaptions ); +} + +void CHudCloseCaption::OnFinishAsyncLoad( int nFileIndex, int nBlockNum, AsyncCaptionData_t *pData ) +{ + // Fill in data for all users of pData->m_nBlockNum + FOR_EACH_LL( m_AsyncWork, i ) + { + CAsyncCaption *item = m_AsyncWork[ i ]; + item->OnDataArrived( m_AsyncCaptions, nFileIndex, nBlockNum, pData ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHudCloseCaption::Lock( void ) +{ + if ( !IsXbox() ) + m_bLocked = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CHudCloseCaption::Unlock( void ) +{ + m_bLocked = false; +} + +static int EmitCaptionCompletion( const char *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) +{ + int current = 0; + if ( !g_pVGuiLocalize || IsX360() ) + return current; + + const char *cmdname = "cc_emit"; + char *substring = NULL; + int substringLen = 0; + if ( Q_strstr( partial, cmdname ) && strlen(partial) > strlen(cmdname) + 1 ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + substringLen = strlen(substring); + } + + StringIndex_t i = g_pVGuiLocalize->GetFirstStringIndex(); + + while ( i != INVALID_LOCALIZE_STRING_INDEX && + current < COMMAND_COMPLETION_MAXITEMS ) + { + const char *ccname = g_pVGuiLocalize->GetNameByIndex( i ); + if ( ccname ) + { + if ( !substring || !Q_strncasecmp( ccname, substring, substringLen ) ) + { + Q_snprintf( commands[ current ], sizeof( commands[ current ] ), "%s %s", cmdname, ccname ); + current++; + } + } + i = g_pVGuiLocalize->GetNextStringIndex( i ); + } + + return current; +} + +CON_COMMAND_F_COMPLETION( cc_emit, "Emits a closed caption", 0, EmitCaptionCompletion ) +{ + if ( args.ArgC() != 2 ) + { + Msg( "usage: cc_emit tokenname\n" ); + return; + } + + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + if ( hudCloseCaption ) + { + hudCloseCaption->ProcessCaption( args[1], 5.0f ); + } +} + +CON_COMMAND( cc_random, "Emits a random caption" ) +{ + int count = 1; + if ( args.ArgC() == 2 ) + { + count = MAX( 1, atoi( args[ 1 ] ) ); + } + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + if ( hudCloseCaption ) + { + for ( int i = 0; i < count; ++i ) + { + hudCloseCaption->PlayRandomCaption(); + } + } +} + + +CON_COMMAND( cc_flush, "Flushes async'd captions." ) +{ + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + if ( hudCloseCaption ) + { + hudCloseCaption->Flush(); + } +} + +CON_COMMAND( cc_showblocks, "Toggles showing which blocks are pending/loaded async." ) +{ + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + if ( hudCloseCaption ) + { + hudCloseCaption->TogglePaintDebug(); + } +} + +void OnCaptionLanguageChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) +{ + if ( !g_pVGuiLocalize ) + return; + + ConVarRef var( pConVar ); + + char fn[ 512 ]; + Q_snprintf( fn, sizeof( fn ), "resource/closecaption_%s.txt", var.GetString() ); + + // Re-adding the file, even if it's "english" will overwrite the tokens as needed + if ( !IsX360() ) + { + g_pVGuiLocalize->AddFile( "resource/closecaption_%language%.txt", "GAME", true ); + } + + char uilanguage[ 64 ]; + uilanguage[0] = 0; + engine->GetUILanguage( uilanguage, sizeof( uilanguage ) ); + + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + + // If it's not the default, load the language on top of the user's default language + if ( Q_strlen( var.GetString() ) > 0 && Q_stricmp( var.GetString(), uilanguage ) ) + { + if ( !IsX360() ) + { + if ( g_pFullFileSystem->FileExists( fn ) ) + { + g_pVGuiLocalize->AddFile( fn, "GAME", true ); + } + else + { + char fallback[ 512 ]; + Q_snprintf( fallback, sizeof( fallback ), "resource/closecaption_%s.txt", uilanguage ); + + Msg( "%s not found\n", fn ); + Msg( "%s will be used\n", fallback ); + } + } + + if ( hudCloseCaption ) + { + char dbfile [ 512 ]; + Q_snprintf( dbfile, sizeof( dbfile ), "resource/closecaption_%s.dat", var.GetString() ); + hudCloseCaption->InitCaptionDictionary( dbfile ); + } + } + else + { + if ( hudCloseCaption ) + { + char dbfile [ 512 ]; + Q_snprintf( dbfile, sizeof( dbfile ), "resource/closecaption_%s.dat", uilanguage ); + hudCloseCaption->InitCaptionDictionary( dbfile ); + } + } + DevMsg( "cc_lang = %s\n", var.GetString() ); +} + + + +ConVar cc_lang( "cc_lang", "", FCVAR_ARCHIVE, "Current close caption language (emtpy = use game UI language)", OnCaptionLanguageChanged ); + +CON_COMMAND( cc_findsound, "Searches for soundname which emits specified text." ) +{ + if ( args.ArgC() != 2 ) + { + Msg( "usage: cc_findsound 'substring'\n" ); + return; + } + + CHudCloseCaption *hudCloseCaption = GET_HUDELEMENT( CHudCloseCaption ); + if ( hudCloseCaption ) + { + hudCloseCaption->FindSound( args.Arg( 1 ) ); + } +} + +void CHudCloseCaption::FindSound( char const *pchANSI ) +{ + // Now do the searching + ucs2 stream[ 1024 ]; + char streamANSI[ 1024 ]; + + for ( int i = 0 ; i < m_AsyncCaptions.Count(); ++i ) + { + AsyncCaption_t &data = m_AsyncCaptions[ i ]; + + byte *block = new byte[ data.m_Header.blocksize ]; + + int nLoadedBlock = -1; + + Q_memset( block, 0, data.m_Header.blocksize ); + CaptionDictionary_t &dict = data.m_CaptionDirectory; + for ( int j = 0; j < dict.Count(); ++j ) + { + CaptionLookup_t &lu = dict[ j ]; + + int blockNum = lu.blockNum; + + const char *dbname = data.m_DataBaseFile.String(); + + // Try and reload it + char fn[ 256 ]; + Q_strncpy( fn, dbname, sizeof( fn ) ); + Q_FixSlashes( fn ); + + asynccaptionparams_t params; + params.dbfile = fn; + params.blocktoload = blockNum; + params.blocksize = data.m_Header.blocksize; + params.blockoffset = data.m_Header.dataoffset + blockNum *data.m_Header.blocksize; + params.fileindex = i; + + if ( blockNum != nLoadedBlock ) + { + nLoadedBlock = blockNum; + + FileHandle_t fh = filesystem->Open( fn, "rb" ); + filesystem->Seek( fh, params.blockoffset, FILESYSTEM_SEEK_CURRENT ); + filesystem->Read( block, data.m_Header.blocksize, fh ); + filesystem->Close( fh ); + } + + // Now we have the data + const ucs2 *pIn = ( const ucs2 *)&block[ lu.offset ]; + Q_memcpy( (void *)stream, pIn, MIN( lu.length, sizeof( stream ) ) ); + + // Now search for search text + V_UCS2ToUTF8( stream, streamANSI, sizeof( streamANSI ) ); + streamANSI[ sizeof( streamANSI ) - 1 ] = 0; + + if ( Q_stristr( streamANSI, pchANSI ) ) + { + CaptionLookup_t search; + + Msg( "found '%s' in %s\n", streamANSI, fn ); + + // Now find the sounds that will hash to this + for ( int k = soundemitterbase->First(); k != soundemitterbase->InvalidIndex(); k = soundemitterbase->Next( k ) ) + { + char const *pchSoundName = soundemitterbase->GetSoundName( k ); + + // Hash it + + search.SetHash( pchSoundName ); + + if ( search.hash == lu.hash ) + { + Msg( " '%s' matches\n", pchSoundName ); + } + } + + if ( IsPC() ) + { + for ( int r = g_pVGuiLocalize->GetFirstStringIndex(); r != INVALID_LOCALIZE_STRING_INDEX; r = g_pVGuiLocalize->GetNextStringIndex( r ) ) + { + const char *strName = g_pVGuiLocalize->GetNameByIndex( r ); + + search.SetHash( strName ); + + if ( search.hash == lu.hash ) + { + Msg( " '%s' localization matches\n", strName ); + } + } + } + } + } + + delete[] block; + } +} |