//========= Copyright Valve Corporation, All rights reserved. ============// // // TODO: // - Use a mempool // - Need to be able to gracefully turn replay ragdolls on/off // //---------------------------------------------------------------------------------------- #include "cbase.h" #if defined( REPLAY_ENABLED ) #include "replay_ragdoll.h" #include "tier1/mempool.h" #include "debugoverlay_shared.h" #include "filesystem.h" //-------------------------------------------------------------------------------- static matrix3x4_t gs_BoneCache[ MAXSTUDIOBONES ]; static int gs_nBytesAllocated = 0; //-------------------------------------------------------------------------------- void OnReplayCacheClientRagdollsCvarChange( IConVar *pVar, const char *pOldValue, float flOldValue ) { // TODO: need to be able to gracefully turn replay ragdolls on/off } //-------------------------------------------------------------------------------- static ConVar replay_ragdoll_dbg( "replay_ragdoll_dbg", "0", FCVAR_CLIENTDLL, "Display replay ragdoll debugging information." ); static ConVar replay_cache_client_ragdolls( "replay_cache_client_ragdolls", "0", FCVAR_CLIENTDLL, "Record ragdolls on the client during.", OnReplayCacheClientRagdollsCvarChange ); //-------------------------------------------------------------------------------- void DrawBones( matrix3x4_t const* pBones, int nNumBones, ragdoll_t const* pRagdoll, int nRed, int nGreen, int nBlue, C_BaseAnimating* pBaseAnimating ) { Assert( pBones ); Assert( pRagdoll ); Assert( pBaseAnimating ); Vector from, to; for ( int i = 0; i < nNumBones; ++i ) { // debugoverlay->AddCoordFrameOverlay( pBones[ i ], 3.0f ); int const iRagdollParentIndex = pRagdoll->list[ i ].parentIndex; if ( iRagdollParentIndex < 0 ) continue; int iBoneIndex = pRagdoll->boneIndex[ i ]; int iParentIndex = pRagdoll->boneIndex[ iRagdollParentIndex ]; MatrixPosition( pBones[ iParentIndex ], from ); MatrixPosition( pBones[ iBoneIndex ], to ); if ( debugoverlay ) { debugoverlay->AddLineOverlay( from, to, nRed, nGreen, nBlue, true, 0.0f ); } } } //-------------------------------------------------------------------------------- inline int GetServerTickCount() { int nTick = TIME_TO_TICKS( engine->GetLastTimeStamp() ); return nTick; } //-------------------------------------------------------------------------------- /*static*/ RagdollSimulationFrame_t* RagdollSimulationFrame_t::Alloc( int nNumBones ) { // TODO: use a mempool RagdollSimulationFrame_t* pNew = new RagdollSimulationFrame_t(); pNew->pPositions = new Vector[ nNumBones ]; pNew->pAngles = new QAngle[ nNumBones ]; gs_nBytesAllocated += sizeof( pNew ) + nNumBones * ( sizeof( Vector ) + sizeof( QAngle ) ); return pNew; } //-------------------------------------------------------------------------------- RagdollSimulationData_t::RagdollSimulationData_t( C_BaseAnimating* pEntity, int nStartTick, int nNumBones ) : m_pEntity( pEntity ), m_nEntityIndex( -1 ), m_nStartTick( nStartTick ), m_nNumBones( nNumBones ), m_nDuration( -1 ) { if ( pEntity ) { m_nEntityIndex = pEntity->entindex(); } Assert( nNumBones >= 0 && nNumBones < MAXSTUDIOBONES ); } bool _ComputeRagdollBones( const ragdoll_t *pRagdoll, matrix3x4_t &parentTransform, matrix3x4_t *pBones, Vector *pPositions, QAngle *pAngles ) { matrix3x4_t inverted, output; #ifdef _DEBUG CBitVec vBonesComputed; vBonesComputed.ClearAll(); #endif for ( int i = 0; i < pRagdoll->listCount; ++i ) { const ragdollelement_t& element = pRagdoll->list[ i ]; // during restore if a model has changed since the file was saved, this could be NULL if ( !element.pObject ) return false; int const boneIndex = pRagdoll->boneIndex[ i ]; if ( boneIndex < 0 ) { AssertMsg( 0, "Replay: No mapping for ragdoll bone\n" ); return false; } // Get global transform and put it into the bone cache element.pObject->GetPositionMatrix( &pBones[ boneIndex ] ); // Ensure a fixed translation from the parent (no stretching) if ( element.parentIndex >= 0 && !pRagdoll->allowStretch ) { int parentIndex = pRagdoll->boneIndex[ element.parentIndex ]; #ifdef _DEBUG // Make sure we computed the parent already Assert( vBonesComputed.IsBitSet(parentIndex) ); #endif // overwrite the position from physics to force rigid attachment // NOTE: On the client we actually override this with the proper parent bone in each LOD Vector out; VectorTransform( element.originParentSpace, pBones[ parentIndex ], out ); MatrixSetColumn( out, 3, pBones[ boneIndex ] ); MatrixInvert( pBones[ parentIndex ], inverted ); } else if ( element.parentIndex == - 1 ) { // Decompose into parent space MatrixInvert( parentTransform, inverted ); } #ifdef _DEBUG vBonesComputed.Set( boneIndex, true ); #endif // Compute local transform and put into 'output' ConcatTransforms( inverted, pBones[ boneIndex ], output ); // Cache as Euler/position MatrixAngles( output, pAngles[ i ], pPositions[ i ] ); } return true; } void RagdollSimulationData_t::Record() { Assert( m_pEntity->m_pRagdoll ); // Allocate a frame RagdollSimulationFrame_t* pNewFrame = RagdollSimulationFrame_t::Alloc( m_nNumBones ); if ( !pNewFrame ) return; // Set the current tick pNewFrame->nTick = GetServerTickCount(); // Add new frame to list of frames m_lstFrames.AddToTail( pNewFrame ); // Compute parent transform matrix3x4_t parentTransform; Vector vRootPosition = m_pEntity->GetRenderOrigin(); QAngle angRootAngles = m_pEntity->GetRenderAngles(); AngleMatrix( angRootAngles, vRootPosition, parentTransform ); // debugoverlay->AddCoordFrameOverlay( parentTransform, 100 ); // Cache off root position/orientation pNewFrame->vRootPosition = vRootPosition; pNewFrame->angRootAngles = angRootAngles; // Compute actual ragdoll bones matrix3x4_t* pBones = gs_BoneCache; _ComputeRagdollBones( m_pEntity->m_pRagdoll->GetRagdoll(), parentTransform, pBones, pNewFrame->pPositions, pNewFrame->pAngles ); // Draw bones if ( replay_ragdoll_dbg.GetBool() ) { DrawBones( pBones, m_pEntity->m_pRagdoll->RagdollBoneCount(), m_pEntity->m_pRagdoll->GetRagdoll(), 255, 0, 0, m_pEntity ); } } //-------------------------------------------------------------------------------- CReplayRagdollRecorder::CReplayRagdollRecorder() : m_bIsRecording(false) { } CReplayRagdollRecorder::~CReplayRagdollRecorder() { } /*static*/ CReplayRagdollRecorder& CReplayRagdollRecorder::Instance() { static CReplayRagdollRecorder s_instance; return s_instance; } void CReplayRagdollRecorder::Init() { Assert( !m_bIsRecording ); m_bIsRecording = true; gs_nBytesAllocated = 0; } void CReplayRagdollRecorder::Shutdown() { if ( !m_bIsRecording ) return; m_lstRagdolls.PurgeAndDeleteElements(); gs_nBytesAllocated = 0; // RemoveAll() purges, and there is no UnlinkAll() - is there an easier way to do this? Iterator_t i = m_lstRagdollsToRecord.Head(); while ( i != m_lstRagdollsToRecord.InvalidIndex() ) { m_lstRagdollsToRecord.Unlink( i ); i = m_lstRagdollsToRecord.Head(); } Assert( m_bIsRecording ); m_bIsRecording = false; } void CReplayRagdollRecorder::AddEntry( C_BaseAnimating* pEntity, int nStartTick, int nNumBones ) { DevMsg( "Replay: Processing Ragdoll at time %d\n", nStartTick ); Assert( pEntity ); RagdollSimulationData_t* pNewEntry = new RagdollSimulationData_t( pEntity, nStartTick, nNumBones ); gs_nBytesAllocated += sizeof( RagdollSimulationData_t ); m_lstRagdolls.AddToTail( pNewEntry ); // Also add to list of ragdolls to record m_lstRagdollsToRecord.AddToTail( pNewEntry ); } void CReplayRagdollRecorder::StopRecordingRagdoll( C_BaseAnimating* pEntity ) { Assert( pEntity ); // Find the entry in the recording list Iterator_t nIndex; if ( !FindEntryInRecordingList( pEntity, nIndex ) ) return; StopRecordingRagdollAtIndex( nIndex ); } void CReplayRagdollRecorder::StopRecordingRagdollAtIndex( Iterator_t nIndex ) { // No longer recording - compute duration RagdollSimulationData_t* pData = m_lstRagdollsToRecord[ nIndex ]; // Does duration need to be set? if ( pData->m_nDuration < 0 ) { pData->m_nDuration = GetServerTickCount() - pData->m_nStartTick; Assert( pData->m_nDuration > 0 ); } // Remove it from the recording list m_lstRagdollsToRecord.Unlink( nIndex ); } void CReplayRagdollRecorder::StopRecordingSleepingRagdolls() { Iterator_t i = m_lstRagdollsToRecord.Head(); while ( i != m_lstRagdollsToRecord.InvalidIndex() ) { if ( RagdollIsAsleep( *m_lstRagdollsToRecord[ i ]->m_pEntity->m_pRagdoll->GetRagdoll() ) ) { DevMsg( "entity %d: Removing sleeping ragdoll\n", m_lstRagdollsToRecord[ i ]->m_nEntityIndex ); StopRecordingRagdollAtIndex( i ); i = m_lstRagdollsToRecord.Head(); } else { i = m_lstRagdollsToRecord.Next( i ); } } } bool CReplayRagdollRecorder::FindEntryInRecordingList( C_BaseAnimating* pEntity, CReplayRagdollRecorder::Iterator_t& nOutIndex ) { // Find the entry FOR_EACH_LL( m_lstRagdollsToRecord, i ) { if ( m_lstRagdollsToRecord[ i ]->m_pEntity == pEntity ) { nOutIndex = i; return true; } } nOutIndex = m_lstRagdollsToRecord.InvalidIndex(); return false; } void CReplayRagdollRecorder::Record() { static ConVar* pReplayEnable = NULL; static bool bLookedForConvar = false; if ( bLookedForConvar ) { pReplayEnable = (ConVar*)cvar->FindVar( "replay_enable" ); bLookedForConvar = true; } if ( !pReplayEnable || !pReplayEnable->GetInt() ) return; if ( !replay_cache_client_ragdolls.GetInt() ) return; FOR_EACH_LL( m_lstRagdollsToRecord, i ) { Assert( m_lstRagdollsToRecord[ i ]->m_pEntity->IsRagdoll() ); m_lstRagdollsToRecord[ i ]->Record(); } } void CReplayRagdollRecorder::Think() { if ( !IsRecording() ) return; StopRecordingSleepingRagdolls(); Record(); PrintDebug(); } void CReplayRagdollRecorder::PrintDebug() { if ( !replay_ragdoll_dbg.GetInt() ) return; int nLine = 0; // Print memory usage engine->Con_NPrintf( nLine++, "ragdolls: %.2f MB", gs_nBytesAllocated / 1048576.0f ); // Print server time engine->Con_NPrintf( nLine++, "server time: %d", GetServerTickCount() ); ++nLine; // Blank line // Print info about each ragdoll FOR_EACH_LL( m_lstRagdolls, i ) { engine->Con_NPrintf( nLine++, "entity %d: start time=%d duration=%d num bones=%d", m_lstRagdolls[i]->m_nEntityIndex, m_lstRagdolls[i]->m_nStartTick, m_lstRagdolls[i]->m_nDuration, m_lstRagdolls[i]->m_nNumBones ); } } void CReplayRagdollRecorder::CleanupStartupTicksAndDurations( int nStartTick ) { FOR_EACH_LL( m_lstRagdolls, i ) { RagdollSimulationData_t* pRagdollData = m_lstRagdolls[ i ]; // Offset start tick with start tick, sent over from server pRagdollData->m_nStartTick -= nStartTick; Assert( pRagdollData->m_nStartTick >= 0 ); // Setup duration pRagdollData->m_nDuration = GetServerTickCount() - nStartTick; Assert( pRagdollData->m_nDuration > 0 ); // Go through all frames and subtract the start tick FOR_EACH_LL( pRagdollData->m_lstFrames, j ) { pRagdollData->m_lstFrames[ j ]->nTick -= nStartTick; } } } BEGIN_DMXELEMENT_UNPACK( RagdollSimulationData_t ) DMXELEMENT_UNPACK_FIELD( "nEntityIndex", "0", int, m_nEntityIndex ) DMXELEMENT_UNPACK_FIELD( "nStartTick", "0", int, m_nStartTick ) DMXELEMENT_UNPACK_FIELD( "nDuration", "0", int, m_nDuration ) DMXELEMENT_UNPACK_FIELD( "nNumBones", "0", int, m_nNumBones ) END_DMXELEMENT_UNPACK( RagdollSimulationData_t, s_RagdollSimulationDataUnpack ) bool CReplayRagdollRecorder::DumpRagdollsToDisk( char const* pFilename ) const { MEM_ALLOC_CREDIT(); DECLARE_DMX_CONTEXT(); CDmxElement* pSimulations = CreateDmxElement( "Simulations" ); CDmxElementModifyScope modify( pSimulations ); int const nNumRagdolls = m_lstRagdolls.Count(); pSimulations->SetValue( "iNumRagdolls", nNumRagdolls ); CDmxAttribute* pRagdolls = pSimulations->AddAttribute( "ragdolls" ); CUtlVector< CDmxElement* >& ragdolls = pRagdolls->GetArrayForEdit< CDmxElement* >(); modify.Release(); char name[32]; FOR_EACH_LL( m_lstRagdolls, i ) { RagdollSimulationData_t const* pData = m_lstRagdolls[ i ]; // Make sure we've setup all durations properly Assert( pData->m_nDuration >= 0 ); CDmxElement* pRagdoll = CreateDmxElement( "ragdoll" ); ragdolls.AddToTail( pRagdoll ); V_snprintf( name, sizeof(name), "ragdoll %d", i ); pRagdoll->SetValue( "name", name ); CDmxElementModifyScope modifyClass( pRagdoll ); pRagdoll->AddAttributesFromStructure( pData, s_RagdollSimulationDataUnpack ); CDmxAttribute* pFrames = pRagdoll->AddAttribute( "frames" ); CUtlVector< CDmxElement* >& frames = pFrames->GetArrayForEdit< CDmxElement* >(); FOR_EACH_LL( pData->m_lstFrames, j ) { CDmxElement* pFrame = CreateDmxElement( "frame" ); frames.AddToTail( pFrame ); V_snprintf( name, sizeof(name), "frame %d", j ); pFrame->SetValue( "name", name ); // Store tick pFrame->SetValue( "tick", pData->m_lstFrames[ j ]->nTick ); // Store root pos/orientation pFrame->SetValue( "root_pos" , pData->m_lstFrames[ j ]->vRootPosition ); pFrame->SetValue( "root_angles", pData->m_lstFrames[ j ]->angRootAngles ); for ( int k = 0; k < pData->m_nNumBones; ++k ) { CDmxAttribute* pPositions = pFrame->AddAttribute( "positions" ); CUtlVector< Vector >& positions = pPositions->GetArrayForEdit< Vector >(); CDmxAttribute* pAngles = pFrame->AddAttribute( "angles" ); CUtlVector< QAngle >& angles = pAngles->GetArrayForEdit< QAngle >(); positions.AddToTail( pData->m_lstFrames[ j ]->pPositions[ k ] ); angles.AddToTail( pData->m_lstFrames[ j ]->pAngles[ k ] ); } } } { MEM_ALLOC_CREDIT(); CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); if ( !SerializeDMX( buf, pSimulations, pFilename ) ) { Warning( "Replay: Failed to write ragdoll cache, %s.\n", pFilename ); return false; } // Write the file filesystem->WriteFile( pFilename, "MOD", buf ); } CleanupDMX( pSimulations ); Msg( "Replay: Cached ragdoll data.\n" ); return true; } //-------------------------------------------------------------------------------- CReplayRagdollCache::CReplayRagdollCache() : m_bInit( false ) { } /*static*/ CReplayRagdollCache& CReplayRagdollCache::Instance() { static CReplayRagdollCache s_instance; return s_instance; } bool CReplayRagdollCache::Init( char const* pFilename ) { Assert( !m_bInit ); // Make sure valid filename if ( !pFilename || pFilename[0] == 0 ) return false; DECLARE_DMX_CONTEXT(); // Attempt to read from disk CDmxElement* pRagdolls = NULL; if ( !UnserializeDMX( pFilename, "MOD", true, &pRagdolls ) ) // if ( !UnserializeDMX( pFilename, "GAME", false, &pRagdolls ) ) return false; CUtlVector< CDmxElement* > const& ragdolls = pRagdolls->GetArray< CDmxElement* >( "ragdolls" ); for ( int i = 0; i < ragdolls.Count(); ++i ) { CDmxElement* pCurRagdollInput = ragdolls[ i ]; // Create a new ragdoll entry and add to list RagdollSimulationData_t* pNewSimData = new RagdollSimulationData_t(); m_lstRagdolls.AddToTail( pNewSimData ); // Read pCurRagdollInput->UnpackIntoStructure( pNewSimData, sizeof( *pNewSimData ), s_RagdollSimulationDataUnpack ); // NOTE: Entity ptr doesn't get linked up here because it doesn't necessarily exist at this point // Read frames CUtlVector< CDmxElement* > const& frames = pCurRagdollInput->GetArray< CDmxElement* >( "frames" ); for ( int j = 0; j < frames.Count(); ++j ) { CDmxElement* pCurFrameInput = frames[ j ]; // Create a new frame and add it to list of frames RagdollSimulationFrame_t* pNewFrame = RagdollSimulationFrame_t::Alloc( pNewSimData->m_nNumBones ); pNewSimData->m_lstFrames.AddToTail( pNewFrame ); // Read tick pNewFrame->nTick = pCurFrameInput->GetValue( "tick", -1 ); Assert( pNewFrame->nTick != -1 ); // Read root pos/orientation pNewFrame->vRootPosition = pCurFrameInput->GetValue( "root_pos" , vec3_origin ); pNewFrame->angRootAngles = pCurFrameInput->GetValue( "root_angles", vec3_angle ); CUtlVector< Vector > const& positions = pCurFrameInput->GetArray< Vector >( "positions" ); CUtlVector< QAngle > const& angles = pCurFrameInput->GetArray< QAngle >( "angles" ); for ( int k = 0; k < pNewSimData->m_nNumBones; ++k ) { pNewFrame->pPositions[ k ] = positions[ k ]; pNewFrame->pAngles[ k ] = angles[ k ]; } } } // Cleanup CleanupDMX( pRagdolls ); m_bInit = true; return true; } void CReplayRagdollCache::Shutdown() { if ( !m_bInit ) return; m_lstRagdolls.PurgeAndDeleteElements(); m_bInit = false; } ConVar replay_ragdoll_blending( "replay_ragdoll_blending", "1", FCVAR_DEVELOPMENTONLY ); ConVar replay_ragdoll_tickoffset( "replay_ragdoll_tickoffset", "0", FCVAR_DEVELOPMENTONLY ); bool CReplayRagdollCache::GetFrame( C_BaseAnimating* pEntity, int nTick, bool* pBoneSimulated, CBoneAccessor* pBoneAccessor ) const { nTick += replay_ragdoll_tickoffset.GetInt(); Assert( pEntity ); Assert( pBoneSimulated ); Assert( pEntity->m_pRagdoll ); // Find ragdoll for the given entity - will return NULL if nTick is out of the entry's time window const RagdollSimulationData_t* pRagdollEntry = FindRagdollEntry( pEntity, nTick ); if ( !pRagdollEntry ) return false; // Find frame for the given tick RagdollSimulationFrame_t* pFrame; RagdollSimulationFrame_t* pNextFrame; if ( !FindFrame( pFrame, pNextFrame, pRagdollEntry, nTick ) ) return false; // Compute root transform matrix3x4_t rootTransform; float flInterpAmount = gpGlobals->interpolation_amount; if ( pNextFrame ) { AngleMatrix( (const QAngle &)Lerp( flInterpAmount, pFrame->angRootAngles, pNextFrame->angRootAngles ), // Actually does a slerp Lerp( flInterpAmount, pFrame->vRootPosition, pNextFrame->vRootPosition ), rootTransform ); } else { AngleMatrix( pFrame->angRootAngles, pFrame->vRootPosition, rootTransform ); } // Compute each bone ragdoll_t* pRagdoll = pEntity->m_pRagdoll->GetRagdoll(); Assert( pRagdoll ); for ( int k = 0; k < pRagdoll->listCount; ++k ) { int objectIndex = k; const ragdollelement_t& element = pRagdoll->list[ objectIndex ]; int const boneIndex = pRagdoll->boneIndex[ objectIndex ]; Assert( boneIndex >= 0 ); // Compute blended transform if possible matrix3x4_t localTransform; if ( pNextFrame && replay_ragdoll_blending.GetInt() ) { // Get blended Eular angles - NOTE: The Lerp() here actually calls Lerp() which converts to quats and back flInterpAmount = gpGlobals->interpolation_amount; Assert( flInterpAmount >= 0.0f && flInterpAmount <= 1.0f ); AngleMatrix( (const QAngle &)Lerp( flInterpAmount, pFrame->pAngles [ objectIndex ], pNextFrame->pAngles [ objectIndex ] ), Lerp( flInterpAmount, pFrame->pPositions[ objectIndex ], pNextFrame->pPositions[ objectIndex ] ), localTransform ); } else { // Last frame AngleMatrix( pFrame->pAngles[ objectIndex ], pFrame->pPositions[ objectIndex ], localTransform ); } matrix3x4_t& boneMatrix = pBoneAccessor->GetBoneForWrite( boneIndex ); if ( element.parentIndex < 0 ) { ConcatTransforms( rootTransform, localTransform, boneMatrix ); } else { int parentBoneIndex = pRagdoll->boneIndex[ element.parentIndex ]; Assert( parentBoneIndex >= 0 ); Assert( pBoneSimulated[ parentBoneIndex ] ); matrix3x4_t const& parentMatrix = pBoneAccessor->GetBone( parentBoneIndex ); ConcatTransforms( parentMatrix, localTransform, boneMatrix ); } // Simulated this bone pBoneSimulated[ boneIndex ] = true; } if ( replay_ragdoll_dbg.GetBool() ) { DrawBones( pBoneAccessor->GetBoneArrayForWrite(), pRagdollEntry->m_nNumBones, pRagdoll, 0, 0, 255, pEntity ); } return true; } RagdollSimulationData_t* CReplayRagdollCache::FindRagdollEntry( C_BaseAnimating* pEntity, int nTick ) { Assert( pEntity ); int const nEntIndex = pEntity->entindex(); FOR_EACH_LL( m_lstRagdolls, i ) { RagdollSimulationData_t* pRagdollData = m_lstRagdolls[ i ]; // If not the right entity or the tick is out range, continue. if ( pRagdollData->m_nEntityIndex != nEntIndex ) continue; // We've got the ragdoll, but only return it if nTick is in the window if ( nTick < pRagdollData->m_nStartTick || nTick > pRagdollData->m_nStartTick + pRagdollData->m_nDuration ) return NULL; return pRagdollData; } return NULL; } bool CReplayRagdollCache::FindFrame( RagdollSimulationFrame_t*& pFrameOut, RagdollSimulationFrame_t*& pNextFrameOut, const RagdollSimulationData_t* pRagdollEntry, int nTick ) { // Look for the appropriate frame FOR_EACH_LL( pRagdollEntry->m_lstFrames, j ) { RagdollSimulationFrame_t* pFrame = pRagdollEntry->m_lstFrames[ j ]; // Get next frame if possible int const nNext = pRagdollEntry->m_lstFrames.Next( j ); RagdollSimulationFrame_t* pNextFrame = nNext == pRagdollEntry->m_lstFrames.InvalidIndex() ? NULL : pRagdollEntry->m_lstFrames[ nNext ]; // Use this frame? if ( nTick >= pFrame->nTick && ( (pNextFrame && nTick <= pNextFrame->nTick) || !pNextFrame ) ) // Use the last frame if the tick is past the range of frames - { // this is the "sleeping" ragdoll frame pFrameOut = pFrame; pNextFrameOut = pNextFrame; return true; } } pFrameOut = NULL; pNextFrameOut = NULL; return false; } void CReplayRagdollCache::Think() { // TODO: Add IsPlayingReplayDemo() to engine interface /* engine->Con_NPrintf( 8, "time: %d", engine->GetDemoPlaybackTick() ); FOR_EACH_LL( m_lstRagdolls, i ) { engine->Con_NPrintf( 10 + i, "entity %d: start time=%d duration=%d num bones=%d", m_lstRagdolls[i]->m_nEntityIndex, m_lstRagdolls[i]->m_nStartTick, m_lstRagdolls[i]->m_nDuration, m_lstRagdolls[i]->m_nNumBones ); } */ } //-------------------------------------------------------------------------------- bool Replay_CacheRagdolls( const char* pFilename, int nStartTick ) { CReplayRagdollRecorder::Instance().CleanupStartupTicksAndDurations( nStartTick ); return CReplayRagdollRecorder::Instance().DumpRagdollsToDisk( pFilename ); } #endif