mirror of
https://github.com/nillerusr/source-engine.git
synced 2024-12-23 14:46:53 +00:00
632 lines
17 KiB
C++
632 lines
17 KiB
C++
//========= Copyright Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "cbase.h"
|
|
#include "filters.h"
|
|
#include "entitylist.h"
|
|
#include "ai_squad.h"
|
|
#include "ai_basenpc.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
// ###################################################################
|
|
// > BaseFilter
|
|
// ###################################################################
|
|
LINK_ENTITY_TO_CLASS(filter_base, CBaseFilter);
|
|
|
|
BEGIN_DATADESC( CBaseFilter )
|
|
|
|
DEFINE_KEYFIELD(m_bNegated, FIELD_BOOLEAN, "Negated"),
|
|
|
|
// Inputs
|
|
DEFINE_INPUTFUNC( FIELD_INPUT, "TestActivator", InputTestActivator ),
|
|
|
|
// Outputs
|
|
DEFINE_OUTPUT( m_OnPass, "OnPass"),
|
|
DEFINE_OUTPUT( m_OnFail, "OnFail"),
|
|
|
|
END_DATADESC()
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool CBaseFilter::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
bool CBaseFilter::PassesFilter( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
bool baseResult = PassesFilterImpl( pCaller, pEntity );
|
|
return (m_bNegated) ? !baseResult : baseResult;
|
|
}
|
|
|
|
|
|
bool CBaseFilter::PassesDamageFilter(const CTakeDamageInfo &info)
|
|
{
|
|
bool baseResult = PassesDamageFilterImpl(info);
|
|
return (m_bNegated) ? !baseResult : baseResult;
|
|
}
|
|
|
|
|
|
bool CBaseFilter::PassesDamageFilterImpl( const CTakeDamageInfo &info )
|
|
{
|
|
return PassesFilterImpl( NULL, info.GetAttacker() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for testing the activator. If the activator passes the
|
|
// filter test, the OnPass output is fired. If not, the OnFail output is fired.
|
|
//-----------------------------------------------------------------------------
|
|
void CBaseFilter::InputTestActivator( inputdata_t &inputdata )
|
|
{
|
|
if ( PassesFilter( inputdata.pCaller, inputdata.pActivator ) )
|
|
{
|
|
m_OnPass.FireOutput( inputdata.pActivator, this );
|
|
}
|
|
else
|
|
{
|
|
m_OnFail.FireOutput( inputdata.pActivator, this );
|
|
}
|
|
}
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterMultiple
|
|
//
|
|
// Allows one to filter through mutiple filters
|
|
// ###################################################################
|
|
#define MAX_FILTERS 5
|
|
enum filter_t
|
|
{
|
|
FILTER_AND,
|
|
FILTER_OR,
|
|
};
|
|
|
|
class CFilterMultiple : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( CFilterMultiple, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
filter_t m_nFilterType;
|
|
string_t m_iFilterName[MAX_FILTERS];
|
|
EHANDLE m_hFilter[MAX_FILTERS];
|
|
|
|
bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity );
|
|
bool PassesDamageFilterImpl(const CTakeDamageInfo &info);
|
|
void Activate(void);
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS(filter_multi, CFilterMultiple);
|
|
|
|
BEGIN_DATADESC( CFilterMultiple )
|
|
|
|
|
|
// Keys
|
|
DEFINE_KEYFIELD(m_nFilterType, FIELD_INTEGER, "FilterType"),
|
|
|
|
// Silence, Classcheck!
|
|
// DEFINE_ARRAY( m_iFilterName, FIELD_STRING, MAX_FILTERS ),
|
|
|
|
DEFINE_KEYFIELD(m_iFilterName[0], FIELD_STRING, "Filter01"),
|
|
DEFINE_KEYFIELD(m_iFilterName[1], FIELD_STRING, "Filter02"),
|
|
DEFINE_KEYFIELD(m_iFilterName[2], FIELD_STRING, "Filter03"),
|
|
DEFINE_KEYFIELD(m_iFilterName[3], FIELD_STRING, "Filter04"),
|
|
DEFINE_KEYFIELD(m_iFilterName[4], FIELD_STRING, "Filter05"),
|
|
DEFINE_ARRAY( m_hFilter, FIELD_EHANDLE, MAX_FILTERS ),
|
|
|
|
END_DATADESC()
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Purpose : Called after all entities have been loaded
|
|
//------------------------------------------------------------------------------
|
|
void CFilterMultiple::Activate( void )
|
|
{
|
|
BaseClass::Activate();
|
|
|
|
// We may reject an entity specified in the array of names, but we want the array of valid filters to be contiguous!
|
|
int nNextFilter = 0;
|
|
|
|
// Get handles to my filter entities
|
|
for ( int i = 0; i < MAX_FILTERS; i++ )
|
|
{
|
|
if ( m_iFilterName[i] != NULL_STRING )
|
|
{
|
|
CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_iFilterName[i] );
|
|
CBaseFilter *pFilter = dynamic_cast<CBaseFilter *>(pEntity);
|
|
if ( pFilter == NULL )
|
|
{
|
|
Warning("filter_multi: Tried to add entity (%s) which is not a filter entity!\n", STRING( m_iFilterName[i] ) );
|
|
continue;
|
|
}
|
|
|
|
// Take this entity and increment out array pointer
|
|
m_hFilter[nNextFilter] = pFilter;
|
|
nNextFilter++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns true if the entity passes our filter, false if not.
|
|
// Input : pEntity - Entity to test.
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterMultiple::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
// Test against each filter
|
|
if (m_nFilterType == FILTER_AND)
|
|
{
|
|
for (int i=0;i<MAX_FILTERS;i++)
|
|
{
|
|
if (m_hFilter[i] != NULL)
|
|
{
|
|
CBaseFilter* pFilter = (CBaseFilter *)(m_hFilter[i].Get());
|
|
if (!pFilter->PassesFilter( pCaller, pEntity ) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else // m_nFilterType == FILTER_OR
|
|
{
|
|
for (int i=0;i<MAX_FILTERS;i++)
|
|
{
|
|
if (m_hFilter[i] != NULL)
|
|
{
|
|
CBaseFilter* pFilter = (CBaseFilter *)(m_hFilter[i].Get());
|
|
if (pFilter->PassesFilter( pCaller, pEntity ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns true if the entity passes our filter, false if not.
|
|
// Input : pEntity - Entity to test.
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterMultiple::PassesDamageFilterImpl(const CTakeDamageInfo &info)
|
|
{
|
|
// Test against each filter
|
|
if (m_nFilterType == FILTER_AND)
|
|
{
|
|
for (int i=0;i<MAX_FILTERS;i++)
|
|
{
|
|
if (m_hFilter[i] != NULL)
|
|
{
|
|
CBaseFilter* pFilter = (CBaseFilter *)(m_hFilter[i].Get());
|
|
if (!pFilter->PassesDamageFilter(info))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else // m_nFilterType == FILTER_OR
|
|
{
|
|
for (int i=0;i<MAX_FILTERS;i++)
|
|
{
|
|
if (m_hFilter[i] != NULL)
|
|
{
|
|
CBaseFilter* pFilter = (CBaseFilter *)(m_hFilter[i].Get());
|
|
if (pFilter->PassesDamageFilter(info))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterName
|
|
// ###################################################################
|
|
class CFilterName : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( CFilterName, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
public:
|
|
string_t m_iFilterName;
|
|
|
|
bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
// special check for !player as GetEntityName for player won't return "!player" as a name
|
|
if (FStrEq(STRING(m_iFilterName), "!player"))
|
|
{
|
|
return pEntity->IsPlayer();
|
|
}
|
|
else
|
|
{
|
|
return pEntity->NameMatches( STRING(m_iFilterName) );
|
|
}
|
|
}
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_activator_name, CFilterName );
|
|
|
|
BEGIN_DATADESC( CFilterName )
|
|
|
|
// Keyfields
|
|
DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ),
|
|
|
|
END_DATADESC()
|
|
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterClass
|
|
// ###################################################################
|
|
class CFilterClass : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( CFilterClass, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
public:
|
|
string_t m_iFilterClass;
|
|
|
|
bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
return pEntity->ClassMatches( STRING(m_iFilterClass) );
|
|
}
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_activator_class, CFilterClass );
|
|
|
|
BEGIN_DATADESC( CFilterClass )
|
|
|
|
// Keyfields
|
|
DEFINE_KEYFIELD( m_iFilterClass, FIELD_STRING, "filterclass" ),
|
|
|
|
END_DATADESC()
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterTeam
|
|
// ###################################################################
|
|
class FilterTeam : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( FilterTeam, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
public:
|
|
int m_iFilterTeam;
|
|
|
|
bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
return ( pEntity->GetTeamNumber() == m_iFilterTeam );
|
|
}
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_activator_team, FilterTeam );
|
|
|
|
BEGIN_DATADESC( FilterTeam )
|
|
|
|
// Keyfields
|
|
DEFINE_KEYFIELD( m_iFilterTeam, FIELD_INTEGER, "filterteam" ),
|
|
|
|
END_DATADESC()
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterMassGreater
|
|
// ###################################################################
|
|
class CFilterMassGreater : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( CFilterMassGreater, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
public:
|
|
float m_fFilterMass;
|
|
|
|
bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
if ( pEntity->VPhysicsGetObject() == NULL )
|
|
return false;
|
|
|
|
return ( pEntity->VPhysicsGetObject()->GetMass() > m_fFilterMass );
|
|
}
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_activator_mass_greater, CFilterMassGreater );
|
|
|
|
BEGIN_DATADESC( CFilterMassGreater )
|
|
|
|
// Keyfields
|
|
DEFINE_KEYFIELD( m_fFilterMass, FIELD_FLOAT, "filtermass" ),
|
|
|
|
END_DATADESC()
|
|
|
|
|
|
// ###################################################################
|
|
// > FilterDamageType
|
|
// ###################################################################
|
|
class FilterDamageType : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( FilterDamageType, CBaseFilter );
|
|
DECLARE_DATADESC();
|
|
|
|
protected:
|
|
|
|
bool PassesFilterImpl(CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
ASSERT( false );
|
|
return true;
|
|
}
|
|
|
|
bool PassesDamageFilterImpl(const CTakeDamageInfo &info)
|
|
{
|
|
return info.GetDamageType() == m_iDamageType;
|
|
}
|
|
|
|
int m_iDamageType;
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_damage_type, FilterDamageType );
|
|
|
|
BEGIN_DATADESC( FilterDamageType )
|
|
|
|
// Keyfields
|
|
DEFINE_KEYFIELD( m_iDamageType, FIELD_INTEGER, "damagetype" ),
|
|
|
|
END_DATADESC()
|
|
|
|
// ###################################################################
|
|
// > CFilterEnemy
|
|
// ###################################################################
|
|
|
|
#define SF_FILTER_ENEMY_NO_LOSE_AQUIRED (1<<0)
|
|
|
|
class CFilterEnemy : public CBaseFilter
|
|
{
|
|
DECLARE_CLASS( CFilterEnemy, CBaseFilter );
|
|
// NOT SAVED
|
|
// m_iszPlayerName
|
|
DECLARE_DATADESC();
|
|
|
|
public:
|
|
|
|
virtual bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity );
|
|
virtual bool PassesDamageFilterImpl( const CTakeDamageInfo &info );
|
|
|
|
private:
|
|
|
|
bool PassesNameFilter( CBaseEntity *pCaller );
|
|
bool PassesProximityFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy );
|
|
bool PassesMobbedFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy );
|
|
|
|
string_t m_iszEnemyName; // Name or classname
|
|
float m_flRadius; // Radius (enemies are acquired at this range)
|
|
float m_flOuterRadius; // Outer radius (enemies are LOST at this range)
|
|
int m_nMaxSquadmatesPerEnemy; // Maximum number of squadmates who may share the same enemy
|
|
string_t m_iszPlayerName; // "!player"
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterEnemy::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity )
|
|
{
|
|
if ( pCaller == NULL || pEntity == NULL )
|
|
return false;
|
|
|
|
// If asked to, we'll never fail to pass an already acquired enemy
|
|
// This allows us to use test criteria to initially pick an enemy, then disregard the test until a new enemy comes along
|
|
if ( HasSpawnFlags( SF_FILTER_ENEMY_NO_LOSE_AQUIRED ) && ( pEntity == pCaller->GetEnemy() ) )
|
|
return true;
|
|
|
|
// This is a little weird, but it's saying that if we're not the entity we're excluding the filter to, then just pass it throughZ
|
|
if ( PassesNameFilter( pEntity ) == false )
|
|
return true;
|
|
|
|
if ( PassesProximityFilter( pCaller, pEntity ) == false )
|
|
return false;
|
|
|
|
// NOTE: This can result in some weird NPC behavior if used improperly
|
|
if ( PassesMobbedFilter( pCaller, pEntity ) == false )
|
|
return false;
|
|
|
|
// The filter has been passed, meaning:
|
|
// - If we wanted all criteria to fail, they have
|
|
// - If we wanted all criteria to succeed, they have
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterEnemy::PassesDamageFilterImpl( const CTakeDamageInfo &info )
|
|
{
|
|
// NOTE: This function has no meaning to this implementation of the filter class!
|
|
Assert( 0 );
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Tests the enemy's name or classname
|
|
// Input : *pEnemy - Entity being assessed
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterEnemy::PassesNameFilter( CBaseEntity *pEnemy )
|
|
{
|
|
// If there is no name specified, we're not using it
|
|
if ( m_iszEnemyName == NULL_STRING )
|
|
return true;
|
|
|
|
// Cache off the special case player name
|
|
if ( m_iszPlayerName == NULL_STRING )
|
|
{
|
|
m_iszPlayerName = FindPooledString( "!player" );
|
|
}
|
|
|
|
if ( m_iszEnemyName == m_iszPlayerName )
|
|
{
|
|
if ( pEnemy->IsPlayer() )
|
|
{
|
|
if ( m_bNegated )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// May be either a targetname or classname
|
|
bool bNameOrClassnameMatches = ( m_iszEnemyName == pEnemy->GetEntityName() || m_iszEnemyName == pEnemy->m_iClassname );
|
|
|
|
// We only leave this code block in a state meaning we've "succeeded" in any context
|
|
if ( m_bNegated )
|
|
{
|
|
// We wanted the names to not match, but they did
|
|
if ( bNameOrClassnameMatches )
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// We wanted them to be the same, but they weren't
|
|
if ( bNameOrClassnameMatches == false )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Tests the enemy's proximity to the caller's position
|
|
// Input : *pCaller - Entity assessing the target
|
|
// *pEnemy - Entity being assessed
|
|
// Output : Returns true if potential enemy passes this filter stage
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterEnemy::PassesProximityFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy )
|
|
{
|
|
// If there is no radius specified, we're not testing it
|
|
if ( m_flRadius <= 0.0f )
|
|
return true;
|
|
|
|
// We test the proximity differently when we've already picked up this enemy before
|
|
bool bAlreadyEnemy = ( pCaller->GetEnemy() == pEnemy );
|
|
|
|
// Get our squared length to the enemy from the caller
|
|
float flDistToEnemySqr = ( pCaller->GetAbsOrigin() - pEnemy->GetAbsOrigin() ).LengthSqr();
|
|
|
|
// Two radii are used to control oscillation between true/false cases
|
|
// The larger radius is either specified or defaulted to be double or half the size of the inner radius
|
|
float flLargerRadius = m_flOuterRadius;
|
|
if ( flLargerRadius == 0 )
|
|
{
|
|
flLargerRadius = ( m_bNegated ) ? (m_flRadius*0.5f) : (m_flRadius*2.0f);
|
|
}
|
|
|
|
float flSmallerRadius = m_flRadius;
|
|
if ( flSmallerRadius > flLargerRadius )
|
|
{
|
|
::V_swap( flLargerRadius, flSmallerRadius );
|
|
}
|
|
|
|
float flDist;
|
|
if ( bAlreadyEnemy )
|
|
{
|
|
flDist = ( m_bNegated ) ? flSmallerRadius : flLargerRadius;
|
|
}
|
|
else
|
|
{
|
|
flDist = ( m_bNegated ) ? flLargerRadius : flSmallerRadius;
|
|
}
|
|
|
|
// Test for success
|
|
if ( flDistToEnemySqr <= (flDist*flDist) )
|
|
{
|
|
// We wanted to fail but didn't
|
|
if ( m_bNegated )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// We wanted to succeed but didn't
|
|
if ( m_bNegated == false )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Attempt to govern how many squad members can target any given entity
|
|
// Input : *pCaller - Entity assessing the target
|
|
// *pEnemy - Entity being assessed
|
|
// Output : Returns true if potential enemy passes this filter stage
|
|
//-----------------------------------------------------------------------------
|
|
bool CFilterEnemy::PassesMobbedFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy )
|
|
{
|
|
// Must be a valid candidate
|
|
CAI_BaseNPC *pNPC = pCaller->MyNPCPointer();
|
|
if ( pNPC == NULL || pNPC->GetSquad() == NULL )
|
|
return true;
|
|
|
|
// Make sure we're checking for this
|
|
if ( m_nMaxSquadmatesPerEnemy <= 0 )
|
|
return true;
|
|
|
|
AISquadIter_t iter;
|
|
int nNumMatchingSquadmates = 0;
|
|
|
|
// Look through our squad members to see how many of them are already mobbing this entity
|
|
for ( CAI_BaseNPC *pSquadMember = pNPC->GetSquad()->GetFirstMember( &iter ); pSquadMember != NULL; pSquadMember = pNPC->GetSquad()->GetNextMember( &iter ) )
|
|
{
|
|
// Disregard ourself
|
|
if ( pSquadMember == pNPC )
|
|
continue;
|
|
|
|
// If the enemies match, count it
|
|
if ( pSquadMember->GetEnemy() == pEnemy )
|
|
{
|
|
nNumMatchingSquadmates++;
|
|
|
|
// If we're at or passed the max we stop
|
|
if ( nNumMatchingSquadmates >= m_nMaxSquadmatesPerEnemy )
|
|
{
|
|
// We wanted to find more than allowed and we did
|
|
if ( m_bNegated )
|
|
return true;
|
|
|
|
// We wanted to be less but we're not
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We wanted to find more than the allowed amount but we didn't
|
|
if ( m_bNegated )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
LINK_ENTITY_TO_CLASS( filter_enemy, CFilterEnemy );
|
|
|
|
BEGIN_DATADESC( CFilterEnemy )
|
|
|
|
DEFINE_KEYFIELD( m_iszEnemyName, FIELD_STRING, "filtername" ),
|
|
DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "filter_radius" ),
|
|
DEFINE_KEYFIELD( m_flOuterRadius, FIELD_FLOAT, "filter_outer_radius" ),
|
|
DEFINE_KEYFIELD( m_nMaxSquadmatesPerEnemy, FIELD_INTEGER, "filter_max_per_enemy" ),
|
|
DEFINE_FIELD( m_iszPlayerName, FIELD_STRING ),
|
|
|
|
END_DATADESC()
|