From 39ed87570bdb2f86969d4be821c94b722dc71179 Mon Sep 17 00:00:00 2001 From: Joe Ludwig Date: Wed, 26 Jun 2013 15:22:04 -0700 Subject: First version of the SOurce SDK 2013 --- mp/src/game/server/doors.cpp | 1414 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1414 insertions(+) create mode 100644 mp/src/game/server/doors.cpp (limited to 'mp/src/game/server/doors.cpp') diff --git a/mp/src/game/server/doors.cpp b/mp/src/game/server/doors.cpp new file mode 100644 index 00000000..d44d6632 --- /dev/null +++ b/mp/src/game/server/doors.cpp @@ -0,0 +1,1414 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements two types of doors: linear and rotating. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "doors.h" +#include "entitylist.h" +#include "physics.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "physics_npc_solver.h" + +#ifdef HL1_DLL +#include "filters.h" +#endif + +#ifdef CSTRIKE_DLL +#include "KeyValues.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define CLOSE_AREAPORTAL_THINK_CONTEXT "CloseAreaportalThink" + +BEGIN_DATADESC( CBaseDoor ) + + DEFINE_KEYFIELD( m_vecMoveDir, FIELD_VECTOR, "movedir" ), + + DEFINE_FIELD( m_bLockedSentence, FIELD_CHARACTER ), + DEFINE_FIELD( m_bUnlockedSentence, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_NoiseMoving, FIELD_SOUNDNAME, "noise1" ), + DEFINE_KEYFIELD( m_NoiseArrived, FIELD_SOUNDNAME, "noise2" ), + DEFINE_KEYFIELD( m_NoiseMovingClosed, FIELD_SOUNDNAME, "startclosesound" ), + DEFINE_KEYFIELD( m_NoiseArrivedClosed, FIELD_SOUNDNAME, "closesound" ), + DEFINE_KEYFIELD( m_ChainTarget, FIELD_STRING, "chainstodoor" ), + // DEFINE_FIELD( m_isChaining, FIELD_BOOLEAN ), + // DEFINE_FIELD( m_ls, locksound_t ), +// DEFINE_FIELD( m_isChaining, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_ls.sLockedSound, FIELD_SOUNDNAME, "locked_sound" ), + DEFINE_KEYFIELD( m_ls.sUnlockedSound, FIELD_SOUNDNAME, "unlocked_sound" ), + DEFINE_FIELD( m_bLocked, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_flWaveHeight, FIELD_FLOAT, "WaveHeight" ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + DEFINE_KEYFIELD( m_eSpawnPosition, FIELD_INTEGER, "spawnpos" ), + + DEFINE_KEYFIELD( m_bForceClosed, FIELD_BOOLEAN, "forceclosed" ), + DEFINE_FIELD( m_bDoorGroup, FIELD_BOOLEAN ), + +#ifdef HL1_DLL + DEFINE_KEYFIELD( m_iBlockFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hBlockFilter, FIELD_EHANDLE ), +#endif + + DEFINE_KEYFIELD( m_bLoopMoveSound, FIELD_BOOLEAN, "loopmovesound" ), + DEFINE_KEYFIELD( m_bIgnoreDebris, FIELD_BOOLEAN, "ignoredebris" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Open", InputOpen ), + DEFINE_INPUTFUNC( FIELD_VOID, "Close", InputClose ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetToggleState", InputSetToggleState ), + + DEFINE_OUTPUT( m_OnBlockedOpening, "OnBlockedOpening" ), + DEFINE_OUTPUT( m_OnBlockedClosing, "OnBlockedClosing" ), + DEFINE_OUTPUT( m_OnUnblockedOpening, "OnUnblockedOpening" ), + DEFINE_OUTPUT( m_OnUnblockedClosing, "OnUnblockedClosing" ), + DEFINE_OUTPUT( m_OnFullyClosed, "OnFullyClosed" ), + DEFINE_OUTPUT( m_OnFullyOpen, "OnFullyOpen" ), + DEFINE_OUTPUT( m_OnClose, "OnClose" ), + DEFINE_OUTPUT( m_OnOpen, "OnOpen" ), + DEFINE_OUTPUT( m_OnLockedUse, "OnLockedUse" ), + + // Function Pointers + DEFINE_FUNCTION( DoorTouch ), + DEFINE_FUNCTION( DoorGoUp ), + DEFINE_FUNCTION( DoorGoDown ), + DEFINE_FUNCTION( DoorHitTop ), + DEFINE_FUNCTION( DoorHitBottom ), + DEFINE_THINKFUNC( MovingSoundThink ), + DEFINE_THINKFUNC( CloseAreaPortalsThink ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_door, CBaseDoor ); + +// +// func_water is implemented as a linear door so we can raise/lower the water level. +// +LINK_ENTITY_TO_CLASS( func_water, CBaseDoor ); + + +// SendTable stuff. +IMPLEMENT_SERVERCLASS_ST(CBaseDoor, DT_BaseDoor) + SendPropFloat (SENDINFO(m_flWaveHeight), 8, SPROP_ROUNDUP, 0.0f, 8.0f), +END_SEND_TABLE() + +#define DOOR_SENTENCEWAIT 6 +#define DOOR_SOUNDWAIT 1 +#define BUTTON_SOUNDWAIT 0.5 + + +//----------------------------------------------------------------------------- +// Purpose: play door or button locked or unlocked sounds. +// NOTE: this routine is shared by doors and buttons +// Input : pEdict - +// pls - +// flocked - if true, play 'door is locked' sound, otherwise play 'door +// is unlocked' sound. +// fbutton - +//----------------------------------------------------------------------------- +void PlayLockSounds(CBaseEntity *pEdict, locksound_t *pls, int flocked, int fbutton) +{ + if ( pEdict->HasSpawnFlags( SF_DOOR_SILENT ) ) + { + return; + } + float flsoundwait = ( fbutton ) ? BUTTON_SOUNDWAIT : DOOR_SOUNDWAIT; + + if ( flocked ) + { + int fplaysound = (pls->sLockedSound != NULL_STRING && gpGlobals->curtime > pls->flwaitSound); + int fplaysentence = (pls->sLockedSentence != NULL_STRING && !pls->bEOFLocked && gpGlobals->curtime > pls->flwaitSentence); + float fvol = ( fplaysound && fplaysentence ) ? 0.25f : 1.0f; + + // if there is a locked sound, and we've debounced, play sound + if (fplaysound) + { + // play 'door locked' sound + CPASAttenuationFilter filter( pEdict ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = (char*)STRING(pls->sLockedSound); + ep.m_flVolume = fvol; + ep.m_SoundLevel = SNDLVL_NORM; + + CBaseEntity::EmitSound( filter, pEdict->entindex(), ep ); + pls->flwaitSound = gpGlobals->curtime + flsoundwait; + } + + // if there is a sentence, we've not played all in list, and we've debounced, play sound + if (fplaysentence) + { + // play next 'door locked' sentence in group + int iprev = pls->iLockedSentence; + + pls->iLockedSentence = SENTENCEG_PlaySequentialSz( pEdict->edict(), + STRING(pls->sLockedSentence), + 0.85f, + SNDLVL_NORM, + 0, + 100, + pls->iLockedSentence, + FALSE); + pls->iUnlockedSentence = 0; + + // make sure we don't keep calling last sentence in list + pls->bEOFLocked = (iprev == pls->iLockedSentence); + + pls->flwaitSentence = gpGlobals->curtime + DOOR_SENTENCEWAIT; + } + } + else + { + // UNLOCKED SOUND + + int fplaysound = (pls->sUnlockedSound != NULL_STRING && gpGlobals->curtime > pls->flwaitSound); + int fplaysentence = (pls->sUnlockedSentence != NULL_STRING && !pls->bEOFUnlocked && gpGlobals->curtime > pls->flwaitSentence); + float fvol; + + // if playing both sentence and sound, lower sound volume so we hear sentence + fvol = ( fplaysound && fplaysentence ) ? 0.25f : 1.0f; + + // play 'door unlocked' sound if set + if (fplaysound) + { + CPASAttenuationFilter filter( pEdict ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = (char*)STRING(pls->sUnlockedSound); + ep.m_flVolume = fvol; + ep.m_SoundLevel = SNDLVL_NORM; + + CBaseEntity::EmitSound( filter, pEdict->entindex(), ep ); + pls->flwaitSound = gpGlobals->curtime + flsoundwait; + } + + // play next 'door unlocked' sentence in group + if (fplaysentence) + { + int iprev = pls->iUnlockedSentence; + + pls->iUnlockedSentence = SENTENCEG_PlaySequentialSz(pEdict->edict(), STRING(pls->sUnlockedSentence), + 0.85, SNDLVL_NORM, 0, 100, pls->iUnlockedSentence, FALSE); + pls->iLockedSentence = 0; + + // make sure we don't keep calling last sentence in list + pls->bEOFUnlocked = (iprev == pls->iUnlockedSentence); + pls->flwaitSentence = gpGlobals->curtime + DOOR_SENTENCEWAIT; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Cache user-entity-field values until spawn is called. +// Input : szKeyName - +// szValue - +// Output : Returns true. +//----------------------------------------------------------------------------- +bool CBaseDoor::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "locked_sentence")) + { + m_bLockedSentence = atof(szValue); + } + else if (FStrEq(szKeyName, "unlocked_sentence")) + { + m_bUnlockedSentence = atof(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Spawn() +{ + Precache(); + +#ifdef HL1_DLL + SetSolid( SOLID_BSP ); +#else + if ( GetMoveParent() && GetRootMoveParent()->GetSolid() == SOLID_BSP ) + { + SetSolid( SOLID_BSP ); + } + else + { + SetSolid( SOLID_VPHYSICS ); + } +#endif + + // Convert movedir from angles to a vector + QAngle angMoveDir = QAngle( m_vecMoveDir.x, m_vecMoveDir.y, m_vecMoveDir.z ); + AngleVectors( angMoveDir, &m_vecMoveDir ); + + SetModel( STRING( GetModelName() ) ); + m_vecPosition1 = GetLocalOrigin(); + + // Subtract 2 from size because the engine expands bboxes by 1 in all directions making the size too big + Vector vecOBB = CollisionProp()->OBBSize(); + vecOBB -= Vector( 2, 2, 2 ); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * (DotProductAbs( m_vecMoveDir, vecOBB ) - m_flLip)); + + if ( !IsRotatingDoor() ) + { + if ( ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) || HasSpawnFlags( SF_DOOR_START_OPEN_OBSOLETE ) ) + { // swap pos1 and pos2, put door at pos2 + UTIL_SetOrigin( this, m_vecPosition2); + m_toggle_state = TS_AT_TOP; + } + else + { + m_toggle_state = TS_AT_BOTTOM; + } + } + + if (HasSpawnFlags(SF_DOOR_LOCKED)) + { + m_bLocked = true; + } + + SetMoveType( MOVETYPE_PUSH ); + + if (m_flSpeed == 0) + { + m_flSpeed = 100; + } + + SetTouch( &CBaseDoor::DoorTouch ); + + if ( !FClassnameIs( this, "func_water" ) ) + { + if ( HasSpawnFlags(SF_DOOR_PASSABLE) ) + { + //normal door + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + if ( HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ) + { + SetCollisionGroup( COLLISION_GROUP_PASSABLE_DOOR ); + // HACKHACK: Set this hoping that any children of the door that get blocked by the player + // will get fixed up by vphysics + // NOTE: We could decouple this as a separate behavior, but managing player collisions is already complex enough. + // NOTE: This is necessary to prevent the player from blocking the wrecked train car in ep2_outland_01 + AddFlag( FL_UNBLOCKABLE_BY_PLAYER ); + } + if ( m_bIgnoreDebris ) + { + // both of these flags want to set the collision group and + // there isn't a combo group + Assert( !HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ); + if ( HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ) + { + Warning("Door %s with conflicting collision settings, removing ignoredebris\n", GetDebugName() ); + } + else + { + SetCollisionGroup( COLLISION_GROUP_INTERACTIVE ); + } + } + } + + if ( ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) && HasSpawnFlags( SF_DOOR_START_OPEN_OBSOLETE ) ) + { + Warning("Door %s using obsolete 'Start Open' spawnflag with 'Spawn Position' set to 'Open'. Reverting to old behavior.\n", GetDebugName() ); + } + + CreateVPhysics(); +} + +void CBaseDoor::MovingSoundThink( void ) +{ + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + if ( m_NoiseMovingClosed == NULL_STRING || m_toggle_state == TS_GOING_DOWN || m_toggle_state == TS_AT_BOTTOM ) + { + ep.m_pSoundName = (char*)STRING(m_NoiseMoving); + } + else + { + ep.m_pSoundName = (char*)STRING(m_NoiseMovingClosed); + } + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + + //Only loop sounds in HL1 to maintain HL2 behavior + if( ShouldLoopMoveSound() ) + { + float duration = enginesound->GetSoundDuration( ep.m_pSoundName ); + SetContextThink( &CBaseDoor::MovingSoundThink, gpGlobals->curtime + duration, "MovingSound" ); + } +} + +void CBaseDoor::StartMovingSound( void ) +{ + MovingSoundThink(); + +#ifdef CSTRIKE_DLL // this event is only used by CS:S bots + + CBasePlayer *player = ToBasePlayer(m_hActivator); + IGameEvent * event = gameeventmanager->CreateEvent( "door_moving" ); + if( event ) + { + event->SetInt( "entindex", entindex() ); + event->SetInt( "userid", (player)?player->GetUserID():0 ); + gameeventmanager->FireEvent( event ); + } + +#endif +} + +void CBaseDoor::StopMovingSound(void) +{ + SetContextThink( NULL, gpGlobals->curtime, "MovingSound" ); + char *pSoundName; + if ( m_NoiseMovingClosed == NULL_STRING || m_toggle_state == TS_GOING_UP || m_toggle_state == TS_AT_TOP ) + { + pSoundName = (char*)STRING(m_NoiseMoving); + } + else + { + pSoundName = (char*)STRING(m_NoiseMovingClosed); + } + StopSound( entindex(), CHAN_STATIC, pSoundName ); +} + + +bool CBaseDoor::ShouldSavePhysics() +{ + // don't save physics if you're func_water + return !FClassnameIs( this, "func_water" ); +} + +//----------------------------------------------------------------------------- +bool CBaseDoor::CreateVPhysics( ) +{ + if ( !FClassnameIs( this, "func_water" ) ) + { + //normal door + // NOTE: Create this even when the door is not solid to support constraints. + VPhysicsInitShadow( false, false ); + } + else + { + // special contents + AddSolidFlags( FSOLID_VOLUME_CONTENTS ); + SETBITS( m_spawnflags, SF_DOOR_SILENT ); // water is silent for now + + IPhysicsObject *pPhysics = VPhysicsInitShadow( false, false ); + fluidparams_t fluid; + + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + fluid.damping = 0.01f; + fluid.surfacePlane[0] = 0; + fluid.surfacePlane[1] = 0; + fluid.surfacePlane[2] = 1; + fluid.surfacePlane[3] = CollisionProp()->GetCollisionOrigin().z + CollisionProp()->OBBMaxs().z - 1; + fluid.currentVelocity.Init(0,0,0); + fluid.torqueFactor = 0.1f; + fluid.viscosityFactor = 0.01f; + fluid.pGameData = static_cast(this); + + //FIXME: Currently there's no way to specify that you want slime + fluid.contents = CONTENTS_WATER; + + physenv->CreateFluidController( pPhysics, &fluid ); + } + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Activate( void ) +{ + BaseClass::Activate(); + + CBaseDoor *pDoorList[64]; + m_bDoorGroup = true; + + // force movement groups to sync!!! + int doorCount = GetDoorMovementGroup( pDoorList, ARRAYSIZE(pDoorList) ); + for ( int i = 0; i < doorCount; i++ ) + { + if ( pDoorList[i]->m_vecMoveDir == m_vecMoveDir ) + { + bool error = false; + if ( pDoorList[i]->IsRotatingDoor() ) + { + error = ( pDoorList[i]->GetLocalAngles() != GetLocalAngles() ) ? true : false; + } + else + { + error = ( pDoorList[i]->GetLocalOrigin() != GetLocalOrigin() ) ? true : false; + } + if ( error ) + { + // don't do group blocking + m_bDoorGroup = false; +#ifdef HL1_DLL + // UNDONE: This should probably fixup m_vecPosition1 & m_vecPosition2 + Warning("Door group %s has misaligned origin!\n", STRING(GetEntityName()) ); +#endif + } + } + } + + switch ( m_toggle_state ) + { + case TS_AT_TOP: + UpdateAreaPortals( true ); + break; + case TS_AT_BOTTOM: + UpdateAreaPortals( false ); + break; + } + +#ifdef HL1_DLL + // Get a handle to my filter entity if there is one + if (m_iBlockFilterName != NULL_STRING) + { + m_hBlockFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iBlockFilterName, NULL )); + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +// This is ONLY used by the node graph to test movement through a door +void CBaseDoor::InputSetToggleState( inputdata_t &inputdata ) +{ + SetToggleState( inputdata.value.Int() ); +} + +void CBaseDoor::SetToggleState( int state ) +{ + if ( state == TS_AT_TOP ) + UTIL_SetOrigin( this, m_vecPosition2 ); + else + UTIL_SetOrigin( this, m_vecPosition1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Precache( void ) +{ + //Fill in a default value if necessary + if ( IsRotatingDoor() ) + { + UTIL_ValidateSoundName( m_NoiseMoving, "RotDoorSound.DefaultMove" ); + UTIL_ValidateSoundName( m_NoiseArrived, "RotDoorSound.DefaultArrive" ); + UTIL_ValidateSoundName( m_ls.sLockedSound, "RotDoorSound.DefaultLocked" ); + UTIL_ValidateSoundName( m_ls.sUnlockedSound,"DoorSound.Null" ); + } + else + { + UTIL_ValidateSoundName( m_NoiseMoving, "DoorSound.DefaultMove" ); + UTIL_ValidateSoundName( m_NoiseArrived, "DoorSound.DefaultArrive" ); +#ifndef HL1_DLL + UTIL_ValidateSoundName( m_ls.sLockedSound, "DoorSound.DefaultLocked" ); +#endif + UTIL_ValidateSoundName( m_ls.sUnlockedSound,"DoorSound.Null" ); + } + +#ifdef HL1_DLL + if( m_ls.sLockedSound != NULL_STRING && strlen((char*)STRING(m_ls.sLockedSound)) < 4 ) + { + // Too short to be ANYTHING ".wav", so it must be an old index into a long-lost + // array of sound choices. slam it to a known "deny" sound. We lose the designer's + // original selection, but we don't get unresponsive doors. + m_ls.sLockedSound = AllocPooledString("buttons/button2.wav"); + } +#endif//HL1_DLL + + //Precache them all + PrecacheScriptSound( (char *) STRING(m_NoiseMoving) ); + PrecacheScriptSound( (char *) STRING(m_NoiseArrived) ); + PrecacheScriptSound( (char *) STRING(m_NoiseMovingClosed) ); + PrecacheScriptSound( (char *) STRING(m_NoiseArrivedClosed) ); + PrecacheScriptSound( (char *) STRING(m_ls.sLockedSound) ); + PrecacheScriptSound( (char *) STRING(m_ls.sUnlockedSound) ); + + //Get sentence group names, for doors which are directly 'touched' to open + switch (m_bLockedSentence) + { + case 1: m_ls.sLockedSentence = AllocPooledString("NA"); break; // access denied + case 2: m_ls.sLockedSentence = AllocPooledString("ND"); break; // security lockout + case 3: m_ls.sLockedSentence = AllocPooledString("NF"); break; // blast door + case 4: m_ls.sLockedSentence = AllocPooledString("NFIRE"); break; // fire door + case 5: m_ls.sLockedSentence = AllocPooledString("NCHEM"); break; // chemical door + case 6: m_ls.sLockedSentence = AllocPooledString("NRAD"); break; // radiation door + case 7: m_ls.sLockedSentence = AllocPooledString("NCON"); break; // gen containment + case 8: m_ls.sLockedSentence = AllocPooledString("NH"); break; // maintenance door + case 9: m_ls.sLockedSentence = AllocPooledString("NG"); break; // broken door + + default: m_ls.sLockedSentence = NULL_STRING; break; + } + + switch (m_bUnlockedSentence) + { + case 1: m_ls.sUnlockedSentence = AllocPooledString("EA"); break; // access granted + case 2: m_ls.sUnlockedSentence = AllocPooledString("ED"); break; // security door + case 3: m_ls.sUnlockedSentence = AllocPooledString("EF"); break; // blast door + case 4: m_ls.sUnlockedSentence = AllocPooledString("EFIRE"); break; // fire door + case 5: m_ls.sUnlockedSentence = AllocPooledString("ECHEM"); break; // chemical door + case 6: m_ls.sUnlockedSentence = AllocPooledString("ERAD"); break; // radiation door + case 7: m_ls.sUnlockedSentence = AllocPooledString("ECON"); break; // gen containment + case 8: m_ls.sUnlockedSentence = AllocPooledString("EH"); break; // maintenance door + + default: m_ls.sUnlockedSentence = NULL_STRING; break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Doors not tied to anything (e.g. button, another door) can be touched, +// to make them activate. +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBaseDoor::DoorTouch( CBaseEntity *pOther ) +{ + if( m_ChainTarget != NULL_STRING ) + ChainTouch( pOther ); + + // Ignore touches by anything but players. + if ( !pOther->IsPlayer() ) + { +#ifdef HL1_DLL + if( PassesBlockTouchFilter( pOther ) && m_toggle_state == TS_GOING_DOWN ) + { + DoorGoUp(); + } +#endif + return; + } + + // If door is not opened by touch, do nothing. + if ( !HasSpawnFlags(SF_DOOR_PTOUCH) ) + { +#ifdef HL1_DLL + if( m_toggle_state == TS_AT_BOTTOM ) + { + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } +#endif//HL1_DLL + + return; + } + + // If door has master, and it's not ready to trigger, + // play 'locked' sound. + if (m_sMaster != NULL_STRING && !UTIL_IsMasterTriggered(m_sMaster, pOther)) + { + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } + + if (m_bLocked) + { + m_OnLockedUse.FireOutput( pOther, pOther ); + PlayLockSounds(this, &m_ls, TRUE, FALSE); + return; + } + + // Remember who activated the door. + m_hActivator = pOther; + + if (DoorActivate( )) + { + // Temporarily disable the touch function, until movement is finished. + SetTouch( NULL ); + } +} + +#ifdef HL1_DLL +bool CBaseDoor::PassesBlockTouchFilter(CBaseEntity *pOther) +{ + CBaseFilter* pFilter = (CBaseFilter*)(m_hBlockFilter.Get()); + return ( pFilter && pFilter->PassesFilter( this, pOther ) ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Delays turning off area portals when closing doors to prevent visual artifacts +//----------------------------------------------------------------------------- +void CBaseDoor::CloseAreaPortalsThink( void ) +{ + UpdateAreaPortals( false ); + SetContextThink( NULL, gpGlobals->curtime, CLOSE_AREAPORTAL_THINK_CONTEXT ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : isOpen - +//----------------------------------------------------------------------------- +void CBaseDoor::UpdateAreaPortals( bool isOpen ) +{ + // cancel pending close + SetContextThink( NULL, gpGlobals->curtime, CLOSE_AREAPORTAL_THINK_CONTEXT ); + + if ( IsRotatingDoor() && HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) // logic inverted when using rot doors that start open + isOpen = !isOpen; + + string_t name = GetEntityName(); + if ( !name ) + return; + + CBaseEntity *pPortal = NULL; + while ( ( pPortal = gEntList.FindEntityByClassname( pPortal, "func_areaportal" ) ) != NULL ) + { + if ( pPortal->HasTarget( name ) ) + { + // USE_ON means open the portal, off means close it + pPortal->Use( this, this, isOpen?USE_ON:USE_OFF, 0 ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when the player uses the door. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBaseDoor::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_hActivator = pActivator; + + if( m_ChainTarget != NULL_STRING ) + ChainUse(); + + // We can't +use this if it can't be +used + if ( m_hActivator != NULL && m_hActivator->IsPlayer() && HasSpawnFlags( SF_DOOR_PUSE ) == false ) + { + PlayLockSounds( this, &m_ls, TRUE, FALSE ); + return; + } + + bool bAllowUse = false; + + // if not ready to be used, ignore "use" command. + if( HasSpawnFlags(SF_DOOR_NEW_USE_RULES) ) + { + //New behavior: + // If not ready to be used, ignore "use" command. + // Allow use in these cases: + // - when the door is closed/closing + // - when the door is open/opening and can be manually closed + if ( ( m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN ) || ( HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && ( m_toggle_state == TS_AT_TOP || m_toggle_state == TS_GOING_UP ) ) ) + bAllowUse = true; + } + else + { + // Legacy behavior: + if (m_toggle_state == TS_AT_BOTTOM || (HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && m_toggle_state == TS_AT_TOP) ) + bAllowUse = true; + } + + if( bAllowUse ) + { + if (m_bLocked) + { + m_OnLockedUse.FireOutput( pActivator, pCaller ); + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } + else + { + DoorActivate(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Passes Use along to certain named doors. +//----------------------------------------------------------------------------- +void CBaseDoor::ChainUse( void ) +{ + if ( m_isChaining ) + return; + + CBaseEntity *ent = NULL; + while ( ( ent = gEntList.FindEntityByName( ent, m_ChainTarget, NULL ) ) != NULL ) + { + if ( ent == this ) + continue; + + CBaseDoor *door = dynamic_cast< CBaseDoor * >( ent ); + if ( door ) + { + door->SetChaining( true ); + door->Use( m_hActivator, NULL, USE_TOGGLE, 0.0f ); // only the first param is used + door->SetChaining( false ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Passes Touch along to certain named doors. +//----------------------------------------------------------------------------- +void CBaseDoor::ChainTouch( CBaseEntity *pOther ) +{ + if ( m_isChaining ) + return; + + CBaseEntity *ent = NULL; + while ( ( ent = gEntList.FindEntityByName( ent, m_ChainTarget, NULL ) ) != NULL ) + { + if ( ent == this ) + continue; + + CBaseDoor *door = dynamic_cast< CBaseDoor * >( ent ); + if ( door ) + { + door->SetChaining( true ); + door->Touch( pOther ); + door->SetChaining( false ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Closes the door if it is not already closed. +//----------------------------------------------------------------------------- +void CBaseDoor::InputClose( inputdata_t &inputdata ) +{ + if ( m_toggle_state != TS_AT_BOTTOM ) + { + DoorGoDown(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that locks the door. +//----------------------------------------------------------------------------- +void CBaseDoor::InputLock( inputdata_t &inputdata ) +{ + Lock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBaseDoor::InputOpen( inputdata_t &inputdata ) +{ + if (m_toggle_state != TS_AT_TOP && m_toggle_state != TS_GOING_UP ) + { + // I'm locked, can't open + if (m_bLocked) + return; + + // Play door unlock sounds. + PlayLockSounds(this, &m_ls, false, false); + DoorGoUp(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBaseDoor::InputToggle( inputdata_t &inputdata ) +{ + // I'm locked, can't open + if (m_bLocked) + return; + + if (m_toggle_state == TS_AT_BOTTOM) + { + DoorGoUp(); + } + else if (m_toggle_state == TS_AT_TOP) + { + DoorGoDown(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that unlocks the door. +//----------------------------------------------------------------------------- +void CBaseDoor::InputUnlock( inputdata_t &inputdata ) +{ + Unlock(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::InputSetSpeed( inputdata_t &inputdata ) +{ + m_flSpeed = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: Locks the door so that it cannot be opened. +//----------------------------------------------------------------------------- +void CBaseDoor::Lock( void ) +{ + m_bLocked = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the door so that it can be opened. +//----------------------------------------------------------------------------- +void CBaseDoor::Unlock( void ) +{ + m_bLocked = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Causes the door to "do its thing", i.e. start moving, and cascade activation. +// Output : int +//----------------------------------------------------------------------------- +int CBaseDoor::DoorActivate( ) +{ + if (!UTIL_IsMasterTriggered(m_sMaster, m_hActivator)) + return 0; + + if (HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && m_toggle_state == TS_AT_TOP) + { + // door should close + DoorGoDown(); + } + else + { + // door should open + // play door unlock sounds + PlayLockSounds(this, &m_ls, FALSE, FALSE); + + if ( m_toggle_state != TS_AT_TOP && m_toggle_state != TS_GOING_UP ) + { + DoorGoUp(); + } + } + + return 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the door going to its "up" position (simply ToggleData->vecPosition2). +//----------------------------------------------------------------------------- +void CBaseDoor::DoorGoUp( void ) +{ + edict_t *pevActivator; + + UpdateAreaPortals( true ); + // It could be going-down, if blocked. + ASSERT(m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN); + + // emit door moving and stop sounds on CHAN_STATIC so that the multicast doesn't + // filter them out and leave a client stuck with looping door sounds! + if ( !HasSpawnFlags(SF_DOOR_SILENT ) ) + { + // If we're not moving already, start the moving noise + if ( m_toggle_state != TS_GOING_UP && m_toggle_state != TS_GOING_DOWN ) + { + StartMovingSound(); + } + } + + m_toggle_state = TS_GOING_UP; + + SetMoveDone( &CBaseDoor::DoorHitTop ); + if ( IsRotatingDoor() ) // !!! BUGBUG Triggered doors don't work with this yet + { + float sign = 1.0; + + if ( m_hActivator != NULL ) + { + pevActivator = m_hActivator->edict(); + + if ( !HasSpawnFlags( SF_DOOR_ONEWAY ) && m_vecMoveAng.y ) // Y axis rotation, move away from the player + { + // Positive is CCW, negative is CW, so make 'sign' 1 or -1 based on which way we want to open. + // Important note: All doors face East at all times, and twist their local angle to open. + // So you can't look at the door's facing to determine which way to open. + + Vector nearestPoint; + CollisionProp()->CalcNearestPoint( m_hActivator->GetAbsOrigin(), &nearestPoint ); + Vector activatorToNearestPoint = nearestPoint - m_hActivator->GetAbsOrigin(); + activatorToNearestPoint.z = 0; + + Vector activatorToOrigin = GetAbsOrigin() - m_hActivator->GetAbsOrigin(); + activatorToOrigin.z = 0; + + // Point right hand at door hinge, curl hand towards closest spot on door, if thumb + // is up, open door CW. -- Department of Basic Cross Product Understanding for Noobs + Vector cross = activatorToOrigin.Cross( activatorToNearestPoint ); + + if( cross.z > 0.0f ) + { + sign = -1.0f; + } + } + } + AngularMove(m_vecAngle2*sign, m_flSpeed); + } + else + { + LinearMove(m_vecPosition2, m_flSpeed); + } + + //Fire our open ouput + m_OnOpen.FireOutput( this, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the "up" position. Either go back down, or +// wait for another activation. +//----------------------------------------------------------------------------- +void CBaseDoor::DoorHitTop( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + StopMovingSound(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = (char*)STRING(m_NoiseArrived); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_UP); + m_toggle_state = TS_AT_TOP; + + // toggle-doors don't come down automatically, they wait for refire. + if (HasSpawnFlags( SF_DOOR_NO_AUTO_RETURN)) + { + // Re-instate touch method, movement is complete + SetTouch( &CBaseDoor::DoorTouch ); + } + else + { + // In flWait seconds, DoorGoDown will fire, unless wait is -1, then door stays open + SetMoveDoneTime( m_flWait ); + SetMoveDone( &CBaseDoor::DoorGoDown ); + + if ( m_flWait == -1 ) + { + SetNextThink( TICK_NEVER_THINK ); + } + } + + if (HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) + { + m_OnFullyClosed.FireOutput(this, this); + } + else + { + m_OnFullyOpen.FireOutput(this, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the door going to its "down" position (simply ToggleData->vecPosition1). +//----------------------------------------------------------------------------- +void CBaseDoor::DoorGoDown( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + // If we're not moving already, start the moving noise + if ( m_toggle_state != TS_GOING_UP && m_toggle_state != TS_GOING_DOWN ) + { + StartMovingSound(); + } + } + +#ifdef DOOR_ASSERT + ASSERT(m_toggle_state == TS_AT_TOP); +#endif // DOOR_ASSERT + m_toggle_state = TS_GOING_DOWN; + + SetMoveDone( &CBaseDoor::DoorHitBottom ); + if ( IsRotatingDoor() )//rotating door + AngularMove( m_vecAngle1, m_flSpeed); + else + LinearMove( m_vecPosition1, m_flSpeed); + + //Fire our closed output + m_OnClose.FireOutput( this, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the "down" position. Back to quiescence. +//----------------------------------------------------------------------------- +void CBaseDoor::DoorHitBottom( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + + StopMovingSound(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + if ( m_NoiseArrivedClosed == NULL_STRING ) + ep.m_pSoundName = (char*)STRING(m_NoiseArrived); + else + ep.m_pSoundName = (char*)STRING(m_NoiseArrivedClosed); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_DOWN); + m_toggle_state = TS_AT_BOTTOM; + + // Re-instate touch method, cycle is complete + SetTouch( &CBaseDoor::DoorTouch ); + + if (HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE)) + { + m_OnFullyOpen.FireOutput(m_hActivator, this); + } + else + { + m_OnFullyClosed.FireOutput(m_hActivator, this); + } + + // Close the area portals just after the door closes, to prevent visual artifacts in multiplayer games + SetContextThink( &CBaseDoor::CloseAreaPortalsThink, gpGlobals->curtime + 0.5f, CLOSE_AREAPORTAL_THINK_CONTEXT ); +} + + +// Lists all doors in the same movement group as this one +int CBaseDoor::GetDoorMovementGroup( CBaseDoor *pDoorList[], int listMax ) +{ + int count = 0; + CBaseEntity *pTarget = NULL; + + // Block all door pieces with the same targetname here. + if ( GetEntityName() != NULL_STRING ) + { + for (;;) + { + pTarget = gEntList.FindEntityByName( pTarget, GetEntityName(), NULL ); + + if ( pTarget != this ) + { + if ( !pTarget ) + break; + + CBaseDoor *pDoor = dynamic_cast(pTarget); + + if ( pDoor && count < listMax ) + { + pDoorList[count] = pDoor; + count++; + } + } + } + } + + return count; +} + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBaseDoor::StartBlocked( CBaseEntity *pOther ) +{ + // + // Fire whatever events we need to due to our blocked state. + // + if (m_toggle_state == TS_GOING_DOWN) + { + m_OnBlockedClosing.FireOutput(pOther, this); + } + else + { + m_OnBlockedOpening.FireOutput(pOther, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called every frame when the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBaseDoor::Blocked( CBaseEntity *pOther ) +{ + // Hurt the blocker a little. + if ( m_flBlockDamage ) + { + // if the door is marked "force closed" or it has a negative wait, then there's nothing to do but + // push/damage the object. + // If block damage is set, but this object is a physics prop that can't be damaged, just + // give up and disable collisions + if ( (m_bForceClosed || m_flWait < 0) && pOther->GetMoveType() == MOVETYPE_VPHYSICS && + (pOther->m_takedamage == DAMAGE_NO || pOther->m_takedamage == DAMAGE_EVENTS_ONLY) ) + { + EntityPhysics_CreateSolver( this, pOther, true, 4.0f ); + } + else + { + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); + } + } + + // If we're set to force ourselves closed, keep going + if ( m_bForceClosed ) + return; + + // if a door has a negative wait, it would never come back if blocked, + // so let it just squash the object to death real fast + if (m_flWait >= 0) + { + if (m_toggle_state == TS_GOING_DOWN) + { + DoorGoUp(); + } + else + { + DoorGoDown(); + } + } + + // Block all door pieces with the same targetname here. + if ( GetEntityName() != NULL_STRING ) + { + CBaseDoor *pDoorList[64]; + int doorCount = GetDoorMovementGroup( pDoorList, ARRAYSIZE(pDoorList) ); + + for ( int i = 0; i < doorCount; i++ ) + { + CBaseDoor *pDoor = pDoorList[i]; + + if ( pDoor->m_flWait >= 0) + { + if (m_bDoorGroup && pDoor->m_vecMoveDir == m_vecMoveDir && pDoor->GetAbsVelocity() == GetAbsVelocity() && pDoor->GetLocalAngularVelocity() == GetLocalAngularVelocity()) + { + pDoor->m_nSimulationTick = m_nSimulationTick; // don't run simulation this frame if you haven't run yet + + // this is the most hacked, evil, bastardized thing I've ever seen. kjb + if ( !pDoor->IsRotatingDoor() ) + {// set origin to realign normal doors + pDoor->SetLocalOrigin( GetLocalOrigin() ); + pDoor->SetAbsVelocity( vec3_origin );// stop! + + } + else + {// set angles to realign rotating doors + pDoor->SetLocalAngles( GetLocalAngles() ); + pDoor->SetLocalAngularVelocity( vec3_angle ); + } + } + + if ( pDoor->m_toggle_state == TS_GOING_DOWN) + pDoor->DoorGoUp(); + else + pDoor->DoorGoDown(); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is unblocked while opening or closing. +//----------------------------------------------------------------------------- +void CBaseDoor::EndBlocked( void ) +{ + // + // Fire whatever events we need to due to our unblocked state. + // + if (m_toggle_state == TS_GOING_DOWN) + { + m_OnUnblockedClosing.FireOutput(this, this); + } + else + { + m_OnUnblockedOpening.FireOutput(this, this); + } +} + + +/*func_door_rotating + +TOGGLE causes the door to wait in both the start and end states for +a trigger event. + +START_OPEN causes the door to move to its destination when spawned, +and operate in reverse. It is used to temporarily or permanently +close off an area when triggered (not usefull for touch or +takedamage doors). + +You need to have an origin brush as part of this entity. The +center of that brush will be +the point around which it is rotated. It will rotate around the Z +axis by default. You can +check either the X_AXIS or Y_AXIS box to change that. + +"distance" is how many degrees the door will be rotated. +"speed" determines how fast the door moves; default value is 100. + +REVERSE will cause the door to rotate in the opposite direction. + +"angle" determines the opening direction +"targetname" if set, no touch field will be spawned and a remote +button or trigger field activates the door. +"health" if set, door must be shot open +"speed" movement speed (100 default) +"wait" wait before returning (3 default, -1 = never return) +"dmg" damage to inflict when blocked (2 default) +*/ + +//================================================== +// CRotDoor +//================================================== + +class CRotDoor : public CBaseDoor +{ +public: + DECLARE_CLASS( CRotDoor, CBaseDoor ); + + void Spawn( void ); + bool CreateVPhysics(); + // This is ONLY used by the node graph to test movement through a door + virtual void SetToggleState( int state ); + virtual bool IsRotatingDoor() { return true; } + + bool m_bSolidBsp; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( func_door_rotating, CRotDoor ); + +BEGIN_DATADESC( CRotDoor ) + DEFINE_KEYFIELD( m_bSolidBsp, FIELD_BOOLEAN, "solidbsp" ), +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRotDoor::Spawn( void ) +{ + BaseClass::Spawn(); + + // set the axis of rotation + CBaseToggle::AxisDir(); + + // check for clockwise rotation + if ( HasSpawnFlags(SF_DOOR_ROTATE_BACKWARDS) ) + m_vecMoveAng = m_vecMoveAng * -1; + + //m_flWait = 2; who the hell did this? (sjb) + m_vecAngle1 = GetLocalAngles(); + m_vecAngle2 = GetLocalAngles() + m_vecMoveAng * m_flMoveDistance; + + ASSERTSZ(m_vecAngle1 != m_vecAngle2, "rotating door start/end positions are equal\n"); + + // Starting open allows a func_door to be lighted in the closed position but + // spawn in the open position + // + // SF_DOOR_START_OPEN_OBSOLETE is an old broken way of spawning open that has + // been deprecated. + if ( HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) + { + // swap pos1 and pos2, put door at pos2, invert movement direction + QAngle vecNewAngles = m_vecAngle2; + m_vecAngle2 = m_vecAngle1; + m_vecAngle1 = vecNewAngles; + m_vecMoveAng = -m_vecMoveAng; + + // We've already had our physics setup in BaseClass::Spawn, so teleport to our + // current position. If we don't do this, our vphysics shadow will not update. + Teleport( NULL, &m_vecAngle1, NULL ); + + m_toggle_state = TS_AT_BOTTOM; + } + else if ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) + { + // We've already had our physics setup in BaseClass::Spawn, so teleport to our + // current position. If we don't do this, our vphysics shadow will not update. + Teleport( NULL, &m_vecAngle2, NULL ); + m_toggle_state = TS_AT_TOP; + } + else + { + m_toggle_state = TS_AT_BOTTOM; + } + +#ifdef HL1_DLL + SetSolid( SOLID_VPHYSICS ); +#endif + + // Slam the object back to solid - if we really want it to be solid. + if ( m_bSolidBsp ) + { + SetSolid( SOLID_BSP ); + } +} + +//----------------------------------------------------------------------------- + +bool CRotDoor::CreateVPhysics() +{ + if ( !IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + { + VPhysicsInitShadow( false, false ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +// This is ONLY used by the node graph to test movement through a door +void CRotDoor::SetToggleState( int state ) +{ + if ( state == TS_AT_TOP ) + SetLocalAngles( m_vecAngle2 ); + else + SetLocalAngles( m_vecAngle1 ); +} -- cgit v1.2.3