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.
2893 lines
85 KiB
2893 lines
85 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "tf_shareddefs.h" |
|
#include "tf_playermodelpanel.h" |
|
#include "tf_classdata.h" |
|
#include "tf_item_inventory.h" |
|
#include "vgui/IVGui.h" |
|
#include "game_item_schema.h" |
|
#include "econ_item_system.h" |
|
#include "animation.h" |
|
#include "choreoscene.h" |
|
#include "choreoevent.h" |
|
#include "choreoactor.h" |
|
#include "choreochannel.h" |
|
#include "scenefilecache/ISceneFileCache.h" |
|
#include "c_sceneentity.h" |
|
#include "c_baseflex.h" |
|
#include "sentence.h" |
|
#include "engine/IEngineSound.h" |
|
#include "c_tf_player.h" |
|
#include "tier2/renderutils.h" |
|
#include "bone_setup.h" |
|
#include "halloween/tf_weapon_spellbook.h" |
|
#include "matsys_controls/matsyscontrols.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include <tier0/memdbgon.h> |
|
|
|
DECLARE_BUILD_FACTORY( CTFPlayerModelPanel ); |
|
|
|
char g_szSceneTmpName[256]; |
|
|
|
static bool IsTauntItem( GameItemDefinition_t *pItemDef, const int iTeam, const int iClass, const char **ppSequence = NULL, const char **ppRequiredItem = NULL, const char **ppScene = NULL ) |
|
{ |
|
CTFTauntInfo *pTauntData = pItemDef->GetTauntData(); |
|
if ( pTauntData ) |
|
{ |
|
if ( ppScene ) |
|
{ |
|
int iTauntIndex = RandomInt( 0, pTauntData->GetIntroSceneCount( iClass ) - 1 ); |
|
*ppScene = pTauntData->GetIntroScene( iClass, iTauntIndex ); |
|
} |
|
|
|
if ( ppRequiredItem ) |
|
{ |
|
*ppRequiredItem = pTauntData->GetProp( iClass ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
for ( int i=0; i<pItemDef->GetNumAnimations( iTeam ); ++i ) |
|
{ |
|
animation_on_wearable_t* pAnim = pItemDef->GetAnimationData( iTeam, i ); |
|
if ( pAnim && pAnim->pszActivity && !Q_stricmp( pAnim->pszActivity, "taunt_concept" ) ) |
|
{ |
|
// If we have a scene, use it first |
|
const char *pszScene = pAnim->pszScene; |
|
if ( pszScene && (iClass >= TF_FIRST_NORMAL_CLASS && iClass < TF_LAST_NORMAL_CLASS) ) |
|
{ |
|
if ( ppScene ) |
|
{ |
|
Q_snprintf( g_szSceneTmpName, sizeof(g_szSceneTmpName), "scenes/player/%s/low/%s", g_aPlayerClassNames_NonLocalized[iClass], pszScene ); |
|
*ppScene = g_szSceneTmpName; |
|
} |
|
} |
|
|
|
const char *pszSequence = pAnim->pszSequence; |
|
if ( pszSequence ) |
|
{ |
|
if ( ppSequence ) |
|
{ |
|
*ppSequence = pszSequence; |
|
} |
|
if ( ppRequiredItem ) |
|
{ |
|
*ppRequiredItem = pAnim->pszRequiredItem; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CTFPlayerModelPanel::CTFPlayerModelPanel( vgui::Panel *pParent, const char *pName ) : BaseClass( pParent, pName ), |
|
m_LocalToGlobal( 0, 0, FlexSettingLessFunc ) |
|
{ |
|
m_iCurrentClassIndex = TF_CLASS_UNDEFINED; |
|
m_iCurrentSlotIndex = -1; |
|
|
|
m_nBody = 0; |
|
m_pHeldItem = NULL; |
|
m_iTeam = TF_TEAM_RED; |
|
m_bZoomedToHead = false; |
|
m_pszVCD = NULL; |
|
m_pszWeaponEntityRequired = NULL; |
|
m_bLoopVCD = true; |
|
m_bVCDFileNameOnly = true; |
|
|
|
InitPhonemeMappings(); |
|
|
|
m_pScene = NULL; |
|
ClearScene(); |
|
memset( m_flexWeight, 0, sizeof( m_flexWeight ) ); |
|
|
|
SetIgnoreDoubleClick( true ); |
|
|
|
for ( int i = 0; i < ARRAYSIZE( m_aParticleSystems ); i++ ) |
|
{ |
|
m_aParticleSystems[i] = NULL; |
|
} |
|
|
|
m_bPlaySparks = false; |
|
m_pszEyeGlowParticleName[0] = '\0'; |
|
m_bDrawActionSlotEffects = false; |
|
m_bDrawTauntParticles = false; |
|
m_bIsRobot = false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CTFPlayerModelPanel::~CTFPlayerModelPanel( void ) |
|
{ |
|
m_vecItemsLoaded.PurgeAndDeleteElements(); |
|
m_ItemsToCarry.PurgeAndDeleteElements(); |
|
|
|
for ( int i = 0; i < ARRAYSIZE( m_aParticleSystems ); i++ ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[i] ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ApplySettings( KeyValues *inResourceData ) |
|
{ |
|
BaseClass::ApplySettings( inResourceData ); |
|
|
|
m_angPlayerOrg = m_angPlayer; |
|
|
|
static ConVarRef cl_hud_minmode( "cl_hud_minmode", true ); |
|
if ( cl_hud_minmode.IsValid() && cl_hud_minmode.GetBool() ) |
|
{ |
|
inResourceData->ProcessResolutionKeys( "_minmode" ); |
|
} |
|
|
|
// custom class data |
|
m_customClassData.Purge(); |
|
KeyValues *pCustomData = inResourceData->FindKey( "customclassdata" ); |
|
if ( pCustomData ) |
|
{ |
|
for ( KeyValues *pData = pCustomData->GetFirstSubKey(); pData != NULL; pData = pData->GetNextKey() ) |
|
{ |
|
CustomClassData_t data; |
|
data.m_flFOV = pData->GetFloat( "fov" ); |
|
data.m_vPosition.Init( pData->GetFloat( "origin_x" ), pData->GetFloat( "origin_y" ), pData->GetFloat( "origin_z" ) ); |
|
data.m_vAngles.Init( pData->GetFloat( "angles_x" ), pData->GetFloat( "angles_y" ), pData->GetFloat( "angles_z" ) ); |
|
m_customClassData.AddToTail( data ); |
|
} |
|
|
|
Assert( m_customClassData.Count() == TF_LAST_NORMAL_CLASS ); |
|
} |
|
|
|
// always allow particle for this panel |
|
m_bUseParticle = true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SetToPlayerClass( int iClass, bool bIsRobot, bool bForceRefresh /*= false*/ ) |
|
{ |
|
if ( m_bIsRobot != bIsRobot ) |
|
{ |
|
bForceRefresh = true; |
|
} |
|
m_bIsRobot = bIsRobot; |
|
|
|
if ( m_iCurrentClassIndex == iClass && !bForceRefresh ) |
|
return; |
|
|
|
if ( m_bZoomedToHead ) |
|
{ |
|
ToggleZoom(); |
|
} |
|
|
|
m_iCurrentClassIndex = iClass; |
|
ClearScene(); |
|
|
|
if ( IsValidTFPlayerClass( m_iCurrentClassIndex ) ) |
|
{ |
|
if ( bIsRobot ) |
|
{ |
|
SetMDL( g_szPlayerRobotModels[ m_iCurrentClassIndex ] ); |
|
} |
|
else |
|
{ |
|
TFPlayerClassData_t *pData = GetPlayerClassData( m_iCurrentClassIndex ); |
|
SetMDL( pData->GetModelName() ); |
|
} |
|
|
|
HoldFirstValidItem(); |
|
|
|
// set custom class data |
|
if ( m_customClassData.IsValidIndex( m_iCurrentClassIndex ) ) |
|
{ |
|
SetCameraFOV( m_customClassData[m_iCurrentClassIndex].m_flFOV ); |
|
m_vecPlayerPos = m_customClassData[m_iCurrentClassIndex].m_vPosition; |
|
m_angPlayer = m_customClassData[m_iCurrentClassIndex].m_vAngles; |
|
} |
|
else |
|
{ |
|
m_angPlayer = m_angPlayerOrg; |
|
} |
|
} |
|
else |
|
{ |
|
SetMDL( MDLHANDLE_INVALID ); |
|
RemoveAdditionalModels(); |
|
} |
|
|
|
InitPhonemeMappings(); |
|
|
|
SetTeam( TF_TEAM_RED ); |
|
|
|
m_nBody = 0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::HoldFirstValidItem( void ) |
|
{ |
|
RemoveAdditionalModels(); |
|
|
|
if ( m_iCurrentClassIndex == TF_CLASS_UNDEFINED ) |
|
return; |
|
|
|
int iDesiredSlot = -1; |
|
|
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
bool bIsTauntItem = IsTauntItem( pItem->GetStaticData(), GetTeam(), m_iCurrentClassIndex ); |
|
if ( !bIsTauntItem ) |
|
{ |
|
if ( pItem->GetStaticData()->IsAWearable() ) |
|
continue; |
|
if ( pItem->GetAnimationSlot() == -2 ) |
|
continue; |
|
} |
|
|
|
// Found a weapon. Wield it. |
|
iDesiredSlot = pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ); |
|
break; |
|
} |
|
|
|
if ( iDesiredSlot != -1 ) |
|
{ |
|
UpdateHeldItem( iDesiredSlot ); |
|
return; |
|
} |
|
|
|
// If we didn't find a weapon to wield, we wield the class's base primary weapon |
|
CEconItemView *pItem = TFInventoryManager()->GetBaseItemForClass( m_iCurrentClassIndex, LOADOUT_POSITION_PRIMARY ); |
|
if ( !pItem || !pItem->IsValid() ) |
|
{ |
|
// Some classes only have secondary weapons. Fall back to that. |
|
pItem = TFInventoryManager()->GetBaseItemForClass( m_iCurrentClassIndex, LOADOUT_POSITION_SECONDARY ); |
|
} |
|
|
|
if ( pItem && pItem->IsValid() ) |
|
{ |
|
SwitchHeldItemTo( pItem ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::HoldItemInSlot( int iSlot ) |
|
{ |
|
if ( m_iCurrentClassIndex == TF_CLASS_UNDEFINED ) |
|
return false; |
|
|
|
return UpdateHeldItem( iSlot ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::HoldItem( int iItemNumber ) |
|
{ |
|
if ( m_iCurrentClassIndex == TF_CLASS_UNDEFINED ) |
|
return false; |
|
|
|
if ( iItemNumber >= m_ItemsToCarry.Count() ) |
|
return false; |
|
|
|
CEconItemView *pItem = m_ItemsToCarry[iItemNumber]; |
|
|
|
bool bIsTauntitem = IsTauntItem( pItem->GetStaticData(), GetTeam(), m_iCurrentClassIndex ); |
|
|
|
// Ignore requests to equip wearables, because they're always equipped |
|
// Also ignore requests to equip non-wearables that are never actively equipped |
|
if ( bIsTauntitem || ( !pItem->GetStaticData()->IsAWearable() && pItem->GetAnimationSlot() != -2 ) ) |
|
{ |
|
SwitchHeldItemTo( pItem ); |
|
return true; |
|
} |
|
|
|
// If we were trying to switch to a new item, and it's not valid, stick to our current |
|
if ( pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ) != m_iCurrentSlotIndex ) |
|
{ |
|
UpdateHeldItem( m_iCurrentSlotIndex ); |
|
return false; |
|
} |
|
|
|
// We were trying to stay on the current weapon, and it's not valid. Find anything. |
|
HoldFirstValidItem(); |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::UpdateHeldItem( int iDesiredSlot ) |
|
{ |
|
m_pHeldItem = NULL; |
|
|
|
CEconItemView *pItem = GetItemInSlot( iDesiredSlot ); |
|
if ( pItem ) |
|
{ |
|
bool bIsTauntItem = IsTauntItem( pItem->GetStaticData(), GetTeam(), m_iCurrentClassIndex ); |
|
// Ignore requests to equip wearables, because they're always equipped |
|
// Also ignore requests to equip non-wearables that are never actively equipped |
|
if ( bIsTauntItem || ( !pItem->GetStaticData()->IsAWearable() && pItem->GetAnimationSlot() != -2 ) ) |
|
{ |
|
SwitchHeldItemTo( pItem ); |
|
m_pHeldItem = pItem; |
|
return true; |
|
} |
|
} |
|
|
|
// If we were trying to switch to a new item, and it's not valid, stick to our current |
|
if ( iDesiredSlot != m_iCurrentSlotIndex ) |
|
{ |
|
UpdateHeldItem( m_iCurrentSlotIndex ); |
|
return false; |
|
} |
|
|
|
// We were trying to stay on the current weapon, and it's not valid. Find anything. |
|
HoldFirstValidItem(); |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ClearScene( void ) |
|
{ |
|
if ( m_pScene ) |
|
{ |
|
delete m_pScene; |
|
} |
|
|
|
m_pScene = NULL; |
|
m_flSceneTime = 0; |
|
m_flSceneEndTime = 0; |
|
m_flLastTickTime = 0; |
|
m_bLoopScene = true; |
|
//memset( m_flexWeight, 0, sizeof( m_flexWeight ) ); |
|
} |
|
|
|
extern CChoreoStringPool g_ChoreoStringPool; |
|
CChoreoScene *LoadSceneForModel( const char *filename, IChoreoEventCallback *pCallback, float *flSceneEndTime ) |
|
{ |
|
char loadfile[ 512 ]; |
|
V_strcpy_safe( loadfile, filename ); |
|
V_SetExtension( loadfile, ".vcd", sizeof( loadfile ) ); |
|
V_FixSlashes( loadfile ); |
|
|
|
char *pBuffer = NULL; |
|
size_t bufsize = scenefilecache->GetSceneBufferSize( loadfile ); |
|
if ( bufsize <= 0 ) |
|
return NULL; |
|
|
|
pBuffer = new char[ bufsize ]; |
|
if ( !scenefilecache->GetSceneData( filename, (byte *)pBuffer, bufsize ) ) |
|
{ |
|
delete[] pBuffer; |
|
return NULL; |
|
} |
|
|
|
CChoreoScene *pScene; |
|
if ( IsBufferBinaryVCD( pBuffer, bufsize ) ) |
|
{ |
|
pScene = new CChoreoScene( pCallback ); |
|
CUtlBuffer buf( pBuffer, bufsize, CUtlBuffer::READ_ONLY ); |
|
if ( !pScene->RestoreFromBinaryBuffer( buf, loadfile, &g_ChoreoStringPool ) ) |
|
{ |
|
Warning( "Unable to restore binary scene '%s'\n", loadfile ); |
|
delete pScene; |
|
pScene = NULL; |
|
} |
|
else |
|
{ |
|
pScene->SetPrintFunc( Scene_Printf ); |
|
pScene->SetEventCallbackInterface( pCallback ); |
|
} |
|
} |
|
else |
|
{ |
|
g_TokenProcessor.SetBuffer( pBuffer ); |
|
pScene = ChoreoLoadScene( loadfile, pCallback, &g_TokenProcessor, Scene_Printf ); |
|
} |
|
|
|
delete[] pBuffer; |
|
|
|
if ( flSceneEndTime != NULL ) |
|
{ |
|
// find the scene length |
|
// The scene is as long as the end point for the last event unless one of the events is a loop |
|
*flSceneEndTime = 0.0f; |
|
bool bSetEndTime = false; |
|
for ( int i = 0; i < pScene->GetNumEvents(); i++ ) |
|
{ |
|
CChoreoEvent *pEvent = pScene->GetEvent( i ); |
|
if ( pEvent->GetType() == CChoreoEvent::LOOP ) |
|
{ |
|
*flSceneEndTime = -1.0f; |
|
bSetEndTime = false; |
|
break; |
|
} |
|
|
|
if ( pEvent->GetEndTime() > *flSceneEndTime ) |
|
{ |
|
*flSceneEndTime = pEvent->GetEndTime(); |
|
bSetEndTime = true; |
|
} |
|
} |
|
|
|
if ( bSetEndTime ) |
|
{ |
|
*flSceneEndTime += 0.1f; // give time for lerp to idle pose |
|
} |
|
} |
|
|
|
return pScene; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::PlayVCD( const char *pszVCD, const char *pszWeaponEntityRequired /*= NULL*/, bool bLoopVCD /*= true*/, bool bFileNameOnly /*= true*/ ) |
|
{ |
|
m_pszVCD = pszVCD; |
|
m_pszWeaponEntityRequired = pszWeaponEntityRequired; |
|
m_bLoopVCD = bLoopVCD; |
|
m_bVCDFileNameOnly = bFileNameOnly; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::FireEvent( const char *pszEventName, const char *pszEventOptions ) |
|
{ |
|
//Plat_DebugString( CFmtStr( "********* ANIM EVENT: %s\n", pszEventName ) ); |
|
|
|
if ( V_strcmp( pszEventName, "AE_WPN_HIDE" ) == 0 ) |
|
{ |
|
int nWeaponIndex = GetMergeMDLIndex( static_cast<IClientRenderable*>(m_pHeldItem) ); |
|
if ( nWeaponIndex >= 0 ) |
|
{ |
|
m_aMergeMDLs[nWeaponIndex].m_bDisabled = true; |
|
} |
|
} |
|
else if ( V_strcmp( pszEventName, "AE_WPN_UNHIDE" ) == 0 ) |
|
{ |
|
int nWeaponIndex = GetMergeMDLIndex( static_cast<IClientRenderable*>(m_pHeldItem) ); |
|
if ( nWeaponIndex >= 0 ) |
|
{ |
|
m_aMergeMDLs[nWeaponIndex].m_bDisabled = false; |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SwitchHeldItemTo( CEconItemView *pItem ) |
|
{ |
|
m_nBody = 0; |
|
|
|
ClearScene(); |
|
|
|
// Clear out visible items, and re-equip out wearables |
|
RemoveAdditionalModels(); |
|
EquipAllWearables( pItem ); |
|
|
|
// Then equip the held item |
|
EquipItem( pItem ); |
|
m_iCurrentSlotIndex = pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ); |
|
m_pHeldItem = pItem; |
|
|
|
m_StatTrackModel.m_bDisabled = true; |
|
m_StatTrackModel.m_MDL.SetMDL( MDLHANDLE_INVALID ); |
|
CAttribute_String attrModule; |
|
static CSchemaAttributeDefHandle pAttr_module( "weapon_uses_stattrak_module" ); |
|
if ( m_pHeldItem->FindAttribute( pAttr_module, &attrModule ) && attrModule.has_value() ) |
|
{ |
|
// Allow for already strange items |
|
bool bIsStrange = false; |
|
if ( m_pHeldItem->GetQuality() == AE_STRANGE ) |
|
{ |
|
bIsStrange = true; |
|
} |
|
|
|
if ( !bIsStrange ) |
|
{ |
|
// Go over the attributes of the item, if it has any strange attributes the item is strange and don't apply |
|
for ( int i = 0; i < GetKillEaterAttrCount(); i++ ) |
|
{ |
|
if ( m_pHeldItem->FindAttribute( GetKillEaterAttr_Score( i ) ) ) |
|
{ |
|
bIsStrange = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if ( bIsStrange ) |
|
{ |
|
static CSchemaAttributeDefHandle pAttr_moduleScale( "weapon_stattrak_module_scale" ); |
|
// Does it have a stat track module |
|
m_flStatTrackScale = 1.0f; |
|
uint32 unFloatAsUint32 = 1; |
|
if ( m_pHeldItem->FindAttribute( pAttr_moduleScale, &unFloatAsUint32 ) ) |
|
{ |
|
m_flStatTrackScale = (float&)unFloatAsUint32; |
|
} |
|
|
|
MDLHandle_t hStatTrackMDL = mdlcache->FindMDL( "models/weapons/c_models/stattrack.mdl" ); |
|
if ( mdlcache->IsErrorModel( hStatTrackMDL ) ) |
|
{ |
|
hStatTrackMDL = MDLHANDLE_INVALID; |
|
} |
|
m_StatTrackModel.m_MDL.SetMDL( hStatTrackMDL ); |
|
mdlcache->Release( hStatTrackMDL ); // counterbalance addref from within FindMDL |
|
|
|
m_StatTrackModel.m_MDL.m_pProxyData = static_cast<IClientRenderable*>(pItem); |
|
m_StatTrackModel.m_bDisabled = false; |
|
m_StatTrackModel.m_MDL.m_nSequence = ACT_IDLE; |
|
SetIdentityMatrix( m_StatTrackModel.m_MDLToWorld ); |
|
} |
|
} |
|
|
|
SetSequenceLayers( NULL, 0 ); |
|
|
|
// See if our VCD is overridden |
|
if ( m_pszVCD ) |
|
{ |
|
// Make sure we're holding the weapon, if it's required |
|
bool bCanRunScene = true; |
|
if ( m_pszWeaponEntityRequired && *m_pszWeaponEntityRequired ) |
|
{ |
|
bCanRunScene = false; |
|
|
|
if ( pItem && pItem->IsValid() ) |
|
{ |
|
const char *pszClassName = pItem->GetStaticData()->GetItemClass(); |
|
if ( pszClassName && *pszClassName ) |
|
{ |
|
bCanRunScene = V_stricmp( pszClassName, m_pszWeaponEntityRequired ) == 0; |
|
} |
|
} |
|
} |
|
|
|
if ( bCanRunScene ) |
|
{ |
|
if ( m_bVCDFileNameOnly ) |
|
{ |
|
// auto complete relative path for the vcd file |
|
V_sprintf_safe( g_szSceneTmpName, "scenes/player/%s/low/%s", g_aPlayerClassNames_NonLocalized[m_iCurrentClassIndex], m_pszVCD ); |
|
} |
|
else |
|
{ |
|
// m_pszVCD should be a valid relative path |
|
V_strcpy_safe( g_szSceneTmpName, m_pszVCD ); |
|
} |
|
|
|
m_pScene = LoadSceneForModel( g_szSceneTmpName, this, &m_flSceneEndTime ); |
|
m_bLoopScene = m_bLoopVCD; |
|
|
|
return; |
|
} |
|
} |
|
|
|
const char *pScene = NULL; |
|
const char *pSequence = NULL; |
|
const char *pRequiredItem = NULL; |
|
bool bRemoveTauntParticles = true; |
|
|
|
if ( IsTauntItem( pItem->GetStaticData(), GetTeam(), m_iCurrentClassIndex, &pSequence, &pRequiredItem, &pScene ) ) |
|
{ |
|
MDLCACHE_CRITICAL_SECTION(); |
|
|
|
if ( pScene ) |
|
{ |
|
m_pScene = LoadSceneForModel( pScene, this, &m_flSceneEndTime ); |
|
|
|
// load custom prop for taunt |
|
const char *pszProp = pItem->GetStaticData()->GetTauntData()->GetProp( m_iCurrentClassIndex ); |
|
if ( pszProp ) |
|
{ |
|
LoadAndAttachAdditionalModel( pszProp, pItem ); |
|
} |
|
|
|
// force taunt to equip certain slot |
|
static CSchemaAttributeDefHandle pAttrDef_TauntForceWeaponSlot( "taunt force weapon slot" ); |
|
const char* pszTauntForceWeaponSlotName = NULL; |
|
if ( FindAttribute_UnsafeBitwiseCast<CAttribute_String>( pItem, pAttrDef_TauntForceWeaponSlot, &pszTauntForceWeaponSlotName ) ) |
|
{ |
|
int iForceWeaponSlot = StringFieldToInt( pszTauntForceWeaponSlotName, GetItemSchema()->GetWeaponTypeSubstrings() ); |
|
EquipRequiredLoadoutSlot( iForceWeaponSlot ); |
|
} |
|
} |
|
else |
|
{ |
|
ClearScene(); |
|
|
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
int iSequence = LookupSequence( &studioHdr, pSequence ); |
|
if ( iSequence >= 0 ) |
|
{ |
|
// does a weapon need to be equipped? |
|
loadout_positions_t requiredLoadoutItem = LOADOUT_POSITION_INVALID; |
|
if ( pRequiredItem ) |
|
{ |
|
requiredLoadoutItem = (loadout_positions_t)StringFieldToInt( pRequiredItem, ItemSystem()->GetItemSchema()->GetLoadoutStrings( pItem->GetItemDefinition()->GetEquipType() ) ); |
|
} |
|
|
|
EquipRequiredLoadoutSlot( requiredLoadoutItem ); |
|
|
|
// finally, set the sequence layers |
|
MDLSquenceLayer_t tmpSequenceLayers[1]; |
|
tmpSequenceLayers[0].m_nSequenceIndex = iSequence; |
|
tmpSequenceLayers[0].m_flWeight = 1.0; |
|
tmpSequenceLayers[0].m_bNoLoop = false; |
|
tmpSequenceLayers[0].m_flCycleBeganAt = m_RootMDL.m_MDL.m_flTime; |
|
SetSequenceLayers( tmpSequenceLayers, 1 ); |
|
} |
|
} |
|
|
|
// Taunt Particles |
|
static CSchemaAttributeDefHandle pAttrDef_OnTauntAttachParticleIndex( "on taunt attach particle index" ); |
|
uint32 unUnusualEffectIndex = 0; |
|
if ( pItem->FindAttribute( pAttrDef_OnTauntAttachParticleIndex, &unUnusualEffectIndex ) && unUnusualEffectIndex > 0 ) |
|
{ |
|
const attachedparticlesystem_t *pParticleSystem = GetItemSchema()->GetAttributeControlledParticleSystem( unUnusualEffectIndex ); |
|
if ( pParticleSystem ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[ SYSTEM_TAUNT ] ); |
|
m_aParticleSystems[ SYSTEM_TAUNT ] = CreateParticleData( pParticleSystem->pszSystemName ); |
|
m_flTauntParticleRefireTime = gpGlobals->curtime + pParticleSystem->fRefireTime; |
|
m_flTauntParticleRefireRate = pParticleSystem->fRefireTime; |
|
m_bDrawTauntParticles = true; |
|
bRemoveTauntParticles = false; |
|
} |
|
} |
|
} |
|
|
|
// Clear out taunt particles |
|
if ( bRemoveTauntParticles && m_aParticleSystems[SYSTEM_TAUNT] ) |
|
{ |
|
m_bDrawTauntParticles = false; |
|
SafeDeleteParticleData( &m_aParticleSystems[SYSTEM_TAUNT] ); |
|
} |
|
|
|
// Check if it has a PoseParameter Attributes (r_hand_grip) |
|
float flPose = 0; |
|
static CSchemaAttributeDefHandle pAttrDef_RightHandPose( "righthand pose parameter" ); |
|
uint32 iValue = 0; |
|
if ( pItem->FindAttribute( pAttrDef_RightHandPose, &iValue ) ) |
|
{ |
|
flPose = (float&)iValue; |
|
} |
|
SetPoseParameterByName( "r_hand_grip", flPose ); |
|
|
|
// Check for hand particles (spell book) |
|
// always nuke |
|
if ( m_aParticleSystems[ SYSTEM_ACTIONSLOT ] ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[ SYSTEM_ACTIONSLOT ] ); |
|
} |
|
m_bDrawActionSlotEffects = false; |
|
if ( pItem->GetStaticData()->GetItemClass() ) |
|
{ |
|
m_bDrawActionSlotEffects = FStrEq( pItem->GetStaticData()->GetItemClass(), "tf_weapon_spellbook" ); |
|
} |
|
|
|
// update eyeglows |
|
m_bUpdateEyeGlows = true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::EquipRequiredLoadoutSlot( int iRequiredLoadoutSlot ) |
|
{ |
|
if ( iRequiredLoadoutSlot != LOADOUT_POSITION_INVALID ) |
|
{ |
|
int iDesiredSlot = -1; |
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
if ( pItem->GetStaticData()->IsAWearable() ) |
|
continue; |
|
if ( pItem->GetAnimationSlot() == -2 ) |
|
continue; |
|
|
|
// Found a weapon. Wield it. |
|
if ( iRequiredLoadoutSlot == pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ) ) |
|
{ |
|
iDesiredSlot = i; |
|
break; |
|
} |
|
} |
|
|
|
if ( iDesiredSlot >= 0 ) |
|
{ |
|
EquipItem( m_ItemsToCarry[iDesiredSlot] ); |
|
} |
|
else |
|
{ |
|
// If we didn't find a weapon in the appropriate slot, get the base item |
|
CEconItemView *pWeapon = TFInventoryManager()->GetBaseItemForClass( m_iCurrentClassIndex, iRequiredLoadoutSlot ); |
|
if ( pWeapon && pWeapon->IsValid() ) |
|
{ |
|
EquipItem( pWeapon ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::UpdateWeaponBodygroups( bool bModifyDeployedOnlyBodygroups ) |
|
{ |
|
for ( int i=0; i<MAX_WEAPON_SLOTS; i++ ) |
|
{ |
|
CEconItemView *pItem = GetItemInSlot( i ); |
|
if ( !pItem ) |
|
continue; |
|
|
|
if ( pItem->GetStaticData()->GetHideBodyGroupsDeployedOnly() != bModifyDeployedOnlyBodygroups ) |
|
continue; |
|
|
|
if ( !(m_pHeldItem == pItem || !pItem->GetStaticData()->GetHideBodyGroupsDeployedOnly()) ) |
|
continue; |
|
|
|
UpdateHiddenBodyGroups( pItem ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::UpdateHiddenBodyGroups( CEconItemView* pItem ) |
|
{ |
|
MDLCACHE_CRITICAL_SECTION(); |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
int iNumBodyGroups = pItem->GetStaticData()->GetNumModifiedBodyGroups( 0 ); |
|
for ( int i=0; i<iNumBodyGroups; ++i ) |
|
{ |
|
int iState = 0; |
|
const char *pszBodyGroup = pItem->GetStaticData()->GetModifiedBodyGroup( 0, i, iState ); |
|
int iBodyGroup = FindBodygroupByName( &studioHdr, pszBodyGroup ); |
|
|
|
if ( iBodyGroup == -1 ) |
|
continue; |
|
|
|
::SetBodygroup( &studioHdr, m_nBody, iBodyGroup, iState ); |
|
SetBody( m_nBody ); |
|
} |
|
|
|
// Handle style-based bodygroups |
|
const CEconItemDefinition *pItemDef = pItem->GetItemDefinition(); |
|
const CEconStyleInfo *pStyle = pItemDef ? pItemDef->GetStyleInfo( pItem->GetStyle() ) : NULL; |
|
if ( pStyle ) |
|
{ |
|
FOR_EACH_VEC( pStyle->GetAdditionalHideBodygroups(), i ) |
|
{ |
|
int iBodyGroup = FindBodygroupByName( &studioHdr, pStyle->GetAdditionalHideBodygroups()[i] ); |
|
|
|
if ( iBodyGroup == -1 ) |
|
continue; |
|
|
|
::SetBodygroup( &studioHdr, m_nBody, iBodyGroup, 1 ); // force state to '1' here to mean hidden |
|
SetBody( m_nBody ); |
|
} |
|
} |
|
|
|
// Handle world model bodygroup overrides |
|
int iBodyOverride = pItem->GetStaticData()->GetWorldmodelBodygroupOverride( m_iTeam ); |
|
int iBodyStateOverride = pItem->GetStaticData()->GetWorldmodelBodygroupStateOverride( m_iTeam ); |
|
if ( iBodyOverride > -1 && iBodyStateOverride > -1 ) |
|
{ |
|
::SetBodygroup( &studioHdr, m_nBody, iBodyOverride, iBodyStateOverride ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CEconItemView *CTFPlayerModelPanel::GetItemInSlot( int iSlot ) |
|
{ |
|
CEconItemView *pOwnedItemInSlot = TFInventoryManager()->GetItemInLoadoutForClass( m_iCurrentClassIndex, iSlot ); |
|
|
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
int iLoadoutSlot = pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ); |
|
if ( iSlot == iLoadoutSlot ) |
|
return pItem; |
|
|
|
// GetLoadoutSlot will not work for misc2, taunt2-8 because it will always return misc/taunt |
|
if ( pOwnedItemInSlot && pOwnedItemInSlot->GetItemDefIndex() == pItem->GetItemDefIndex() ) |
|
return pItem; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::EquipAllWearables( CEconItemView *pHeldItem ) |
|
{ |
|
// First, reset all our bodygroups |
|
MDLCACHE_CRITICAL_SECTION(); |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
const CEconItemSchema::BodygroupStateMap_t& mapBodygroupState = GetItemSchema()->GetDefaultBodygroupStateMap(); |
|
|
|
FOR_EACH_MAP_FAST( mapBodygroupState, i ) |
|
{ |
|
const char *pszBodygroupName = mapBodygroupState.Key(i); |
|
int iBodyGroup = FindBodygroupByName( &studioHdr, pszBodygroupName ); |
|
if ( iBodyGroup > -1 ) |
|
{ |
|
int iState = mapBodygroupState[i]; |
|
::SetBodygroup( &studioHdr, m_nBody, iBodyGroup, iState ); |
|
} |
|
} |
|
|
|
SetBody( m_nBody ); |
|
|
|
UpdateWeaponBodygroups( false ); |
|
|
|
// Now equip each of our wearables |
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
// If it's a wearable item, we put it on. |
|
if ( pItem->GetStaticData()->IsAWearable() ) |
|
{ |
|
EquipItem( pItem ); |
|
} |
|
|
|
// Then see if there's an extra wearable we need to attach for this item |
|
const char *pszAttached = pItem->GetExtraWearableModel(); |
|
if ( pszAttached && pszAttached[ 0 ] ) |
|
{ |
|
const char *pszViewModelAttached = pItem->GetExtraWearableViewModel(); |
|
if ( pHeldItem == pItem || pszViewModelAttached == NULL || pszViewModelAttached[ 0 ] == '\0' || pszViewModelAttached[ 0 ] == '?' ) |
|
{ |
|
LoadAndAttachAdditionalModel( pszAttached, pItem ); |
|
} |
|
} |
|
} |
|
|
|
UpdateWeaponBodygroups( true ); |
|
|
|
SetBody( m_nBody ); |
|
|
|
UpdatePreviewVisuals(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::EquipItem( CEconItemView *pItem ) |
|
{ |
|
if ( m_iCurrentClassIndex == TF_CLASS_UNDEFINED ) |
|
return; |
|
|
|
const GameItemDefinition_t *pItemDef = pItem->GetItemDefinition(); |
|
Assert( pItemDef ); |
|
|
|
// Change team number so skins composite correctly |
|
pItem->SetTeamNumber( m_iTeam ); |
|
|
|
// Non wearables can modify the animation |
|
if ( !pItemDef->IsAWearable() ) |
|
{ |
|
int iAnimSlot = pItem->GetAnimationSlot(); |
|
|
|
// Ignore items that don't want to control player animation |
|
if ( iAnimSlot == -2 ) |
|
return; |
|
|
|
if ( iAnimSlot == -1 ) |
|
{ |
|
iAnimSlot = pItemDef->GetLoadoutSlot( m_iCurrentClassIndex ); |
|
} |
|
|
|
const CUtlVector<const char *>& vecWeaponTypeSubstrings = GetItemSchema()->GetWeaponTypeSubstrings(); |
|
if ( vecWeaponTypeSubstrings.IsValidIndex( iAnimSlot ) ) |
|
{ |
|
int iAnim = FindAnimByName( vecWeaponTypeSubstrings[iAnimSlot] ); |
|
SetModelAnim( iAnim ); |
|
} |
|
} |
|
|
|
// Attach the models for the item |
|
const char *pszAttached = pItem->GetWorldDisplayModel(); |
|
if ( !pszAttached ) |
|
{ |
|
pszAttached = pItem->GetPlayerDisplayModel( m_iCurrentClassIndex, m_iTeam ); |
|
} |
|
|
|
if ( pszAttached && pszAttached[0] ) |
|
{ |
|
LoadAndAttachAdditionalModel( pszAttached, pItem ); |
|
|
|
int iTeam = pItemDef->GetBestVisualTeamData( m_iTeam ); |
|
// Set attached models if viewable third-person. |
|
{ |
|
const int iNumAttachedModels = pItemDef->GetNumAttachedModels( iTeam ); |
|
for ( int i = 0; i < iNumAttachedModels; ++i ) |
|
{ |
|
attachedmodel_t *pModel = pItemDef->GetAttachedModelData( iTeam, i ); |
|
|
|
if ( !( pModel->m_iModelDisplayFlags & kAttachedModelDisplayFlag_WorldModel ) ) |
|
continue; |
|
|
|
if ( !pModel->m_pszModelName ) |
|
{ |
|
Warning( "econ item definition '%s' attachment (team %d idx %d) has no model\n", pItemDef->GetDefinitionName(), iTeam, i ); |
|
continue; |
|
} |
|
|
|
LoadAndAttachAdditionalModel( pModel->m_pszModelName, pItem ); |
|
} |
|
} |
|
|
|
// Festive |
|
// Set attached models if viewable third-person. |
|
static CSchemaAttributeDefHandle pAttr_is_festivized( "is_festivized" ); |
|
if ( pAttr_is_festivized && pItem->FindAttribute( pAttr_is_festivized ) ) |
|
{ |
|
const int iNumAttachedModels = pItemDef->GetNumAttachedModelsFestivized( iTeam ); |
|
for ( int i = 0; i < iNumAttachedModels; ++i ) |
|
{ |
|
attachedmodel_t *pModel = pItemDef->GetAttachedModelDataFestivized( iTeam, i ); |
|
|
|
if ( !( pModel->m_iModelDisplayFlags & kAttachedModelDisplayFlag_WorldModel ) ) |
|
continue; |
|
|
|
if ( !pModel->m_pszModelName ) |
|
{ |
|
Warning( "econ item definition '%s' attachment (team %d idx %d) has no model\n", pItemDef->GetDefinitionName(), iTeam, i ); |
|
continue; |
|
} |
|
|
|
LoadAndAttachAdditionalModel( pModel->m_pszModelName, pItem ); |
|
} |
|
} |
|
} |
|
|
|
// Hide any item associated groups. |
|
UpdateHiddenBodyGroups( pItem ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CTFPlayerModelPanel::AddCarriedItem( CEconItemView *pItem ) |
|
{ |
|
CEconItemView *pNewItem = new CEconItemView; |
|
*pNewItem = *pItem; |
|
int iIdx = m_ItemsToCarry.AddToTail( pNewItem ); |
|
|
|
// This is a terrible hack. If we have team paint, we need an entity to find out what team |
|
// we're on, but in this panel we don't have one. Instead, we force a flag all the way through |
|
// the system on the CEconItemView so that the low-level paint code can pull from it if necessary. |
|
if ( GetTeam() == TF_TEAM_BLUE ) |
|
{ |
|
pNewItem->SetClientItemFlags( kEconItemFlagClient_ForceBlueTeam ); |
|
} |
|
|
|
return iIdx; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ClearCarriedItems( void ) |
|
{ |
|
RemoveAdditionalModels(); |
|
m_ItemsToCarry.PurgeAndDeleteElements(); |
|
m_pHeldItem = NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::RemoveAdditionalModels( void ) |
|
{ |
|
ClearMergeMDLs(); |
|
|
|
// Unregister for all callbacks |
|
modelinfo->UnregisterModelLoadCallback( -1, this ); |
|
m_vecDynamicAssetsLoaded.Purge(); |
|
m_vecItemsLoaded.PurgeAndDeleteElements(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::LoadAndAttachAdditionalModel( const char *pMDLName, CEconItemView *pItem ) |
|
{ |
|
int nModelIndex = -1; |
|
|
|
if ( pItem->GetStaticData()->IsContentStreamable() ) |
|
{ |
|
// Get the client-only dynamic model index. The auto-addref |
|
// of vecDynamicAssetsLoaded will actually trigger the load. |
|
nModelIndex = modelinfo->RegisterDynamicModel( pMDLName, true ); |
|
// Dynamic models never fail to register in this engine. |
|
Assert( nModelIndex != -1 ); |
|
} |
|
else |
|
{ |
|
// Is the (non-streamable) model already precached? If so, use it. |
|
nModelIndex = modelinfo->GetModelIndex( pMDLName ); |
|
} |
|
|
|
if ( nModelIndex == -1 ) |
|
{ |
|
MDLHandle_t hMDL = vgui::MDLCache()->FindMDL( pMDLName ); |
|
Assert( hMDL != MDLHANDLE_INVALID ); |
|
if ( hMDL != MDLHANDLE_INVALID ) |
|
{ |
|
// Model not loaded, not dynamic. Hard load and exit out. |
|
SetMergeMDL( hMDL, static_cast<IClientRenderable*>(pItem), pItem->GetSkin( m_iTeam ) ); |
|
} |
|
m_MergeMDL = hMDL; |
|
return; |
|
} |
|
|
|
CEconItemView *pClone = new CEconItemView; |
|
*pClone = *pItem; |
|
m_vecDynamicAssetsLoaded[ m_vecDynamicAssetsLoaded.AddToTail() ] = nModelIndex; |
|
m_vecItemsLoaded.AddToTail( pClone ); |
|
|
|
// callback triggers immediately if not dynamic |
|
modelinfo->RegisterModelLoadCallback( nModelIndex, this, true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
static void SetMDLSkinForTeam( CMDL *pMDL, const CEconItemView *pItem, int iTeam ) |
|
{ |
|
Assert( pItem ); |
|
|
|
if ( !pMDL ) |
|
return; |
|
|
|
// Ask the item for a skin... |
|
int nSkin = pItem->GetSkin( iTeam ); |
|
|
|
if ( nSkin == -1 ) |
|
{ |
|
// ... if not, use the team skin. |
|
nSkin = iTeam == TF_TEAM_RED ? 0 : 1; |
|
} |
|
|
|
pMDL->m_nSkin = nSkin; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::OnModelLoadComplete( const model_t *pModel ) |
|
{ |
|
CEconItemView *pItem = NULL; |
|
FOR_EACH_VEC_BACK( m_vecDynamicAssetsLoaded, i ) |
|
{ |
|
if ( modelinfo->GetModel( m_vecDynamicAssetsLoaded[ i ] ) == pModel ) |
|
{ |
|
pItem = GetPreviewItem( m_vecItemsLoaded[ i ] ); |
|
break; |
|
} |
|
} |
|
|
|
Assert( pItem ); |
|
if ( pItem ) |
|
{ |
|
MDLHandle_t hMDL = modelinfo->GetCacheHandle( pModel ); |
|
Assert( hMDL != MDLHANDLE_INVALID ); |
|
if ( hMDL != MDLHANDLE_INVALID ) |
|
{ |
|
SetMergeMDL( hMDL, static_cast<IClientRenderable*>(pItem) ); |
|
|
|
int nBody = 0; |
|
if ( pItem->GetStaticData()->UsesPerClassBodygroups( m_iTeam ) ) |
|
{ |
|
CMDL *pMDL = GetMergeMDL(hMDL); |
|
if ( pMDL ) |
|
{ |
|
// Classes start at 1, bodygroups at 0, so we shift them all back 1. |
|
MDLCACHE_CRITICAL_SECTION(); |
|
CStudioHdr sHDR( pMDL->GetStudioHdr(), g_pMDLCache ); |
|
::SetBodygroup( &sHDR, nBody, 1, m_iCurrentClassIndex-1 ); |
|
pMDL->m_nBody = nBody; |
|
} |
|
} |
|
|
|
// Set the custom skin. |
|
SetMDLSkinForTeam( GetMergeMDL( hMDL ), pItem, m_iTeam ); |
|
} |
|
} |
|
} |
|
|
|
void CTFPlayerModelPanel::SetTeam( int iTeam ) |
|
{ |
|
m_iTeam = iTeam; |
|
|
|
UpdatePreviewVisuals(); |
|
} |
|
|
|
void CTFPlayerModelPanel::UpdatePreviewVisuals() |
|
{ |
|
// Assume skin will be chosen based only on the preview team |
|
int iSkin = m_iTeam == TF_TEAM_RED ? 0 : 1; |
|
|
|
// Check if any of the items we're carrying should override this |
|
static CSchemaAttributeDefHandle pAttrDef_PlayerSkinOverride( "player skin override" ); |
|
Assert( pAttrDef_PlayerSkinOverride ); |
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
if ( !pItem ) |
|
continue; |
|
float fSkinOverride = 0.0f; |
|
if ( FindAttribute_UnsafeBitwiseCast<attrib_value_t>( pItem, pAttrDef_PlayerSkinOverride, &fSkinOverride ) && fSkinOverride == 1.0f ) |
|
{ |
|
C_TFPlayer::AdjustSkinIndexForZombie( m_iCurrentClassIndex, iSkin ); |
|
break; |
|
} |
|
Assert( fSkinOverride == 0.0f ); |
|
} |
|
|
|
// Set the player model skin. |
|
SetSkin( iSkin ); |
|
|
|
// Set the weapon's skin. |
|
if ( m_MergeMDL && m_pHeldItem ) |
|
{ |
|
SetMDLSkinForTeam( GetMergeMDL( m_MergeMDL ), GetPreviewItem( m_pHeldItem ), m_iTeam ); |
|
} |
|
|
|
// Set the skin for all other equipped items (wearables, etc). |
|
for ( int i=0; i<m_vecDynamicAssetsLoaded.Count(); i++ ) |
|
{ |
|
const model_t *pModel = modelinfo->GetModel( m_vecDynamicAssetsLoaded[i] ); |
|
if ( pModel ) |
|
{ |
|
MDLHandle_t hMDL = modelinfo->GetCacheHandle( pModel ); |
|
|
|
// We're iterating over a list of the dynamic assets that we've completed streaming in, but |
|
// we want to set the style based on the "preview item" definition if possible. |
|
SetMDLSkinForTeam( GetMergeMDL( hMDL ), GetPreviewItem( m_vecItemsLoaded[i] ), m_iTeam ); |
|
} |
|
} |
|
} |
|
|
|
CEconItemView *CTFPlayerModelPanel::GetPreviewItem( CEconItemView *pMatchItem ) |
|
{ |
|
Assert( pMatchItem ); |
|
if ( !pMatchItem ) |
|
return NULL; |
|
|
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
if ( *pMatchItem == *pItem ) |
|
return pItem; |
|
} |
|
|
|
return pMatchItem; |
|
} |
|
|
|
int ClassZoomZ[] = |
|
{ |
|
0, |
|
20, // TF_CLASS_SCOUT, |
|
25, // TF_CLASS_SNIPER, |
|
20, // TF_CLASS_SOLDIER, |
|
22, // TF_CLASS_DEMOMAN, |
|
30, // TF_CLASS_MEDIC, |
|
30, // TF_CLASS_HEAVYWEAPONS, |
|
22, // TF_CLASS_PYRO, |
|
27, // TF_CLASS_SPY, |
|
20, // TF_CLASS_ENGINEER, |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ToggleZoom() |
|
{ |
|
m_bZoomedToHead = !m_bZoomedToHead; |
|
|
|
// NOTE: GetZoomOffset() relies on m_bZoomedToHead being up to date |
|
m_vecPlayerPos += GetZoomOffset(); |
|
|
|
SetModelAnglesAndPosition( m_angPlayer, m_vecPlayerPos ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
Vector CTFPlayerModelPanel::GetZoomOffset() |
|
{ |
|
const Vector vecOffset( 100, 0, ClassZoomZ[m_iCurrentClassIndex] ); |
|
return m_bZoomedToHead ? -vecOffset : vecOffset; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::PrePaint3D( IMatRenderContext *pRenderContext ) |
|
{ |
|
if ( g_PlayerPreviewEffect.GetEffect() == C_TFPlayerPreviewEffect::PREVIEW_EFFECT_UBER ) |
|
{ |
|
modelrender->ForcedMaterialOverride( *g_PlayerPreviewEffect.GetInvulnMaterialRef() ); |
|
} |
|
|
|
BaseClass::PrePaint3D( pRenderContext ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::PostPaint3D( IMatRenderContext *pRenderContext ) |
|
{ |
|
if ( g_PlayerPreviewEffect.GetEffect() == C_TFPlayerPreviewEffect::PREVIEW_EFFECT_UBER ) |
|
{ |
|
modelrender->ForcedMaterialOverride( NULL ); |
|
} |
|
|
|
static bool bAlternate = false; |
|
Vector vColor = bAlternate ? m_vEyeGlowColor1 : m_vEyeGlowColor2; |
|
bAlternate = !bAlternate; |
|
// Eye glows |
|
if ( m_aParticleSystems[ SYSTEM_EYEGLOW_RIGHT ] ) |
|
{ |
|
m_aParticleSystems[ SYSTEM_EYEGLOW_RIGHT ]->m_pParticleSystem->SetControlPoint( CUSTOM_COLOR_CP1, vColor ); |
|
} |
|
if ( m_aParticleSystems[ SYSTEM_EYESPARK_RIGHT ] ) |
|
{ |
|
m_aParticleSystems[ SYSTEM_EYESPARK_RIGHT ]->m_pParticleSystem->SetControlPoint( CUSTOM_COLOR_CP1, vColor ); |
|
} |
|
|
|
if ( m_aParticleSystems[ SYSTEM_EYEGLOW_LEFT ] ) |
|
{ |
|
m_aParticleSystems[ SYSTEM_EYEGLOW_LEFT ]->m_pParticleSystem->SetControlPoint( CUSTOM_COLOR_CP1, vColor ); |
|
} |
|
if ( m_aParticleSystems[ SYSTEM_EYESPARK_LEFT ]) |
|
{ |
|
m_aParticleSystems[ SYSTEM_EYESPARK_LEFT ]->m_pParticleSystem->SetControlPoint( CUSTOM_COLOR_CP1, vColor ); |
|
} |
|
|
|
m_bUpdateEyeGlows = false; |
|
m_bPlaySparks = false; |
|
|
|
// remove all particles that are not up-to-date before simulating the updated ones in the base |
|
for ( int i = 0; i < ARRAYSIZE( m_aParticleSystems ); i++ ) |
|
{ |
|
if ( m_aParticleSystems[i] && !m_aParticleSystems[i]->m_bIsUpdateToDate ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[i] ); |
|
} |
|
} |
|
|
|
BaseClass::PostPaint3D( pRenderContext ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose : Called by base Mdlpanel when a merged mdl has been drawn |
|
// For TF we use this as a way to render effects on top of model as appropriate (ie Unusual effects) |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::RenderingRootModel( IMatRenderContext *pRenderContext, CStudioHdr *pStudioHdr, MDLHandle_t mdlHandle, matrix3x4_t *pWorldMatrix ) |
|
{ |
|
if ( !m_bUseParticle ) |
|
return; |
|
|
|
// Eye Glows |
|
UpdateEyeGlows( pRenderContext, pStudioHdr, mdlHandle, pWorldMatrix, true ); |
|
UpdateEyeGlows( pRenderContext, pStudioHdr, mdlHandle, pWorldMatrix, false ); |
|
|
|
// Right hand |
|
UpdateActionSlotEffects( pRenderContext, pStudioHdr, mdlHandle, pWorldMatrix ); |
|
|
|
// Taunt Effects |
|
UpdateTauntEffects( pRenderContext, pStudioHdr, mdlHandle, pWorldMatrix ); |
|
} |
|
|
|
CEconItemView *CTFPlayerModelPanel::GetLoadoutItemFromMDLHandle( loadout_positions_t iPosition, MDLHandle_t mdlHandle ) |
|
{ |
|
// Check if we have a particle hat, if not ignore |
|
CEconItemView *pEconItem = NULL; |
|
|
|
// Find this item |
|
FOR_EACH_VEC( m_ItemsToCarry, i ) |
|
{ |
|
CEconItemView *pItem = m_ItemsToCarry[i]; |
|
int iLoadoutSlot = pItem->GetStaticData()->GetLoadoutSlot( m_iCurrentClassIndex ); |
|
if ( ( IsMiscSlot( iLoadoutSlot ) && IsMiscSlot( iPosition ) ) || |
|
( IsValidPickupWeaponSlot( iLoadoutSlot ) && iLoadoutSlot == iPosition ) ) |
|
{ |
|
const char * pDisplayModel = pItem->GetPlayerDisplayModel( m_iCurrentClassIndex, m_iTeam ); |
|
if ( pDisplayModel ) |
|
{ |
|
MDLHandle_t hMDLFindResult = vgui::MDLCache()->FindMDL( pDisplayModel ); |
|
// compare the model to make sure that this is the same item |
|
if ( hMDLFindResult == mdlHandle ) |
|
{ |
|
pEconItem = pItem; |
|
vgui::MDLCache()->Release(hMDLFindResult); // counterbalance addref from within FindMDL |
|
break; |
|
} |
|
vgui::MDLCache()->Release(hMDLFindResult); |
|
} |
|
} |
|
} |
|
|
|
return pEconItem; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::RenderingMergedModel( IMatRenderContext *pRenderContext, CStudioHdr *pStudioHdr, MDLHandle_t mdlHandle, matrix3x4_t *pWorldMatrix ) |
|
{ |
|
if ( !m_bUseParticle ) |
|
return; |
|
|
|
static struct MergeModelSlot_t |
|
{ |
|
loadout_positions_t iPosition; |
|
modelpanel_particle_system_t iSystem; |
|
} s_mergeModelSlot[] = |
|
{ |
|
{ LOADOUT_POSITION_HEAD, SYSTEM_HEAD }, |
|
{ LOADOUT_POSITION_MISC, SYSTEM_MISC1 }, |
|
{ LOADOUT_POSITION_MISC2, SYSTEM_MISC2 }, |
|
{ LOADOUT_POSITION_PRIMARY, SYSTEM_WEAPON }, |
|
{ LOADOUT_POSITION_SECONDARY, SYSTEM_WEAPON }, |
|
{ LOADOUT_POSITION_MELEE, SYSTEM_WEAPON }, |
|
}; |
|
|
|
modelpanel_particle_system_t iSystem = SYSTEM_HEAD; |
|
loadout_positions_t iPosition = LOADOUT_POSITION_INVALID; |
|
CEconItemView *pEconItem = NULL; |
|
int count = ARRAYSIZE( s_mergeModelSlot ); |
|
for ( int i=0; i<count; ++i ) |
|
{ |
|
// find the item for this model |
|
pEconItem = GetLoadoutItemFromMDLHandle( s_mergeModelSlot[i].iPosition, mdlHandle ); |
|
if ( pEconItem ) |
|
{ |
|
iPosition = s_mergeModelSlot[i].iPosition; |
|
iSystem = s_mergeModelSlot[i].iSystem; |
|
|
|
// this is horrible but this fixes multiple unusual cosmetics with same default loadout to update their particles |
|
if ( m_aParticleSystems[ iSystem ] && m_aParticleSystems[ iSystem ]->m_bIsUpdateToDate ) |
|
continue; |
|
|
|
break; |
|
} |
|
} |
|
|
|
// couldn't find matching item for this model, do nothing |
|
if ( !pEconItem ) |
|
return; |
|
|
|
// Unusual Particles |
|
// Update Misc Particles 1 by 1, Unfortunately the equip location is generic (MISC_SLOT) and not the specific slot |
|
// so we have to test each slot individually |
|
UpdateCosmeticParticles( pRenderContext, pStudioHdr, mdlHandle, pWorldMatrix, iSystem, pEconItem ); |
|
|
|
if ( m_iCurrentSlotIndex == iPosition ) |
|
{ |
|
RenderStatTrack( pStudioHdr, pWorldMatrix ); |
|
} |
|
} |
|
|
|
IMaterial* CTFPlayerModelPanel::GetOverrideMaterial( MDLHandle_t mdlHandle ) |
|
{ |
|
loadout_positions_t s_iPosition[] = { |
|
LOADOUT_POSITION_HEAD, |
|
LOADOUT_POSITION_MISC, |
|
LOADOUT_POSITION_MISC2, |
|
LOADOUT_POSITION_PRIMARY, |
|
LOADOUT_POSITION_SECONDARY, |
|
LOADOUT_POSITION_MELEE |
|
}; |
|
|
|
int count = ARRAYSIZE( s_iPosition ); |
|
for ( int i = 0; i < count; ++i ) |
|
{ |
|
CEconItemView *pEconItem = GetLoadoutItemFromMDLHandle( s_iPosition[ i ], mdlHandle ); |
|
if ( pEconItem ) |
|
return pEconItem->GetMaterialOverride( m_iTeam ); |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::RenderStatTrack( CStudioHdr *pStudioHdr, matrix3x4_t *pWorldMatrix ) |
|
{ |
|
// Draw the merge MDLs. |
|
if ( !m_StatTrackModel.m_bDisabled ) |
|
{ |
|
matrix3x4_t matMergeBoneToWorld[MAXSTUDIOBONES]; |
|
|
|
// Get the merge studio header. |
|
studiohdr_t *pStatTrackStudioHdr = m_StatTrackModel.m_MDL.GetStudioHdr(); |
|
matrix3x4_t *pMergeBoneToWorld = &matMergeBoneToWorld[0]; |
|
|
|
// If we have a valid mesh, bonemerge it. If we have an invalid mesh we can't bonemerge because |
|
// it'll crash trying to pull data from the missing header. |
|
if ( pStatTrackStudioHdr != NULL ) |
|
{ |
|
CStudioHdr mergeHdr( pStatTrackStudioHdr, g_pMDLCache ); |
|
m_StatTrackModel.m_MDL.SetupBonesWithBoneMerge( &mergeHdr, pMergeBoneToWorld, pStudioHdr, pWorldMatrix, m_StatTrackModel.m_MDLToWorld ); |
|
for ( int i=0; i<mergeHdr.numbones(); ++i ) |
|
{ |
|
MatrixScaleBy( m_flStatTrackScale, pMergeBoneToWorld[i] ); |
|
} |
|
m_StatTrackModel.m_MDL.Draw( m_StatTrackModel.m_MDLToWorld, pMergeBoneToWorld ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::UpdateCosmeticParticles( |
|
IMatRenderContext *pRenderContext, |
|
CStudioHdr *pStudioHdr, |
|
MDLHandle_t mdlHandle, |
|
matrix3x4_t *pWorldMatrix, |
|
modelpanel_particle_system_t iSystem, |
|
CEconItemView *pEconItem |
|
) |
|
{ |
|
if ( m_aParticleSystems[ iSystem ] && m_aParticleSystems[ iSystem ]->m_bIsUpdateToDate ) |
|
return false; |
|
|
|
attachedparticlesystem_t *pParticleSystem = NULL; |
|
|
|
// do community_sparkle effect if this is a community item? |
|
const int iQualityParticleType = pEconItem->GetQualityParticleType(); |
|
if ( iQualityParticleType > 0 ) |
|
{ |
|
pParticleSystem = GetItemSchema()->GetAttributeControlledParticleSystem( iQualityParticleType ); |
|
} |
|
|
|
if ( !pParticleSystem ) |
|
{ |
|
// does this hat even have a particle effect |
|
static CSchemaAttributeDefHandle pAttrDef_AttachParticleEffect( "attach particle effect" ); |
|
uint32 iValue = 0; |
|
if ( !pEconItem->FindAttribute( pAttrDef_AttachParticleEffect, &iValue ) ) |
|
{ |
|
return false; |
|
} |
|
|
|
const float& value_as_float = (float&)iValue; |
|
pParticleSystem = GetItemSchema()->GetAttributeControlledParticleSystem( value_as_float ); |
|
} |
|
|
|
// failed to find any particle effect |
|
if ( !pParticleSystem ) |
|
{ |
|
return false; |
|
} |
|
|
|
// Team Color |
|
if ( GetTeam() == TF_TEAM_BLUE && V_stristr( pParticleSystem->pszSystemName, "_teamcolor_red" )) |
|
{ |
|
static char pBlue[256]; |
|
V_StrSubst( pParticleSystem->pszSystemName, "_teamcolor_red", "_teamcolor_blue", pBlue, 256 ); |
|
pParticleSystem = GetItemSchema()->FindAttributeControlledParticleSystem( pBlue ); |
|
if ( !pParticleSystem ) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
// if this thing has a bip_head or prp_helmet (aka a hat) |
|
int iBone = Studio_BoneIndexByName( pStudioHdr, "bip_head" ); |
|
if ( iBone < 0 ) |
|
{ |
|
iBone = Studio_BoneIndexByName( pStudioHdr, "prp_helmet" ); |
|
if ( iBone < 0 ) |
|
{ |
|
iBone = Studio_BoneIndexByName( pStudioHdr, "prp_hat" ); |
|
} |
|
} |
|
|
|
// default to root |
|
if ( iBone < 0 ) |
|
{ |
|
iBone = 0; |
|
} |
|
|
|
// Get Use Head Origin |
|
CUtlVector< int > vecAttachments; |
|
static CSchemaAttributeDefHandle pAttrDef_UseHead( "particle effect use head origin" ); |
|
uint32 iUseHead = 0; |
|
if ( !pEconItem->FindAttribute( pAttrDef_UseHead, &iUseHead ) || !iUseHead == 0 ) |
|
{ |
|
// not using head? try searching for attachment points |
|
for ( int i=0; i<ARRAYSIZE( pParticleSystem->pszControlPoints ); ++i ) |
|
{ |
|
const char *pszAttachmentName = pParticleSystem->pszControlPoints[i]; |
|
if ( pszAttachmentName && pszAttachmentName[0] ) |
|
{ |
|
int iAttachment = Studio_FindAttachment( pStudioHdr, pszAttachmentName ); |
|
if ( iAttachment < 0 ) |
|
continue; |
|
|
|
vecAttachments.AddToTail( iAttachment ); |
|
} |
|
} |
|
} |
|
|
|
static char pszFullname[256]; |
|
const char* pszSystemName = pParticleSystem->pszSystemName; |
|
// Weapon Remap for a Base Effect to be used on a specific weapon |
|
if ( pParticleSystem->bUseSuffixName && pEconItem && pEconItem->GetItemDefinition()->GetParticleSuffix() ) |
|
{ |
|
V_strcpy_safe( pszFullname, pParticleSystem->pszSystemName ); |
|
V_strcat_safe( pszFullname, "_" ); |
|
V_strcat_safe( pszFullname, pEconItem->GetItemDefinition()->GetParticleSuffix() ); |
|
pszSystemName = pszFullname; |
|
} |
|
|
|
// Update the Particles and render them |
|
if ( m_aParticleSystems[ iSystem ] ) |
|
{ |
|
// Check if its a new particle system |
|
if ( V_strcmp( m_aParticleSystems[ iSystem ]->m_pParticleSystem->GetName(), pszSystemName ) ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[ iSystem ] ); |
|
m_aParticleSystems[ iSystem ] = CreateParticleData( pszSystemName ); |
|
} |
|
} |
|
else |
|
{ |
|
// create |
|
m_aParticleSystems[ iSystem ] = CreateParticleData( pszSystemName ); |
|
} |
|
|
|
// Particle system does not exist |
|
if ( !m_aParticleSystems[ iSystem ] ) |
|
return false; |
|
|
|
// Get offset if it exists (and if we're using head offset) |
|
static CSchemaAttributeDefHandle pAttrDef_VerticalOffset( "particle effect vertical offset" ); |
|
uint32 iOffset = 0; |
|
Vector vecParticleOffset( 0, 0, 0 ); |
|
if ( iUseHead > 0 && pEconItem->FindAttribute( pAttrDef_VerticalOffset, &iOffset ) ) |
|
{ |
|
vecParticleOffset.z = (float&)iOffset; |
|
} |
|
|
|
m_aParticleSystems[ iSystem ]->UpdateControlPoints( pStudioHdr, pWorldMatrix, vecAttachments, iBone, vecParticleOffset ); |
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::UpdateEyeGlows( |
|
IMatRenderContext *pRenderContext, |
|
CStudioHdr *pStudioHdr, |
|
MDLHandle_t mdlHandle, |
|
matrix3x4_t *pWorldMatrix, |
|
bool bIsRightEye |
|
) { |
|
float flOffset = 0; |
|
modelpanel_particle_system_t eyeSystem = bIsRightEye ? SYSTEM_EYEGLOW_RIGHT : SYSTEM_EYEGLOW_LEFT; |
|
modelpanel_particle_system_t sparkSystem = bIsRightEye ? SYSTEM_EYESPARK_RIGHT : SYSTEM_EYESPARK_LEFT; |
|
const char* pszAttach = bIsRightEye ? "eyeglow_R" : "eyeglow_L"; |
|
|
|
// is this a model we care about? |
|
int iAttachment = Studio_FindAttachment( pStudioHdr, pszAttach ); |
|
if ( iAttachment == INVALID_PARTICLE_ATTACHMENT || iAttachment == -1 ) |
|
return; |
|
|
|
if ( m_bUpdateEyeGlows ) |
|
{ |
|
const char* pszGlowEffectName = m_pszEyeGlowParticleName; |
|
|
|
// kill old effects |
|
SafeDeleteParticleData( &m_aParticleSystems[ eyeSystem ] ); |
|
|
|
if ( !bIsRightEye && GetPlayerClass() == TF_CLASS_DEMOMAN ) |
|
{ |
|
// demo man has a green eyeglow for eyelander if applicable |
|
C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
if ( pPlayer ) |
|
{ |
|
int iDecaps = pPlayer->m_Shared.GetDecapitations(); |
|
pszGlowEffectName = pPlayer->GetDemomanEyeEffectName( iDecaps ); |
|
} |
|
} |
|
|
|
if ( pszGlowEffectName && pszGlowEffectName[0] != '\0' ) |
|
{ |
|
m_aParticleSystems[ eyeSystem ] = CreateParticleData( pszGlowEffectName ); |
|
} |
|
} |
|
|
|
if ( m_bPlaySparks && !m_vEyeGlowColor1.IsZero() ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[ sparkSystem ] ); |
|
|
|
// Generate an eye spark as well not for demo |
|
m_aParticleSystems[ sparkSystem ] = CreateParticleData( "killstreak_t0_lvl1_flash" ); |
|
} |
|
|
|
// Tick Update on position |
|
if ( m_aParticleSystems[ eyeSystem ] || m_aParticleSystems[ sparkSystem ] ) |
|
{ |
|
// Figure out where our attach point is |
|
matrix3x4_t matAttachToWorld; |
|
|
|
CUtlVector< int > vecAttachments; |
|
vecAttachments.AddToTail( iAttachment ); |
|
|
|
// Update control points which is updating the position of the particles |
|
Vector vecForward; |
|
MatrixGetColumn( matAttachToWorld, 0, vecForward ); |
|
|
|
Vector vecParticleOffset = vecForward * flOffset; |
|
if ( m_aParticleSystems[ eyeSystem ] ) |
|
{ |
|
m_aParticleSystems[ eyeSystem ]->UpdateControlPoints( pStudioHdr, pWorldMatrix, vecAttachments, 0, vecParticleOffset ); |
|
} |
|
|
|
if ( m_aParticleSystems[ sparkSystem ] ) |
|
{ |
|
m_aParticleSystems[ sparkSystem ]->UpdateControlPoints( pStudioHdr, pWorldMatrix, vecAttachments, 0, vecParticleOffset ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::UpdateActionSlotEffects( |
|
IMatRenderContext *pRenderContext, |
|
CStudioHdr *pStudioHdr, |
|
MDLHandle_t mdlHandle, |
|
matrix3x4_t *pWorldMatrix |
|
) { |
|
// is this a model we care about? |
|
int iAttachment = Studio_FindAttachment( pStudioHdr, "effect_hand_R" ); |
|
if ( iAttachment == INVALID_PARTICLE_ATTACHMENT || iAttachment == -1 ) |
|
return; |
|
|
|
if ( !m_bDrawActionSlotEffects ) |
|
return; |
|
|
|
if ( !m_aParticleSystems[ SYSTEM_ACTIONSLOT ] ) |
|
{ |
|
m_aParticleSystems[ SYSTEM_ACTIONSLOT ] = CreateParticleData( CTFSpellBook::GetHandEffect( m_pHeldItem, 0 ) ); |
|
} |
|
|
|
if ( !m_aParticleSystems[ SYSTEM_ACTIONSLOT ] ) |
|
return; |
|
|
|
CUtlVector< int > vecAttachments; |
|
vecAttachments.AddToTail( iAttachment ); |
|
|
|
m_aParticleSystems[ SYSTEM_ACTIONSLOT ]->UpdateControlPoints( pStudioHdr, pWorldMatrix, vecAttachments ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::UpdateTauntEffects( |
|
IMatRenderContext *pRenderContext, |
|
CStudioHdr *pStudioHdr, |
|
MDLHandle_t mdlHandle, |
|
matrix3x4_t *pWorldMatrix |
|
) { |
|
if ( !m_bDrawTauntParticles ) |
|
return; |
|
|
|
if ( !m_aParticleSystems[SYSTEM_TAUNT] ) |
|
return; |
|
|
|
// Check if refire is needed |
|
if ( m_flTauntParticleRefireRate > 0 && m_flTauntParticleRefireTime < gpGlobals->curtime ) |
|
{ |
|
m_flTauntParticleRefireTime = gpGlobals->curtime + m_flTauntParticleRefireRate; |
|
|
|
// safe off current particle name |
|
CUtlString strParticleName = m_aParticleSystems[SYSTEM_TAUNT]->m_pParticleSystem->GetName(); |
|
|
|
// remove old particle |
|
SafeDeleteParticleData( &m_aParticleSystems[SYSTEM_TAUNT] ); |
|
|
|
// create new particle |
|
m_aParticleSystems[SYSTEM_TAUNT] = CreateParticleData( strParticleName.String() ); |
|
} |
|
|
|
matrix3x4_t matAttachToWorld; |
|
SetIdentityMatrix( matAttachToWorld ); |
|
|
|
CUtlVector< int > vecAttachments; |
|
m_aParticleSystems[SYSTEM_TAUNT]->UpdateControlPoints( pStudioHdr, &matAttachToWorld, vecAttachments, 0, m_vecPlayerPos ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Called Externally |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SetEyeGlowEffect( const char *pEffectName, Vector vColor1, Vector vColor2, bool bForceUpdate, bool bPlaySparks ) |
|
{ |
|
m_vEyeGlowColor1 = vColor1; |
|
m_vEyeGlowColor2 = vColor2; |
|
m_bPlaySparks = bPlaySparks; |
|
|
|
if ( bForceUpdate ) |
|
{ |
|
m_bUpdateEyeGlows = true; |
|
} |
|
|
|
if ( !pEffectName ) |
|
{ |
|
if ( m_pszEyeGlowParticleName[0] != '\0' ) |
|
{ |
|
m_bUpdateEyeGlows = true; |
|
} |
|
m_pszEyeGlowParticleName[0] = '\0'; |
|
} |
|
else if ( !FStrEq( m_pszEyeGlowParticleName, pEffectName) ) |
|
{ |
|
V_strcpy_safe( m_pszEyeGlowParticleName, pEffectName ); |
|
m_bUpdateEyeGlows = true; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: clear all particles |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::InvalidateParticleEffects() |
|
{ |
|
for ( int i=0; i<ARRAYSIZE(m_aParticleSystems); ++i ) |
|
{ |
|
if ( m_aParticleSystems[i] ) |
|
{ |
|
SafeDeleteParticleData( &m_aParticleSystems[i] ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::StartEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
if ( !event || !event->GetActive() ) |
|
return; |
|
|
|
CChoreoActor *actor = event->GetActor(); |
|
if ( actor && !actor->GetActive() ) |
|
return; |
|
|
|
CChoreoChannel *channel = event->GetChannel(); |
|
if ( channel && !channel->GetActive() ) |
|
return; |
|
|
|
//Msg( "Got STARTEVENT at %.2f\n", currenttime ); |
|
//Msg( "%8.4f: start %s\n", currenttime, event->GetDescription() ); |
|
|
|
switch ( event->GetType() ) |
|
{ |
|
case CChoreoEvent::SEQUENCE: |
|
ProcessSequence( scene, event ); |
|
break; |
|
|
|
case CChoreoEvent::SPEAK: |
|
{ |
|
if ( m_bDisableSpeakEvent ) |
|
return; |
|
|
|
// FIXME: dB hack. soundlevel needs to be moved into inside of wav? |
|
soundlevel_t iSoundlevel = SNDLVL_TALKING; |
|
if ( event->GetParameters2() ) |
|
{ |
|
iSoundlevel = (soundlevel_t)atoi( event->GetParameters2() ); |
|
if ( iSoundlevel == SNDLVL_NONE ) |
|
{ |
|
iSoundlevel = SNDLVL_TALKING; |
|
} |
|
} |
|
|
|
float time_in_past = currenttime - event->GetStartTime() ; |
|
float soundtime = gpGlobals->curtime - time_in_past; |
|
|
|
EmitSound_t es; |
|
es.m_nChannel = CHAN_VOICE; |
|
es.m_flVolume = 1; |
|
es.m_SoundLevel = iSoundlevel; |
|
es.m_flSoundTime = soundtime; |
|
es.m_bEmitCloseCaption = false; |
|
es.m_pSoundName = event->GetParameters(); |
|
|
|
C_RecipientFilter filter; |
|
C_BaseEntity::EmitSound( filter, SOUND_FROM_UI_PANEL, es ); |
|
} |
|
break; |
|
|
|
case CChoreoEvent::STOPPOINT: |
|
{ |
|
// Nothing, this is a symbolic event for keeping the vcd alive for ramping out after the last true event |
|
//ClearScene(); |
|
} |
|
break; |
|
|
|
case CChoreoEvent::LOOP: |
|
ProcessLoop( scene, event ); |
|
break; |
|
|
|
// Not supported in TF2's model previews |
|
case CChoreoEvent::SUBSCENE: |
|
case CChoreoEvent::SECTION: |
|
{ |
|
Assert(0); |
|
} |
|
break; |
|
|
|
default: |
|
break; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::EndEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
if ( !event || !event->GetActive() ) |
|
return; |
|
|
|
CChoreoActor *actor = event->GetActor(); |
|
if ( actor && !actor->GetActive() ) |
|
return; |
|
|
|
CChoreoChannel *channel = event->GetChannel(); |
|
if ( channel && !channel->GetActive() ) |
|
return; |
|
|
|
//Msg( "Got ENDEVENT at %.2f\n", currenttime ); |
|
//Msg( "%8.4f: end %s %i\n", currenttime, event->GetDescription(), event->GetType() ); |
|
|
|
switch ( event->GetType() ) |
|
{ |
|
case CChoreoEvent::SUBSCENE: |
|
{ |
|
// Not supported in TF2's model previews |
|
Assert(0); |
|
} |
|
break; |
|
case CChoreoEvent::SPEAK: |
|
{ |
|
} |
|
break; |
|
case CChoreoEvent::STOPPOINT: |
|
{ |
|
//SetSequenceLayers( NULL, 0 ); |
|
//ClearScene(); |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
if ( !event || !event->GetActive() ) |
|
return; |
|
|
|
CChoreoActor *actor = event->GetActor(); |
|
if ( actor && !actor->GetActive() ) |
|
return; |
|
|
|
CChoreoChannel *channel = event->GetChannel(); |
|
if ( channel && !channel->GetActive() ) |
|
return; |
|
|
|
//Msg("PROCESSEVENT at %.2f\n", currenttime ); |
|
|
|
switch( event->GetType() ) |
|
{ |
|
case CChoreoEvent::EXPRESSION: |
|
if ( !m_bShouldRunFlexEvents ) |
|
{ |
|
ProcessFlexSettingSceneEvent( scene, event ); |
|
} |
|
break; |
|
|
|
case CChoreoEvent::FLEXANIMATION: |
|
if ( m_bShouldRunFlexEvents ) |
|
{ |
|
ProcessFlexAnimation( scene, event ); |
|
} |
|
break; |
|
|
|
case CChoreoEvent::SEQUENCE: |
|
case CChoreoEvent::SPEAK: |
|
case CChoreoEvent::STOPPOINT: |
|
// Nothing |
|
break; |
|
|
|
// Not supported in TF2's model previews |
|
case CChoreoEvent::LOOKAT: |
|
case CChoreoEvent::FACE: |
|
case CChoreoEvent::SUBSCENE: |
|
case CChoreoEvent::MOVETO: |
|
case CChoreoEvent::INTERRUPT: |
|
case CChoreoEvent::PERMIT_RESPONSES: |
|
case CChoreoEvent::GESTURE: |
|
Assert(0); |
|
break; |
|
|
|
default: |
|
break; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::CheckEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Apply a sequence |
|
// Input : *event - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessSequence( CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
Assert( event->GetType() == CChoreoEvent::SEQUENCE ); |
|
|
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
if ( !event->GetActor() ) |
|
return; |
|
|
|
int iSequence = LookupSequence( &studioHdr, event->GetParameters() ); |
|
if (iSequence < 0) |
|
return; |
|
|
|
// making sure the mdl has correct playback rate |
|
mstudioseqdesc_t &seqdesc = studioHdr.pSeqdesc( iSequence ); |
|
mstudioanimdesc_t &animdesc = studioHdr.pAnimdesc( studioHdr.iRelativeAnim( iSequence, seqdesc.anim(0,0) ) ); |
|
m_RootMDL.m_MDL.m_flPlaybackRate = animdesc.fps; |
|
|
|
MDLSquenceLayer_t tmpSequenceLayers[1]; |
|
tmpSequenceLayers[0].m_nSequenceIndex = iSequence; |
|
tmpSequenceLayers[0].m_flWeight = 1.0; |
|
tmpSequenceLayers[0].m_bNoLoop = true; |
|
tmpSequenceLayers[0].m_flCycleBeganAt = m_RootMDL.m_MDL.m_flTime; |
|
SetSequenceLayers( tmpSequenceLayers, 1 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *scene - |
|
// *event - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessLoop( CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
Assert( event->GetType() == CChoreoEvent::LOOP ); |
|
|
|
float backtime = (float)atof( event->GetParameters() ); |
|
|
|
bool process = true; |
|
int counter = event->GetLoopCount(); |
|
if ( counter != -1 ) |
|
{ |
|
int remaining = event->GetNumLoopsRemaining(); |
|
if ( remaining <= 0 ) |
|
{ |
|
process = false; |
|
} |
|
else |
|
{ |
|
event->SetNumLoopsRemaining( --remaining ); |
|
} |
|
} |
|
|
|
if ( !process ) |
|
return; |
|
|
|
//Msg("LOOP: %.2f (%.2f)\n", m_flSceneTime, scene->GetTime() ); |
|
|
|
float flPrevTime = m_flSceneTime; |
|
scene->LoopToTime( backtime ); |
|
m_flSceneTime = backtime; |
|
|
|
//Msg(" -> %.2f (%.2f)\n", m_flSceneTime, scene->GetTime() ); |
|
|
|
float flDelta = flPrevTime - backtime; |
|
|
|
//Msg(" -> Delta %.2f\n", flDelta ); |
|
|
|
// If we're running noloop sequences, we need to push out their begin time, so they keep playing |
|
for ( int i = 0; i < m_nNumSequenceLayers; i++ ) |
|
{ |
|
if ( m_SequenceLayers[i].m_bNoLoop ) |
|
{ |
|
m_SequenceLayers[i].m_flCycleBeganAt += flDelta; |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
LocalFlexController_t CTFPlayerModelPanel::GetNumFlexControllers( void ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
return studioHdr.numflexcontrollers(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *CTFPlayerModelPanel::GetFlexDescFacs( int iFlexDesc ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
mstudioflexdesc_t *pflexdesc = studioHdr.pFlexdesc( iFlexDesc ); |
|
|
|
return pflexdesc->pszFACS( ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *CTFPlayerModelPanel::GetFlexControllerName( LocalFlexController_t iFlexController ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
mstudioflexcontroller_t *pflexcontroller = studioHdr.pFlexcontroller( iFlexController ); |
|
|
|
return pflexcontroller->pszName( ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *CTFPlayerModelPanel::GetFlexControllerType( LocalFlexController_t iFlexController ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
mstudioflexcontroller_t *pflexcontroller = studioHdr.pFlexcontroller( iFlexController ); |
|
|
|
return pflexcontroller->pszType( ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
LocalFlexController_t CTFPlayerModelPanel::FindFlexController( const char *szName ) |
|
{ |
|
for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) |
|
{ |
|
if (stricmp( GetFlexControllerName( i ), szName ) == 0) |
|
{ |
|
return i; |
|
} |
|
} |
|
|
|
// AssertMsg( 0, UTIL_VarArgs( "flexcontroller %s couldn't be mapped!!!\n", szName ) ); |
|
return LocalFlexController_t(-1); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SetFlexWeight( LocalFlexController_t index, float value ) |
|
{ |
|
if (index >= 0 && index < GetNumFlexControllers()) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
mstudioflexcontroller_t *pflexcontroller = studioHdr.pFlexcontroller( index ); |
|
|
|
if (pflexcontroller->max != pflexcontroller->min) |
|
{ |
|
value = (value - pflexcontroller->min) / (pflexcontroller->max - pflexcontroller->min); |
|
value = clamp( value, 0.0f, 1.0f ); |
|
} |
|
|
|
m_flexWeight[ index ] = value; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float CTFPlayerModelPanel::GetFlexWeight( LocalFlexController_t index ) |
|
{ |
|
if (index >= 0 && index < GetNumFlexControllers()) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
|
|
mstudioflexcontroller_t *pflexcontroller = studioHdr.pFlexcontroller( index ); |
|
|
|
if (pflexcontroller->max != pflexcontroller->min) |
|
{ |
|
return m_flexWeight[index] * (pflexcontroller->max - pflexcontroller->min) + pflexcontroller->min; |
|
} |
|
|
|
return m_flexWeight[index]; |
|
} |
|
return 0.0; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: During paint, apply the flex weights to the model |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SetupFlexWeights( void ) |
|
{ |
|
if ( m_RootMDL.m_MDL.GetMDL() == MDLHANDLE_INVALID ) |
|
return; |
|
|
|
// initialize the models local to global flex controller mappings |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
if (studioHdr.pFlexcontroller( LocalFlexController_t(0) )->localToGlobal == -1) |
|
{ |
|
for ( LocalFlexController_t i = LocalFlexController_t(0); i < studioHdr.numflexcontrollers(); i++) |
|
{ |
|
int j = C_BaseFlex::AddGlobalFlexController( studioHdr.pFlexcontroller( i )->pszName() ); |
|
studioHdr.pFlexcontroller( i )->localToGlobal = j; |
|
} |
|
} |
|
|
|
int iControllers = GetNumFlexControllers(); |
|
for ( int j = 0; j < iControllers; j++ ) |
|
{ |
|
m_RootMDL.m_MDL.m_pFlexControls[j] = 0; |
|
} |
|
|
|
LocalFlexController_t i; |
|
|
|
// Decay to neutral |
|
for ( i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) |
|
{ |
|
SetFlexWeight( i, GetFlexWeight( i ) * 0.95 ); |
|
} |
|
|
|
// Run scene |
|
if ( m_pScene ) |
|
{ |
|
m_bShouldRunFlexEvents = true; |
|
m_pScene->Think( m_flSceneTime ); |
|
} |
|
|
|
// get the networked flexweights and convert them from 0..1 to real dynamic range |
|
for (i = LocalFlexController_t(0); i < studioHdr.numflexcontrollers(); i++) |
|
{ |
|
mstudioflexcontroller_t *pflex = studioHdr.pFlexcontroller( i ); |
|
|
|
m_RootMDL.m_MDL.m_pFlexControls[pflex->localToGlobal] = m_flexWeight[i]; |
|
// rescale |
|
m_RootMDL.m_MDL.m_pFlexControls[pflex->localToGlobal] = m_RootMDL.m_MDL.m_pFlexControls[pflex->localToGlobal] * (pflex->max - pflex->min) + pflex->min; |
|
} |
|
|
|
if ( m_pScene ) |
|
{ |
|
m_bShouldRunFlexEvents = false; |
|
m_pScene->Think( m_flSceneTime ); |
|
} |
|
|
|
ProcessVisemes( m_PhonemeClasses ); |
|
|
|
if ( m_pScene ) |
|
{ |
|
// Advance time |
|
if ( m_flLastTickTime < FLT_EPSILON ) |
|
{ |
|
m_flLastTickTime = m_RootMDL.m_MDL.m_flTime - 0.1; |
|
} |
|
|
|
m_flSceneTime += (m_RootMDL.m_MDL.m_flTime - m_flLastTickTime); |
|
m_flLastTickTime = m_RootMDL.m_MDL.m_flTime; |
|
|
|
if ( m_flSceneEndTime > FLT_EPSILON && m_flSceneTime > m_flSceneEndTime ) |
|
{ |
|
bool bLoopScene = m_bLoopScene; |
|
char filename[MAX_PATH]; |
|
V_strcpy_safe( filename, m_pScene->GetFilename() ); |
|
|
|
SetSequenceLayers( NULL, 0 ); |
|
ClearScene(); |
|
|
|
if ( bLoopScene ) |
|
{ |
|
m_pScene = LoadSceneForModel( filename, this, &m_flSceneEndTime ); |
|
} |
|
else |
|
{ |
|
m_pszVCD = NULL; |
|
} |
|
} |
|
} |
|
} |
|
|
|
extern CFlexSceneFileManager g_FlexSceneFileManager; |
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessExpression( CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
// Flexanimations have to have an end time!!! |
|
if ( !event->HasEndTime() ) |
|
return; |
|
|
|
// Look up the actual strings |
|
const char *scenefile = event->GetParameters(); |
|
const char *name = event->GetParameters2(); |
|
|
|
// Have to find both strings |
|
if ( scenefile && name ) |
|
{ |
|
// Find the scene file |
|
const flexsettinghdr_t *pExpHdr = ( const flexsettinghdr_t * )g_FlexSceneFileManager.FindSceneFile( this, scenefile, true ); |
|
if ( pExpHdr ) |
|
{ |
|
float scenetime = scene->GetTime(); |
|
|
|
float flIntensity = event->GetIntensity( scenetime ); |
|
|
|
int i; |
|
const flexsetting_t *pSetting = NULL; |
|
|
|
// Find the named setting in the base |
|
for ( i = 0; i < pExpHdr->numflexsettings; i++ ) |
|
{ |
|
pSetting = pExpHdr->pSetting( i ); |
|
if ( !pSetting ) |
|
continue; |
|
|
|
if ( !V_stricmp( pSetting->pszName(), name ) ) |
|
break; |
|
} |
|
|
|
if ( i>=pExpHdr->numflexsettings ) |
|
return; |
|
|
|
flexweight_t *pWeights = NULL; |
|
int truecount = pSetting->psetting( (byte *)pExpHdr, 0, &pWeights ); |
|
if ( !pWeights ) |
|
return; |
|
|
|
for (i = 0; i < truecount; i++, pWeights++) |
|
{ |
|
int j = FlexControllerLocalToGlobal( pExpHdr, pWeights->key ); |
|
|
|
float s = clamp( pWeights->influence * flIntensity, 0.0f, 1.0f ); |
|
m_RootMDL.m_MDL.m_pFlexControls[ j ] = m_RootMDL.m_MDL.m_pFlexControls[j] * (1.0f - s) + pWeights->weight * s; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessFlexSettingSceneEvent( CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
// Flexanimations have to have an end time!!! |
|
if ( !event->HasEndTime() ) |
|
return; |
|
|
|
VPROF( "C_BaseFlex::ProcessFlexSettingSceneEvent" ); |
|
|
|
// Look up the actual strings |
|
const char *scenefile = event->GetParameters(); |
|
const char *name = event->GetParameters2(); |
|
|
|
// Have to find both strings |
|
if ( scenefile && name ) |
|
{ |
|
// Find the scene file |
|
const flexsettinghdr_t *pExpHdr = ( const flexsettinghdr_t * )g_FlexSceneFileManager.FindSceneFile( this, scenefile, true ); |
|
if ( pExpHdr ) |
|
{ |
|
float scenetime = scene->GetTime(); |
|
|
|
float scale = event->GetIntensity( scenetime ); |
|
|
|
// Add the named expression |
|
AddFlexSetting( name, scale, pExpHdr ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *expr - |
|
// scale - |
|
// *pSettinghdr - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::AddFlexSetting( const char *expr, float scale, const flexsettinghdr_t *pSettinghdr ) |
|
{ |
|
int i; |
|
const flexsetting_t *pSetting = NULL; |
|
|
|
// Find the named setting in the base |
|
for ( i = 0; i < pSettinghdr->numflexsettings; i++ ) |
|
{ |
|
pSetting = pSettinghdr->pSetting( i ); |
|
if ( !pSetting ) |
|
continue; |
|
|
|
const char *name = pSetting->pszName(); |
|
|
|
if ( !V_stricmp( name, expr ) ) |
|
break; |
|
} |
|
|
|
if ( i>=pSettinghdr->numflexsettings ) |
|
{ |
|
return; |
|
} |
|
|
|
flexweight_t *pWeights = NULL; |
|
int truecount = pSetting->psetting( (byte *)pSettinghdr, 0, &pWeights ); |
|
if ( !pWeights ) |
|
return; |
|
|
|
for (i = 0; i < truecount; i++, pWeights++) |
|
{ |
|
// Translate to local flex controller |
|
// this is translating from the settings's local index to the models local index |
|
int index = FlexControllerLocalToGlobal( pSettinghdr, pWeights->key ); |
|
|
|
// blend scaled weighting in to total (post networking g_flexweight!!!!) |
|
float s = clamp( scale * pWeights->influence, 0.0f, 1.0f ); |
|
m_RootMDL.m_MDL.m_pFlexControls[index] = m_RootMDL.m_MDL.m_pFlexControls[index] * (1.0f - s) + pWeights->weight * s; |
|
|
|
for ( int iMergeMDL=0; iMergeMDL<m_aMergeMDLs.Count(); ++iMergeMDL ) |
|
{ |
|
m_aMergeMDLs[iMergeMDL].m_MDL.m_pFlexControls[index] = m_aMergeMDLs[iMergeMDL].m_MDL.m_pFlexControls[index] * (1.0f - s) + pWeights->weight * s; |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Apply flexanimation to actor's face |
|
// Input : *event - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessFlexAnimation( CChoreoScene *scene, CChoreoEvent *event ) |
|
{ |
|
Assert( event->GetType() == CChoreoEvent::FLEXANIMATION ); |
|
|
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
CStudioHdr *hdr = &studioHdr; |
|
if ( !hdr ) |
|
return; |
|
|
|
if ( !event->GetTrackLookupSet() ) |
|
{ |
|
// Create lookup data |
|
for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) |
|
{ |
|
CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); |
|
if ( !track ) |
|
continue; |
|
|
|
if ( track->IsComboType() ) |
|
{ |
|
char name[ 512 ]; |
|
Q_strncpy( name, "right_" ,sizeof(name)); |
|
Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); |
|
|
|
track->SetFlexControllerIndex( MAX( FindFlexController( name ), LocalFlexController_t(0) ), 0, 0 ); |
|
|
|
Q_strncpy( name, "left_" ,sizeof(name)); |
|
Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); |
|
|
|
track->SetFlexControllerIndex( MAX( FindFlexController( name ), LocalFlexController_t(0) ), 0, 1 ); |
|
} |
|
else |
|
{ |
|
track->SetFlexControllerIndex( MAX( FindFlexController( (char *)track->GetFlexControllerName() ), LocalFlexController_t(0)), 0 ); |
|
} |
|
} |
|
|
|
event->SetTrackLookupSet( true ); |
|
} |
|
|
|
float scenetime = scene->GetTime(); |
|
|
|
float weight = event->GetIntensity( scenetime ); |
|
|
|
// Iterate animation tracks |
|
for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) |
|
{ |
|
CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); |
|
if ( !track ) |
|
continue; |
|
|
|
// Disabled |
|
if ( !track->IsTrackActive() ) |
|
continue; |
|
|
|
// Map track flex controller to global name |
|
if ( track->IsComboType() ) |
|
{ |
|
for ( int side = 0; side < 2; side++ ) |
|
{ |
|
LocalFlexController_t controller = track->GetRawFlexControllerIndex( side ); |
|
|
|
// Get spline intensity for controller |
|
float flIntensity = track->GetIntensity( scenetime, side ); |
|
if ( controller >= LocalFlexController_t(0) ) |
|
{ |
|
float orig = GetFlexWeight( controller ); |
|
float value = orig * (1 - weight) + flIntensity * weight; |
|
SetFlexWeight( controller, value ); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
LocalFlexController_t controller = track->GetRawFlexControllerIndex( 0 ); |
|
|
|
// Get spline intensity for controller |
|
float flIntensity = track->GetIntensity( scenetime, 0 ); |
|
if ( controller >= LocalFlexController_t(0) ) |
|
{ |
|
float orig = GetFlexWeight( controller ); |
|
float value = orig * (1 - weight) + flIntensity * weight; |
|
SetFlexWeight( controller, value ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
extern ConVar g_CV_PhonemeDelay; |
|
extern ConVar g_CV_PhonemeFilter; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ProcessVisemes( Emphasized_Phoneme *classes ) |
|
{ |
|
// Any sounds being played? |
|
if ( !MouthInfo().IsActive() ) |
|
return; |
|
|
|
// Multiple phoneme tracks can overlap, look across all such tracks. |
|
for ( int source = 0 ; source < MouthInfo().GetNumVoiceSources(); source++ ) |
|
{ |
|
CVoiceData *vd = MouthInfo().GetVoiceSource( source ); |
|
if ( !vd || vd->ShouldIgnorePhonemes() ) |
|
continue; |
|
|
|
CSentence *sentence = engine->GetSentence( vd->GetSource() ); |
|
if ( !sentence ) |
|
continue; |
|
|
|
float sentence_length = engine->GetSentenceLength( vd->GetSource() ); |
|
float timesincestart = vd->GetElapsedTime(); |
|
|
|
// This sound should be done...why hasn't it been removed yet??? |
|
if ( timesincestart >= ( sentence_length + 2.0f ) ) |
|
continue; |
|
|
|
// Adjust actual time |
|
float t = timesincestart - g_CV_PhonemeDelay.GetFloat(); |
|
|
|
// Get box filter duration |
|
float dt = g_CV_PhonemeFilter.GetFloat(); |
|
|
|
// Streaming sounds get an additional delay... |
|
/* |
|
// Tracker 20534: Probably not needed any more with the async sound stuff that |
|
// we now have (we don't have a disk i/o hitch on startup which might have been |
|
// messing up the startup timing a bit ) |
|
bool streaming = engine->IsStreaming( vd->m_pAudioSource ); |
|
if ( streaming ) |
|
{ |
|
t -= g_CV_PhonemeDelayStreaming.GetFloat(); |
|
} |
|
*/ |
|
|
|
// Assume sound has been playing for a while... |
|
bool juststarted = false; |
|
|
|
// Get intensity setting for this time (from spline) |
|
float emphasis_intensity = sentence->GetIntensity( t, sentence_length ); |
|
|
|
// Blend and add visemes together |
|
AddVisemesForSentence( classes, emphasis_intensity, sentence, t, dt, juststarted ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::AddVisemesForSentence( Emphasized_Phoneme *classes, float emphasis_intensity, CSentence *sentence, float t, float dt, bool juststarted ) |
|
{ |
|
int pcount = sentence->GetRuntimePhonemeCount(); |
|
for ( int k = 0; k < pcount; k++ ) |
|
{ |
|
const CBasePhonemeTag *phoneme = sentence->GetRuntimePhoneme( k ); |
|
|
|
if (t > phoneme->GetStartTime() && t < phoneme->GetEndTime()) |
|
{ |
|
bool bCrossfade = true; |
|
if (bCrossfade) |
|
{ |
|
if (k < pcount-1) |
|
{ |
|
const CBasePhonemeTag *next = sentence->GetRuntimePhoneme( k + 1 ); |
|
// if I have a neighbor |
|
if ( next ) |
|
{ |
|
// and they're touching |
|
if (next->GetStartTime() == phoneme->GetEndTime() ) |
|
{ |
|
// no gap, so increase the blend length to the end of the next phoneme, as long as it's not longer than the current phoneme |
|
dt = MAX( dt, MIN( next->GetEndTime() - t, phoneme->GetEndTime() - phoneme->GetStartTime() ) ); |
|
} |
|
else |
|
{ |
|
// dead space, so increase the blend length to the start of the next phoneme, as long as it's not longer than the current phoneme |
|
dt = MAX( dt, MIN( next->GetStartTime() - t, phoneme->GetEndTime() - phoneme->GetStartTime() ) ); |
|
} |
|
} |
|
else |
|
{ |
|
// last phoneme in list, increase the blend length to the length of the current phoneme |
|
dt = MAX( dt, phoneme->GetEndTime() - phoneme->GetStartTime() ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
float t1 = ( phoneme->GetStartTime() - t) / dt; |
|
float t2 = ( phoneme->GetEndTime() - t) / dt; |
|
|
|
if (t1 < 1.0 && t2 > 0) |
|
{ |
|
float scale; |
|
|
|
// clamp |
|
if (t2 > 1) |
|
t2 = 1; |
|
if (t1 < 0) |
|
t1 = 0; |
|
|
|
// FIXME: simple box filter. Should use something fancier |
|
scale = (t2 - t1); |
|
|
|
AddViseme( classes, emphasis_intensity, phoneme->GetPhonemeCode(), scale, juststarted ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *classes - |
|
// phoneme - |
|
// scale - |
|
// newexpression - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::AddViseme( Emphasized_Phoneme *classes, float emphasis_intensity, int phoneme, float scale, bool newexpression ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
CStudioHdr *hdr = &studioHdr; |
|
if ( !hdr ) |
|
return; |
|
|
|
int type; |
|
|
|
// Setup weights for any emphasis blends |
|
bool skip = SetupEmphasisBlend( classes, phoneme ); |
|
|
|
phoneme = 230; |
|
scale = 1.0; |
|
|
|
// Uh-oh, missing or unknown phoneme??? |
|
if ( skip ) |
|
{ |
|
return; |
|
} |
|
|
|
// Compute blend weights |
|
ComputeBlendedSetting( classes, emphasis_intensity ); |
|
|
|
for ( type = 0; type < NUM_PHONEME_CLASSES; type++ ) |
|
{ |
|
Emphasized_Phoneme *info = &classes[ type ]; |
|
if ( !info->valid || info->amount == 0.0f ) |
|
continue; |
|
|
|
const flexsettinghdr_t *actual_flexsetting_header = info->base; |
|
const flexsetting_t *pSetting = actual_flexsetting_header->pIndexedSetting( phoneme ); |
|
if (!pSetting) |
|
{ |
|
continue; |
|
} |
|
|
|
flexweight_t *pWeights = NULL; |
|
|
|
int truecount = pSetting->psetting( (byte *)actual_flexsetting_header, 0, &pWeights ); |
|
if ( pWeights ) |
|
{ |
|
for ( int i = 0; i < truecount; i++) |
|
{ |
|
// Translate to global controller number |
|
int j = FlexControllerLocalToGlobal( actual_flexsetting_header, pWeights->key ); |
|
// Add scaled weighting in |
|
if ( pWeights->weight > 0 ) |
|
{ |
|
m_RootMDL.m_MDL.m_pFlexControls[j] += info->amount * scale * pWeights->weight; |
|
} |
|
// Go to next setting |
|
pWeights++; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: A lot of the one time setup and also resets amount to 0.0f default |
|
// for strong/weak/normal tracks |
|
// Returning true == skip this phoneme |
|
// Input : *classes - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CTFPlayerModelPanel::SetupEmphasisBlend( Emphasized_Phoneme *classes, int phoneme ) |
|
{ |
|
int i; |
|
|
|
bool skip = false; |
|
|
|
for ( i = 0; i < NUM_PHONEME_CLASSES; i++ ) |
|
{ |
|
Emphasized_Phoneme *info = &classes[ i ]; |
|
|
|
// Assume it's bogus |
|
info->valid = false; |
|
info->amount = 0.0f; |
|
|
|
// One time setup |
|
if ( !info->basechecked ) |
|
{ |
|
info->basechecked = true; |
|
info->base = (flexsettinghdr_t *)g_FlexSceneFileManager.FindSceneFile( this, info->classname, false ); |
|
} |
|
info->exp = NULL; |
|
if ( info->base ) |
|
{ |
|
Assert( info->base->id == ('V' << 16) + ('F' << 8) + ('E') ); |
|
info->exp = info->base->pIndexedSetting( phoneme ); |
|
} |
|
|
|
if ( info->required && ( !info->base || !info->exp ) ) |
|
{ |
|
skip = true; |
|
break; |
|
} |
|
|
|
if ( info->exp ) |
|
{ |
|
info->valid = true; |
|
} |
|
} |
|
|
|
return skip; |
|
} |
|
|
|
#define STRONG_CROSSFADE_START 0.60f |
|
#define WEAK_CROSSFADE_START 0.40f |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Here's the formula |
|
// 0.5 is neutral 100 % of the default setting |
|
// Crossfade starts at STRONG_CROSSFADE_START and is full at STRONG_CROSSFADE_END |
|
// If there isn't a strong then the intensity of the underlying phoneme is fixed at 2 x STRONG_CROSSFADE_START |
|
// so we don't get huge numbers |
|
// Input : *classes - |
|
// emphasis_intensity - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::ComputeBlendedSetting( Emphasized_Phoneme *classes, float emphasis_intensity ) |
|
{ |
|
// See which blends are available for the current phoneme |
|
bool has_weak = classes[ PHONEME_CLASS_WEAK ].valid; |
|
bool has_strong = classes[ PHONEME_CLASS_STRONG ].valid; |
|
|
|
// Better have phonemes in general |
|
Assert( classes[ PHONEME_CLASS_NORMAL ].valid ); |
|
|
|
if ( emphasis_intensity > STRONG_CROSSFADE_START ) |
|
{ |
|
if ( has_strong ) |
|
{ |
|
// Blend in some of strong |
|
float dist_remaining = 1.0f - emphasis_intensity; |
|
float frac = dist_remaining / ( 1.0f - STRONG_CROSSFADE_START ); |
|
|
|
classes[ PHONEME_CLASS_NORMAL ].amount = (frac) * 2.0f * STRONG_CROSSFADE_START; |
|
classes[ PHONEME_CLASS_STRONG ].amount = 1.0f - frac; |
|
} |
|
else |
|
{ |
|
emphasis_intensity = MIN( emphasis_intensity, STRONG_CROSSFADE_START ); |
|
classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; |
|
} |
|
} |
|
else if ( emphasis_intensity < WEAK_CROSSFADE_START ) |
|
{ |
|
if ( has_weak ) |
|
{ |
|
// Blend in some weak |
|
float dist_remaining = WEAK_CROSSFADE_START - emphasis_intensity; |
|
float frac = dist_remaining / ( WEAK_CROSSFADE_START ); |
|
|
|
classes[ PHONEME_CLASS_NORMAL ].amount = (1.0f - frac) * 2.0f * WEAK_CROSSFADE_START; |
|
classes[ PHONEME_CLASS_WEAK ].amount = frac; |
|
} |
|
else |
|
{ |
|
emphasis_intensity = MAX( emphasis_intensity, WEAK_CROSSFADE_START ); |
|
classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; |
|
} |
|
} |
|
else |
|
{ |
|
// Assume 0.5 (neutral) becomes a scaling of 1.0f |
|
classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::InitPhonemeMappings( void ) |
|
{ |
|
CStudioHdr studioHdr( GetStudioHdr(), g_pMDLCache ); |
|
if ( studioHdr.IsValid() ) |
|
{ |
|
char szBasename[MAX_PATH]; |
|
Q_StripExtension( studioHdr.pszName(), szBasename, sizeof( szBasename ) ); |
|
|
|
char szExpressionName[MAX_PATH]; |
|
Q_snprintf( szExpressionName, sizeof( szExpressionName ), "%s/phonemes/phonemes", szBasename ); |
|
if ( g_FlexSceneFileManager.FindSceneFile( this, szExpressionName, false ) ) |
|
{ |
|
SetupMappings( szExpressionName ); |
|
return; |
|
} |
|
} |
|
|
|
SetupMappings( "phonemes" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::SetupMappings( char const *pchFileRoot ) |
|
{ |
|
// Fill in phoneme class lookup |
|
memset( m_PhonemeClasses, 0, sizeof( m_PhonemeClasses ) ); |
|
|
|
Emphasized_Phoneme *normal = &m_PhonemeClasses[ PHONEME_CLASS_NORMAL ]; |
|
Q_snprintf( normal->classname, sizeof( normal->classname ), "%s", pchFileRoot ); |
|
normal->required = true; |
|
|
|
Emphasized_Phoneme *weak = &m_PhonemeClasses[ PHONEME_CLASS_WEAK ]; |
|
Q_snprintf( weak->classname, sizeof( weak->classname ), "%s_weak", pchFileRoot ); |
|
Emphasized_Phoneme *strong = &m_PhonemeClasses[ PHONEME_CLASS_STRONG ]; |
|
Q_snprintf( strong->classname, sizeof( strong->classname ), "%s_strong", pchFileRoot ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Since everyone shared a pSettinghdr now, we need to set up the localtoglobal mapping per entity, but |
|
// we just do this in memory with an array of integers (could be shorts, I suppose) |
|
// Input : *pSettinghdr - |
|
//----------------------------------------------------------------------------- |
|
void CTFPlayerModelPanel::EnsureTranslations( const flexsettinghdr_t *pSettinghdr ) |
|
{ |
|
Assert( pSettinghdr ); |
|
|
|
FS_LocalToGlobal_t entry( pSettinghdr ); |
|
|
|
unsigned short idx = m_LocalToGlobal.Find( entry ); |
|
if ( idx != m_LocalToGlobal.InvalidIndex() ) |
|
return; |
|
|
|
entry.SetCount( pSettinghdr->numkeys ); |
|
|
|
for ( int i = 0; i < pSettinghdr->numkeys; ++i ) |
|
{ |
|
entry.m_Mapping[ i ] = C_BaseFlex::AddGlobalFlexController( pSettinghdr->pLocalName( i ) ); |
|
} |
|
|
|
m_LocalToGlobal.Insert( entry ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Look up instance specific mapping |
|
// Input : *pSettinghdr - |
|
// key - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CTFPlayerModelPanel::FlexControllerLocalToGlobal( const flexsettinghdr_t *pSettinghdr, int key ) |
|
{ |
|
FS_LocalToGlobal_t entry( pSettinghdr ); |
|
|
|
int idx = m_LocalToGlobal.Find( entry ); |
|
if ( idx == m_LocalToGlobal.InvalidIndex() ) |
|
{ |
|
// This should never happen!!! |
|
Assert( 0 ); |
|
Warning( "Unable to find mapping for flexcontroller %i, settings %p on CTFPlayerModelPanel\n", key, pSettinghdr ); |
|
EnsureTranslations( pSettinghdr ); |
|
idx = m_LocalToGlobal.Find( entry ); |
|
if ( idx == m_LocalToGlobal.InvalidIndex() ) |
|
{ |
|
Error( "CTFPlayerModelPanel::FlexControllerLocalToGlobal failed!\n" ); |
|
} |
|
} |
|
|
|
FS_LocalToGlobal_t& result = m_LocalToGlobal[ idx ]; |
|
// Validate lookup |
|
Assert( result.m_nCount != 0 && key < result.m_nCount ); |
|
int index = result.m_Mapping[ key ]; |
|
return index; |
|
} |
|
|
|
|