//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Interface layer for ipion IVP physics. // // $Workfile: $ // $Date: $ // $NoKeywords: $ //===========================================================================// #include "cbase.h" #include "coordsize.h" #include "entitylist.h" #include "vcollide_parse.h" #include "soundenvelope.h" #include "game.h" #include "utlvector.h" #include "init_factory.h" #include "igamesystem.h" #include "hierarchy.h" #include "IEffects.h" #include "engine/IEngineSound.h" #include "world.h" #include "decals.h" #include "physics_fx.h" #include "vphysics_sound.h" #include "vphysics/vehicles.h" #include "vehicle_sounds.h" #include "movevars_shared.h" #include "physics_saverestore.h" #include "solidsetdefaults.h" #include "tier0/vprof.h" #include "engine/IStaticPropMgr.h" #include "physics_prop_ragdoll.h" #if HL2_EPISODIC #include "particle_parse.h" #endif #include "vphysics/object_hash.h" #include "vphysics/collision_set.h" #include "vphysics/friction.h" #include "fmtstr.h" #include "physics_npc_solver.h" #include "physics_collisionevent.h" #include "vphysics/performance.h" #include "positionwatcher.h" #include "tier1/callqueue.h" #include "vphysics/constraints.h" #ifdef PORTAL #include "portal_physics_collisionevent.h" #include "physicsshadowclone.h" #include "PortalSimulation.h" void PortalPhysFrame( float deltaTime ); //small wrapper for PhysFrame that simulates all 3 environments at once #endif void PrecachePhysicsSounds( void ); // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar phys_speeds( "phys_speeds", "0" ); // defined in phys_constraint extern IPhysicsConstraintEvent *g_pConstraintEvents; CEntityList *g_pShadowEntities = NULL; #ifdef PORTAL CEntityList *g_pShadowEntities_Main = NULL; #endif // local variables static float g_PhysAverageSimTime; CCallQueue g_PostSimulationQueue; // local routines static IPhysicsObject *PhysCreateWorld( CBaseEntity *pWorld ); static void PhysFrame( float deltaTime ); static bool IsDebris( int collisionGroup ); void TimescaleChanged( IConVar *var, const char *pOldString, float flOldValue ) { if ( physenv ) { physenv->ResetSimulationClock(); } } ConVar phys_timescale( "phys_timescale", "1", 0, "Scale time for physics", TimescaleChanged ); #if _DEBUG ConVar phys_dontprintint( "phys_dontprintint", "1", FCVAR_NONE, "Don't print inter-penetration warnings." ); #endif #ifdef PORTAL CPortal_CollisionEvent g_Collisions; #else CCollisionEvent g_Collisions; #endif IPhysicsCollisionSolver * const g_pCollisionSolver = &g_Collisions; IPhysicsCollisionEvent * const g_pCollisionEventHandler = &g_Collisions; IPhysicsObjectEvent * const g_pObjectEventHandler = &g_Collisions; struct vehiclescript_t { string_t scriptName; vehicleparams_t params; vehiclesounds_t sounds; }; class CPhysicsHook : public CBaseGameSystemPerFrame { public: virtual const char *Name() { return "CPhysicsHook"; } virtual bool Init(); virtual void LevelInitPreEntity(); virtual void LevelInitPostEntity(); virtual void LevelShutdownPreEntity(); virtual void LevelShutdownPostEntity(); virtual void FrameUpdatePostEntityThink(); virtual void PreClientUpdate(); bool FindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pVehicle, vehiclesounds_t *pSounds ); void FlushVehicleScripts() { m_vehicleScripts.RemoveAll(); } bool ShouldSimulate() { return (physenv && !m_bPaused) ? true : false; } physicssound::soundlist_t m_impactSounds; CUtlVector m_breakSounds; CUtlVector m_massCenterOverrides; CUtlVector m_vehicleScripts; float m_impactSoundTime; bool m_bPaused; bool m_isFinalTick; }; CPhysicsHook g_PhysicsHook; //----------------------------------------------------------------------------- // Singleton access //----------------------------------------------------------------------------- IGameSystem* PhysicsGameSystem() { return &g_PhysicsHook; } //----------------------------------------------------------------------------- // Purpose: The physics hook callback implementations //----------------------------------------------------------------------------- bool CPhysicsHook::Init( void ) { factorylist_t factories; // Get the list of interface factories to extract the physics DLL's factory FactoryList_Retrieve( factories ); if ( !factories.physicsFactory ) return false; if ((physics = (IPhysics *)factories.physicsFactory( VPHYSICS_INTERFACE_VERSION, NULL )) == NULL || (physcollision = (IPhysicsCollision *)factories.physicsFactory( VPHYSICS_COLLISION_INTERFACE_VERSION, NULL )) == NULL || (physprops = (IPhysicsSurfaceProps *)factories.physicsFactory( VPHYSICS_SURFACEPROPS_INTERFACE_VERSION, NULL )) == NULL ) return false; PhysParseSurfaceData( physprops, filesystem ); m_isFinalTick = true; m_impactSoundTime = 0; m_vehicleScripts.EnsureCapacity(4); return true; } // a little debug wrapper to help fix bugs when entity pointers get trashed #if 0 struct physcheck_t { IPhysicsObject *pPhys; char string[512]; }; CUtlVector< physcheck_t > physCheck; void PhysCheckAdd( IPhysicsObject *pPhys, const char *pString ) { physcheck_t tmp; tmp.pPhys = pPhys; Q_strncpy( tmp.string, pString ,sizeof(tmp.string)); physCheck.AddToTail( tmp ); } const char *PhysCheck( IPhysicsObject *pPhys ) { for ( int i = 0; i < physCheck.Size(); i++ ) { if ( physCheck[i].pPhys == pPhys ) return physCheck[i].string; } return "unknown"; } #endif void CPhysicsHook::LevelInitPreEntity() { physenv = physics->CreateEnvironment(); physics_performanceparams_t params; params.Defaults(); params.maxCollisionsPerObjectPerTimestep = 10; physenv->SetPerformanceSettings( ¶ms ); #ifdef PORTAL physenv_main = physenv; #endif { g_EntityCollisionHash = physics->CreateObjectPairHash(); } factorylist_t factories; FactoryList_Retrieve( factories ); physenv->SetDebugOverlay( factories.engineFactory ); physenv->EnableDeleteQueue( true ); physenv->SetCollisionSolver( &g_Collisions ); physenv->SetCollisionEventHandler( &g_Collisions ); physenv->SetConstraintEventHandler( g_pConstraintEvents ); physenv->EnableConstraintNotify( true ); // callback when an object gets deleted that is attached to a constraint physenv->SetObjectEventHandler( &g_Collisions ); physenv->SetSimulationTimestep( gpGlobals->interval_per_tick ); // 15 ms per tick // HL Game gravity, not real-world gravity physenv->SetGravity( Vector( 0, 0, -GetCurrentGravity() ) ); g_PhysAverageSimTime = 0; g_PhysWorldObject = PhysCreateWorld( GetWorldEntity() ); g_pShadowEntities = new CEntityList; #ifdef PORTAL g_pShadowEntities_Main = g_pShadowEntities; #endif PrecachePhysicsSounds(); m_bPaused = true; } void CPhysicsHook::LevelInitPostEntity() { m_bPaused = false; } void CPhysicsHook::LevelShutdownPreEntity() { if ( !physenv ) return; physenv->SetQuickDelete( true ); } void CPhysicsHook::LevelShutdownPostEntity() { if ( !physenv ) return; g_pPhysSaveRestoreManager->ForgetAllModels(); g_Collisions.LevelShutdown(); physics->DestroyEnvironment( physenv ); physenv = NULL; physics->DestroyObjectPairHash( g_EntityCollisionHash ); g_EntityCollisionHash = NULL; physics->DestroyAllCollisionSets(); g_PhysWorldObject = NULL; delete g_pShadowEntities; g_pShadowEntities = NULL; m_impactSounds.RemoveAll(); m_breakSounds.RemoveAll(); m_massCenterOverrides.Purge(); FlushVehicleScripts(); } bool CPhysicsHook::FindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pVehicle, vehiclesounds_t *pSounds ) { bool bLoadedSounds = false; int index = -1; for ( int i = 0; i < m_vehicleScripts.Count(); i++ ) { if ( !Q_stricmp(m_vehicleScripts[i].scriptName.ToCStr(), pScriptName) ) { index = i; bLoadedSounds = true; break; } } if ( index < 0 ) { byte *pFile = UTIL_LoadFileForMe( pScriptName, NULL ); if ( pFile ) { // new script, parse it and write to the table index = m_vehicleScripts.AddToTail(); m_vehicleScripts[index].scriptName = AllocPooledString(pScriptName); m_vehicleScripts[index].sounds.Init(); IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( (char *)pFile ); while ( !pParse->Finished() ) { const char *pBlock = pParse->GetCurrentBlockName(); if ( !strcmpi( pBlock, "vehicle" ) ) { pParse->ParseVehicle( &m_vehicleScripts[index].params, NULL ); } else if ( !Q_stricmp( pBlock, "vehicle_sounds" ) ) { bLoadedSounds = true; CVehicleSoundsParser soundParser; pParse->ParseCustom( &m_vehicleScripts[index].sounds, &soundParser ); } else { pParse->SkipBlock(); } } physcollision->VPhysicsKeyParserDestroy( pParse ); UTIL_FreeFile( pFile ); } } if ( index >= 0 ) { if ( pVehicle ) { *pVehicle = m_vehicleScripts[index].params; } if ( pSounds ) { // We must pass back valid data here! if ( bLoadedSounds == false ) return false; *pSounds = m_vehicleScripts[index].sounds; } return true; } return false; } // called after entities think void CPhysicsHook::FrameUpdatePostEntityThink( ) { VPROF_BUDGET( "CPhysicsHook::FrameUpdatePostEntityThink", VPROF_BUDGETGROUP_PHYSICS ); // Tracker 24846: If game is paused, don't simulate vphysics float interval = ( gpGlobals->frametime > 0.0f ) ? TICK_INTERVAL : 0.0f; // update the physics simulation, not we don't use gpGlobals->frametime, since that can be 30 msec or 15 msec // depending on whether IsSimulatingOnAlternateTicks is true or not if ( CBaseEntity::IsSimulatingOnAlternateTicks() ) { m_isFinalTick = false; #ifdef PORTAL //slight detour if we're the portal mod PortalPhysFrame( interval ); #else PhysFrame( interval ); #endif } m_isFinalTick = true; #ifdef PORTAL //slight detour if we're the portal mod PortalPhysFrame( interval ); #else PhysFrame( interval ); #endif } void CPhysicsHook::PreClientUpdate() { m_impactSoundTime += gpGlobals->frametime; if ( m_impactSoundTime > 0.05f ) { physicssound::PlayImpactSounds( m_impactSounds ); m_impactSoundTime = 0.0f; physicssound::PlayBreakSounds( m_breakSounds ); } } bool PhysIsFinalTick() { return g_PhysicsHook.m_isFinalTick; } IPhysicsObject *PhysCreateWorld( CBaseEntity *pWorld ) { staticpropmgr->CreateVPhysicsRepresentations( physenv, &g_SolidSetup, pWorld ); return PhysCreateWorld_Shared( pWorld, modelinfo->GetVCollide(1), g_PhysDefaultObjectParams ); } // vehicle wheels can only collide with things that can't get stuck in them during game physics // because they aren't in the game physics world at present static bool WheelCollidesWith( IPhysicsObject *pObj, CBaseEntity *pEntity ) { #if defined( INVASION_DLL ) if ( pEntity->GetCollisionGroup() == TFCOLLISION_GROUP_OBJECT ) return false; #endif // Cull against interactive debris if ( pEntity->GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS ) return false; // Hit physics ents if ( pEntity->GetMoveType() == MOVETYPE_PUSH || pEntity->GetMoveType() == MOVETYPE_VPHYSICS || pObj->IsStatic() ) return true; return false; } CCollisionEvent::CCollisionEvent() { m_inCallback = 0; m_bBufferTouchEvents = false; m_lastTickFrictionError = 0; } int CCollisionEvent::ShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ) #if _DEBUG { int x0 = ShouldCollide_2(pObj0, pObj1, pGameData0, pGameData1); int x1 = ShouldCollide_2(pObj1, pObj0, pGameData1, pGameData0); Assert(x0==x1); return x0; } int CCollisionEvent::ShouldCollide_2( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ) #endif { CallbackContext check(this); CBaseEntity *pEntity0 = static_cast(pGameData0); CBaseEntity *pEntity1 = static_cast(pGameData1); if ( !pEntity0 || !pEntity1 ) return 1; unsigned short gameFlags0 = pObj0->GetGameFlags(); unsigned short gameFlags1 = pObj1->GetGameFlags(); if ( pEntity0 == pEntity1 ) { // allow all-or-nothing per-entity disable if ( (gameFlags0 | gameFlags1) & FVPHYSICS_NO_SELF_COLLISIONS ) return 0; IPhysicsCollisionSet *pSet = physics->FindCollisionSet( pEntity0->GetModelIndex() ); if ( pSet ) return pSet->ShouldCollide( pObj0->GetGameIndex(), pObj1->GetGameIndex() ); return 1; } // objects that are both constrained to the world don't collide with each other if ( (gameFlags0 & gameFlags1) & FVPHYSICS_CONSTRAINT_STATIC ) { return 0; } // Special collision rules for vehicle wheels // Their entity collides with stuff using the normal rules, but they // have different rules than the vehicle body for various reasons. // sort of a hack because we don't have spheres to represent them in the game // world for speculative collisions. if ( pObj0->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL ) { if ( !WheelCollidesWith( pObj1, pEntity1 ) ) return false; } if ( pObj1->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL ) { if ( !WheelCollidesWith( pObj0, pEntity0 ) ) return false; } if ( pEntity0->ForceVPhysicsCollide( pEntity1 ) || pEntity1->ForceVPhysicsCollide( pEntity0 ) ) return 1; if ( pEntity0->edict() && pEntity1->edict() ) { // don't collide with your owner if ( pEntity0->GetOwnerEntity() == pEntity1 || pEntity1->GetOwnerEntity() == pEntity0 ) return 0; } if ( pEntity0->GetMoveParent() || pEntity1->GetMoveParent() ) { CBaseEntity *pParent0 = pEntity0->GetRootMoveParent(); CBaseEntity *pParent1 = pEntity1->GetRootMoveParent(); // NOTE: Don't let siblings/parents collide. If you want this behavior, do it // with constraints, not hierarchy! if ( pParent0 == pParent1 ) return 0; if ( g_EntityCollisionHash->IsObjectPairInHash( pParent0, pParent1 ) ) return 0; IPhysicsObject *p0 = pParent0->VPhysicsGetObject(); IPhysicsObject *p1 = pParent1->VPhysicsGetObject(); if ( p0 && p1 ) { if ( g_EntityCollisionHash->IsObjectPairInHash( p0, p1 ) ) return 0; } } int solid0 = pEntity0->GetSolid(); int solid1 = pEntity1->GetSolid(); int nSolidFlags0 = pEntity0->GetSolidFlags(); int nSolidFlags1 = pEntity1->GetSolidFlags(); int movetype0 = pEntity0->GetMoveType(); int movetype1 = pEntity1->GetMoveType(); // entities with non-physical move parents or entities with MOVETYPE_PUSH // are considered as "AI movers". They are unchanged by collision; they exert // physics forces on the rest of the system. bool aiMove0 = (movetype0==MOVETYPE_PUSH) ? true : false; bool aiMove1 = (movetype1==MOVETYPE_PUSH) ? true : false; if ( pEntity0->GetMoveParent() ) { // if the object & its parent are both MOVETYPE_VPHYSICS, then this must be a special case // like a prop_ragdoll_attached if ( !(movetype0 == MOVETYPE_VPHYSICS && pEntity0->GetRootMoveParent()->GetMoveType() == MOVETYPE_VPHYSICS) ) { aiMove0 = true; } } if ( pEntity1->GetMoveParent() ) { // if the object & its parent are both MOVETYPE_VPHYSICS, then this must be a special case. if ( !(movetype1 == MOVETYPE_VPHYSICS && pEntity1->GetRootMoveParent()->GetMoveType() == MOVETYPE_VPHYSICS) ) { aiMove1 = true; } } // AI movers don't collide with the world/static/pinned objects or other AI movers if ( (aiMove0 && !pObj1->IsMoveable()) || (aiMove1 && !pObj0->IsMoveable()) || (aiMove0 && aiMove1) ) return 0; // two objects under shadow control should not collide. The AI will figure it out if ( pObj0->GetShadowController() && pObj1->GetShadowController() ) return 0; // BRJ 1/24/03 // You can remove the assert if it's problematic; I *believe* this condition // should be met, but I'm not sure. //Assert ( (solid0 != SOLID_NONE) && (solid1 != SOLID_NONE) ); if ( (solid0 == SOLID_NONE) || (solid1 == SOLID_NONE) ) return 0; // not solid doesn't collide with anything if ( (nSolidFlags0|nSolidFlags1) & FSOLID_NOT_SOLID ) { // might be a vphysics trigger, collide with everything but "not solid" if ( pObj0->IsTrigger() && !(nSolidFlags1 & FSOLID_NOT_SOLID) ) return 1; if ( pObj1->IsTrigger() && !(nSolidFlags0 & FSOLID_NOT_SOLID) ) return 1; return 0; } if ( (nSolidFlags0 & FSOLID_TRIGGER) && !(solid1 == SOLID_VPHYSICS || solid1 == SOLID_BSP || movetype1 == MOVETYPE_VPHYSICS) ) return 0; if ( (nSolidFlags1 & FSOLID_TRIGGER) && !(solid0 == SOLID_VPHYSICS || solid0 == SOLID_BSP || movetype0 == MOVETYPE_VPHYSICS) ) return 0; if ( !g_pGameRules->ShouldCollide( pEntity0->GetCollisionGroup(), pEntity1->GetCollisionGroup() ) ) return 0; // check contents if ( !(pObj0->GetContents() & pEntity1->PhysicsSolidMaskForEntity()) || !(pObj1->GetContents() & pEntity0->PhysicsSolidMaskForEntity()) ) return 0; if ( g_EntityCollisionHash->IsObjectPairInHash( pGameData0, pGameData1 ) ) return 0; if ( g_EntityCollisionHash->IsObjectPairInHash( pObj0, pObj1 ) ) return 0; return 1; } bool FindMaxContact( IPhysicsObject *pObject, float minForce, IPhysicsObject **pOtherObject, Vector *contactPos, Vector *pForce ) { float mass = pObject->GetMass(); float maxForce = minForce; *pOtherObject = NULL; IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); while ( pSnapshot->IsValid() ) { IPhysicsObject *pOther = pSnapshot->GetObject(1); if ( pOther->IsMoveable() && pOther->GetMass() > mass ) { float force = pSnapshot->GetNormalForce(); if ( force > maxForce ) { *pOtherObject = pOther; pSnapshot->GetContactPoint( *contactPos ); pSnapshot->GetSurfaceNormal( *pForce ); *pForce *= force; } } pSnapshot->NextFrictionData(); } pObject->DestroyFrictionSnapshot( pSnapshot ); if ( *pOtherObject ) return true; return false; } bool CCollisionEvent::ShouldFreezeObject( IPhysicsObject *pObject ) { extern bool PropIsGib(CBaseEntity *pEntity); // for now, don't apply a per-object limit to ai MOVETYPE_PUSH objects // NOTE: If this becomes a problem (too many collision checks this tick) we should add a path // to inform the logic in VPhysicsUpdatePusher() about the limit being applied so // that it doesn't falsely block the object when it's simply been temporarily frozen // for performance reasons CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( pEntity ) { if (pEntity->GetMoveType() == MOVETYPE_PUSH ) return false; // don't limit vehicle collisions either, limit can make breaking through a pile of breakable // props very hitchy if (pEntity->GetServerVehicle() && !(pObject->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL)) return false; } // if we're freezing a debris object, then it's probably due to some kind of solver issue // usually this is a large object resting on the debris object in question which is not // very stable. // After doing the experiment of constraining the dynamic range of mass while solving friction // contacts, I like the results of this tradeoff better. So damage or remove the debris object // wherever possible once we hit this case: if ( IsDebris( pEntity->GetCollisionGroup()) && !pEntity->IsNPC() ) { IPhysicsObject *pOtherObject = NULL; Vector contactPos; Vector force; // find the contact with the moveable object applying the most contact force if ( FindMaxContact( pObject, pObject->GetMass() * 10, &pOtherObject, &contactPos, &force ) ) { CBaseEntity *pOther = static_cast(pOtherObject->GetGameData()); // this object can take damage, crush it if ( pEntity->m_takedamage > DAMAGE_EVENTS_ONLY ) { CTakeDamageInfo dmgInfo( pOther, pOther, force, contactPos, force.Length() * 0.1f, DMG_CRUSH ); PhysCallbackDamage( pEntity, dmgInfo ); } else { // can't be damaged, so do something else: if ( PropIsGib(pEntity) ) { // it's always safe to delete gibs, so kill this one to avoid simulation problems PhysCallbackRemove( pEntity->NetworkProp() ); } else { // not a gib, create a solver: // UNDONE: Add a property to override this in gameplay critical scenarios? g_PostSimulationQueue.QueueCall( EntityPhysics_CreateSolver, pOther, pEntity, true, 1.0f ); } } } } return true; } bool CCollisionEvent::ShouldFreezeContacts( IPhysicsObject **pObjectList, int objectCount ) { if ( m_lastTickFrictionError > gpGlobals->tickcount || m_lastTickFrictionError < (gpGlobals->tickcount-1) ) { DevWarning("Performance Warning: large friction system (%d objects)!!!\n", objectCount ); #if _DEBUG for ( int i = 0; i < objectCount; i++ ) { CBaseEntity *pEntity = static_cast(pObjectList[i]->GetGameData()); pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT | OVERLAY_PIVOT_BIT; } #endif } m_lastTickFrictionError = gpGlobals->tickcount; return false; } // NOTE: these are fully edge triggered events // called when an object wakes up (starts simulating) void CCollisionEvent::ObjectWake( IPhysicsObject *pObject ) { CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( pEntity && pEntity->HasDataObjectType( VPHYSICSWATCHER ) ) { ReportVPhysicsStateChanged( pObject, pEntity, true ); } } // called when an object goes to sleep (no longer simulating) void CCollisionEvent::ObjectSleep( IPhysicsObject *pObject ) { CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( pEntity && pEntity->HasDataObjectType( VPHYSICSWATCHER ) ) { ReportVPhysicsStateChanged( pObject, pEntity, false ); } } bool PhysShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1 ) { void *pGameData0 = pObj0->GetGameData(); void *pGameData1 = pObj1->GetGameData(); if ( !pGameData0 || !pGameData1 ) return false; return g_Collisions.ShouldCollide( pObj0, pObj1, pGameData0, pGameData1 ) ? true : false; } bool PhysIsInCallback() { if ( (physenv && physenv->IsInSimulation()) || g_Collisions.IsInCallback() ) return true; return false; } static void ReportPenetration( CBaseEntity *pEntity, float duration ) { if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) { if ( g_pDeveloper->GetInt() > 1 ) { pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT; } pEntity->AddTimedOverlay( UTIL_VarArgs("VPhysics Penetration Error (%s)!", pEntity->GetDebugName()), duration ); } } static bool IsDebris( int collisionGroup ) { switch ( collisionGroup ) { case COLLISION_GROUP_DEBRIS: case COLLISION_GROUP_INTERACTIVE_DEBRIS: case COLLISION_GROUP_DEBRIS_TRIGGER: return true; default: break; } return false; } static void UpdateEntityPenetrationFlag( CBaseEntity *pEntity, bool isPenetrating ) { if ( !pEntity ) return; IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); for ( int i = 0; i < count; i++ ) { if ( !pList[i]->IsStatic() ) { if ( isPenetrating ) { PhysSetGameFlags( pList[i], FVPHYSICS_PENETRATING ); } else { PhysClearGameFlags( pList[i], FVPHYSICS_PENETRATING ); } } } } void CCollisionEvent::GetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ) { for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) { if ( m_penetrateEvents[i].hEntity0 == pSearch && m_penetrateEvents[i].hEntity1.Get() != NULL ) { list.AddToTail( m_penetrateEvents[i].hEntity1 ); } else if ( m_penetrateEvents[i].hEntity1 == pSearch && m_penetrateEvents[i].hEntity0.Get() != NULL ) { list.AddToTail( m_penetrateEvents[i].hEntity0 ); } } } void CCollisionEvent::UpdatePenetrateEvents( void ) { for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) { CBaseEntity *pEntity0 = m_penetrateEvents[i].hEntity0; CBaseEntity *pEntity1 = m_penetrateEvents[i].hEntity1; if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYDISABLE ) { if ( pEntity0 && pEntity1 ) { IPhysicsObject *pObj0 = pEntity0->VPhysicsGetObject(); if ( pObj0 ) { PhysForceEntityToSleep( pEntity0, pObj0 ); } IPhysicsObject *pObj1 = pEntity1->VPhysicsGetObject(); if ( pObj1 ) { PhysForceEntityToSleep( pEntity1, pObj1 ); } m_penetrateEvents[i].collisionState = COLLSTATE_DISABLED; continue; } // missing entity or object, clear event } else if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYNPCSOLVER ) { if ( pEntity0 && pEntity1 ) { CAI_BaseNPC *pNPC = pEntity0->MyNPCPointer(); CBaseEntity *pBlocker = pEntity1; if ( !pNPC ) { pNPC = pEntity1->MyNPCPointer(); Assert(pNPC); pBlocker = pEntity0; } NPCPhysics_CreateSolver( pNPC, pBlocker, true, 1.0f ); } // transferred to solver, clear event } else if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYENTITYSOLVER ) { if ( pEntity0 && pEntity1 ) { if ( !IsDebris(pEntity1->GetCollisionGroup()) || pEntity1->GetMoveType() != MOVETYPE_VPHYSICS ) { CBaseEntity *pTmp = pEntity0; pEntity0 = pEntity1; pEntity1 = pTmp; } EntityPhysics_CreateSolver( pEntity0, pEntity1, true, 1.0f ); } // transferred to solver, clear event } else if ( gpGlobals->curtime - m_penetrateEvents[i].timeStamp > 1.0 ) { if ( m_penetrateEvents[i].collisionState == COLLSTATE_DISABLED ) { if ( pEntity0 && pEntity1 ) { IPhysicsObject *pObj0 = pEntity0->VPhysicsGetObject(); IPhysicsObject *pObj1 = pEntity1->VPhysicsGetObject(); if ( pObj0 && pObj1 ) { m_penetrateEvents[i].collisionState = COLLSTATE_ENABLED; continue; } } } // haven't penetrated for 1 second, so remove } else { // recent timestamp, don't remove the event yet continue; } // done, clear event m_penetrateEvents.FastRemove(i); UpdateEntityPenetrationFlag( pEntity0, false ); UpdateEntityPenetrationFlag( pEntity1, false ); } } penetrateevent_t &CCollisionEvent::FindOrAddPenetrateEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ) { int index = -1; for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) { if ( m_penetrateEvents[i].hEntity0.Get() == pEntity0 && m_penetrateEvents[i].hEntity1.Get() == pEntity1 ) { index = i; break; } } if ( index < 0 ) { index = m_penetrateEvents.AddToTail(); penetrateevent_t &event = m_penetrateEvents[index]; event.hEntity0 = pEntity0; event.hEntity1 = pEntity1; event.startTime = gpGlobals->curtime; event.collisionState = COLLSTATE_ENABLED; UpdateEntityPenetrationFlag( pEntity0, true ); UpdateEntityPenetrationFlag( pEntity1, true ); } penetrateevent_t &event = m_penetrateEvents[index]; event.timeStamp = gpGlobals->curtime; return event; } static ConVar phys_penetration_error_time( "phys_penetration_error_time", "10", 0, "Controls the duration of vphysics penetration error boxes." ); static bool CanResolvePenetrationWithNPC( CBaseEntity *pEntity, IPhysicsObject *pObject ) { if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) { // hinged objects won't be able to be pushed out anyway, so don't try the npc solver if ( !pObject->IsHinged() && !pObject->IsAttachedToConstraint(true) ) { if ( pObject->IsMoveable() || pEntity->GetServerVehicle() ) return true; } } return false; } int CCollisionEvent::ShouldSolvePenetration( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1, float dt ) { CallbackContext check(this); // Pointers to the entity for each physics object CBaseEntity *pEntity0 = static_cast(pGameData0); CBaseEntity *pEntity1 = static_cast(pGameData1); // this can get called as entities are being constructed on the other side of a game load or level transition // Some entities may not be fully constructed, so don't call into their code until the level is running if ( g_PhysicsHook.m_bPaused ) return true; // solve it yourself here and return 0, or have the default implementation do it if ( pEntity0 > pEntity1 ) { // swap sort CBaseEntity *pTmp = pEntity0; pEntity0 = pEntity1; pEntity1 = pTmp; IPhysicsObject *pTmpObj = pObj0; pObj0 = pObj1; pObj1 = pTmpObj; } if ( pEntity0 == pEntity1 ) { if ( pObj0->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL ) { DevMsg(2, "Solving ragdoll self penetration! %s (%s) (%d v %d)\n", pObj0->GetName(), pEntity0->GetDebugName(), pObj0->GetGameIndex(), pObj1->GetGameIndex() ); ragdoll_t *pRagdoll = Ragdoll_GetRagdoll( pEntity0 ); pRagdoll->pGroup->SolvePenetration( pObj0, pObj1 ); return false; } } penetrateevent_t &event = FindOrAddPenetrateEvent( pEntity0, pEntity1 ); float eventTime = gpGlobals->curtime - event.startTime; // NPC vs. physics object. Create a game DLL solver and remove this event if ( (pEntity0->MyNPCPointer() && CanResolvePenetrationWithNPC(pEntity1, pObj1)) || (pEntity1->MyNPCPointer() && CanResolvePenetrationWithNPC(pEntity0, pObj0)) ) { event.collisionState = COLLSTATE_TRYNPCSOLVER; } if ( (IsDebris( pEntity0->GetCollisionGroup() ) && !pObj1->IsStatic()) || (IsDebris( pEntity1->GetCollisionGroup() ) && !pObj0->IsStatic()) ) { if ( eventTime > 0.5f ) { //Msg("Debris stuck in non-static!\n"); event.collisionState = COLLSTATE_TRYENTITYSOLVER; } } #if _DEBUG if ( phys_dontprintint.GetBool() == false ) { const char *pName1 = STRING(pEntity0->GetModelName()); const char *pName2 = STRING(pEntity1->GetModelName()); if ( pEntity0 == pEntity1 ) { int index0 = physcollision->CollideIndex( pObj0->GetCollide() ); int index1 = physcollision->CollideIndex( pObj1->GetCollide() ); DevMsg(1, "***Inter-penetration on %s (%d & %d) (%.0f, %.0f)\n", pName1?pName1:"(null)", index0, index1, gpGlobals->curtime, eventTime ); } else { DevMsg(1, "***Inter-penetration between %s(%s) AND %s(%s) (%.0f, %.0f)\n", pName1?pName1:"(null)", pEntity0->GetDebugName(), pName2?pName2:"(null)", pEntity1->GetDebugName(), gpGlobals->curtime, eventTime ); } } #endif if ( eventTime > 3 ) { // don't report penetrations on ragdolls with themselves, or outside of developer mode if ( g_pDeveloper->GetInt() && pEntity0 != pEntity1 ) { ReportPenetration( pEntity0, phys_penetration_error_time.GetFloat() ); ReportPenetration( pEntity1, phys_penetration_error_time.GetFloat() ); } event.startTime = gpGlobals->curtime; // don't put players or game physics controlled objects to sleep if ( !pEntity0->IsPlayer() && !pEntity1->IsPlayer() && !pObj0->GetShadowController() && !pObj1->GetShadowController() ) { // two objects have been stuck for more than 3 seconds, try disabling simulation event.collisionState = COLLSTATE_TRYDISABLE; return false; } } return true; } void CCollisionEvent::FluidStartTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ) { CallbackContext check(this); if ( ( pObject == NULL ) || ( pFluid == NULL ) ) return; CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( !pEntity ) return; pEntity->AddEFlags( EFL_TOUCHING_FLUID ); pEntity->OnEntityEvent( ENTITY_EVENT_WATER_TOUCH, (void*)pFluid->GetContents() ); float timeSinceLastCollision = DeltaTimeSinceLastFluid( pEntity ); if ( timeSinceLastCollision < 0.5f ) return; // UNDONE: Use this for splash logic instead? // UNDONE: Use angular term too - push splashes in rotAxs cross normal direction? Vector normal; float dist; pFluid->GetSurfacePlane( &normal, &dist ); Vector vel; AngularImpulse angVel; pObject->GetVelocity( &vel, &angVel ); Vector unitVel = vel; VectorNormalize( unitVel ); // normal points out of the surface, we want the direction that points in float dragScale = pFluid->GetDensity() * physenv->GetSimulationTimestep(); normal = -normal; float linearScale = 0.5f * DotProduct( unitVel, normal ) * pObject->CalculateLinearDrag( normal ) * dragScale; linearScale = clamp( linearScale, 0.0f, 1.0f ); vel *= -linearScale; // UNDONE: Figure out how much of the surface area has crossed the water surface and scale angScale by that // For now assume 25% Vector rotAxis = angVel; VectorNormalize(rotAxis); float angScale = 0.25f * pObject->CalculateAngularDrag( angVel ) * dragScale; angScale = clamp( angScale, 0.0f, 1.0f ); angVel *= -angScale; // compute the splash before we modify the velocity PhysicsSplash( pFluid, pObject, pEntity ); // now damp out some motion toward the surface pObject->AddVelocity( &vel, &angVel ); } void CCollisionEvent::FluidEndTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ) { CallbackContext check(this); if ( ( pObject == NULL ) || ( pFluid == NULL ) ) return; CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( !pEntity ) return; float timeSinceLastCollision = DeltaTimeSinceLastFluid( pEntity ); if ( timeSinceLastCollision >= 0.5f ) { PhysicsSplash( pFluid, pObject, pEntity ); } pEntity->RemoveEFlags( EFL_TOUCHING_FLUID ); pEntity->OnEntityEvent( ENTITY_EVENT_WATER_UNTOUCH, (void*)pFluid->GetContents() ); } class CSkipKeys : public IVPhysicsKeyHandler { public: virtual void ParseKeyValue( void *pData, const char *pKey, const char *pValue ) {} virtual void SetDefaults( void *pData ) {} }; void PhysSolidOverride( solid_t &solid, string_t overrideScript ) { if ( overrideScript != NULL_STRING) { // parser destroys this data bool collisions = solid.params.enableCollisions; char pTmpString[4096]; // write a header for a solid_t Q_strncpy( pTmpString, "solid { ", sizeof(pTmpString) ); // suck out the comma delimited tokens and turn them into quoted key/values char szToken[256]; const char *pStr = nexttoken(szToken, STRING(overrideScript), ','); while ( szToken[0] != 0 ) { Q_strncat( pTmpString, "\"", sizeof(pTmpString), COPY_ALL_CHARACTERS ); Q_strncat( pTmpString, szToken, sizeof(pTmpString), COPY_ALL_CHARACTERS ); Q_strncat( pTmpString, "\" ", sizeof(pTmpString), COPY_ALL_CHARACTERS ); pStr = nexttoken(szToken, pStr, ','); } // terminate the script Q_strncat( pTmpString, "}", sizeof(pTmpString), COPY_ALL_CHARACTERS ); // parse that sucker IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( pTmpString ); CSkipKeys tmp; pParse->ParseSolid( &solid, &tmp ); physcollision->VPhysicsKeyParserDestroy( pParse ); // parser destroys this data solid.params.enableCollisions = collisions; } } void PhysSetMassCenterOverride( masscenteroverride_t &override ) { if ( override.entityName != NULL_STRING ) { g_PhysicsHook.m_massCenterOverrides.AddToTail( override ); } } // NOTE: This will remove the entry from the list as well int PhysGetMassCenterOverrideIndex( string_t name ) { if ( name != NULL_STRING && g_PhysicsHook.m_massCenterOverrides.Count() ) { for ( int i = 0; i < g_PhysicsHook.m_massCenterOverrides.Count(); i++ ) { if ( g_PhysicsHook.m_massCenterOverrides[i].entityName == name ) { return i; } } } return -1; } void PhysGetMassCenterOverride( CBaseEntity *pEntity, vcollide_t *pCollide, solid_t &solidOut ) { int index = PhysGetMassCenterOverrideIndex( pEntity->GetEntityName() ); if ( index >= 0 ) { masscenteroverride_t &override = g_PhysicsHook.m_massCenterOverrides[index]; Vector massCenterWS = override.center; switch ( override.alignType ) { case masscenteroverride_t::ALIGN_POINT: VectorITransform( massCenterWS, pEntity->EntityToWorldTransform(), solidOut.massCenterOverride ); break; case masscenteroverride_t::ALIGN_AXIS: { Vector massCenterLocal, defaultMassCenterWS; physcollision->CollideGetMassCenter( pCollide->solids[solidOut.index], &massCenterLocal ); VectorTransform( massCenterLocal, pEntity->EntityToWorldTransform(), defaultMassCenterWS ); massCenterWS += override.axis * ( DotProduct(defaultMassCenterWS,override.axis) - DotProduct( override.axis, override.center ) ); VectorITransform( massCenterWS, pEntity->EntityToWorldTransform(), solidOut.massCenterOverride ); } break; } g_PhysicsHook.m_massCenterOverrides.FastRemove( index ); if ( solidOut.massCenterOverride.Length() > DIST_EPSILON ) { solidOut.params.massCenterOverride = &solidOut.massCenterOverride; } } } float PhysGetEntityMass( CBaseEntity *pEntity ) { IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); float otherMass = 0; for ( int i = 0; i < physCount; i++ ) { otherMass += pList[i]->GetMass(); } return otherMass; } typedef void (*EntityCallbackFunction) ( CBaseEntity *pEntity ); void IterateActivePhysicsEntities( EntityCallbackFunction func ) { int activeCount = physenv->GetActiveObjectCount(); IPhysicsObject **pActiveList = NULL; if ( activeCount ) { pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); physenv->GetActiveObjects( pActiveList ); for ( int i = 0; i < activeCount; i++ ) { CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); if ( pEntity ) { func( pEntity ); } } } } static void CallbackHighlight( CBaseEntity *pEntity ) { pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT | OVERLAY_PIVOT_BIT; } static void CallbackReport( CBaseEntity *pEntity ) { const char *pName = STRING(pEntity->GetEntityName()); if ( !Q_strlen(pName) ) { pName = STRING(pEntity->GetModelName()); } Msg( "%s - %s\n", pEntity->GetClassname(), pName ); } CON_COMMAND(physics_highlight_active, "Turns on the absbox for all active physics objects") { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; IterateActivePhysicsEntities( CallbackHighlight ); } CON_COMMAND(physics_report_active, "Lists all active physics objects") { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; IterateActivePhysicsEntities( CallbackReport ); } CON_COMMAND_F(surfaceprop, "Reports the surface properties at the cursor", FCVAR_CHEAT ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; CBasePlayer *pPlayer = UTIL_GetCommandClient(); trace_t tr; Vector forward; pPlayer->EyeVectors( &forward ); UTIL_TraceLine(pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_COORD_RANGE, MASK_SHOT_HULL|CONTENTS_GRATE|CONTENTS_DEBRIS, pPlayer, COLLISION_GROUP_NONE, &tr ); if ( tr.DidHit() ) { const model_t *pModel = modelinfo->GetModel( tr.m_pEnt->GetModelIndex() ); const char *pModelName = STRING(tr.m_pEnt->GetModelName()); if ( tr.DidHitWorld() && tr.hitbox > 0 ) { ICollideable *pCollide = staticpropmgr->GetStaticPropByIndex( tr.hitbox-1 ); pModel = pCollide->GetCollisionModel(); pModelName = modelinfo->GetModelName( pModel ); } CFmtStr modelStuff; if ( pModel ) { modelStuff.sprintf("%s.%s ", modelinfo->IsTranslucent( pModel ) ? "Translucent" : "Opaque", modelinfo->IsTranslucentTwoPass( pModel ) ? " Two-pass." : "" ); } // Calculate distance to surface that was hit Vector vecVelocity = tr.startpos - tr.endpos; int length = vecVelocity.Length(); Msg("Hit surface \"%s\" (entity %s, model \"%s\" %s), texture \"%s\"\n", physprops->GetPropName( tr.surface.surfaceProps ), tr.m_pEnt->GetClassname(), pModelName, modelStuff.Access(), tr.surface.name); Msg("Distance to surface: %d\n", length ); } } static void OutputVPhysicsDebugInfo( CBaseEntity *pEntity ) { if ( pEntity ) { Msg("Entity %s (%s) %s Collision Group %d\n", pEntity->GetClassname(), pEntity->GetDebugName(), pEntity->IsNavIgnored() ? "NAV IGNORE" : "", pEntity->GetCollisionGroup() ); CUtlVector list; g_Collisions.GetListOfPenetratingEntities( pEntity, list ); for ( int i = 0; i < list.Count(); i++ ) { Msg(" penetration with entity %s (%s)\n", list[i]->GetDebugName(), STRING(list[i]->GetModelName()) ); } IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); if ( physCount ) { if ( physCount > 1 ) { for ( int i = 0; i < physCount; i++ ) { Msg("Object %d (of %d) =========================\n", i+1, physCount ); pList[i]->OutputDebugInfo(); } } else { pList[0]->OutputDebugInfo(); } } } } class CConstraintFloodEntry { public: CConstraintFloodEntry() : isMarked(false), isConstraint(false) {} CUtlVector linkList; bool isMarked; bool isConstraint; }; class CConstraintFloodList { public: CConstraintFloodList() { SetDefLessFunc( m_list ); m_list.EnsureCapacity(64); m_entryList.EnsureCapacity(64); } bool IsWorldEntity( CBaseEntity *pEnt ) { if ( pEnt->edict() ) return pEnt->IsWorld(); return false; } void AddLink( CBaseEntity *pEntity, CBaseEntity *pLink, bool bIsConstraint ) { if ( !pEntity || !pLink || IsWorldEntity(pEntity) || IsWorldEntity(pLink) ) return; int listIndex = m_list.Find(pEntity); if ( listIndex == m_list.InvalidIndex() ) { int entryIndex = m_entryList.AddToTail(); m_entryList[entryIndex].isConstraint = bIsConstraint; listIndex = m_list.Insert( pEntity, entryIndex ); } int entryIndex = m_list.Element(listIndex); CConstraintFloodEntry &entry = m_entryList.Element(entryIndex); Assert( entry.isConstraint == bIsConstraint ); if ( entry.linkList.Find(pLink) < 0 ) { entry.linkList.AddToTail( pLink ); } } void BuildGraphFromEntity( CBaseEntity *pEntity, CUtlVector &constraintList ) { int listIndex = m_list.Find(pEntity); if ( listIndex != m_list.InvalidIndex() ) { int entryIndex = m_list.Element(listIndex); CConstraintFloodEntry &entry = m_entryList.Element(entryIndex); if ( !entry.isMarked ) { if ( entry.isConstraint ) { Assert( constraintList.Find(pEntity) < 0); constraintList.AddToTail( pEntity ); } entry.isMarked = true; for ( int i = 0; i < entry.linkList.Count(); i++ ) { // now recursively traverse the graph from here BuildGraphFromEntity( entry.linkList[i], constraintList ); } } } } CUtlMap m_list; CUtlVector m_entryList; }; // traverses the graph of attachments (currently supports springs & constraints) starting at an entity // Then turns on debug info for each link in the graph (springs/constraints are links) static void DebugConstraints( CBaseEntity *pEntity ) { extern bool GetSpringAttachments( CBaseEntity *pEntity, CBaseEntity *pAttach[2], IPhysicsObject *pAttachVPhysics[2] ); extern bool GetConstraintAttachments( CBaseEntity *pEntity, CBaseEntity *pAttach[2], IPhysicsObject *pAttachVPhysics[2] ); extern void DebugConstraint(CBaseEntity *pEntity); if ( !pEntity ) return; CBaseEntity *pAttach[2]; IPhysicsObject *pAttachVPhysics[2]; CConstraintFloodList list; for ( CBaseEntity *pList = gEntList.FirstEnt(); pList != NULL; pList = gEntList.NextEnt(pList) ) { if ( GetConstraintAttachments(pList, pAttach, pAttachVPhysics) || GetSpringAttachments(pList, pAttach, pAttachVPhysics) ) { list.AddLink( pList, pAttach[0], true ); list.AddLink( pList, pAttach[1], true ); list.AddLink( pAttach[0], pList, false ); list.AddLink( pAttach[1], pList, false ); } } CUtlVector constraints; list.BuildGraphFromEntity( pEntity, constraints ); for ( int i = 0; i < constraints.Count(); i++ ) { if ( !GetConstraintAttachments(constraints[i], pAttach, pAttachVPhysics) ) { GetSpringAttachments(constraints[i], pAttach, pAttachVPhysics); } const char *pName0 = "world"; const char *pName1 = "world"; const char *pModel0 = ""; const char *pModel1 = ""; int index0 = 0; int index1 = 0; if ( pAttach[0] ) { pName0 = pAttach[0]->GetClassname(); pModel0 = STRING(pAttach[0]->GetModelName()); index0 = pAttachVPhysics[0]->GetGameIndex(); } if ( pAttach[1] ) { pName1 = pAttach[1]->GetClassname(); pModel1 = STRING(pAttach[1]->GetModelName()); index1 = pAttachVPhysics[1]->GetGameIndex(); } Msg("**********************\n%s connects %s(%s:%d) to %s(%s:%d)\n", constraints[i]->GetClassname(), pName0, pModel0, index0, pName1, pModel1, index1 ); DebugConstraint(constraints[i]); constraints[i]->m_debugOverlays |= OVERLAY_BBOX_BIT | OVERLAY_TEXT_BIT; } } static void MarkVPhysicsDebug( CBaseEntity *pEntity ) { if ( pEntity ) { IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); if ( pPhysics ) { unsigned short callbacks = pPhysics->GetCallbackFlags(); callbacks ^= CALLBACK_MARKED_FOR_TEST; pPhysics->SetCallbackFlags( callbacks ); } } } void PhysicsCommand( const CCommand &args, void (*func)( CBaseEntity *pEntity ) ) { if ( args.ArgC() < 2 ) { CBasePlayer *pPlayer = UTIL_GetCommandClient(); trace_t tr; Vector forward; pPlayer->EyeVectors( &forward ); UTIL_TraceLine(pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_COORD_RANGE, MASK_SHOT_HULL|CONTENTS_GRATE|CONTENTS_DEBRIS, pPlayer, COLLISION_GROUP_NONE, &tr ); if ( tr.DidHit() ) { func( tr.m_pEnt ); } } else { CBaseEntity *pEnt = NULL; while ( ( pEnt = gEntList.FindEntityGeneric( pEnt, args[1] ) ) != NULL ) { func( pEnt ); } } } CON_COMMAND(physics_constraints, "Highlights constraint system graph for an entity") { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; PhysicsCommand( args, DebugConstraints ); } CON_COMMAND(physics_debug_entity, "Dumps debug info for an entity") { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; PhysicsCommand( args, OutputVPhysicsDebugInfo ); } CON_COMMAND(physics_select, "Dumps debug info for an entity") { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; PhysicsCommand( args, MarkVPhysicsDebug ); } CON_COMMAND( physics_budget, "Times the cost of each active object" ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; int activeCount = physenv->GetActiveObjectCount(); IPhysicsObject **pActiveList = NULL; CUtlVector ents; if ( activeCount ) { int i; pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); physenv->GetActiveObjects( pActiveList ); for ( i = 0; i < activeCount; i++ ) { CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); if ( pEntity ) { int index = -1; for ( int j = 0; j < ents.Count(); j++ ) { if ( pEntity == ents[j] ) { index = j; break; } } if ( index >= 0 ) continue; ents.AddToTail( pEntity ); } } stackfree( pActiveList ); if ( !ents.Count() ) return; CUtlVector times; float totalTime = 0.f; g_Collisions.BufferTouchEvents( true ); float full = engine->Time(); physenv->Simulate( gpGlobals->interval_per_tick ); full = engine->Time() - full; float lastTime = full; times.SetSize( ents.Count() ); // NOTE: This is just a heuristic. Attempt to estimate cost by putting each object to sleep in turn. // note that simulation may wake the objects again and some costs scale with sets of objects/constraints/etc // so these are only generally useful for broad questions, not real metrics! for ( i = 0; i < ents.Count(); i++ ) { for ( int j = 0; j < i; j++ ) { PhysForceEntityToSleep( ents[j], ents[j]->VPhysicsGetObject() ); } float start = engine->Time(); physenv->Simulate( gpGlobals->interval_per_tick ); float end = engine->Time(); float elapsed = end - start; float avgTime = lastTime - elapsed; times[i] = clamp( avgTime, 0.00001f, 1.0f ); totalTime += times[i]; lastTime = elapsed; } totalTime = MAX( totalTime, 0.001 ); for ( i = 0; i < ents.Count(); i++ ) { float fraction = times[i] / totalTime; Msg( "%s (%s): %.3fms (%.3f%%) @ %s\n", ents[i]->GetClassname(), ents[i]->GetDebugName(), fraction * totalTime * 1000.0f, fraction * 100.0f, VecToString(ents[i]->GetAbsOrigin()) ); } g_Collisions.BufferTouchEvents( false ); } } #ifdef PORTAL ConVar sv_fullsyncclones("sv_fullsyncclones", "1", FCVAR_CHEAT ); void PortalPhysFrame( float deltaTime ) //small wrapper for PhysFrame that simulates all environments at once { CPortalSimulator::PrePhysFrame(); if( sv_fullsyncclones.GetBool() ) CPhysicsShadowClone::FullSyncAllClones(); g_Collisions.BufferTouchEvents( true ); PhysFrame( deltaTime ); g_Collisions.PortalPostSimulationFrame(); g_Collisions.BufferTouchEvents( false ); g_Collisions.FrameUpdate(); CPortalSimulator::PostPhysFrame(); } #endif // Advance physics by time (in seconds) void PhysFrame( float deltaTime ) { static int lastObjectCount = 0; entitem_t *pItem; if ( !g_PhysicsHook.ShouldSimulate() ) return; // Trap interrupts and clock changes if ( deltaTime > 1.0f || deltaTime < 0.0f ) { deltaTime = 0; Msg( "Reset physics clock\n" ); } else if ( deltaTime > 0.1f ) // limit incoming time to 100ms { deltaTime = 0.1f; } float simRealTime = 0; deltaTime *= phys_timescale.GetFloat(); // !!!HACKHACK -- hard limit scaled time to avoid spending too much time in here // Limit to 100 ms if ( deltaTime > 0.100f ) deltaTime = 0.100f; bool bProfile = phys_speeds.GetBool(); if ( bProfile ) { simRealTime = engine->Time(); } #ifdef _DEBUG physenv->DebugCheckContacts(); #endif #ifndef PORTAL //instead of wrapping 1 simulation with this, portal needs to wrap 3 g_Collisions.BufferTouchEvents( true ); #endif physenv->Simulate( deltaTime ); int activeCount = physenv->GetActiveObjectCount(); IPhysicsObject **pActiveList = NULL; if ( activeCount ) { pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); physenv->GetActiveObjects( pActiveList ); for ( int i = 0; i < activeCount; i++ ) { CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); if ( pEntity ) { if ( pEntity->CollisionProp()->DoesVPhysicsInvalidateSurroundingBox() ) { pEntity->CollisionProp()->MarkSurroundingBoundsDirty(); } pEntity->VPhysicsUpdate( pActiveList[i] ); } } stackfree( pActiveList ); } for ( pItem = g_pShadowEntities->m_pItemList; pItem; pItem = pItem->pNext ) { CBaseEntity *pEntity = pItem->hEnt.Get(); if ( !pEntity ) { Msg( "Dangling pointer to physics entity!!!\n" ); continue; } IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); // apply updates if ( pPhysics && !pPhysics->IsAsleep() ) { pEntity->VPhysicsShadowUpdate( pPhysics ); } } if ( bProfile ) { simRealTime = engine->Time() - simRealTime; if ( simRealTime < 0 ) simRealTime = 0; g_PhysAverageSimTime *= 0.8; g_PhysAverageSimTime += (simRealTime * 0.2); if ( lastObjectCount != 0 || activeCount != 0 ) { Msg( "Physics: %3d objects, %4.1fms / AVG: %4.1fms\n", activeCount, simRealTime * 1000, g_PhysAverageSimTime * 1000 ); } lastObjectCount = activeCount; } #ifndef PORTAL //instead of wrapping 1 simulation with this, portal needs to wrap 3 g_Collisions.BufferTouchEvents( false ); g_Collisions.FrameUpdate(); #endif } void PhysAddShadow( CBaseEntity *pEntity ) { g_pShadowEntities->AddEntity( pEntity ); } void PhysRemoveShadow( CBaseEntity *pEntity ) { g_pShadowEntities->DeleteEntity( pEntity ); } bool PhysHasShadow( CBaseEntity *pEntity ) { EHANDLE hTestEnt = pEntity; entitem_t *pCurrent = g_pShadowEntities->m_pItemList; while( pCurrent ) { if( pCurrent->hEnt == hTestEnt ) { return true; } pCurrent = pCurrent->pNext; } return false; } void PhysEnableFloating( IPhysicsObject *pObject, bool bEnable ) { if ( pObject != NULL ) { unsigned short flags = pObject->GetCallbackFlags(); if ( bEnable ) { flags |= CALLBACK_DO_FLUID_SIMULATION; } else { flags &= ~CALLBACK_DO_FLUID_SIMULATION; } pObject->SetCallbackFlags( flags ); } } //----------------------------------------------------------------------------- // CollisionEvent system //----------------------------------------------------------------------------- // NOTE: PreCollision/PostCollision ALWAYS come in matched pairs!!! void CCollisionEvent::PreCollision( vcollisionevent_t *pEvent ) { CallbackContext check(this); m_gameEvent.Init( pEvent ); // gather the pre-collision data that the game needs to track for ( int i = 0; i < 2; i++ ) { IPhysicsObject *pObject = pEvent->pObjects[i]; if ( pObject ) { if ( pObject->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) { CBaseEntity *pOtherEntity = reinterpret_cast(pEvent->pObjects[!i]->GetGameData()); if ( pOtherEntity && !pOtherEntity->IsPlayer() ) { Vector velocity; AngularImpulse angVel; // HACKHACK: If we totally clear this out, then Havok will think the objects // are penetrating and generate forces to separate them // so make it fairly small and have a tiny collision instead. pObject->GetVelocity( &velocity, &angVel ); float len = VectorNormalize(velocity); len = MAX( len, 10 ); velocity *= len; len = VectorNormalize(angVel); len = MAX( len, 1 ); angVel *= len; pObject->SetVelocity( &velocity, &angVel ); } } pObject->GetVelocity( &m_gameEvent.preVelocity[i], &m_gameEvent.preAngularVelocity[i] ); } } } void CCollisionEvent::PostCollision( vcollisionevent_t *pEvent ) { CallbackContext check(this); bool isShadow[2] = {false,false}; int i; for ( i = 0; i < 2; i++ ) { IPhysicsObject *pObject = pEvent->pObjects[i]; if ( pObject ) { CBaseEntity *pEntity = reinterpret_cast(pObject->GetGameData()); if ( !pEntity ) return; // UNDONE: This is here to trap crashes due to NULLing out the game data on delete m_gameEvent.pEntities[i] = pEntity; unsigned int flags = pObject->GetCallbackFlags(); pObject->GetVelocity( &m_gameEvent.postVelocity[i], NULL ); if ( flags & CALLBACK_SHADOW_COLLISION ) { isShadow[i] = true; } // Shouldn't get impacts with triggers Assert( !pObject->IsTrigger() ); } } // copy off the post-collision variable data m_gameEvent.collisionSpeed = pEvent->collisionSpeed; m_gameEvent.pInternalData = pEvent->pInternalData; // special case for hitting self, only make one non-shadow call if ( m_gameEvent.pEntities[0] == m_gameEvent.pEntities[1] ) { if ( pEvent->isCollision && m_gameEvent.pEntities[0] ) { m_gameEvent.pEntities[0]->VPhysicsCollision( 0, &m_gameEvent ); } return; } if ( isShadow[0] && isShadow[1] ) { pEvent->isCollision = false; } for ( i = 0; i < 2; i++ ) { if ( pEvent->isCollision ) { m_gameEvent.pEntities[i]->VPhysicsCollision( i, &m_gameEvent ); } if ( pEvent->isShadowCollision && isShadow[i] ) { m_gameEvent.pEntities[i]->VPhysicsShadowCollision( i, &m_gameEvent ); } } } void PhysForceEntityToSleep( CBaseEntity *pEntity, IPhysicsObject *pObject ) { // UNDONE: Check to see if the object is touching the player first? // Might get the player stuck? if ( !pObject || !pObject->IsMoveable() ) return; DevMsg(2, "Putting entity to sleep: %s\n", pEntity->GetClassname() ); MEM_ALLOC_CREDIT(); IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); for ( int i = 0; i < physCount; i++ ) { PhysForceClearVelocity( pList[i] ); pList[i]->Sleep(); } } void CCollisionEvent::Friction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit, IPhysicsCollisionData *pData ) { CallbackContext check(this); //Get our friction information Vector vecPos, vecVel; pData->GetContactPoint( vecPos ); pObject->GetVelocityAtPoint( vecPos, &vecVel ); CBaseEntity *pEntity = reinterpret_cast(pObject->GetGameData()); if ( pEntity ) { friction_t *pFriction = g_Collisions.FindFriction( pEntity ); if ( pFriction && pFriction->pObject) { // in MP mode play sound and effects once every 500 msecs, // no ongoing updates, takes too much bandwidth if ( (pFriction->flLastEffectTime + 0.5f) > gpGlobals->curtime) { pFriction->flLastUpdateTime = gpGlobals->curtime; return; } } pEntity->VPhysicsFriction( pObject, energy, surfaceProps, surfacePropsHit ); } PhysFrictionEffect( vecPos, vecVel, energy, surfaceProps, surfacePropsHit ); } friction_t *CCollisionEvent::FindFriction( CBaseEntity *pObject ) { friction_t *pFree = NULL; for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) { if ( !m_current[i].pObject && !pFree ) pFree = &m_current[i]; if ( m_current[i].pObject == pObject ) return &m_current[i]; } return pFree; } void CCollisionEvent::ShutdownFriction( friction_t &friction ) { // Msg( "Scrape Stop %s \n", STRING(friction.pObject->m_iClassname) ); CSoundEnvelopeController::GetController().SoundDestroy( friction.patch ); friction.patch = NULL; friction.pObject = NULL; } void CCollisionEvent::UpdateRemoveObjects() { Assert(!PhysIsInCallback()); for ( int i = 0 ; i < m_removeObjects.Count(); i++ ) { UTIL_Remove(m_removeObjects[i]); } m_removeObjects.RemoveAll(); } void CCollisionEvent::PostSimulationFrame() { UpdateDamageEvents(); g_PostSimulationQueue.CallQueued(); UpdateRemoveObjects(); } void CCollisionEvent::FlushQueuedOperations() { int loopCount = 0; while ( loopCount < 20 ) { int count = m_triggerEvents.Count() + m_touchEvents.Count() + m_damageEvents.Count() + m_removeObjects.Count() + g_PostSimulationQueue.Count(); if ( !count ) break; // testing, if this assert fires it proves we've fixed the crash // after that the assert + warning can safely be removed Assert(0); Warning("Physics queue not empty, error!\n"); loopCount++; UpdateTouchEvents(); UpdateDamageEvents(); g_PostSimulationQueue.CallQueued(); UpdateRemoveObjects(); } } void CCollisionEvent::FrameUpdate( void ) { UpdateFrictionSounds(); UpdateTouchEvents(); UpdatePenetrateEvents(); UpdateFluidEvents(); UpdateDamageEvents(); // if there was no PSI in physics, we'll still need to do some of these because collisions are solved in between PSIs g_PostSimulationQueue.CallQueued(); UpdateRemoveObjects(); // There are some queued operations that must complete each frame, iterate until these are done FlushQueuedOperations(); } // the delete list is getting flushed, clean up ours void PhysOnCleanupDeleteList() { g_Collisions.FlushQueuedOperations(); if ( physenv ) { physenv->CleanupDeleteList(); } } void CCollisionEvent::UpdateFluidEvents( void ) { for ( int i = m_fluidEvents.Count()-1; i >= 0; --i ) { if ( (gpGlobals->curtime - m_fluidEvents[i].impactTime) > FLUID_TIME_MAX ) { m_fluidEvents.FastRemove(i); } } } float CCollisionEvent::DeltaTimeSinceLastFluid( CBaseEntity *pEntity ) { for ( int i = m_fluidEvents.Count()-1; i >= 0; --i ) { if ( m_fluidEvents[i].hEntity.Get() == pEntity ) { return gpGlobals->curtime - m_fluidEvents[i].impactTime; } } int index = m_fluidEvents.AddToTail(); m_fluidEvents[index].hEntity = pEntity; m_fluidEvents[index].impactTime = gpGlobals->curtime; return FLUID_TIME_MAX; } void CCollisionEvent::UpdateFrictionSounds( void ) { for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) { if ( m_current[i].patch ) { if ( m_current[i].flLastUpdateTime < (gpGlobals->curtime-0.1f) ) { // friction wasn't updated the last 100msec, assume fiction finished ShutdownFriction( m_current[i] ); } } } } void CCollisionEvent::DispatchStartTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1, const Vector &point, const Vector &normal ) { trace_t trace; memset( &trace, 0, sizeof(trace) ); trace.endpos = point; trace.plane.dist = DotProduct( point, normal ); trace.plane.normal = normal; // NOTE: This sets up the touch list for both entities, no call to pEntity1 is needed pEntity0->PhysicsMarkEntitiesAsTouchingEventDriven( pEntity1, trace ); } void CCollisionEvent::DispatchEndTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ) { // frees the event-driven touchlinks pEntity0->PhysicsNotifyOtherOfUntouch( pEntity0, pEntity1 ); pEntity1->PhysicsNotifyOtherOfUntouch( pEntity1, pEntity0 ); } void CCollisionEvent::UpdateTouchEvents( void ) { int i; // Turn on buffering in case new touch events occur during processing bool bOldTouchEvents = m_bBufferTouchEvents; m_bBufferTouchEvents = true; for ( i = 0; i < m_touchEvents.Count(); i++ ) { const touchevent_t &event = m_touchEvents[i]; if ( event.touchType == TOUCH_START ) { DispatchStartTouch( event.pEntity0, event.pEntity1, event.endPoint, event.normal ); } else { // TOUCH_END DispatchEndTouch( event.pEntity0, event.pEntity1 ); } } m_touchEvents.RemoveAll(); for ( i = 0; i < m_triggerEvents.Count(); i++ ) { m_currentTriggerEvent = m_triggerEvents[i]; if ( m_currentTriggerEvent.bStart ) { m_currentTriggerEvent.pTriggerEntity->StartTouch( m_currentTriggerEvent.pEntity ); } else { m_currentTriggerEvent.pTriggerEntity->EndTouch( m_currentTriggerEvent.pEntity ); } } m_triggerEvents.RemoveAll(); m_currentTriggerEvent.Clear(); m_bBufferTouchEvents = bOldTouchEvents; } void CCollisionEvent::UpdateDamageEvents( void ) { for ( int i = 0; i < m_damageEvents.Count(); i++ ) { damageevent_t &event = m_damageEvents[i]; // Track changes in the entity's life state int iEntBits = event.pEntity->IsAlive() ? 0x0001 : 0; iEntBits |= event.pEntity->IsMarkedForDeletion() ? 0x0002 : 0; iEntBits |= (event.pEntity->GetSolidFlags() & FSOLID_NOT_SOLID) ? 0x0004 : 0; #if 0 // Go ahead and compute the current static stress when hit by a large object (with a force high enough to do damage). // That way you die from the impact rather than the stress of the object resting on you whenever possible. // This makes the damage effects cleaner. if ( event.pInflictorPhysics && event.pInflictorPhysics->GetMass() > VPHYSICS_LARGE_OBJECT_MASS ) { CBaseCombatCharacter *pCombat = event.pEntity->MyCombatCharacterPointer(); if ( pCombat ) { vphysics_objectstress_t stressOut; event.info.AddDamage( pCombat->CalculatePhysicsStressDamage( &stressOut, pCombat->VPhysicsGetObject() ) ); } } #endif event.pEntity->TakeDamage( event.info ); int iEntBits2 = event.pEntity->IsAlive() ? 0x0001 : 0; iEntBits2 |= event.pEntity->IsMarkedForDeletion() ? 0x0002 : 0; iEntBits2 |= (event.pEntity->GetSolidFlags() & FSOLID_NOT_SOLID) ? 0x0004 : 0; if ( event.bRestoreVelocity && iEntBits != iEntBits2 ) { // UNDONE: Use ratio of masses to blend in a little of the collision response? // UNDONE: Damage for future events is already computed - it would be nice to // go back and recompute it now that the values have // been adjusted RestoreDamageInflictorState( event.pInflictorPhysics ); } } m_damageEvents.RemoveAll(); m_damageInflictors.RemoveAll(); } void CCollisionEvent::RestoreDamageInflictorState( int inflictorStateIndex, float velocityBlend ) { inflictorstate_t &state = m_damageInflictors[inflictorStateIndex]; if ( state.restored ) return; // so we only restore this guy once state.restored = true; if ( velocityBlend > 0 ) { Vector velocity; AngularImpulse angVel; state.pInflictorPhysics->GetVelocity( &velocity, &angVel ); state.savedVelocity = state.savedVelocity*velocityBlend + velocity*(1-velocityBlend); state.savedAngularVelocity = state.savedAngularVelocity*velocityBlend + angVel*(1-velocityBlend); state.pInflictorPhysics->SetVelocity( &state.savedVelocity, &state.savedAngularVelocity ); } if ( state.nextIndex >= 0 ) { RestoreDamageInflictorState( state.nextIndex, velocityBlend ); } } void CCollisionEvent::RestoreDamageInflictorState( IPhysicsObject *pInflictor ) { if ( !pInflictor ) return; int index = FindDamageInflictor( pInflictor ); if ( index >= 0 ) { inflictorstate_t &state = m_damageInflictors[index]; if ( !state.restored ) { float velocityBlend = 1.0; float inflictorMass = state.pInflictorPhysics->GetMass(); if ( inflictorMass < VPHYSICS_LARGE_OBJECT_MASS && !(state.pInflictorPhysics->GetGameFlags() & FVPHYSICS_DMG_SLICE) ) { float otherMass = state.otherMassMax > 0 ? state.otherMassMax : 1; float massRatio = inflictorMass / otherMass; massRatio = clamp( massRatio, 0.1f, 10.0f ); if ( massRatio < 1 ) { velocityBlend = RemapVal( massRatio, 0.1, 1, 0, 0.5 ); } else { velocityBlend = RemapVal( massRatio, 1.0, 10, 0.5, 1 ); } } RestoreDamageInflictorState( index, velocityBlend ); } } } bool CCollisionEvent::GetInflictorVelocity( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ) { int index = FindDamageInflictor( pInflictor ); if ( index >= 0 ) { inflictorstate_t &state = m_damageInflictors[index]; velocity = state.savedVelocity; angVelocity = state.savedAngularVelocity; return true; } return false; } bool PhysGetDamageInflictorVelocityStartOfFrame( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ) { return g_Collisions.GetInflictorVelocity( pInflictor, velocity, angVelocity ); } void CCollisionEvent::AddTouchEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1, int touchType, const Vector &point, const Vector &normal ) { if ( !pEntity0 || !pEntity1 ) return; int index = m_touchEvents.AddToTail(); touchevent_t &event = m_touchEvents[index]; event.pEntity0 = pEntity0; event.pEntity1 = pEntity1; event.touchType = touchType; event.endPoint = point; event.normal = normal; } void CCollisionEvent::AddDamageEvent( CBaseEntity *pEntity, const CTakeDamageInfo &info, IPhysicsObject *pInflictorPhysics, bool bRestoreVelocity, const Vector &savedVel, const AngularImpulse &savedAngVel ) { if ( pEntity->IsMarkedForDeletion() ) return; int iTimeBasedDamage = g_pGameRules->Damage_GetTimeBased(); if ( !( info.GetDamageType() & (DMG_BURN | DMG_DROWN | iTimeBasedDamage | DMG_PREVENT_PHYSICS_FORCE) ) ) { Assert( info.GetDamageForce() != vec3_origin && info.GetDamagePosition() != vec3_origin ); } int index = m_damageEvents.AddToTail(); damageevent_t &event = m_damageEvents[index]; event.pEntity = pEntity; event.info = info; event.pInflictorPhysics = pInflictorPhysics; event.bRestoreVelocity = bRestoreVelocity; if ( !pInflictorPhysics || !pInflictorPhysics->IsMoveable() ) { event.bRestoreVelocity = false; } if ( event.bRestoreVelocity ) { float otherMass = pEntity->VPhysicsGetObject()->GetMass(); int inflictorIndex = FindDamageInflictor(pInflictorPhysics); if ( inflictorIndex >= 0 ) { // if this is a bigger mass, save that info inflictorstate_t &state = m_damageInflictors[inflictorIndex]; if ( otherMass > state.otherMassMax ) { state.otherMassMax = otherMass; } } else { AddDamageInflictor( pInflictorPhysics, otherMass, savedVel, savedAngVel, true ); } } } //----------------------------------------------------------------------------- // Impulse events //----------------------------------------------------------------------------- static void PostSimulation_ImpulseEvent( IPhysicsObject *pObject, const Vector ¢erForce, const AngularImpulse ¢erTorque ) { pObject->ApplyForceCenter( centerForce ); pObject->ApplyTorqueCenter( centerTorque ); } void PostSimulation_SetVelocityEvent( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ) { pPhysicsObject->SetVelocity( &vecVelocity, NULL ); } void CCollisionEvent::AddRemoveObject(IServerNetworkable *pRemove) { if ( pRemove && m_removeObjects.Find(pRemove) == -1 ) { m_removeObjects.AddToTail(pRemove); } } int CCollisionEvent::FindDamageInflictor( IPhysicsObject *pInflictorPhysics ) { // UNDONE: Linear search? Probably ok with a low count here for ( int i = m_damageInflictors.Count()-1; i >= 0; --i ) { const inflictorstate_t &state = m_damageInflictors[i]; if ( state.pInflictorPhysics == pInflictorPhysics ) return i; } return -1; } int CCollisionEvent::AddDamageInflictor( IPhysicsObject *pInflictorPhysics, float otherMass, const Vector &savedVel, const AngularImpulse &savedAngVel, bool addList ) { // NOTE: Save off the state of the object before collision // restore if the impact is a kill // UNDONE: Should we absorb some energy here? // NOTE: we can't save a delta because there could be subsequent post-fatal collisions int addIndex = m_damageInflictors.AddToTail(); { inflictorstate_t &state = m_damageInflictors[addIndex]; state.pInflictorPhysics = pInflictorPhysics; state.savedVelocity = savedVel; state.savedAngularVelocity = savedAngVel; state.otherMassMax = otherMass; state.restored = false; state.nextIndex = -1; } if ( addList ) { CBaseEntity *pEntity = static_cast(pInflictorPhysics->GetGameData()); if ( pEntity ) { IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); if ( physCount > 1 ) { int currentIndex = addIndex; for ( int i = 0; i < physCount; i++ ) { if ( pList[i] != pInflictorPhysics ) { Vector vel; AngularImpulse angVel; pList[i]->GetVelocity( &vel, &angVel ); int next = AddDamageInflictor( pList[i], otherMass, vel, angVel, false ); m_damageInflictors[currentIndex].nextIndex = next; currentIndex = next; } } } } } return addIndex; } void CCollisionEvent::LevelShutdown( void ) { for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) { if ( m_current[i].patch ) { ShutdownFriction( m_current[i] ); } } } void CCollisionEvent::StartTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ) { CallbackContext check(this); CBaseEntity *pEntity1 = static_cast(pObject1->GetGameData()); CBaseEntity *pEntity2 = static_cast(pObject2->GetGameData()); if ( !pEntity1 || !pEntity2 ) return; Vector endPoint, normal; pTouchData->GetContactPoint( endPoint ); pTouchData->GetSurfaceNormal( normal ); if ( !m_bBufferTouchEvents ) { DispatchStartTouch( pEntity1, pEntity2, endPoint, normal ); } else { AddTouchEvent( pEntity1, pEntity2, TOUCH_START, endPoint, normal ); } } static int CountPhysicsObjectEntityContacts( IPhysicsObject *pObject, CBaseEntity *pEntity ) { IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); int count = 0; while ( pSnapshot->IsValid() ) { IPhysicsObject *pOther = pSnapshot->GetObject(1); CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); if ( pOtherEntity == pEntity ) count++; pSnapshot->NextFrictionData(); } pObject->DestroyFrictionSnapshot( pSnapshot ); return count; } void CCollisionEvent::EndTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ) { CallbackContext check(this); CBaseEntity *pEntity1 = static_cast(pObject1->GetGameData()); CBaseEntity *pEntity2 = static_cast(pObject2->GetGameData()); if ( !pEntity1 || !pEntity2 ) return; // contact point deleted, but entities are still touching? IPhysicsObject *list[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int count = pEntity1->VPhysicsGetObjectList( list, ARRAYSIZE(list) ); int contactCount = 0; for ( int i = 0; i < count; i++ ) { contactCount += CountPhysicsObjectEntityContacts( list[i], pEntity2 ); // still touching if ( contactCount > 1 ) return; } // should have exactly one contact point (the one getting deleted here) //Assert( contactCount == 1 ); Vector endPoint, normal; pTouchData->GetContactPoint( endPoint ); pTouchData->GetSurfaceNormal( normal ); if ( !m_bBufferTouchEvents ) { DispatchEndTouch( pEntity1, pEntity2 ); } else { AddTouchEvent( pEntity1, pEntity2, TOUCH_END, vec3_origin, vec3_origin ); } } // UNDONE: This is functional, but minimally. void CCollisionEvent::ObjectEnterTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ) { CBaseEntity *pTriggerEntity = static_cast(pTrigger->GetGameData()); CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( pTriggerEntity && pEntity ) { // UNDONE: Don't buffer these until we can solve generating touches at object creation time if ( 0 && m_bBufferTouchEvents ) { int index = m_triggerEvents.AddToTail(); m_triggerEvents[index].Init( pTriggerEntity, pTrigger, pEntity, pObject, true ); } else { CallbackContext check(this); m_currentTriggerEvent.Init( pTriggerEntity, pTrigger, pEntity, pObject, true ); pTriggerEntity->StartTouch( pEntity ); m_currentTriggerEvent.Clear(); } } } void CCollisionEvent::ObjectLeaveTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ) { CBaseEntity *pTriggerEntity = static_cast(pTrigger->GetGameData()); CBaseEntity *pEntity = static_cast(pObject->GetGameData()); if ( pTriggerEntity && pEntity ) { // UNDONE: Don't buffer these until we can solve generating touches at object creation time if ( 0 && m_bBufferTouchEvents ) { int index = m_triggerEvents.AddToTail(); m_triggerEvents[index].Init( pTriggerEntity, pTrigger, pEntity, pObject, false ); } else { CallbackContext check(this); m_currentTriggerEvent.Init( pTriggerEntity, pTrigger, pEntity, pObject, false ); pTriggerEntity->EndTouch( pEntity ); m_currentTriggerEvent.Clear(); } } } bool CCollisionEvent::GetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTriggerEntity ) { if ( pEvent && pTriggerEntity == m_currentTriggerEvent.pTriggerEntity ) { *pEvent = m_currentTriggerEvent; return true; } return false; } void PhysGetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ) { g_Collisions.GetListOfPenetratingEntities( pSearch, list ); } bool PhysGetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTriggerEntity ) { return g_Collisions.GetTriggerEvent( pEvent, pTriggerEntity ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // External interface to collision sounds //----------------------------------------------------------------------------- void PhysicsImpactSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float volume, float impactSpeed ) { physicssound::AddImpactSound( g_PhysicsHook.m_impactSounds, pEntity, pEntity->entindex(), channel, pPhysObject, surfaceProps, surfacePropsHit, volume, impactSpeed ); } void PhysCollisionSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float deltaTime, float speed ) { if ( deltaTime < 0.05f || speed < 70.0f ) return; float volume = speed * speed * (1.0f/(320.0f*320.0f)); // max volume at 320 in/s if ( volume > 1.0f ) volume = 1.0f; PhysicsImpactSound( pEntity, pPhysObject, channel, surfaceProps, surfacePropsHit, volume, speed ); } void PhysBreakSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, Vector vecOrigin ) { if ( !pPhysObject) return; physicssound::AddBreakSound( g_PhysicsHook.m_breakSounds, vecOrigin, pPhysObject->GetMaterialIndex() ); } ConVar collision_shake_amp("collision_shake_amp", "0.2"); ConVar collision_shake_freq("collision_shake_freq", "0.5"); ConVar collision_shake_time("collision_shake_time", "0.5"); void PhysCollisionScreenShake( gamevcollisionevent_t *pEvent, int index ) { int otherIndex = !index; float mass = pEvent->pObjects[index]->GetMass(); if ( mass >= VPHYSICS_LARGE_OBJECT_MASS && pEvent->pObjects[otherIndex]->IsStatic() && !(pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PENETRATING) ) { mass = clamp(mass, VPHYSICS_LARGE_OBJECT_MASS, 2000.f); if ( pEvent->collisionSpeed > 30 && pEvent->deltaCollisionTime > 0.25f ) { Vector vecPos; pEvent->pInternalData->GetContactPoint( vecPos ); float impulse = pEvent->collisionSpeed * mass; float amplitude = impulse * (collision_shake_amp.GetFloat() / (30.0f * VPHYSICS_LARGE_OBJECT_MASS)); UTIL_ScreenShake( vecPos, amplitude, collision_shake_freq.GetFloat(), collision_shake_time.GetFloat(), amplitude * 60, SHAKE_START ); } } } #if HL2_EPISODIC // Uses DispatchParticleEffect because, so far as I know, that is the new means of kicking // off flinders for this kind of collision. Should this be in g_pEffects instead? void PhysCollisionWarpEffect( gamevcollisionevent_t *pEvent, surfacedata_t *phit ) { Vector vecPos; QAngle vecAngles; pEvent->pInternalData->GetContactPoint( vecPos ); { Vector vecNormal; pEvent->pInternalData->GetSurfaceNormal(vecNormal); VectorAngles( vecNormal, vecAngles ); } DispatchParticleEffect( "warp_shield_impact", vecPos, vecAngles ); } #endif void PhysCollisionDust( gamevcollisionevent_t *pEvent, surfacedata_t *phit ) { switch ( phit->game.material ) { case CHAR_TEX_SAND: case CHAR_TEX_DIRT: if ( pEvent->collisionSpeed < 200.0f ) return; break; case CHAR_TEX_CONCRETE: if ( pEvent->collisionSpeed < 340.0f ) return; break; #if HL2_EPISODIC // this is probably redundant because BaseEntity::VHandleCollision should have already dispatched us elsewhere case CHAR_TEX_WARPSHIELD: PhysCollisionWarpEffect(pEvent,phit); return; break; #endif default: return; } //Kick up dust Vector vecPos, vecVel; pEvent->pInternalData->GetContactPoint( vecPos ); vecVel.Random( -1.0f, 1.0f ); vecVel.z = random->RandomFloat( 0.3f, 1.0f ); VectorNormalize( vecVel ); g_pEffects->Dust( vecPos, vecVel, 8.0f, pEvent->collisionSpeed ); } void PhysFrictionSound( CBaseEntity *pEntity, IPhysicsObject *pObject, const char *pSoundName, HSOUNDSCRIPTHANDLE& handle, float flVolume ) { if ( !pEntity ) return; // cut out the quiet sounds // UNDONE: Separate threshold for starting a sound vs. continuing? flVolume = clamp( flVolume, 0.0f, 1.0f ); if ( flVolume > (1.0f/128.0f) ) { friction_t *pFriction = g_Collisions.FindFriction( pEntity ); if ( !pFriction ) return; CSoundParameters params; if ( !CBaseEntity::GetParametersForSound( pSoundName, handle, params, NULL ) ) return; if ( !pFriction->pObject ) { // don't create really quiet scrapes if ( params.volume * flVolume <= 0.1f ) return; pFriction->pObject = pEntity; CPASAttenuationFilter filter( pEntity, params.soundlevel ); pFriction->patch = CSoundEnvelopeController::GetController().SoundCreate( filter, pEntity->entindex(), CHAN_BODY, pSoundName, params.soundlevel ); CSoundEnvelopeController::GetController().Play( pFriction->patch, params.volume * flVolume, params.pitch ); } else { float pitch = (flVolume * (params.pitchhigh - params.pitchlow)) + params.pitchlow; CSoundEnvelopeController::GetController().SoundChangeVolume( pFriction->patch, params.volume * flVolume, 0.1f ); CSoundEnvelopeController::GetController().SoundChangePitch( pFriction->patch, pitch, 0.1f ); } pFriction->flLastUpdateTime = gpGlobals->curtime; pFriction->flLastEffectTime = gpGlobals->curtime; } } void PhysCleanupFrictionSounds( CBaseEntity *pEntity ) { friction_t *pFriction = g_Collisions.FindFriction( pEntity ); if ( pFriction && pFriction->patch ) { g_Collisions.ShutdownFriction( *pFriction ); } } //----------------------------------------------------------------------------- // Applies force impulses at a later time //----------------------------------------------------------------------------- void PhysCallbackImpulse( IPhysicsObject *pPhysicsObject, const Vector &vecCenterForce, const AngularImpulse &vecCenterTorque ) { Assert( physenv->IsInSimulation() ); g_PostSimulationQueue.QueueCall( PostSimulation_ImpulseEvent, pPhysicsObject, RefToVal(vecCenterForce), RefToVal(vecCenterTorque) ); } void PhysCallbackSetVelocity( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ) { Assert( physenv->IsInSimulation() ); g_PostSimulationQueue.QueueCall( PostSimulation_SetVelocityEvent, pPhysicsObject, RefToVal(vecVelocity) ); } void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info, gamevcollisionevent_t &event, int hurtIndex ) { Assert( physenv->IsInSimulation() ); int otherIndex = !hurtIndex; g_Collisions.AddDamageEvent( pEntity, info, event.pObjects[otherIndex], true, event.preVelocity[otherIndex], event.preAngularVelocity[otherIndex] ); } void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info ) { if ( PhysIsInCallback() ) { CBaseEntity *pInflictor = info.GetInflictor(); IPhysicsObject *pInflictorPhysics = (pInflictor) ? pInflictor->VPhysicsGetObject() : NULL; g_Collisions.AddDamageEvent( pEntity, info, pInflictorPhysics, false, vec3_origin, vec3_origin ); if ( pEntity && info.GetInflictor() ) { DevMsg( 2, "Warning: Physics damage event with no recovery info!\nObjects: %s, %s\n", pEntity->GetClassname(), info.GetInflictor()->GetClassname() ); } } else { pEntity->TakeDamage( info ); } } void PhysCallbackRemove(IServerNetworkable *pRemove) { if ( PhysIsInCallback() ) { g_Collisions.AddRemoveObject(pRemove); } else { UTIL_Remove(pRemove); } } void PhysSetEntityGameFlags( CBaseEntity *pEntity, unsigned short flags ) { IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); for ( int i = 0; i < count; i++ ) { PhysSetGameFlags( pList[i], flags ); } } bool PhysFindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pParams, vehiclesounds_t *pSounds ) { return g_PhysicsHook.FindOrAddVehicleScript(pScriptName, pParams, pSounds); } void PhysFlushVehicleScripts() { g_PhysicsHook.FlushVehicleScripts(); } IPhysicsObject *FindPhysicsObjectByName( const char *pName, CBaseEntity *pErrorEntity ) { if ( !pName || !strlen(pName) ) return NULL; CBaseEntity *pEntity = NULL; IPhysicsObject *pBestObject = NULL; while (1) { pEntity = gEntList.FindEntityByName( pEntity, pName ); if ( !pEntity ) break; if ( pEntity->VPhysicsGetObject() ) { if ( pBestObject ) { const char *pErrorName = pErrorEntity ? pErrorEntity->GetClassname() : "Unknown"; Vector origin = pErrorEntity ? pErrorEntity->GetAbsOrigin() : vec3_origin; DevWarning("entity %s at %s has physics attachment to more than one entity with the name %s!!!\n", pErrorName, VecToString(origin), pName ); while ( ( pEntity = gEntList.FindEntityByName( pEntity, pName ) ) != NULL ) { DevWarning("Found %s\n", pEntity->GetClassname() ); } break; } pBestObject = pEntity->VPhysicsGetObject(); } } return pBestObject; } void CC_AirDensity( const CCommand &args ) { if ( !physenv ) return; if ( args.ArgC() < 2 ) { Msg( "air_density \nCurrent air density is %.2f\n", physenv->GetAirDensity() ); } else { float density = atof( args[1] ); physenv->SetAirDensity( density ); } } static ConCommand air_density("air_density", CC_AirDensity, "Changes the density of air for drag computations.", FCVAR_CHEAT); void DebugDrawContactPoints(IPhysicsObject *pPhysics) { IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); while ( pSnapshot->IsValid() ) { Vector pt, normal; pSnapshot->GetContactPoint( pt ); pSnapshot->GetSurfaceNormal( normal ); NDebugOverlay::Box( pt, -Vector(1,1,1), Vector(1,1,1), 0, 255, 0, 32, 0 ); NDebugOverlay::Line( pt, pt - normal * 20, 0, 255, 0, false, 0 ); IPhysicsObject *pOther = pSnapshot->GetObject(1); CBaseEntity *pEntity0 = static_cast(pOther->GetGameData()); CFmtStr str("%s (%s): %s [%0.2f]", pEntity0->GetClassname(), STRING(pEntity0->GetModelName()), pEntity0->GetDebugName(), pSnapshot->GetFrictionCoefficient() ); NDebugOverlay::Text( pt, str.Access(), false, 0 ); pSnapshot->NextFrictionData(); } pSnapshot->DeleteAllMarkedContacts( true ); pPhysics->DestroyFrictionSnapshot( pSnapshot ); } #if 0 #include "filesystem.h" //----------------------------------------------------------------------------- // Purpose: This will append a collide to a glview file. Then you can view the // collisionmodels with glview. // Input : *pCollide - collision model // &origin - position of the instance of this model // &angles - orientation of instance // *pFilename - output text file //----------------------------------------------------------------------------- // examples: // world: // DumpCollideToGlView( pWorldCollide->solids[0], vec3_origin, vec3_origin, "jaycollide.txt" ); // static_prop: // DumpCollideToGlView( info.m_pCollide->solids[0], info.m_Origin, info.m_Angles, "jaycollide.txt" ); // //----------------------------------------------------------------------------- void DumpCollideToGlView( CPhysCollide *pCollide, const Vector &origin, const QAngle &angles, const char *pFilename ) { if ( !pCollide ) return; printf("Writing %s...\n", pFilename ); Vector *outVerts; int vertCount = physcollision->CreateDebugMesh( pCollide, &outVerts ); FileHandle_t fp = filesystem->Open( pFilename, "ab" ); int triCount = vertCount / 3; int vert = 0; VMatrix tmp = SetupMatrixOrgAngles( origin, angles ); int i; for ( i = 0; i < vertCount; i++ ) { outVerts[i] = tmp.VMul4x3( outVerts[i] ); } for ( i = 0; i < triCount; i++ ) { filesystem->FPrintf( fp, "3\n" ); filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 1 0 0\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); vert++; filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 0 1 0\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); vert++; filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 0 0 1\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); vert++; } filesystem->Close( fp ); physcollision->DestroyDebugMesh( vertCount, outVerts ); } #endif