//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "cbase.h" #include "toolframework/itoolentity.h" #include "tier1/KeyValues.h" #include "Sprite.h" #include "enginesprite.h" #include "toolframework_client.h" #include "particles/particles.h" #include "particle_parse.h" #include "rendertexture.h" #ifdef PORTAL #include "PortalRender.h" #endif #pragma warning( disable: 4355 ) // warning C4355: 'this' : used in base member initializer list class CClientTools; void DrawSpriteModel( IClientEntity *baseentity, CEngineSprite *psprite, const Vector &origin, float fscale, float frame, int rendermode, int r, int g, int b, int a, const Vector& forward, const Vector& right, const Vector& up, float flHDRColorScale = 1.0f ); float StandardGlowBlend( const pixelvis_queryparams_t ¶ms, pixelvis_handle_t *queryHandle, int rendermode, int renderfx, int alpha, float *pscale ); // Interface from engine to tools for manipulating entities class CClientTools : public IClientTools, public IClientEntityListener { public: CClientTools(); virtual HTOOLHANDLE AttachToEntity( EntitySearchResult entityToAttach ); virtual void DetachFromEntity( EntitySearchResult entityToDetach ); virtual bool IsValidHandle( HTOOLHANDLE handle ); virtual int GetNumRecordables(); virtual HTOOLHANDLE GetRecordable( int index ); // Iterates through ALL entities (separate list for client vs. server) virtual EntitySearchResult NextEntity( EntitySearchResult currentEnt ); // Use this to turn on/off the presence of an underlying game entity virtual void SetEnabled( HTOOLHANDLE handle, bool enabled ); virtual void SetRecording( HTOOLHANDLE handle, bool recording ); virtual bool ShouldRecord( HTOOLHANDLE handle ); virtual int GetModelIndex( HTOOLHANDLE handle ); virtual const char* GetModelName ( HTOOLHANDLE handle ); virtual const char* GetClassname ( HTOOLHANDLE handle ); virtual HTOOLHANDLE GetToolHandleForEntityByIndex( int entindex ); virtual void AddClientRenderable( IClientRenderable *pRenderable, int renderGroup ); virtual void RemoveClientRenderable( IClientRenderable *pRenderable ); virtual void SetRenderGroup( IClientRenderable *pRenderable, int renderGroup ); virtual void MarkClientRenderableDirty( IClientRenderable *pRenderable ); virtual bool DrawSprite( IClientRenderable *pRenderable, float scale, float frame, int rendermode, int renderfx, const Color &color, float flProxyRadius, int *pVisHandle ); virtual bool GetLocalPlayerEyePosition( Vector& org, QAngle& ang, float &fov ); virtual EntitySearchResult GetLocalPlayer(); virtual ClientShadowHandle_t CreateShadow( CBaseHandle h, int nFlags ); virtual void DestroyShadow( ClientShadowHandle_t h ); virtual ClientShadowHandle_t CreateFlashlight( const FlashlightState_t &lightState ); virtual void DestroyFlashlight( ClientShadowHandle_t h ); virtual void UpdateFlashlightState( ClientShadowHandle_t h, const FlashlightState_t &flashlightState ); virtual void AddToDirtyShadowList( ClientShadowHandle_t h, bool force = false ); virtual void MarkRenderToTextureShadowDirty( ClientShadowHandle_t h ); virtual void UpdateProjectedTexture( ClientShadowHandle_t h, bool bForce ); // Global toggle for recording virtual void EnableRecordingMode( bool bEnable ); virtual bool IsInRecordingMode() const; // Trigger a temp entity virtual void TriggerTempEntity( KeyValues *pKeyValues ); // get owning weapon (for viewmodels) virtual int GetOwningWeaponEntIndex( int entindex ); virtual int GetEntIndex( EntitySearchResult entityToAttach ); virtual int FindGlobalFlexcontroller( char const *name ); virtual char const *GetGlobalFlexControllerName( int idx ); // helper for traversing ownership hierarchy virtual EntitySearchResult GetOwnerEntity( EntitySearchResult currentEnt ); // common and useful types to query for hierarchically virtual bool IsPlayer( EntitySearchResult entityToAttach ); virtual bool IsBaseCombatCharacter( EntitySearchResult entityToAttach ); virtual bool IsNPC( EntitySearchResult entityToAttach ); virtual Vector GetAbsOrigin( HTOOLHANDLE handle ); virtual QAngle GetAbsAngles( HTOOLHANDLE handle ); virtual void ReloadParticleDefintions( const char *pFileName, const void *pBufData, int nLen ); // Sends a mesage from the tool to the client virtual void PostToolMessage( KeyValues *pKeyValues ); // Indicates whether the client should render particle systems virtual void EnableParticleSystems( bool bEnable ); // Is the game rendering in 3rd person mode? virtual bool IsRenderingThirdPerson() const; public: C_BaseEntity *LookupEntity( HTOOLHANDLE handle ); // IClientEntityListener methods void OnEntityDeleted( C_BaseEntity *pEntity ); void OnEntityCreated( C_BaseEntity *pEntity ); private: struct HToolEntry_t { HToolEntry_t() : m_Handle( 0 ) {} HToolEntry_t( int handle, C_BaseEntity *pEntity = NULL ) : m_Handle( handle ), m_hEntity( pEntity ) { if ( pEntity ) { m_hEntity->SetToolHandle( m_Handle ); } } int m_Handle; EHANDLE m_hEntity; }; static int s_nNextHandle; static bool HandleLessFunc( const HToolEntry_t& lhs, const HToolEntry_t& rhs ) { return lhs.m_Handle < rhs.m_Handle; } CUtlRBTree< HToolEntry_t > m_Handles; CUtlVector< int > m_ActiveHandles; bool m_bInRecordingMode; }; //----------------------------------------------------------------------------- // Statics //----------------------------------------------------------------------------- int CClientTools::s_nNextHandle = 1; //----------------------------------------------------------------------------- // Singleton instance //----------------------------------------------------------------------------- static CClientTools s_ClientTools; IClientTools *clienttools = &s_ClientTools; EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CClientTools, IClientTools, VCLIENTTOOLS_INTERFACE_VERSION, s_ClientTools ); //----------------------------------------------------------------------------- // Constructor //----------------------------------------------------------------------------- CClientTools::CClientTools() : m_Handles( 0, 0, HandleLessFunc ) { m_bInRecordingMode = false; cl_entitylist->AddListenerEntity( this ); } //----------------------------------------------------------------------------- // Global toggle for recording //----------------------------------------------------------------------------- void CClientTools::EnableRecordingMode( bool bEnable ) { m_bInRecordingMode = bEnable; } bool CClientTools::IsInRecordingMode() const { return m_bInRecordingMode; } //----------------------------------------------------------------------------- // Trigger a temp entity //----------------------------------------------------------------------------- void CClientTools::TriggerTempEntity( KeyValues *pKeyValues ) { te->TriggerTempEntity( pKeyValues ); } //----------------------------------------------------------------------------- // get owning weapon (for viewmodels) //----------------------------------------------------------------------------- int CClientTools::GetOwningWeaponEntIndex( int entindex ) { C_BaseEntity *pEnt = C_BaseEntity::Instance( entindex ); C_BaseViewModel *pViewModel = dynamic_cast< C_BaseViewModel* >( pEnt ); if ( pViewModel ) { C_BaseCombatWeapon *pWeapon = pViewModel->GetOwningWeapon(); if ( pWeapon ) { return pWeapon->entindex(); } } return -1; } int CClientTools::GetEntIndex( EntitySearchResult entityToAttach ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity * >( entityToAttach ); return ent ? ent->entindex() : 0; } void CClientTools::AddClientRenderable( IClientRenderable *pRenderable, int renderGroup ) { Assert( pRenderable ); cl_entitylist->AddNonNetworkableEntity( pRenderable->GetIClientUnknown() ); ClientRenderHandle_t handle = pRenderable->RenderHandle(); if ( INVALID_CLIENT_RENDER_HANDLE == handle ) { // create new renderer handle ClientLeafSystem()->AddRenderable( pRenderable, (RenderGroup_t)renderGroup ); } else { // handle already exists, just update group & origin ClientLeafSystem()->SetRenderGroup( pRenderable->RenderHandle(), (RenderGroup_t)renderGroup ); ClientLeafSystem()->RenderableChanged( pRenderable->RenderHandle() ); } } void CClientTools::RemoveClientRenderable( IClientRenderable *pRenderable ) { ClientRenderHandle_t handle = pRenderable->RenderHandle(); if( handle != INVALID_CLIENT_RENDER_HANDLE ) { ClientLeafSystem()->RemoveRenderable( handle ); } cl_entitylist->RemoveEntity( pRenderable->GetIClientUnknown()->GetRefEHandle() ); } void CClientTools::MarkClientRenderableDirty( IClientRenderable *pRenderable ) { ClientRenderHandle_t handle = pRenderable->RenderHandle(); if ( INVALID_CLIENT_RENDER_HANDLE != handle ) { // handle already exists, just update group & origin ClientLeafSystem()->RenderableChanged( pRenderable->RenderHandle() ); } } void CClientTools::SetRenderGroup( IClientRenderable *pRenderable, int renderGroup ) { ClientRenderHandle_t handle = pRenderable->RenderHandle(); if ( INVALID_CLIENT_RENDER_HANDLE == handle ) { // create new renderer handle ClientLeafSystem()->AddRenderable( pRenderable, (RenderGroup_t)renderGroup ); } else { // handle already exists, just update group & origin ClientLeafSystem()->SetRenderGroup( pRenderable->RenderHandle(), (RenderGroup_t)renderGroup ); ClientLeafSystem()->RenderableChanged( pRenderable->RenderHandle() ); } } bool CClientTools::DrawSprite( IClientRenderable *pRenderable, float scale, float frame, int rendermode, int renderfx, const Color &color, float flProxyRadius, int *pVisHandle ) { Vector origin = pRenderable->GetRenderOrigin(); QAngle angles = pRenderable->GetRenderAngles(); // Get extra data CEngineSprite *psprite = (CEngineSprite *)modelinfo->GetModelExtraData( pRenderable->GetModel() ); if ( !psprite ) return false; // Get orthonormal bases for current view - re-align to current camera (vs. recorded camera) Vector forward, right, up; C_SpriteRenderer::GetSpriteAxes( ( C_SpriteRenderer::SPRITETYPE )psprite->GetOrientation(), origin, angles, forward, right, up ); int r = color.r(); int g = color.g(); int b = color.b(); float oldBlend = render->GetBlend(); if ( rendermode != kRenderNormal ) { // kRenderGlow and kRenderWorldGlow have a special blending function if (( rendermode == kRenderGlow ) || ( rendermode == kRenderWorldGlow )) { pixelvis_queryparams_t params; if ( flProxyRadius != 0.0f ) { params.Init( origin, flProxyRadius ); params.bSizeInScreenspace = true; } else { params.Init( origin ); } float blend = oldBlend * StandardGlowBlend( params, ( pixelvis_handle_t* )pVisHandle, rendermode, renderfx, color.a(), &scale ); if ( blend <= 0.0f ) return false; //Fade out the sprite depending on distance from the view origin. r *= blend; g *= blend; b *= blend; render->SetBlend( blend ); } } DrawSpriteModel( ( IClientEntity* )pRenderable, psprite, origin, scale, frame, rendermode, r, g, b, color.a(), forward, right, up ); if (( rendermode == kRenderGlow ) || ( rendermode == kRenderWorldGlow )) { render->SetBlend( oldBlend ); } return true; } HTOOLHANDLE CClientTools::AttachToEntity( EntitySearchResult entityToAttach ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity * >( entityToAttach ); Assert( ent ); if ( !ent ) return (HTOOLHANDLE)0; HTOOLHANDLE curHandle = ent->GetToolHandle(); if ( curHandle != 0 ) return curHandle; // Already attaached HToolEntry_t newHandle( s_nNextHandle++, ent ); m_Handles.Insert( newHandle ); m_ActiveHandles.AddToTail( newHandle.m_Handle ); return (HTOOLHANDLE)newHandle.m_Handle; } void CClientTools::DetachFromEntity( EntitySearchResult entityToDetach ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity * >( entityToDetach ); Assert( ent ); if ( !ent ) return; HTOOLHANDLE handle = ent->GetToolHandle(); ent->SetToolHandle( (HTOOLHANDLE)0 ); if ( handle == (HTOOLHANDLE)0 ) { Assert( 0 ); return; } int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) { Assert( 0 ); return; } m_Handles.RemoveAt( idx ); m_ActiveHandles.FindAndRemove( handle ); } //----------------------------------------------------------------------------- // Purpose: // Input : handle - // Output : C_BaseEntity //----------------------------------------------------------------------------- C_BaseEntity *CClientTools::LookupEntity( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return NULL; return m_Handles[ idx ].m_hEntity; } int CClientTools::GetNumRecordables() { return m_ActiveHandles.Count(); } HTOOLHANDLE CClientTools::GetRecordable( int index ) { if ( index < 0 || index >= m_ActiveHandles.Count() ) { Assert( 0 ); return (HTOOLHANDLE)0; } return m_ActiveHandles[ index ]; } //----------------------------------------------------------------------------- // Iterates through ALL entities (separate list for client vs. server) //----------------------------------------------------------------------------- EntitySearchResult CClientTools::NextEntity( EntitySearchResult currentEnt ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity* >( currentEnt ); if ( ent == NULL ) { ent = cl_entitylist->FirstBaseEntity(); } else { ent = cl_entitylist->NextBaseEntity( ent ); } return reinterpret_cast< EntitySearchResult >( ent ); } //----------------------------------------------------------------------------- // Use this to turn on/off the presence of an underlying game entity //----------------------------------------------------------------------------- void CClientTools::SetEnabled( HTOOLHANDLE handle, bool enabled ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return; HToolEntry_t *slot = &m_Handles[ idx ]; Assert( slot ); if ( slot == NULL ) return; C_BaseEntity *ent = slot->m_hEntity.Get(); if ( ent == NULL || ent->entindex() == 0 ) return; // Don't disable/enable the "world" ent->EnableInToolView( enabled ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CClientTools::SetRecording( HTOOLHANDLE handle, bool recording ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { entry.m_hEntity->SetToolRecording( recording ); } } bool CClientTools::ShouldRecord( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return false; HToolEntry_t &entry = m_Handles[ idx ]; return entry.m_hEntity && entry.m_hEntity->ShouldRecordInTools(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CClientTools::GetModelIndex( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return NULL; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { return entry.m_hEntity->GetModelIndex(); } Assert( 0 ); return 0; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char* CClientTools::GetModelName( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return NULL; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { return STRING( entry.m_hEntity->GetModelName() ); } Assert( 0 ); return NULL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char* CClientTools::GetClassname( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return NULL; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { return STRING( entry.m_hEntity->GetClassname() ); } Assert( 0 ); return NULL; } //----------------------------------------------------------------------------- // Purpose: // Input : handle - //----------------------------------------------------------------------------- bool CClientTools::IsValidHandle( HTOOLHANDLE handle ) { return m_Handles.Find( HToolEntry_t( handle ) ) != m_Handles.InvalidIndex(); } void CClientTools::OnEntityDeleted( CBaseEntity *pEntity ) { HTOOLHANDLE handle = pEntity ? pEntity->GetToolHandle() : (HTOOLHANDLE)0; if ( handle == (HTOOLHANDLE)0 ) return; if ( m_bInRecordingMode ) { // Send deletion message to tool interface KeyValues *kv = new KeyValues( "deleted" ); ToolFramework_PostToolMessage( handle, kv ); kv->deleteThis(); } DetachFromEntity( pEntity ); } void CClientTools::OnEntityCreated( CBaseEntity *pEntity ) { if ( !m_bInRecordingMode ) return; HTOOLHANDLE h = AttachToEntity( pEntity ); // Send deletion message to tool interface KeyValues *kv = new KeyValues( "created" ); ToolFramework_PostToolMessage( h, kv ); kv->deleteThis(); } HTOOLHANDLE CClientTools::GetToolHandleForEntityByIndex( int entindex ) { C_BaseEntity *ent = C_BaseEntity::Instance( entindex ); if ( !ent ) return (HTOOLHANDLE)0; return ent->GetToolHandle(); } EntitySearchResult CClientTools::GetLocalPlayer() { C_BasePlayer *p = C_BasePlayer::GetLocalPlayer(); return reinterpret_cast< EntitySearchResult >( p ); } bool CClientTools::GetLocalPlayerEyePosition( Vector& org, QAngle& ang, float &fov ) { C_BasePlayer *pl = C_BasePlayer::GetLocalPlayer(); if ( pl == NULL ) return false; org = pl->EyePosition(); ang = pl->EyeAngles(); fov = pl->GetFOV(); return true; } //----------------------------------------------------------------------------- // Create, destroy shadow //----------------------------------------------------------------------------- ClientShadowHandle_t CClientTools::CreateShadow( CBaseHandle h, int nFlags ) { return g_pClientShadowMgr->CreateShadow( h, nFlags ); } void CClientTools::DestroyShadow( ClientShadowHandle_t h ) { g_pClientShadowMgr->DestroyShadow( h ); } ClientShadowHandle_t CClientTools::CreateFlashlight( const FlashlightState_t &lightState ) { return g_pClientShadowMgr->CreateFlashlight( lightState ); } void CClientTools::DestroyFlashlight( ClientShadowHandle_t h ) { g_pClientShadowMgr->DestroyFlashlight( h ); } void CClientTools::UpdateFlashlightState( ClientShadowHandle_t h, const FlashlightState_t &lightState ) { g_pClientShadowMgr->UpdateFlashlightState( h, lightState ); } void CClientTools::AddToDirtyShadowList( ClientShadowHandle_t h, bool force ) { g_pClientShadowMgr->AddToDirtyShadowList( h, force ); } void CClientTools::MarkRenderToTextureShadowDirty( ClientShadowHandle_t h ) { g_pClientShadowMgr->MarkRenderToTextureShadowDirty( h ); } void CClientTools::UpdateProjectedTexture( ClientShadowHandle_t h, bool bForce ) { g_pClientShadowMgr->UpdateProjectedTexture( h, bForce ); } int CClientTools::FindGlobalFlexcontroller( char const *name ) { return C_BaseFlex::AddGlobalFlexController( (char *)name ); } char const *CClientTools::GetGlobalFlexControllerName( int idx ) { return C_BaseFlex::GetGlobalFlexControllerName( idx ); } //----------------------------------------------------------------------------- // helper for traversing ownership hierarchy //----------------------------------------------------------------------------- EntitySearchResult CClientTools::GetOwnerEntity( EntitySearchResult currentEnt ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity* >( currentEnt ); return ent ? ent->GetOwnerEntity() : NULL; } //----------------------------------------------------------------------------- // common and useful types to query for hierarchically //----------------------------------------------------------------------------- bool CClientTools::IsPlayer( EntitySearchResult currentEnt ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity* >( currentEnt ); return ent ? ent->IsPlayer() : false; } bool CClientTools::IsBaseCombatCharacter( EntitySearchResult currentEnt ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity* >( currentEnt ); return ent ? ent->IsBaseCombatCharacter() : false; } bool CClientTools::IsNPC( EntitySearchResult currentEnt ) { C_BaseEntity *ent = reinterpret_cast< C_BaseEntity* >( currentEnt ); return ent ? ent->IsNPC() : false; } Vector CClientTools::GetAbsOrigin( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return vec3_origin; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { return entry.m_hEntity->GetAbsOrigin(); } Assert( 0 ); return vec3_origin; } QAngle CClientTools::GetAbsAngles( HTOOLHANDLE handle ) { int idx = m_Handles.Find( HToolEntry_t( handle ) ); if ( idx == m_Handles.InvalidIndex() ) return vec3_angle; HToolEntry_t &entry = m_Handles[ idx ]; if ( entry.m_hEntity ) { return entry.m_hEntity->GetAbsAngles(); } Assert( 0 ); return vec3_angle; } //----------------------------------------------------------------------------- // Sends a mesage from the tool to the client //----------------------------------------------------------------------------- void CClientTools::PostToolMessage( KeyValues *pKeyValues ) { if ( !Q_stricmp( pKeyValues->GetName(), "QueryParticleManifest" ) ) { // NOTE: This cannot be done during particle system init because tools aren't set up at that point CUtlVector files; GetParticleManifest( files ); int nCount = files.Count(); for ( int i = 0; i < nCount; ++i ) { char pTemp[256]; Q_snprintf( pTemp, sizeof(pTemp), "%d", i ); KeyValues *pSubKey = pKeyValues->FindKey( pTemp, true ); pSubKey->SetString( "file", files[i] ); } return; } if ( !Q_stricmp( pKeyValues->GetName(), "QueryMonitorTexture" ) ) { pKeyValues->SetPtr( "texture", GetCameraTexture() ); return; } #ifdef PORTAL if ( !Q_stricmp( pKeyValues->GetName(), "portals" ) ) { g_pPortalRender->HandlePortalPlaybackMessage( pKeyValues ); return; } if ( !Q_stricmp( pKeyValues->GetName(), "query CPortalRenderer" ) ) { pKeyValues->SetInt( "IsRenderingPortal", g_pPortalRender->IsRenderingPortal() ? 1 : 0 ); return; } #endif } //----------------------------------------------------------------------------- // Indicates whether the client should render particle systems //----------------------------------------------------------------------------- void CClientTools::EnableParticleSystems( bool bEnable ) { ParticleMgr()->RenderParticleSystems( bEnable ); } //----------------------------------------------------------------------------- // Is the game rendering in 3rd person mode? //----------------------------------------------------------------------------- bool CClientTools::IsRenderingThirdPerson() const { return !C_BasePlayer::LocalPlayerInFirstPersonView(); } //----------------------------------------------------------------------------- // Reload particle definitions //----------------------------------------------------------------------------- void CClientTools::ReloadParticleDefintions( const char *pFileName, const void *pBufData, int nLen ) { // Remove all new effects, because we are going to free internal structures they point to ParticleMgr()->RemoveAllNewEffects(); // FIXME: Use file name to determine if we care about this data CUtlBuffer buf( pBufData, nLen, CUtlBuffer::READ_ONLY ); g_pParticleSystemMgr->ReadParticleConfigFile( buf, true ); }