//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "ivieweffects.h" #include "shake.h" #include "hud_macros.h" #include "isaverestore.h" #include "view_shared.h" #include "iviewrender.h" #include "viewrender.h" #include "con_nprint.h" #include "saverestoretypes.h" #include "c_rumble.h" // NVNT haptics interface system #include "haptics/ihaptics.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern IntroData_t *g_pIntroData; // Arbitrary limit so that bad entity logic on the server can't consume tons of memory on the client. #define MAX_SHAKES 32 //----------------------------------------------------------------------------- // Purpose: Screen fade variables //----------------------------------------------------------------------------- struct screenfade_t { float Speed; // How fast to fade (tics / second) (+ fade in, - fade out) float End; // When the fading hits maximum float Reset; // When to reset to not fading (for fadeout and hold) byte r, g, b, alpha; // Fade color int Flags; // Fading flags DECLARE_SIMPLE_DATADESC(); }; BEGIN_SIMPLE_DATADESC( screenfade_t ) DEFINE_FIELD( Speed, FIELD_FLOAT ), DEFINE_FIELD( End, FIELD_TIME ), DEFINE_FIELD( Reset, FIELD_TIME ), DEFINE_FIELD( r, FIELD_CHARACTER ), DEFINE_FIELD( g, FIELD_CHARACTER ), DEFINE_FIELD( b, FIELD_CHARACTER ), DEFINE_FIELD( alpha, FIELD_CHARACTER ), DEFINE_FIELD( Flags, FIELD_INTEGER ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: Screen shake variables //----------------------------------------------------------------------------- struct screenshake_t { float endtime; float duration; float amplitude; float frequency; float nextShake; Vector offset; float angle; int command; DECLARE_SIMPLE_DATADESC(); }; BEGIN_SIMPLE_DATADESC( screenshake_t ) DEFINE_FIELD( endtime, FIELD_TIME ), DEFINE_FIELD( duration, FIELD_FLOAT ), DEFINE_FIELD( amplitude, FIELD_FLOAT ), DEFINE_FIELD( frequency, FIELD_FLOAT ), DEFINE_FIELD( nextShake, FIELD_TIME ), DEFINE_FIELD( offset, FIELD_VECTOR ), DEFINE_FIELD( angle, FIELD_FLOAT ), END_DATADESC() void CC_Shake_Stop(); //----------------------------------------------------------------------------- // Purpose: Implements the view effects interface for the client .dll //----------------------------------------------------------------------------- class CViewEffects : public IViewEffects { public: ~CViewEffects() { ClearAllFades(); } virtual void Init( void ); virtual void LevelInit( void ); virtual void GetFadeParams( byte *r, byte *g, byte *b, byte *a, bool *blend ); virtual void CalcShake( void ); virtual void ApplyShake( Vector& origin, QAngle& angles, float factor ); virtual void Shake( ScreenShake_t &data ); virtual void Fade( ScreenFade_t &data ); virtual void ClearPermanentFades( void ); virtual void FadeCalculate( void ); virtual void ClearAllFades( void ); // Save / Restore virtual void Save( ISave *pSave ); virtual void Restore( IRestore *pRestore, bool fCreatePlayers ); private: void ClearAllShakes(); screenshake_t *FindLongestShake(); CUtlVector m_FadeList; CUtlVector m_ShakeList; Vector m_vecShakeAppliedOffset; float m_flShakeAppliedAngle; int m_FadeColorRGBA[4]; bool m_bModulate; friend void CC_Shake_Stop(); }; static CViewEffects g_ViewEffects; IViewEffects *vieweffects = ( IViewEffects * )&g_ViewEffects; // Callback function to call at end of screen m_Fade. static int s_nCallbackParameter; static void ( *s_pfnFadeDoneCallback )( int parm1 ); //----------------------------------------------------------------------------- // Purpose: // Input : *pszName - // iSize - // *pbuf - // Output : static int //----------------------------------------------------------------------------- void __MsgFunc_Shake( bf_read &msg ) { ScreenShake_t shake; shake.command = msg.ReadByte(); shake.amplitude = msg.ReadFloat(); shake.frequency = msg.ReadFloat(); shake.duration = msg.ReadFloat(); g_ViewEffects.Shake( shake ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pszName - // iSize - // *pbuf - // Output : static int //----------------------------------------------------------------------------- void __MsgFunc_Fade( bf_read &msg ) { ScreenFade_t fade; fade.duration = msg.ReadShort(); // fade lasts this long fade.holdTime = msg.ReadShort(); // fade lasts this long fade.fadeFlags = msg.ReadShort(); // fade type (in / out) fade.r = msg.ReadByte(); // fade red fade.g = msg.ReadByte(); // fade green fade.b = msg.ReadByte(); // fade blue fade.a = msg.ReadByte(); // fade blue g_ViewEffects.Fade( fade ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CViewEffects::Init( void ) { HOOK_MESSAGE( Shake ); HOOK_MESSAGE( Fade ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CViewEffects::LevelInit( void ) { ClearAllShakes(); ClearAllFades(); } static ConVar shake_show( "shake_show", "0", 0, "Displays a list of the active screen shakes." ); static ConCommand shake_stop("shake_stop", CC_Shake_Stop, "Stops all active screen shakes.\n", FCVAR_CHEAT ); //----------------------------------------------------------------------------- // Purpose: Stops all active screen shakes. //----------------------------------------------------------------------------- void CC_Shake_Stop() { g_ViewEffects.ClearAllShakes(); } //----------------------------------------------------------------------------- // Purpose: Apply noise to the eye position. // UNDONE: Feedback a bit of this into the view model position. It shakes too much //----------------------------------------------------------------------------- void CViewEffects::CalcShake( void ) { float fraction, freq; // We'll accumulate the aggregate shake for this frame into these data members. m_vecShakeAppliedOffset.Init(0, 0, 0); m_flShakeAppliedAngle = 0; float flRumbleAngle = 0; // NVNT - haptic shake effect amplitude float hapticShakeAmp = 0; bool bShow = shake_show.GetBool(); int nShakeCount = m_ShakeList.Count(); for ( int nShake = nShakeCount - 1; nShake >= 0; nShake-- ) { screenshake_t *pShake = m_ShakeList.Element( nShake ); if ( pShake->endtime == 0 ) { // Shouldn't be any such shakes in the list. Assert( false ); continue; } if ( ( gpGlobals->curtime > pShake->endtime ) || pShake->duration <= 0 || pShake->amplitude <= 0 || pShake->frequency <= 0 ) { // Retire this shake. delete m_ShakeList.Element( nShake ); m_ShakeList.FastRemove( nShake ); continue; } if ( bShow ) { con_nprint_t np; np.time_to_live = 2.0f; np.fixed_width_font = true; np.color[0] = 1.0; np.color[1] = 0.8; np.color[2] = 0.1; np.index = nShake + 2; engine->Con_NXPrintf( &np, "%02d: dur(%8.2f) amp(%8.2f) freq(%8.2f)", nShake + 1, (double)pShake->duration, (double)pShake->amplitude, (double)pShake->frequency ); } if ( gpGlobals->curtime > pShake->nextShake ) { // Higher frequency means we recalc the extents more often and perturb the display again pShake->nextShake = gpGlobals->curtime + (1.0f / pShake->frequency); // Compute random shake extents (the shake will settle down from this) for (int i = 0; i < 3; i++ ) { pShake->offset[i] = random->RandomFloat( -pShake->amplitude, pShake->amplitude ); } pShake->angle = random->RandomFloat( -pShake->amplitude*0.25, pShake->amplitude*0.25 ); } // Ramp down amplitude over duration (fraction goes from 1 to 0 linearly with slope 1/duration) fraction = ( pShake->endtime - gpGlobals->curtime ) / pShake->duration; // Ramp up frequency over duration if ( fraction ) { freq = (pShake->frequency / fraction); } else { freq = 0; } // square fraction to approach zero more quickly fraction *= fraction; // Sine wave that slowly settles to zero float angle = gpGlobals->curtime * freq; if ( angle > 1e8 ) { angle = 1e8; } fraction = fraction * sin( angle ); if( pShake->command != SHAKE_START_NORUMBLE ) { // As long as this isn't a NO RUMBLE effect, then accumulate rumble flRumbleAngle += pShake->angle * fraction; } if( pShake->command != SHAKE_START_RUMBLEONLY ) { // As long as this isn't a RUMBLE ONLY effect, then accumulate screen shake // Add to view origin m_vecShakeAppliedOffset += pShake->offset * fraction; // Add to roll m_flShakeAppliedAngle += pShake->angle * fraction; } // Drop amplitude a bit, less for higher frequency shakes pShake->amplitude -= pShake->amplitude * ( gpGlobals->frametime / (pShake->duration * pShake->frequency) ); // NVNT - update our amplitude. hapticShakeAmp += pShake->amplitude*fraction; } // NVNT - apply our screen shake update if ( haptics ) haptics->SetShake(hapticShakeAmp,1); // Feed this to the rumble system! UpdateScreenShakeRumble( flRumbleAngle ); } //----------------------------------------------------------------------------- // Purpose: Apply the current screen shake to this origin/angles. Factor is the amount to apply // This is so you can blend in part of the shake // Input : origin - // angles - // factor - //----------------------------------------------------------------------------- void CViewEffects::ApplyShake( Vector& origin, QAngle& angles, float factor ) { VectorMA( origin, factor, m_vecShakeAppliedOffset, origin ); angles.z += m_flShakeAppliedAngle * factor; } //----------------------------------------------------------------------------- // Purpose: Zeros out all active screen shakes. //----------------------------------------------------------------------------- void CViewEffects::ClearAllShakes() { int nShakeCount = m_ShakeList.Count(); for ( int i = 0; i < nShakeCount; i++ ) { delete m_ShakeList.Element( i ); } m_ShakeList.Purge(); } //----------------------------------------------------------------------------- // Purpose: Returns the shake with the longest duration. This is the shake we // use anytime we get an amplitude or frequency command, because the // most likely case is that we're modifying a shake with a long // duration rather than a brief shake caused by an explosion, etc. //----------------------------------------------------------------------------- screenshake_t *CViewEffects::FindLongestShake() { screenshake_t *pLongestShake = NULL; int nShakeCount = m_ShakeList.Count(); for ( int i = 0; i < nShakeCount; i++ ) { screenshake_t *pShake = m_ShakeList.Element( i ); if ( pShake && ( !pLongestShake || ( pShake->duration > pLongestShake->duration ) ) ) { pLongestShake = pShake; } } return pLongestShake; } //----------------------------------------------------------------------------- // Purpose: Message hook to parse ScreenShake messages // Input : pszName - // iSize - // pbuf - // Output : //----------------------------------------------------------------------------- void CViewEffects::Shake( ScreenShake_t &data ) { if ( ( data.command == SHAKE_START || data.command == SHAKE_START_RUMBLEONLY ) && ( m_ShakeList.Count() < MAX_SHAKES ) ) { screenshake_t *pNewShake = new screenshake_t; pNewShake->amplitude = data.amplitude; pNewShake->frequency = data.frequency; pNewShake->duration = data.duration; pNewShake->nextShake = 0; pNewShake->endtime = gpGlobals->curtime + data.duration; pNewShake->command = data.command; m_ShakeList.AddToTail( pNewShake ); } else if ( data.command == SHAKE_STOP) { ClearAllShakes(); } else if ( data.command == SHAKE_AMPLITUDE ) { // Look for the most likely shake to modify. screenshake_t *pShake = FindLongestShake(); if ( pShake ) { pShake->amplitude = data.amplitude; } } else if ( data.command == SHAKE_FREQUENCY ) { // Look for the most likely shake to modify. screenshake_t *pShake = FindLongestShake(); if ( pShake ) { pShake->frequency = data.frequency; } } } //----------------------------------------------------------------------------- // Purpose: Message hook to parse ScreenFade messages // Input : *pszName - // iSize - // *pbuf - // Output : int //----------------------------------------------------------------------------- void CViewEffects::Fade( ScreenFade_t &data ) { // Create a new fade and append it to the list screenfade_t *pNewFade = new screenfade_t; pNewFade->End = data.duration * (1.0f/(float)(1<Reset = data.holdTime * (1.0f/(float)(1<r = data.r; pNewFade->g = data.g; pNewFade->b = data.b; pNewFade->alpha = data.a; pNewFade->Flags = data.fadeFlags; pNewFade->Speed = 0; // Calc fade speed if ( data.duration > 0 ) { if ( data.fadeFlags & FFADE_OUT ) { if ( pNewFade->End ) { pNewFade->Speed = -(float)pNewFade->alpha / pNewFade->End; } pNewFade->End += gpGlobals->curtime; pNewFade->Reset += pNewFade->End; } else { if ( pNewFade->End ) { pNewFade->Speed = (float)pNewFade->alpha / pNewFade->End; } pNewFade->Reset += gpGlobals->curtime; pNewFade->End += pNewFade->Reset; } } if ( data.fadeFlags & FFADE_PURGE ) { ClearAllFades(); } m_FadeList.AddToTail( pNewFade ); } //----------------------------------------------------------------------------- // Purpose: Compute the overall color & alpha of the fades //----------------------------------------------------------------------------- void CViewEffects::FadeCalculate( void ) { // Cycle through all fades and remove any that have finished (work backwards) int i; int iSize = m_FadeList.Size(); for (i = iSize-1; i >= 0; i-- ) { screenfade_t *pFade = m_FadeList[i]; // Keep pushing reset time out indefinitely if ( pFade->Flags & FFADE_STAYOUT ) { pFade->Reset = gpGlobals->curtime + 0.1; } // All done? if ( ( gpGlobals->curtime > pFade->Reset ) && ( gpGlobals->curtime > pFade->End ) ) { // User passed in a callback function, call it now if ( s_pfnFadeDoneCallback ) { s_pfnFadeDoneCallback( s_nCallbackParameter ); s_pfnFadeDoneCallback = NULL; s_nCallbackParameter = 0; } // Remove this Fade from the list m_FadeList.FindAndRemove( pFade ); delete pFade; } } m_bModulate = false; m_FadeColorRGBA[0] = m_FadeColorRGBA[1] = m_FadeColorRGBA[2] = m_FadeColorRGBA[3] = 0; // Cycle through all fades in the list and calculate the overall color/alpha for ( i = 0; i < m_FadeList.Size(); i++ ) { screenfade_t *pFade = m_FadeList[i]; // Color m_FadeColorRGBA[0] += pFade->r; m_FadeColorRGBA[1] += pFade->g; m_FadeColorRGBA[2] += pFade->b; // Fading... int iFadeAlpha; if ( pFade->Flags & (FFADE_OUT|FFADE_IN) ) { iFadeAlpha = pFade->Speed * ( pFade->End - gpGlobals->curtime ); if ( pFade->Flags & FFADE_OUT ) { iFadeAlpha += pFade->alpha; } iFadeAlpha = MIN( iFadeAlpha, pFade->alpha ); iFadeAlpha = MAX( 0, iFadeAlpha ); } else { iFadeAlpha = pFade->alpha; } // Use highest alpha if ( iFadeAlpha > m_FadeColorRGBA[3] ) { m_FadeColorRGBA[3] = iFadeAlpha; } // Modulate? if ( pFade->Flags & FFADE_MODULATE ) { m_bModulate = true; } } // Divide colors if ( m_FadeList.Size() ) { m_FadeColorRGBA[0] /= m_FadeList.Size(); m_FadeColorRGBA[1] /= m_FadeList.Size(); m_FadeColorRGBA[2] /= m_FadeList.Size(); } } //----------------------------------------------------------------------------- // Purpose: Clear only the permanent fades in our fade list //----------------------------------------------------------------------------- void CViewEffects::ClearPermanentFades( void ) { int iSize = m_FadeList.Size(); for (int i = iSize-1; i >= 0; i-- ) { screenfade_t *pFade = m_FadeList[i]; if ( pFade->Flags & FFADE_STAYOUT ) { // Destroy this fade m_FadeList.FindAndRemove( pFade ); delete pFade; } } } //----------------------------------------------------------------------------- // Purpose: Purge & delete all fades in the queue //----------------------------------------------------------------------------- void CViewEffects::ClearAllFades( void ) { int iSize = m_FadeList.Size(); for (int i = iSize-1; i >= 0; i-- ) { delete m_FadeList[i]; } m_FadeList.Purge(); } //----------------------------------------------------------------------------- // Purpose: // Input : context - Which call to Render is this ( CViewSetup::context ) // *r - // *g - // *b - // *a - // *blend - //----------------------------------------------------------------------------- void CViewEffects::GetFadeParams( byte *r, byte *g, byte *b, byte *a, bool *blend ) { // If the intro is overriding our fade, use that instead if ( g_pIntroData && g_pIntroData->m_flCurrentFadeColor[3] ) { *r = g_pIntroData->m_flCurrentFadeColor[0]; *g = g_pIntroData->m_flCurrentFadeColor[1]; *b = g_pIntroData->m_flCurrentFadeColor[2]; *a = g_pIntroData->m_flCurrentFadeColor[3]; *blend = false; return; } FadeCalculate(); *r = m_FadeColorRGBA[0]; *g = m_FadeColorRGBA[1]; *b = m_FadeColorRGBA[2]; *a = m_FadeColorRGBA[3]; *blend = m_bModulate; } //----------------------------------------------------------------------------- // Purpose: // Input : *pSave - //----------------------------------------------------------------------------- void CViewEffects::Save( ISave *pSave ) { // Save the view fades int iCount = m_FadeList.Count(); pSave->WriteInt( &iCount ); for ( int i = 0; i < iCount; i++ ) { pSave->StartBlock(); pSave->WriteAll( m_FadeList[i] ); pSave->EndBlock(); } // Save the view shakes iCount = m_ShakeList.Count(); pSave->WriteInt( &iCount ); for ( int i = 0; i < iCount; i++ ) { pSave->StartBlock(); pSave->WriteAll( m_ShakeList[i] ); pSave->EndBlock(); } } //----------------------------------------------------------------------------- // Purpose: // Input : *pRestore - // fCreatePlayers - //----------------------------------------------------------------------------- void CViewEffects::Restore( IRestore *pRestore, bool fCreatePlayers ) { CGameSaveRestoreInfo *pSaveData = pRestore->GetGameSaveRestoreInfo(); // View effects is a singleton so we only need to restore it once, // from the level that we are going into. if( !pSaveData->levelInfo.fUseLandmark ) { ClearAllFades(); ClearAllShakes(); // Read in the view fades int iCount = pRestore->ReadInt(); for ( int i = 0; i < iCount; i++ ) { screenfade_t *pNewFade = new screenfade_t; pRestore->StartBlock(); pRestore->ReadAll( pNewFade ); pRestore->EndBlock(); m_FadeList.AddToTail( pNewFade ); } // Read in the view shakes iCount = pRestore->ReadInt(); for ( int i = 0; i < iCount; i++ ) { screenshake_t *pNewShake = new screenshake_t; pRestore->StartBlock(); pRestore->ReadAll( pNewShake ); pRestore->EndBlock(); m_ShakeList.AddToTail( pNewShake ); } } } //==================================================================================================== // CLIENTSIDE VIEW EFFECTS SAVE/RESTORE //==================================================================================================== static short VIEWEFFECTS_SAVE_RESTORE_VERSION = 2; class CViewEffectsSaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler { struct QueuedItem_t; public: CViewEffectsSaveRestoreBlockHandler() { } const char *GetBlockName() { return "ViewEffects"; } //--------------------------------- virtual void PreSave( CSaveRestoreData * ) { } //--------------------------------- virtual void Save( ISave *pSave ) { vieweffects->Save( pSave ); } //--------------------------------- virtual void WriteSaveHeaders( ISave *pSave ) { pSave->WriteShort( &VIEWEFFECTS_SAVE_RESTORE_VERSION ); } //--------------------------------- virtual void PostSave() { } //--------------------------------- virtual void PreRestore() { } //--------------------------------- virtual void ReadRestoreHeaders( IRestore *pRestore ) { // No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so. short version = pRestore->ReadShort(); m_bDoLoad = ( version == VIEWEFFECTS_SAVE_RESTORE_VERSION ); } //--------------------------------- virtual void Restore( IRestore *pRestore, bool fCreatePlayers ) { if ( m_bDoLoad ) { vieweffects->Restore( pRestore, fCreatePlayers ); } } //--------------------------------- virtual void PostRestore() { } private: bool m_bDoLoad; }; //----------------------------------------------------------------------------- CViewEffectsSaveRestoreBlockHandler g_ViewEffectsSaveRestoreBlockHandler; ISaveRestoreBlockHandler *GetViewEffectsRestoreBlockHandler() { return &g_ViewEffectsSaveRestoreBlockHandler; }