You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1248 lines
35 KiB
1248 lines
35 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: Clients CBaseObject |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
#include "cbase.h" |
|
#include "c_baseobject.h" |
|
#include "c_tf_player.h" |
|
#include "hud.h" |
|
#include "c_tf_team.h" |
|
#include "engine/IEngineSound.h" |
|
#include "particles_simple.h" |
|
#include "functionproxy.h" |
|
#include "IEffects.h" |
|
#include "model_types.h" |
|
#include "particlemgr.h" |
|
#include "particle_collision.h" |
|
#include "c_tf_weapon_builder.h" |
|
#include "ivrenderview.h" |
|
#include "ObjectControlPanel.h" |
|
#include "engine/ivmodelinfo.h" |
|
#include "c_te_effect_dispatch.h" |
|
#include "toolframework_client.h" |
|
#include "tf_hud_building_status.h" |
|
#include "cl_animevent.h" |
|
#include "eventlist.h" |
|
#include "c_obj_sapper.h" |
|
#include "tf_gamerules.h" |
|
#include "tf_hud_spectator_extras.h" |
|
#include "tf_proxyentity.h" |
|
|
|
// NVNT for building forces |
|
#include "haptics/haptic_utils.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
// forward declarations |
|
void ToolFramework_RecordMaterialParams( IMaterial *pMaterial ); |
|
|
|
#define MAX_VISIBLE_BUILDPOINT_DISTANCE (400 * 400) |
|
|
|
// Remove aliasing of name due to shared code |
|
#undef CBaseObject |
|
|
|
IMPLEMENT_AUTO_LIST( IBaseObjectAutoList ); |
|
|
|
IMPLEMENT_CLIENTCLASS_DT(C_BaseObject, DT_BaseObject, CBaseObject) |
|
RecvPropInt(RECVINFO(m_iHealth)), |
|
RecvPropInt(RECVINFO(m_iMaxHealth)), |
|
RecvPropInt(RECVINFO(m_bHasSapper)), |
|
RecvPropInt(RECVINFO(m_iObjectType)), |
|
RecvPropBool(RECVINFO(m_bBuilding)), |
|
RecvPropBool(RECVINFO(m_bPlacing)), |
|
RecvPropBool(RECVINFO(m_bCarried)), |
|
RecvPropBool(RECVINFO(m_bCarryDeploy)), |
|
RecvPropBool(RECVINFO(m_bMiniBuilding)), |
|
RecvPropFloat(RECVINFO(m_flPercentageConstructed)), |
|
RecvPropInt(RECVINFO(m_fObjectFlags)), |
|
RecvPropEHandle(RECVINFO(m_hBuiltOnEntity)), |
|
RecvPropInt( RECVINFO( m_bDisabled ) ), |
|
RecvPropEHandle( RECVINFO( m_hBuilder ) ), |
|
RecvPropVector( RECVINFO( m_vecBuildMaxs ) ), |
|
RecvPropVector( RECVINFO( m_vecBuildMins ) ), |
|
RecvPropInt( RECVINFO( m_iDesiredBuildRotations ) ), |
|
RecvPropInt( RECVINFO( m_bServerOverridePlacement ) ), |
|
RecvPropInt( RECVINFO(m_iUpgradeLevel) ), |
|
RecvPropInt( RECVINFO(m_iUpgradeMetal) ), |
|
RecvPropInt( RECVINFO(m_iUpgradeMetalRequired) ), |
|
RecvPropInt( RECVINFO(m_iHighestUpgradeLevel) ), |
|
RecvPropInt( RECVINFO(m_iObjectMode) ), |
|
RecvPropBool( RECVINFO( m_bDisposableBuilding ) ), |
|
RecvPropBool( RECVINFO( m_bWasMapPlaced ) ), |
|
RecvPropBool( RECVINFO( m_bPlasmaDisable ) ), |
|
END_RECV_TABLE() |
|
|
|
ConVar cl_obj_test_building_damage( "cl_obj_test_building_damage", "-1", FCVAR_CHEAT, "debug building damage", true, -1, true, BUILDING_DAMAGE_LEVEL_CRITICAL ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
C_BaseObject::C_BaseObject( ) |
|
{ |
|
m_YawPreviewState = YAW_PREVIEW_OFF; |
|
m_bBuilding = false; |
|
m_bPlacing = false; |
|
m_flPercentageConstructed = 0; |
|
m_fObjectFlags = 0; |
|
m_iOldUpgradeLevel = 0; |
|
|
|
m_flCurrentBuildRotation = 0; |
|
|
|
m_damageLevel = BUILDING_DAMAGE_LEVEL_NONE; |
|
|
|
m_iLastPlacementPosValid = -1; |
|
|
|
m_iObjectMode = 0; |
|
|
|
m_bCarryDeploy = false; |
|
m_bOldCarryDeploy = false; |
|
|
|
m_bMiniBuilding = false; |
|
m_bDisposableBuilding = false; |
|
|
|
m_vecBuildForward = vec3_origin; |
|
m_flBuildDistance = 0.0f; |
|
|
|
m_flInvisibilityPercent = 0.f; |
|
|
|
m_bWasMapPlaced = false; |
|
|
|
m_hDamageEffects = NULL; |
|
|
|
m_bPlasmaDisable = false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
C_BaseObject::~C_BaseObject( void ) |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::Spawn( void ) |
|
{ |
|
BaseClass::Spawn(); |
|
|
|
m_bServerOverridePlacement = true; // assume valid at the start |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::UpdateOnRemove( void ) |
|
{ |
|
StopAnimGeneratedSounds(); |
|
|
|
DestroyBoneAttachments(); |
|
|
|
CTFHudSpectatorExtras *pSpectatorExtras = GET_HUDELEMENT( CTFHudSpectatorExtras ); |
|
if ( pSpectatorExtras ) |
|
{ |
|
pSpectatorExtras->RemoveEntity( entindex() ); |
|
} |
|
|
|
BaseClass::UpdateOnRemove(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::PreDataUpdate( DataUpdateType_t updateType ) |
|
{ |
|
BaseClass::PreDataUpdate( updateType ); |
|
|
|
m_iOldHealth = m_iHealth; |
|
m_hOldOwner = GetOwner(); |
|
m_bWasActive = ShouldBeActive(); |
|
m_bWasBuilding = m_bBuilding; |
|
m_bOldDisabled = m_bDisabled; |
|
m_bWasPlacing = m_bPlacing; |
|
|
|
m_nObjectOldSequence = GetSequence(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::OnDataChanged( DataUpdateType_t updateType ) |
|
{ |
|
if (updateType == DATA_UPDATE_CREATED) |
|
{ |
|
CreateBuildPoints(); |
|
// NVNT if the local player created this send a created effect |
|
if(IsOwnedByLocalPlayer() &&haptics) |
|
{ |
|
haptics->ProcessHapticEvent(3, "Game", "Build", GetClassname()); |
|
} |
|
} |
|
|
|
BaseClass::OnDataChanged( updateType ); |
|
|
|
// did we just pick up the object? |
|
if ( !m_bWasPlacing && m_bPlacing ) |
|
{ |
|
m_iLastPlacementPosValid = -1; |
|
} |
|
|
|
// Did we just finish building? |
|
if ( m_bWasBuilding && !m_bBuilding ) |
|
{ |
|
FinishedBuilding(); |
|
} |
|
else if ( !m_bWasBuilding && m_bBuilding ) |
|
{ |
|
ResetClientsideFrame(); |
|
} |
|
|
|
// Did we just go active? |
|
bool bShouldBeActive = ShouldBeActive(); |
|
if ( !m_bWasActive && bShouldBeActive ) |
|
{ |
|
OnGoActive(); |
|
} |
|
else if ( m_bWasActive && !bShouldBeActive ) |
|
{ |
|
OnGoInactive(); |
|
} |
|
|
|
if ( m_bDisabled != m_bOldDisabled ) |
|
{ |
|
if ( m_bDisabled ) |
|
{ |
|
OnStartDisabled(); |
|
} |
|
else |
|
{ |
|
OnEndDisabled(); |
|
} |
|
} |
|
|
|
if ( !IsBuilding() && m_iHealth != m_iOldHealth ) |
|
{ |
|
// recalc our damage particle state |
|
BuildingDamageLevel_t damageLevel = CalculateDamageLevel(); |
|
|
|
if ( damageLevel != m_damageLevel ) |
|
{ |
|
UpdateDamageEffects( damageLevel ); |
|
|
|
m_damageLevel = damageLevel; |
|
} |
|
} |
|
|
|
if ( m_bCarryDeploy != m_bOldCarryDeploy ) |
|
{ |
|
m_bOldCarryDeploy = m_bCarryDeploy; |
|
if ( !m_bCarryDeploy ) |
|
{ |
|
// Update our damage effects when we're done redeploying. |
|
UpdateDamageEffects( CalculateDamageLevel() ); |
|
} |
|
} |
|
|
|
if ( m_iHealth > m_iOldHealth && m_iHealth == m_iMaxHealth ) |
|
{ |
|
// If we were just fully healed, remove all decals |
|
RemoveAllDecals(); |
|
} |
|
|
|
if ( GetOwner() == C_TFPlayer::GetLocalTFPlayer() ) |
|
{ |
|
IGameEvent *event = gameeventmanager->CreateEvent( "building_info_changed" ); |
|
if ( event ) |
|
{ |
|
event->SetInt( "building_type", GetType() ); |
|
event->SetInt( "object_mode", GetObjectMode() ); |
|
gameeventmanager->FireEventClientSide( event ); |
|
} |
|
} |
|
|
|
if ( IsPlacing() && GetSequence() != m_nObjectOldSequence ) |
|
{ |
|
// Ignore server sequences while placing |
|
OnPlacementStateChanged( m_iLastPlacementPosValid > 0 ); |
|
} |
|
|
|
if ( m_iOldUpgradeLevel != m_iUpgradeLevel ) |
|
{ |
|
UpgradeLevelChanged(); |
|
m_iOldUpgradeLevel = m_iUpgradeLevel; |
|
} |
|
// NVNT building status |
|
if(IsOwnedByLocalPlayer()) { |
|
if(m_bWasBuilding!=m_bBuilding) { |
|
if(m_bBuilding && haptics) { |
|
haptics->ProcessHapticEvent(3, "Game", "Building", GetClassname()); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::SetDormant( bool bDormant ) |
|
{ |
|
BaseClass::SetDormant( bDormant ); |
|
//ENTITY_PANEL_ACTIVATE( "analyzed_object", !bDormant ); |
|
} |
|
|
|
#define TF_OBJ_BODYGROUPTURNON 1 |
|
#define TF_OBJ_BODYGROUPTURNOFF 0 |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : origin - |
|
// angles - |
|
// event - |
|
// *options - |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::FireEvent( const Vector& origin, const QAngle& angles, int event, const char *options ) |
|
{ |
|
switch ( event ) |
|
{ |
|
default: |
|
{ |
|
BaseClass::FireEvent( origin, angles, event, options ); |
|
} |
|
break; |
|
case TF_OBJ_PLAYBUILDSOUND: |
|
{ |
|
EmitSound( options ); |
|
} |
|
break; |
|
case TF_OBJ_ENABLEBODYGROUP: |
|
{ |
|
int index_ = FindBodygroupByName( options ); |
|
if ( index_ >= 0 ) |
|
{ |
|
SetBodygroup( index_, TF_OBJ_BODYGROUPTURNON ); |
|
} |
|
} |
|
break; |
|
case TF_OBJ_DISABLEBODYGROUP: |
|
{ |
|
int index_ = FindBodygroupByName( options ); |
|
if ( index_ >= 0 ) |
|
{ |
|
SetBodygroup( index_, TF_OBJ_BODYGROUPTURNOFF ); |
|
} |
|
} |
|
break; |
|
case TF_OBJ_ENABLEALLBODYGROUPS: |
|
case TF_OBJ_DISABLEALLBODYGROUPS: |
|
{ |
|
// Start at 1, because body 0 is the main .mdl body... |
|
// Is this the way we want to do this? |
|
int count = GetNumBodyGroups(); |
|
for ( int i = 1; i < count; i++ ) |
|
{ |
|
int subpartcount = GetBodygroupCount( i ); |
|
if ( subpartcount == 2 ) |
|
{ |
|
SetBodygroup( i, |
|
( event == TF_OBJ_ENABLEALLBODYGROUPS ) ? |
|
TF_OBJ_BODYGROUPTURNON : TF_OBJ_BODYGROUPTURNOFF ); |
|
} |
|
else |
|
{ |
|
DevMsg( "TF_OBJ_ENABLE/DISABLEBODY GROUP: %s has a group with %i subparts, should be exactly 2\n", |
|
GetClassname(), subpartcount ); |
|
} |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
|
|
const char* C_BaseObject::GetStatusName() const |
|
{ |
|
return GetObjectInfo( GetType() )->m_AltModes[GetObjectMode()].pszStatusName; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: placement state has changed, update the model |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::OnPlacementStateChanged( bool bValidPlacement ) |
|
{ |
|
if ( bValidPlacement ) |
|
{ |
|
// NVNT if the local player placed this send a created effect |
|
if(IsOwnedByLocalPlayer()&&haptics) |
|
{ |
|
haptics->ProcessHapticEvent(3, "Game", "Placed", GetClassname()); |
|
} |
|
SetActivity( ACT_OBJ_PLACING ); |
|
} |
|
else |
|
{ |
|
SetActivity( ACT_OBJ_IDLE ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::Simulate( void ) |
|
{ |
|
if ( IsPlacing() && !MustBeBuiltOnAttachmentPoint() ) |
|
{ |
|
int iValidPlacement = ( IsPlacementPosValid() && ServerValidPlacement() ) ? 1 : 0; |
|
|
|
if ( m_iLastPlacementPosValid != iValidPlacement ) |
|
{ |
|
m_iLastPlacementPosValid = iValidPlacement; |
|
OnPlacementStateChanged( m_iLastPlacementPosValid > 0 ); |
|
} |
|
|
|
// We figure out our own placement pos, but we still leave it to the server to |
|
// do collision with other entities and nobuild triggers, so that sets the |
|
// placement animation |
|
|
|
SetLocalOrigin( m_vecBuildOrigin ); |
|
InvalidateBoneCache(); |
|
|
|
// Clear out our origin and rotation interpolation history |
|
// so we don't pop when we teleport in the actual position from the server |
|
|
|
CInterpolatedVar< Vector > &interpolator = GetOriginInterpolator(); |
|
interpolator.ClearHistory(); |
|
|
|
CInterpolatedVar<QAngle> &rotInterpolator = GetRotationInterpolator(); |
|
rotInterpolator.ClearHistory(); |
|
} |
|
else if ( !IsPlacing() && !IsCarried() && m_iLastPlacementPosValid == 0 ) |
|
{ |
|
// HACK HACK: This sentry has been placed, but was placed on the server before the client updated |
|
// from the carry position to see that was a valid placement. |
|
// It missed its chance to set the correct activity, so we're doing it now. |
|
SetActivity( ACT_OBJ_RUNNING ); |
|
|
|
// Check if the activity was valid because it might have still been using the older placement model |
|
if ( GetActivity() != ACT_INVALID ) |
|
{ |
|
// Remember to retest our placement, but don't keep forcing the running activity |
|
m_iLastPlacementPosValid = -1; |
|
} |
|
} |
|
|
|
BaseClass::Simulate(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return false if the server is telling us we can't place right now |
|
// could be due to placing in a nobuild or respawn room |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::ServerValidPlacement( void ) |
|
{ |
|
return m_bServerOverridePlacement; |
|
} |
|
|
|
bool C_BaseObject::WasLastPlacementPosValid( void ) |
|
{ |
|
if ( MustBeBuiltOnAttachmentPoint() ) |
|
{ |
|
return ( !IsEffectActive(EF_NODRAW) ); |
|
} |
|
|
|
return ( m_iLastPlacementPosValid > 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int C_BaseObject::DrawModel( int flags ) |
|
{ |
|
int drawn; |
|
|
|
// If we're a brush-built, map-defined object chain up to baseentity draw |
|
if ( modelinfo->GetModelType( GetModel() ) == mod_brush ) |
|
{ |
|
drawn = CBaseEntity::DrawModel(flags); |
|
} |
|
else |
|
{ |
|
drawn = BaseClass::DrawModel(flags); |
|
} |
|
|
|
HighlightBuildPoints( flags ); |
|
|
|
return drawn; |
|
} |
|
|
|
float C_BaseObject::GetReversesBuildingConstructionSpeed( void ) |
|
{ |
|
if ( HasSapper() ) |
|
{ |
|
C_ObjectSapper *pSapper = dynamic_cast< C_ObjectSapper* >( FirstMoveChild() ); |
|
if ( pSapper ) |
|
{ |
|
return pSapper->GetReversesBuildingConstructionSpeed(); |
|
} |
|
} |
|
|
|
return 0.0f; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::HighlightBuildPoints( int flags ) |
|
{ |
|
C_TFPlayer *pLocal = C_TFPlayer::GetLocalTFPlayer(); |
|
if ( !pLocal ) |
|
return; |
|
|
|
if ( !GetNumBuildPoints() || !InLocalTeam() ) |
|
return; |
|
|
|
C_TFWeaponBuilder *pBuilderWpn = dynamic_cast< C_TFWeaponBuilder * >( pLocal->GetActiveWeaponForSelection() ); |
|
if ( !pBuilderWpn ) |
|
return; |
|
if ( !pBuilderWpn->IsPlacingObject() ) |
|
return; |
|
C_BaseObject *pPlacementObj = pBuilderWpn->GetPlacementModel(); |
|
if ( !pPlacementObj || pPlacementObj == this ) |
|
return; |
|
|
|
// Near enough? |
|
if ( (GetAbsOrigin() - pLocal->GetAbsOrigin()).LengthSqr() < MAX_VISIBLE_BUILDPOINT_DISTANCE ) |
|
{ |
|
bool bRestoreModel = false; |
|
Vector vecPrevAbsOrigin = pPlacementObj->GetAbsOrigin(); |
|
QAngle vecPrevAbsAngles = pPlacementObj->GetAbsAngles(); |
|
|
|
Vector orgColor; |
|
render->GetColorModulation( orgColor.Base() ); |
|
float orgBlend = render->GetBlend(); |
|
|
|
bool bSameTeam = ( pPlacementObj->GetTeamNumber() == GetTeamNumber() ); |
|
|
|
if ( pPlacementObj->IsHostileUpgrade() && bSameTeam ) |
|
{ |
|
// Don't hilight hostile upgrades on friendly objects |
|
return; |
|
} |
|
else if ( !bSameTeam ) |
|
{ |
|
// Don't hilight upgrades on enemy objects |
|
return; |
|
} |
|
|
|
// Any empty buildpoints? |
|
for ( int i = 0; i < GetNumBuildPoints(); i++ ) |
|
{ |
|
// Can this object build on this point? |
|
if ( CanBuildObjectOnBuildPoint( i, pPlacementObj->GetType() ) ) |
|
{ |
|
Vector vecBPOrigin; |
|
QAngle vecBPAngles; |
|
if ( GetBuildPoint(i, vecBPOrigin, vecBPAngles) ) |
|
{ |
|
pPlacementObj->InvalidateBoneCaches(); |
|
|
|
Vector color( 0, 255, 0 ); |
|
render->SetColorModulation( color.Base() ); |
|
float frac = fmod( gpGlobals->curtime, 3 ); |
|
frac *= 2 * M_PI; |
|
frac = cos( frac ); |
|
render->SetBlend( (175 + (int)( frac * 75.0f )) / 255.0 ); |
|
|
|
// FIXME: This truly sucks! The bone cache should use |
|
// render location for this computation instead of directly accessing AbsAngles |
|
// Necessary for bone cache computations to work |
|
pPlacementObj->SetAbsOrigin( vecBPOrigin ); |
|
pPlacementObj->SetAbsAngles( vecBPAngles ); |
|
|
|
modelrender->DrawModel( |
|
flags, |
|
pPlacementObj, |
|
pPlacementObj->GetModelInstance(), |
|
pPlacementObj->index, |
|
pPlacementObj->GetModel(), |
|
vecBPOrigin, |
|
vecBPAngles, |
|
pPlacementObj->m_nSkin, |
|
pPlacementObj->m_nBody, |
|
pPlacementObj->m_nHitboxSet |
|
); |
|
|
|
bRestoreModel = true; |
|
} |
|
} |
|
} |
|
|
|
if ( bRestoreModel ) |
|
{ |
|
pPlacementObj->SetAbsOrigin(vecPrevAbsOrigin); |
|
pPlacementObj->SetAbsAngles(vecPrevAbsAngles); |
|
pPlacementObj->InvalidateBoneCaches(); |
|
|
|
render->SetColorModulation( orgColor.Base() ); |
|
render->SetBlend( orgBlend ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Builder preview... |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::ActivateYawPreview( bool enable ) |
|
{ |
|
m_YawPreviewState = enable ? YAW_PREVIEW_ON : YAW_PREVIEW_WAITING_FOR_UPDATE; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::PreviewYaw( float yaw ) |
|
{ |
|
m_fYawPreview = yaw; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::IsPreviewingYaw() const |
|
{ |
|
return m_YawPreviewState != YAW_PREVIEW_OFF; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
BuildingDamageLevel_t C_BaseObject::CalculateDamageLevel( void ) |
|
{ |
|
float flPercentHealth = (float)m_iHealth / (float)m_iMaxHealth; |
|
|
|
BuildingDamageLevel_t damageLevel = BUILDING_DAMAGE_LEVEL_NONE; |
|
|
|
if ( flPercentHealth < 0.25 ) |
|
{ |
|
damageLevel = BUILDING_DAMAGE_LEVEL_CRITICAL; |
|
} |
|
else if ( flPercentHealth < 0.45 ) |
|
{ |
|
damageLevel = BUILDING_DAMAGE_LEVEL_HEAVY; |
|
} |
|
else if ( flPercentHealth < 0.65 ) |
|
{ |
|
damageLevel = BUILDING_DAMAGE_LEVEL_MEDIUM; |
|
} |
|
else if ( flPercentHealth < 0.85 ) |
|
{ |
|
damageLevel = BUILDING_DAMAGE_LEVEL_LIGHT; |
|
} |
|
|
|
if ( cl_obj_test_building_damage.GetInt() >= 0 ) |
|
{ |
|
damageLevel = (BuildingDamageLevel_t)cl_obj_test_building_damage.GetInt(); |
|
} |
|
|
|
return damageLevel; |
|
} |
|
|
|
/* |
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::Release( void ) |
|
{ |
|
// Remove any reticles on this entity |
|
C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
if ( pPlayer ) |
|
{ |
|
pPlayer->Remove_Target( this ); |
|
} |
|
|
|
BaseClass::Release(); |
|
} |
|
*/ |
|
|
|
//----------------------------------------------------------------------------- |
|
// Ownership: |
|
//----------------------------------------------------------------------------- |
|
C_TFPlayer *C_BaseObject::GetOwner() |
|
{ |
|
return m_hBuilder; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::IsOwnedByLocalPlayer() const |
|
{ |
|
if ( !m_hBuilder ) |
|
return false; |
|
|
|
return ( m_hBuilder == C_TFPlayer::GetLocalTFPlayer() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Add entity to visibile entities list |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::AddEntity( void ) |
|
{ |
|
// If set to invisible, skip. Do this before resetting the entity pointer so it has |
|
// valid data to decide whether it's visible. |
|
if ( !ShouldDraw() ) |
|
{ |
|
return; |
|
} |
|
|
|
// Update the entity position |
|
//UpdatePosition(); |
|
|
|
// Yaw preview |
|
if (m_YawPreviewState != YAW_PREVIEW_OFF) |
|
{ |
|
// This piece of code makes it so we keep using the preview |
|
// until we get a network update which matches the update value |
|
if (m_YawPreviewState == YAW_PREVIEW_WAITING_FOR_UPDATE) |
|
{ |
|
if (fmod( fabs(GetLocalAngles().y - m_fYawPreview), 360.0f) < 1.0f) |
|
{ |
|
m_YawPreviewState = YAW_PREVIEW_OFF; |
|
} |
|
} |
|
|
|
if (GetLocalOrigin().y != m_fYawPreview) |
|
{ |
|
SetLocalAnglesDim( Y_INDEX, m_fYawPreview ); |
|
InvalidateBoneCache(); |
|
} |
|
} |
|
|
|
// Create flashlight effects, etc. |
|
CreateLightEffects(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::Select( void ) |
|
{ |
|
C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
pPlayer->SetSelectedObject( this ); |
|
} |
|
|
|
void C_BaseObject::ResetClientsideFrame( void ) |
|
{ |
|
SetCycle( GetReversesBuildingConstructionSpeed() != 0.0f ? 1.0f : 0.0f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Sends client commands back to the server: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::SendClientCommand( const char *pCmd ) |
|
{ |
|
char szbuf[128]; |
|
Q_snprintf( szbuf, sizeof( szbuf ), "objcmd %d %s", entindex(), pCmd ); |
|
engine->ClientCmd(szbuf); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Get a text description for the object target |
|
//----------------------------------------------------------------------------- |
|
const char *C_BaseObject::GetTargetDescription( void ) const |
|
{ |
|
return GetStatusName(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Get a text description for the object target (more verbose) |
|
//----------------------------------------------------------------------------- |
|
const char *C_BaseObject::GetIDString( void ) |
|
{ |
|
m_szIDString[0] = 0; |
|
RecalculateIDString(); |
|
return m_szIDString; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// It's a valid ID target when it's building |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::IsValidIDTarget( void ) |
|
{ |
|
return InSameTeam( C_TFPlayer::GetLocalTFPlayer() ) && m_bBuilding; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::RecalculateIDString( void ) |
|
{ |
|
// Subclasses may have filled this out with a string |
|
if ( !m_szIDString[0] ) |
|
{ |
|
Q_strncpy( m_szIDString, GetTargetDescription(), sizeof(m_szIDString) ); |
|
} |
|
|
|
// Have I taken damage? |
|
if ( m_iHealth < m_iMaxHealth ) |
|
{ |
|
char szHealth[ MAX_ID_STRING ]; |
|
if ( m_bBuilding ) |
|
{ |
|
Q_snprintf( szHealth, sizeof(szHealth), "\nConstruction at %.0f percent\nHealth at %.0f percent", (m_flPercentageConstructed * 100), ceil(((float)m_iHealth / (float)m_iMaxHealth) * 100) ); |
|
} |
|
else |
|
{ |
|
Q_snprintf( szHealth, sizeof(szHealth), "\nHealth at %.0f percent", ceil(((float)m_iHealth / (float)m_iMaxHealth) * 100) ); |
|
} |
|
Q_strncat( m_szIDString, szHealth, sizeof(m_szIDString), COPY_ALL_CHARACTERS ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Player has waved his crosshair over this entity. Display appropriate hints. |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::DisplayHintTo( C_BasePlayer *pPlayer ) |
|
{ |
|
bool bHintPlayed = false; |
|
|
|
C_TFPlayer *pTFPlayer = ToTFPlayer(pPlayer); |
|
if ( InSameTeam( pPlayer ) ) |
|
{ |
|
// We're looking at a friendly object. |
|
|
|
if ( HasSapper() ) |
|
{ |
|
bHintPlayed = pPlayer->HintMessage( HINT_OBJECT_HAS_SAPPER, true, true ); |
|
} |
|
|
|
if ( pTFPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) ) |
|
{ |
|
// I'm an engineer. |
|
|
|
// If I'm looking at a constructing object, let me know I can help build it (but not |
|
// if I built it myself, since I've already got that hint from the wrench). |
|
if ( !bHintPlayed && IsBuilding() && GetBuilder() != pTFPlayer ) |
|
{ |
|
bHintPlayed = pPlayer->HintMessage( HINT_ENGINEER_USE_WRENCH_ONOTHER, false, true ); |
|
} |
|
|
|
// If it's damaged, I can repair it |
|
if ( !bHintPlayed && !IsBuilding() && GetHealth() < GetMaxHealth() ) |
|
{ |
|
bHintPlayed = pPlayer->HintMessage( HINT_ENGINEER_REPAIR_OBJECT, false, true ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::GetGlowEffectColor( float *r, float *g, float *b ) |
|
{ |
|
if ( TFGameRules() ) |
|
{ |
|
TFGameRules()->GetTeamGlowColor( GetTeamNumber(), *r, *g, *b ); |
|
} |
|
else |
|
{ |
|
*r = 0.76f; |
|
*g = 0.76f; |
|
*b = 0.76f; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Does this object have a sapper on it |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::HasSapper( void ) |
|
{ |
|
return m_bHasSapper; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool C_BaseObject::IsPlasmaDisabled( void ) |
|
{ |
|
return m_bPlasmaDisable; |
|
} |
|
|
|
void C_BaseObject::OnStartDisabled() |
|
{ |
|
} |
|
|
|
void C_BaseObject::OnEndDisabled() |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::GetTargetIDString( OUT_Z_BYTECAP( iMaxLenInBytes ) wchar_t *sIDString, int iMaxLenInBytes, bool bSpectator ) |
|
{ |
|
Assert( iMaxLenInBytes >= sizeof(sIDString[0]) ); |
|
sIDString[0] = '\0'; |
|
|
|
C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
|
|
if ( !pLocalPlayer ) |
|
return; |
|
|
|
if ( pLocalPlayer->InSameDisguisedTeam( this ) || pLocalPlayer->IsPlayerClass( TF_CLASS_SPY ) || bSpectator ) |
|
{ |
|
wchar_t wszBuilderName[ MAX_PLAYER_NAME_LENGTH ]; |
|
|
|
const char *pszStatusName = GetStatusName(); |
|
const wchar_t *wszObjectName = g_pVGuiLocalize->Find( pszStatusName ); |
|
|
|
bool bHasMode = false; |
|
const char *printFormatString = "#TF_playerid_object"; |
|
|
|
if ( IsMiniBuilding() && !IsDisposableBuilding() ) |
|
{ |
|
printFormatString = "#TF_playerid_object_mini"; |
|
} |
|
|
|
const wchar_t *wszModeName = L""; |
|
const CObjectInfo* pObjectInfo = GetObjectInfo( GetType() ); |
|
if ( pObjectInfo && (pObjectInfo->m_iNumAltModes > 0) ) |
|
{ |
|
const char *pszModeName = pObjectInfo->m_AltModes[GetObjectMode()].pszModeName; |
|
wszModeName = g_pVGuiLocalize->Find( pszModeName ); |
|
printFormatString = "TF_playerid_object_mode"; |
|
bHasMode = true; |
|
} |
|
|
|
if ( !wszObjectName ) |
|
{ |
|
wszObjectName = L""; |
|
} |
|
|
|
C_BasePlayer *pBuilder = GetOwner(); |
|
|
|
if ( pBuilder ) |
|
{ |
|
g_pVGuiLocalize->ConvertANSIToUnicode( pBuilder->GetPlayerName(), wszBuilderName, sizeof(wszBuilderName) ); |
|
} |
|
else |
|
{ |
|
wszBuilderName[0] = '\0'; |
|
} |
|
|
|
// building or live, show health |
|
wchar_t * localizedString = g_pVGuiLocalize->Find( printFormatString ); |
|
if ( localizedString ) |
|
{ |
|
if ( bHasMode ) |
|
{ |
|
g_pVGuiLocalize->ConstructString( sIDString, iMaxLenInBytes, localizedString, |
|
3, wszObjectName, wszBuilderName, wszModeName ); |
|
} |
|
else |
|
{ |
|
g_pVGuiLocalize->ConstructString( sIDString, iMaxLenInBytes, localizedString, |
|
2, wszObjectName, wszBuilderName ); |
|
} |
|
} |
|
|
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::GetTargetIDDataString( OUT_Z_BYTECAP(iMaxLenInBytes) wchar_t *sDataString, int iMaxLenInBytes ) |
|
{ |
|
Assert( iMaxLenInBytes >= sizeof(sDataString[0]) ); |
|
sDataString[0] = '\0'; |
|
|
|
C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
if ( !pLocalPlayer ) |
|
return; |
|
|
|
// Sentryguns have models for each level, so we don't show it in their target ID. |
|
bool bShowLevel = ( GetType() != OBJ_SENTRYGUN ); |
|
|
|
wchar_t wszLevel[32]; |
|
if ( bShowLevel ) |
|
{ |
|
_snwprintf( wszLevel, ARRAYSIZE(wszLevel) - 1, L"%d", m_iUpgradeLevel ); |
|
wszLevel[ ARRAYSIZE(wszLevel)-1 ] = '\0'; |
|
} |
|
|
|
if ( m_iUpgradeLevel >= 3 ) |
|
{ |
|
if ( bShowLevel ) |
|
{ |
|
g_pVGuiLocalize->ConstructString( sDataString, iMaxLenInBytes, g_pVGuiLocalize->Find("#TF_playerid_object_level"), |
|
1, |
|
wszLevel ); |
|
} |
|
return; |
|
} |
|
|
|
wchar_t wszBuilderName[ MAX_PLAYER_NAME_LENGTH ]; |
|
wchar_t wszObjectName[ 32 ]; |
|
wchar_t wszUpgradeProgress[ 32 ]; |
|
|
|
g_pVGuiLocalize->ConvertANSIToUnicode( GetStatusName(), wszObjectName, sizeof(wszObjectName) ); |
|
|
|
C_BasePlayer *pBuilder = GetOwner(); |
|
|
|
if ( pBuilder ) |
|
{ |
|
g_pVGuiLocalize->ConvertANSIToUnicode( pBuilder->GetPlayerName(), wszBuilderName, sizeof(wszBuilderName) ); |
|
} |
|
else |
|
{ |
|
wszBuilderName[0] = '\0'; |
|
} |
|
|
|
// level 1 and 2 show upgrade progress |
|
if ( !IsMiniBuilding() && !IsDisposableBuilding() ) |
|
{ |
|
_snwprintf( wszUpgradeProgress, ARRAYSIZE(wszUpgradeProgress) - 1, L"%d / %d", m_iUpgradeMetal, GetUpgradeMetalRequired() ); |
|
wszUpgradeProgress[ ARRAYSIZE(wszUpgradeProgress)-1 ] = '\0'; |
|
if ( bShowLevel ) |
|
{ |
|
g_pVGuiLocalize->ConstructString( sDataString, iMaxLenInBytes, g_pVGuiLocalize->Find("#TF_playerid_object_upgrading_level"), |
|
2, |
|
wszLevel, |
|
wszUpgradeProgress ); |
|
} |
|
else |
|
{ |
|
g_pVGuiLocalize->ConstructString( sDataString, iMaxLenInBytes, g_pVGuiLocalize->Find("#TF_playerid_object_upgrading"), |
|
1, |
|
wszUpgradeProgress ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int C_BaseObject::GetDisplayPriority( void ) |
|
{ |
|
return GetObjectInfo( GetType() )->m_iDisplayPriority; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
const char *C_BaseObject::GetHudStatusIcon( void ) |
|
{ |
|
return GetObjectInfo( GetType() )->m_pHudStatusIcon; |
|
} |
|
|
|
ConVar cl_obj_fake_alert( "cl_obj_fake_alert", "0", 0, "", true, BUILDING_HUD_ALERT_NONE, true, MAX_BUILDING_HUD_ALERT_LEVEL-1 ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
BuildingHudAlert_t C_BaseObject::GetBuildingAlertLevel( void ) |
|
{ |
|
float flHealthPercent = GetHealth() / GetMaxHealth(); |
|
|
|
BuildingHudAlert_t alertLevel = BUILDING_HUD_ALERT_NONE; |
|
|
|
if ( HasSapper() ) |
|
{ |
|
alertLevel = BUILDING_HUD_ALERT_SAPPER; |
|
} |
|
else if ( !IsBuilding() && flHealthPercent < 0.33 ) |
|
{ |
|
alertLevel = BUILDING_HUD_ALERT_VERY_LOW_HEALTH; |
|
} |
|
else if ( !IsBuilding() && flHealthPercent < 0.66 ) |
|
{ |
|
alertLevel = BUILDING_HUD_ALERT_LOW_HEALTH; |
|
} |
|
|
|
BuildingHudAlert_t iFakeAlert = (BuildingHudAlert_t)cl_obj_fake_alert.GetInt(); |
|
|
|
if ( iFakeAlert > BUILDING_HUD_ALERT_NONE && |
|
iFakeAlert < MAX_BUILDING_HUD_ALERT_LEVEL ) |
|
{ |
|
alertLevel = iFakeAlert; |
|
} |
|
|
|
return alertLevel; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
ShadowType_t C_BaseObject::ShadowCastType( void ) |
|
{ |
|
if ( GetInvisibilityLevel() == 1.f ) |
|
return SHADOWS_NONE; |
|
|
|
return BaseClass::ShadowCastType(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float C_BaseObject::GetInvisibilityLevel( void ) |
|
{ |
|
#ifdef STAGING_ONLY |
|
C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); |
|
C_TFPlayer *pOwner = GetOwner(); |
|
if ( pLocalPlayer && pLocalPlayer->m_Shared.InCond( TF_COND_STEALTHED_PHASE ) && pLocalPlayer != pOwner ) |
|
return 1.f; |
|
#endif // STAGING_ONLY |
|
|
|
return m_flInvisibilityPercent; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::SetInvisibilityLevel( float flValue ) |
|
{ |
|
m_flPrevInvisibilityPercent = m_flInvisibilityPercent; |
|
m_flInvisibilityPercent = clamp( flValue, 0.f, 1.f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: find the anim events that may have started sounds, and stop them. |
|
//----------------------------------------------------------------------------- |
|
void C_BaseObject::StopAnimGeneratedSounds( void ) |
|
{ |
|
MDLCACHE_CRITICAL_SECTION(); |
|
|
|
CStudioHdr *pStudioHdr = GetModelPtr(); |
|
if ( !pStudioHdr ) |
|
return; |
|
|
|
mstudioseqdesc_t &seqdesc = pStudioHdr->pSeqdesc( GetSequence() ); |
|
if ( seqdesc.numevents == 0 ) |
|
return; |
|
|
|
float flCurrentCycle = GetCycle(); |
|
mstudioevent_t *pevent = GetEventIndexForSequence( seqdesc ); |
|
|
|
for (int i = 0; i < (int)seqdesc.numevents; i++) |
|
{ |
|
if ( pevent[i].cycle < flCurrentCycle ) |
|
{ |
|
if ( pevent[i].event == CL_EVENT_SOUND || pevent[i].event == AE_CL_PLAYSOUND ) |
|
{ |
|
StopSound( entindex(), pevent[i].options ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//============================================================================================================ |
|
// POWER PROXY |
|
//============================================================================================================ |
|
class CObjectPowerProxy : public CResultProxy |
|
{ |
|
public: |
|
bool Init( IMaterial *pMaterial, KeyValues *pKeyValues ); |
|
void OnBind( void *pC_BaseEntity ); |
|
|
|
private: |
|
CFloatInput m_Factor; |
|
}; |
|
|
|
bool CObjectPowerProxy::Init( IMaterial *pMaterial, KeyValues *pKeyValues ) |
|
{ |
|
if (!CResultProxy::Init( pMaterial, pKeyValues )) |
|
return false; |
|
|
|
if (!m_Factor.Init( pMaterial, pKeyValues, "scale", 1 )) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
void CObjectPowerProxy::OnBind( void *pRenderable ) |
|
{ |
|
// Find the view angle between the player and this entity.... |
|
IClientRenderable *pRend = (IClientRenderable *)pRenderable; |
|
C_BaseEntity *pEntity = pRend->GetIClientUnknown()->GetBaseEntity(); |
|
C_BaseObject *pObject = dynamic_cast<C_BaseObject*>(pEntity); |
|
if (!pObject) |
|
return; |
|
|
|
SetFloatResult( m_Factor.GetFloat() ); |
|
|
|
if ( ToolsEnabled() ) |
|
{ |
|
ToolFramework_RecordMaterialParams( GetMaterial() ); |
|
} |
|
} |
|
|
|
EXPOSE_INTERFACE( CObjectPowerProxy, IMaterialProxy, "ObjectPower" IMATERIAL_PROXY_INTERFACE_VERSION ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Control screen |
|
//----------------------------------------------------------------------------- |
|
class CBasicControlPanel : public CObjectControlPanel |
|
{ |
|
DECLARE_CLASS( CBasicControlPanel, CObjectControlPanel ); |
|
|
|
public: |
|
CBasicControlPanel( vgui::Panel *parent, const char *panelName ); |
|
}; |
|
|
|
|
|
DECLARE_VGUI_SCREEN_FACTORY( CBasicControlPanel, "basic_control_panel" ); |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Constructor: |
|
//----------------------------------------------------------------------------- |
|
CBasicControlPanel::CBasicControlPanel( vgui::Panel *parent, const char *panelName ) |
|
: BaseClass( parent, "CBasicControlPanel" ) |
|
{ |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Used for spy invisiblity material |
|
//----------------------------------------------------------------------------- |
|
class CBuildingInvisProxy : public CBaseInvisMaterialProxy |
|
{ |
|
public: |
|
virtual void OnBind( C_BaseEntity *pBaseEntity ) OVERRIDE; |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
//----------------------------------------------------------------------------- |
|
void CBuildingInvisProxy::OnBind( C_BaseEntity *pBaseEntity ) |
|
{ |
|
if ( !m_pPercentInvisible ) |
|
return; |
|
|
|
if ( !pBaseEntity->IsBaseObject() ) |
|
return; |
|
|
|
C_BaseObject *pObject = static_cast< C_BaseObject* >( pBaseEntity ); |
|
if ( !pObject ) |
|
return; |
|
|
|
CTFPlayer *pOwner = ToTFPlayer( pObject->GetOwner() ); |
|
if ( !pOwner ) |
|
{ |
|
m_pPercentInvisible->SetFloatValue( 0.0f ); |
|
return; |
|
} |
|
|
|
m_pPercentInvisible->SetFloatValue( pObject->GetInvisibilityLevel() ); |
|
} |
|
|
|
EXPOSE_INTERFACE( CBuildingInvisProxy, IMaterialProxy, "building_invis" IMATERIAL_PROXY_INTERFACE_VERSION );
|
|
|