Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3302 lines
77 KiB

//========= Copyright (c) 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//===========================================================================//
#include "cbase.h"
#include <ctype.h>
#include <vstdlib/vstrtools.h>
#ifdef _PS3
#include <wctype.h>
int wcsnlen( wchar_t const *wsz, int nMaxLen )
{
int nLen = 0;
while ( nMaxLen -- > 0 )
{
if ( *( wsz ++ ) )
++ nLen;
else
break;
}
return nLen;
}
#endif
#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"
#include "tier3/tier3.h"
#include "characterset.h"
#include "gamerules.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#define CC_INSET 12
extern ISoundEmitterSystemBase *soundemitterbase;
static bool GetDefaultSubtitlesState()
{
return XBX_IsLocalized() && !XBX_IsAudioLocalized();
}
// Marked as FCVAR_USERINFO so that the server can cull CC messages before networking them down to us!!!
ConVar closecaption( "closecaption", GetDefaultSubtitlesState() ? "1" : "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_GAMECONSOLE, "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", GetDefaultSubtitlesState() ? "1" : "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_GAMECONSOLE, "If set, don't show sound effect captions, just voice overs (i.e., won't help hearing impaired players)." );
ConVar english( "english", "1", FCVAR_HIDDEN, "If set to 1, running the english language set of assets." );
#define MAX_CAPTION_CHARACTERS 10200
#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;
#ifdef WIN32
int len = wcsnlen( stream, ( MAX_CAPTION_CHARACTERS - 1 ) );
#else
int len = wcslen( stream );
#endif
Assert( len < ( MAX_CAPTION_CHARACTERS - 1 ) );
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,
bool bSFXEntry,
bool bLowPriorityEntry
) :
m_flTimeToLive( 0.0f ),
m_flAddedTime( addedtime ),
m_bValid( false ),
m_nTotalWidth( 0 ),
m_nTotalHeight( 0 ),
m_bSizeComputed( false ),
m_bFromPlayer( fromplayer ),
m_bSFXEntry( bSFXEntry ),
m_bLowPriorityEntry( bLowPriorityEntry )
{
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;
}
bool IsSFXEntry() const
{
return m_bSFXEntry;
}
bool IsLowPriorityEntry() const
{
return m_bLowPriorityEntry;
}
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;
bool m_bSFXEntry;
bool m_bLowPriorityEntry;
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 &params )
{
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 &params )
{
// 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( g_pDataCache, "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, bytesTotal;
float percent;
bytesUsed = status.nBytes;
bytesTotal = limits.nMaxBytes;
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 );
Q_strlower( 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 );
Q_strlower( 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_FLAGS( CHudCloseCaption, HUDELEMENT_SS_FULLSCREEN_ONLY );
DECLARE_HUD_MESSAGE( CHudCloseCaption, CloseCaption );
DECLARE_HUD_MESSAGE( CHudCloseCaption, CloseCaptionDirect );
CHudCloseCaption::CHudCloseCaption( const char *pElementName )
: CHudElement( pElementName ),
vgui::Panel( NULL, "HudCloseCaption" ),
m_CloseCaptionRepeats( 0, 0, CaptionTokenLessFunc ),
m_CurrentLanguage( UTL_INVAL_SYMBOL ),
m_bPaintDebugInfo( false ),
m_ColorMap( k_eDictCompareTypeCaseInsensitive )
{
vgui::Panel *pParent = GetFullscreenClientMode()->GetViewport();
SetParent( pParent );
SetProportional( true );
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 ( !IsGameConsole() )
{
g_pVGuiLocalize->AddFile( "resource/closecaption_%language%.txt", "GAME", true );
}
HOOK_HUD_MESSAGE( CHudCloseCaption, CloseCaption );
HOOK_HUD_MESSAGE( CHudCloseCaption, CloseCaptionDirect );
char uilanguage[ 64 ];
engine->GetUILanguage( uilanguage, sizeof( uilanguage ) );
if ( !Q_stricmp( uilanguage, "english" ) )
{
english.SetValue( 1 );
}
else
{
english.SetValue( 0 );
}
InitCaptionDictionary( uilanguage );
LoadColorMap( "resource/captioning_colors.txt" );
m_bLevelShutDown = false;
m_bUseAsianWordWrapping = false;
}
//-----------------------------------------------------------------------------
// 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 );
int iSizeCheck = 0;
// Pop the oldest SFX items off the stack first if we're out of space
while ( totalheight > avail_height &&
visibleitems.Count() > cc_minvisibleitems.GetInt() &&
iSizeCheck <= visibleitems.Count() - 1 )
{
VisibleStreamItem & pop = visibleitems[ iSizeCheck ];
if ( pop.item->IsSFXEntry() )
{
totalheight -= pop.height;
// And make it die right away...
pop.item->SetTimeToLive( 0.0f );
visibleitems.Remove( iSizeCheck );
continue;
}
iSizeCheck++;
}
iSizeCheck = 0;
// Pop the oldest low priority items off the stack next if we're still out of space
while ( totalheight > avail_height &&
visibleitems.Count() > cc_minvisibleitems.GetInt() &&
iSizeCheck <= visibleitems.Count() - 1 )
{
VisibleStreamItem & pop = visibleitems[ iSizeCheck ];
if ( pop.item->IsLowPriorityEntry() )
{
totalheight -= pop.height;
// And make it die right away...
pop.item->SetTimeToLive( 0.0f );
visibleitems.Remove( iSizeCheck );
continue;
}
iSizeCheck++;
}
// Start popping really old items off the stack if we're still 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 extraheight = (height - avail_height);
for ( int j = 0 ; j < units; j++ )
{
CCloseCaptionWorkUnit *wu = item->GetWorkUnit( j );
extraheight -= wu->GetHeight();
if ( extraheight <= 0 )
{
units = j+2; // Add an extra line since we want to scroll over the lifetime of the audio and the last scroll is at the end time
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( floor( 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.0 - flFadeTime, 0, 1 );
}
else if ( flTimePostMove < (CAPTION_PAN_FADE_TIME+CAPTION_PAN_SLIDE_TIME) )
{
flTimePostMove -= CAPTION_PAN_FADE_TIME;
float flSlideTime = clamp( flTimePostMove / 0.25, 0, 1 );
iHeightToMove += ceil((iCurrentLineHeight - iHeightToMove) * flSlideTime);
iFadeLine = iLinesToMove-1;
flFadeLineAlpha = 0.0f;
}
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::ApplySchemeSettings( vgui::IScheme *pScheme )
{
BaseClass::ApplySchemeSettings( pScheme );
// must get/reget fonts when scheme changes due to video resolution changes
CreateFonts();
SetUseAsianWordWrapping();
}
void CHudCloseCaption::Reset( void )
{
if ( m_bLevelShutDown || !g_pGameRules || !g_pGameRules->IsMultiplayer() )
{
m_Items.PurgeAndDeleteElements();
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 != 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'>' && !V_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, bool fromplayer, bool direct )
{
if ( !direct )
{
if ( !closecaption.GetBool() )
return;
// If we're locked, ignore all closecaption commands
if ( m_bLocked )
return;
}
// Nothing to do...
if ( IsAllSpaces( stream) )
{
return;
}
bool bSFXEntry = StreamHasCommand( stream, L"sfx" );
bool bLowPriorityEntry = StreamHasCommand( stream, L"low" );
// If subtitling, don't show sfx captions at all
if ( cc_subtitles.GetBool() && bSFXEntry )
{
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", stream );
}
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 ( phrase[ 0 ] != L'\0' )
{
CCloseCaptionItem *item = new CCloseCaptionItem( phrase, lifespan, addedlife, delay, valid, fromplayer, bSFXEntry, bLowPriorityEntry );
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 ( phrase[ 0 ] != L'\0' )
{
CCloseCaptionItem *item = new CCloseCaptionItem( phrase, lifespan, addedlife, delay, valid, fromplayer, bSFXEntry, bLowPriorityEntry );
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( vgui::scheme()->GetScheme( "basemodui_scheme" ) );
if ( !IsGameConsole() )
{
m_hFonts[CCFONT_NORMAL] = pScheme->GetFont( "CloseCaption_Normal", true );
m_hFonts[CCFONT_BOLD] = pScheme->GetFont( "CloseCaption_Bold", true );
m_hFonts[CCFONT_ITALIC] = pScheme->GetFont( "CloseCaption_Italic", true );
m_hFonts[CCFONT_ITALICBOLD] = pScheme->GetFont( "CloseCaption_BoldItalic", true );
m_nLineHeight = MAX( 6, vgui::surface()->GetFontTall( m_hFonts[ CCFONT_NORMAL ] ) );
}
else
{
m_hFonts[CCFONT_CONSOLE] = pScheme->GetFont( "CloseCaption_Console", true );
m_nLineHeight = MAX( 6, vgui::surface()->GetFontTall( m_hFonts[ CCFONT_CONSOLE ] ) );
}
}
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 ) );
if ( params.stream[ 0 ] != L'\0' )
{
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();
CUtlVector< Color > colorStack;
// Need to distinguish wspace breaks from Asian word wrapping breaks
bool most_recent_break_was_wspace = true;
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, g, b;
Color newcolor;
bool bColorValid = false;
if ( 3 == swscanf( args, L"%i,%i,%i", &r, &g, &b ) )
{
newcolor = Color( r, g, b, 255 );
bColorValid = true;
}
if ( bColorValid )
{
// lookup the alternate color using the color map
if ( curpos[0] == L'>' )
{
// identify a possible announcer tag
wchar_t announcerName[128];
const wchar_t *pColon = wcsstr( curpos+1, L":" );
if ( pColon )
{
// parse away any <???> commands after the color
const wchar_t *pStart = curpos + 1;
while ( pStart[0] == L'<' )
{
pStart = wcsstr( pStart, L">" );
if ( !pStart )
{
// should have found matched >
// go back to original position
pStart = curpos + 1;
break;
}
pStart++;
}
int nLength = MIN( sizeof( announcerName ), ( pColon - pStart + 1 ) * 2 );
V_wcsncpy( announcerName, pStart, nLength );
Color tagColor;
bool bFound = FindColorForTag( announcerName, tagColor );
if ( bFound )
{
newcolor = tagColor;
}
}
}
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, pg, pb, nr, ng, nb;
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 ( !IsGameConsole() )
{
font = params.GetFontNumber();
}
else
{
// consoles cannot support the varied fonts
font = CCFONT_CONSOLE;
}
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 + ( most_recent_break_was_wspace ? 1 : 0 ) ); // Don't drop the character before the wrap if it's not whitespace!
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 ( isbreakablewspace( *curpos ) )
{
most_recent_break_was_wspace = true;
most_recent_space = curpos;
most_recent_space_w = params.width;
}
else if ( m_bUseAsianWordWrapping && AsianWordWrap::CanBreakAfter( curpos ) )
{
most_recent_break_was_wspace = false;
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.bottom > rcWindow.bottom )
{
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 );
#ifdef WIN32
int len = wcsnlen( wu->GetStream(), MAX_CAPTION_CHARACTERS ) ;
#else
int len = wcslen( wu->GetStream() ) ;
#endif
vgui::surface()->DrawPrintText( wu->GetStream(), len );
}
}
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 || caption->stream != NULL || caption->fileindex != nFileIndex )
continue;
// Lookup the data
CaptionLookup_t &entry = directories[ nFileIndex ].m_CaptionDirectory[ caption->dirindex ];
if ( entry.blockNum != nBlockNum )
continue;
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 );
}
}
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 )
{
buf[ 0 ] = L'\0';
int c = m_Tokens.Count();
for ( int i = 0; i < c; ++i )
{
caption_t *caption = m_Tokens[ i ];
if ( !caption || 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 ];
#ifdef WIN32
unsigned int len = wcsnlen( caption->stream, maxlen ) + 1;
#else
unsigned int len = wcslen( caption->stream ) + 1;
#endif
Assert( len < maxlen );
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 );
CaptionLookup_t help;
help.SetHash( foo );
caption->hash = help.hash;
caption->dirindex = idx;
caption->stream = NULL;
caption->fileindex = fileindex;
m_Tokens.AddToTail( caption );
}
bool AddTokenByHash
(
CUtlVector< AsyncCaption_t >& directories,
unsigned int hash,
char const *pchToken
)
{
CaptionLookup_t search;
search.hash = hash;
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 )
{
AssertMsgOnce( *pchToken, "Should never fail to find a caption by hash, since server side has searched for hash!!!" );
return false;
}
caption_t *caption = new caption_t;
caption->token = strdup( pchToken );
caption->hash = hash;
caption->dirindex = idx;
caption->stream = NULL;
caption->fileindex = i;
m_Tokens.AddToTail( caption );
return true;
}
bool AddToken
(
CUtlVector< AsyncCaption_t >& directories,
const char *token
)
{
CaptionLookup_t search;
search.SetHash( token );
return AddTokenByHash( directories, search.hash, token );
}
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;
}
unsigned int GetHash() const
{
if ( m_bIsStream )
return 0u;
if ( m_Tokens.Count() == 0 )
return 0u;
return m_Tokens[ 0 ]->hash;
}
private:
float m_flDuration;
bool m_bIsStream : 1;
bool m_bFromPlayer : 1;
bool m_bDirect : 1;
struct caption_t
{
caption_t() :
token( 0 ),
hash( 0u ),
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;
#ifdef WIN32
int len = wcsnlen( in, ( MAX_CAPTION_CHARACTERS - 1 ) );
#else
int len = wcslen( in );
#endif
Assert( len < ( MAX_CAPTION_CHARACTERS - 1 ) );
stream = new wchar_t[ len + 1 ];
wcsncpy( stream, in, len + 1 );
}
char *token;
unsigned int hash;
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 ];
Assert( item );
if ( item )
{
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 || !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
{
#if defined( POSIX ) && !defined( PLATFORM_PS3 )
wchar_t localStream[ MAX_CAPTION_CHARACTERS ];
// we persist to disk as ucs2 so convert back to real unicode here
V_UCS2ToUnicode( (ucs2 *)stream, localStream, sizeof(localStream) );
_ProcessCaption( localStream, item->GetHash(), item->GetDuration(), item->IsFromPlayer(), item->IsDirect() );
#else
_ProcessCaption( stream, item->GetHash(), item->GetDuration(), item->IsFromPlayer(), item->IsDirect() );
#endif
}
}
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()
{
if ( !closecaption.GetBool() )
return;
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( char const *tokenstream, bool bIsStream, float duration, bool fromplayer, bool direct /*=false*/ )
{
if ( !closecaption.GetBool() && !direct )
return 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;
}
bool CHudCloseCaption::AddAsyncWorkByHash( unsigned int hash, float duration, bool fromplayer, bool direct /*=false*/ )
{
if ( !closecaption.GetBool() && !direct )
return false;
bool bret = true;
CAsyncCaption *async = new CAsyncCaption();
async->SetIsStream( false );
async->SetDirect( direct );
bret = async->AddTokenByHash
(
m_AsyncCaptions,
hash,
""
);
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
{
CaptionRepeat &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 ( caption_full[ 0 ] != L'\0' )
{
Process( caption_full, ( wordCount + 1 ) * 0.75f, 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 );
}
bool CHudCloseCaption::ProcessCaptionByHash( unsigned int hash, float duration, bool fromplayer, bool direct /* = false */ )
{
return AddAsyncWorkByHash( hash, duration, fromplayer, direct );
}
void CHudCloseCaption::_ProcessCaption( const wchar_t *caption, unsigned int hash, 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 = hash;
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
{
CaptionRepeat &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, fromplayer, direct );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pszName -
// iSize -
// *pbuf -
//-----------------------------------------------------------------------------
bool CHudCloseCaption::MsgFunc_CloseCaption(const CCSUsrMsg_CloseCaption &msg)
{
unsigned int hash;
hash = msg.hash();
float duration = msg.duration() * 0.1f;
bool fromplayer = msg.from_player();
ProcessCaptionByHash( hash, duration, fromplayer );
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pszName -
// iSize -
// *pbuf -
//-----------------------------------------------------------------------------
bool CHudCloseCaption::MsgFunc_CloseCaptionDirect(const CCSUsrMsg_CloseCaptionDirect &msg)
{
unsigned int hash;
hash = msg.hash();
float duration = msg.duration() * 0.1f;
bool fromplayer = msg.from_player();
m_bVisibleDueToDirect = true;
ProcessCaptionByHash( hash, duration, fromplayer, true );
return true;
}
int CHudCloseCaption::GetFontNumber( bool bold, bool italic )
{
if ( IsGameConsole() )
{
return CHudCloseCaption::CCFONT_CONSOLE;
}
if ( 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 *language, bool bForce )
{
if ( !bForce && m_CurrentLanguage.IsValid() && !Q_stricmp( m_CurrentLanguage.String(), language ) )
return;
if ( !language && bForce && m_CurrentLanguage.IsValid() )
{
// use existing language
language = m_CurrentLanguage.String();
}
else
{
m_CurrentLanguage = language;
}
// Make sure we've finished all out async work before rebuilding the dictionary
const float flMaxAsyncProcessTime = 0.5f;
float flStartProcessTime = Plat_FloatTime();
while ( m_AsyncWork.Count() > 0 )
{
ProcessAsyncWork();
if ( Plat_FloatTime() - flStartProcessTime >= flMaxAsyncProcessTime )
{
DevWarning( "Could not finish async caption work after %f seconds of processing before caption dictionary init!\n", flMaxAsyncProcessTime );
m_AsyncWork.RemoveAll();
break;
}
}
m_AsyncCaptions.Purge();
g_AsyncCaptionResourceManager.Clear();
bool bAdded = AddFileToCaptionDictionary( VarArgs( "resource/closecaption_%s.dat", language ) );
if ( !bAdded && V_stricmp( language, "english" ) )
{
// non-english can fallback to english
AddFileToCaptionDictionary( "resource/closecaption_english.dat" );
}
bAdded = AddFileToCaptionDictionary( VarArgs( "resource/subtitles_%s.dat", language ) );
if ( !bAdded && V_stricmp( language, "english" ) )
{
// non-english can fallback to english
AddFileToCaptionDictionary( "resource/subtitles_english.dat" );
}
g_AsyncCaptionResourceManager.SetDbInfo( m_AsyncCaptions );
}
bool CHudCloseCaption::AddFileToCaptionDictionary( const char *filename )
{
int searchPathLen = filesystem->GetSearchPath( "GAME", true, NULL, 0 );
char *searchPaths = (char *)stackalloc( searchPathLen + 1 );
filesystem->GetSearchPath( "GAME", true, searchPaths, searchPathLen );
bool bAddedCaptions = false;
for ( char *path = strtok( searchPaths, ";" ); path; path = strtok( NULL, ";" ) )
{
if ( IsGameConsole() && ( filesystem->GetDVDMode() == DVDMODE_STRICT ) && !V_stristr( path, ".zip" ) )
{
// only want zip paths
continue;
}
if ( IsPS3() && !V_stristr( path, ".zip" ) )
{
// PS3 cannot convert dvddev subtitles files, must be in the zip
Warning( "Client: skipped dvddev caption file: %s\n", filename );
continue;
}
char fullpath[MAX_PATH];
Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", path, filename );
Q_FixSlashes( fullpath );
Q_strlower( fullpath );
if ( IsGameConsole() )
{
char fullpath360[MAX_PATH];
UpdateOrCreateCaptionFile( fullpath, fullpath360, sizeof( fullpath360 ) );
Q_strncpy( fullpath, fullpath360, sizeof( fullpath ) );
}
int idx = m_AsyncCaptions.AddToTail();
AsyncCaption_t& entry = m_AsyncCaptions[ idx ];
if ( !entry.LoadFromFile( fullpath ) )
{
m_AsyncCaptions.Remove( idx );
}
else
{
DevMsg( "Client: added caption file: %s\n", fullpath );
bAddedCaptions = true;
}
}
return bAddedCaptions;
}
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 ];
if ( item )
{
item->OnDataArrived( m_AsyncCaptions, nFileIndex, nBlockNum, pData );
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudCloseCaption::Lock( void )
{
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 || IsGameConsole() )
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);
}
vgui::StringIndex_t i = g_pVGuiLocalize->GetFirstStringIndex();
while ( i != vgui::INVALID_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_FULLSCREEN_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_FULLSCREEN_HUDELEMENT( CHudCloseCaption );
if ( hudCloseCaption )
{
for ( int i = 0; i < count; ++i )
{
hudCloseCaption->PlayRandomCaption();
}
}
}
CON_COMMAND( cc_flush, "Flushes async'd captions." )
{
CHudCloseCaption *hudCloseCaption = GET_FULLSCREEN_HUDELEMENT( CHudCloseCaption );
if ( hudCloseCaption )
{
hudCloseCaption->Flush();
}
}
CON_COMMAND( cc_showblocks, "Toggles showing which blocks are pending/loaded async." )
{
CHudCloseCaption *hudCloseCaption = GET_FULLSCREEN_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 ( !IsGameConsole() )
{
g_pVGuiLocalize->AddFile( "resource/closecaption_%language%.txt", "GAME", true );
}
char uilanguage[ 64 ];
if (engine)
{
engine->GetUILanguage( uilanguage, sizeof( uilanguage ) );
}
else
{
Msg( "Unable to get default ui language, using \'english\'\n" );
Q_strcpy( uilanguage, "english" );
}
CHudCloseCaption *hudCloseCaption = GET_FULLSCREEN_HUDELEMENT( CHudCloseCaption );
if ( !hudCloseCaption )
return;
// 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 ( !IsGameConsole() )
{
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 );
}
}
hudCloseCaption->InitCaptionDictionary( var.GetString() );
}
else
{
hudCloseCaption->InitCaptionDictionary( uilanguage );
}
DevMsg( "cc_lang = %s\n", var.GetString() );
}
ConVar cc_lang( "cc_lang", "", FCVAR_ARCHIVE, "Current close caption language (emtpy = use game UI language)", OnCaptionLanguageChanged );
#if defined( _GAMECONSOLE )
// internal issued command evented by DLC mount, not meant for users
CON_COMMAND_F( cc_reload, "", 0 )
{
CHudCloseCaption *hudCloseCaption = GET_FULLSCREEN_HUDELEMENT( CHudCloseCaption );
if ( hudCloseCaption )
{
// minimal changes, TU DLC hack
// Clears a private store that otherwise prevents the cc's from a rull re-init, the language hasn't changed, but the underlying data has
// due to new search path mounts.
hudCloseCaption->ClearCurrentLanguage();
OnCaptionLanguageChanged( &cc_lang, cc_lang.GetString(), cc_lang.GetFloat() );
}
}
#endif
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_FULLSCREEN_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 );
Q_strlower( 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 ( LocalizeStringIndex_t r = g_pVGuiLocalize->GetFirstStringIndex(); r != vgui::INVALID_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;
}
}
void CHudCloseCaption::ClearCurrentLanguage()
{
m_CurrentLanguage = UTL_INVAL_SYMBOL;
}
void CHudCloseCaption::LoadColorMap( const char *pFilename )
{
CUtlBuffer colorMapBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER );
if ( !g_pFullFileSystem->ReadFile( pFilename, "MOD", colorMapBuffer ) )
return;
characterset_t breakSet;
CharacterSetBuild( &breakSet, "" );
for ( ;; )
{
char tagToken[MAX_PATH];
int nTokenSize = colorMapBuffer.ParseToken( &breakSet, tagToken, sizeof( tagToken ) );
if ( nTokenSize <= 0 )
{
break;
}
char colorToken[MAX_PATH];
nTokenSize = colorMapBuffer.ParseToken( &breakSet, colorToken, sizeof( colorToken ) );
if ( nTokenSize <= 0 )
{
break;
}
if ( !StringHasPrefix( colorToken, "<clr:" ) )
{
// unrecognized color format
continue;
}
int r, g, b;
if ( sscanf( colorToken + V_strlen( "<clr:" ), "%i,%i,%i", &r, &g, &b ) != 3 )
{
// bad color format
continue;
}
Color tagColor = Color( r, g, b, 255 );
int iIndex = m_ColorMap.Find( tagToken );
if ( iIndex == m_ColorMap.InvalidIndex() )
{
iIndex = m_ColorMap.Insert( tagToken );
}
m_ColorMap[iIndex] = tagColor;
}
}
bool CHudCloseCaption::FindColorForTag( wchar_t *pTag, Color &tagColor )
{
char buf[128];
g_pVGuiLocalize->ConvertUnicodeToANSI( pTag, buf, sizeof( buf ) );
int iIndex = m_ColorMap.Find( buf );
if ( iIndex != m_ColorMap.InvalidIndex() )
{
tagColor = m_ColorMap[iIndex];
return true;
}
// not found
return false;
}
void CHudCloseCaption::SetUseAsianWordWrapping()
{
static bool bCheckForAsianLanguage = false;
static bool bIsAsianLanguage = false;
if ( !bCheckForAsianLanguage )
{
bCheckForAsianLanguage = true;
const char *pLanguage = vgui::scheme()->GetLanguage();
if ( pLanguage )
{
if ( !V_stricmp( pLanguage, "japanese" ) ||
!V_stricmp( pLanguage, "schinese" ) ||
!V_stricmp( pLanguage, "tchinese" ) )
{
bIsAsianLanguage = true;
}
}
}
m_bUseAsianWordWrapping = bIsAsianLanguage;
}