Xash3D FWGS engine.
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.
 
 
 
 

1695 lines
42 KiB

/*
sv_world.c - world query functions
Copyright (C) 2008 Uncle Mike
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "common.h"
#include "server.h"
#include "const.h"
#include "pm_local.h"
#include "studio.h"
typedef struct moveclip_s
{
vec3_t boxmins, boxmaxs; // enclose the test object along entire move
float *mins, *maxs; // size of the moving object
vec3_t mins2, maxs2; // size when clipping against mosnters
const float *start, *end;
edict_t *passedict;
trace_t trace;
int type; // move type
qboolean ignoretrans;
qboolean monsterclip;
} moveclip_t;
/*
===============================================================================
HULL BOXES
===============================================================================
*/
static hull_t box_hull;
static mclipnode_t box_clipnodes[6];
static mplane_t box_planes[6];
/*
===================
SV_InitBoxHull
Set up the planes and clipnodes so that the six floats of a bounding box
can just be stored out and get a proper hull_t structure.
===================
*/
void SV_InitBoxHull( void )
{
int i, side;
box_hull.clipnodes = box_clipnodes;
box_hull.planes = box_planes;
box_hull.firstclipnode = 0;
box_hull.lastclipnode = 5;
for( i = 0; i < 6; i++ )
{
box_clipnodes[i].planenum = i;
side = i & 1;
box_clipnodes[i].children[side] = CONTENTS_EMPTY;
if( i != 5 ) box_clipnodes[i].children[side^1] = i + 1;
else box_clipnodes[i].children[side^1] = CONTENTS_SOLID;
box_planes[i].type = i>>1;
box_planes[i].normal[i>>1] = 1;
box_planes[i].signbits = 0;
}
}
/*
====================
StudioPlayerBlend
====================
*/
void SV_StudioPlayerBlend( mstudioseqdesc_t *pseqdesc, int *pBlend, float *pPitch )
{
// calc up/down pointing
*pBlend = (*pPitch * 3);
if( *pBlend < pseqdesc->blendstart[0] )
{
*pPitch -= pseqdesc->blendstart[0] / 3.0f;
*pBlend = 0;
}
else if( *pBlend > pseqdesc->blendend[0] )
{
*pPitch -= pseqdesc->blendend[0] / 3.0f;
*pBlend = 255;
}
else
{
if( pseqdesc->blendend[0] - pseqdesc->blendstart[0] < 0.1f ) // catch qc error
*pBlend = 127;
else *pBlend = 255.0f * (*pBlend - pseqdesc->blendstart[0]) / (pseqdesc->blendend[0] - pseqdesc->blendstart[0]);
*pPitch = 0;
}
}
/*
====================
SV_CheckSphereIntersection
check clients only
====================
*/
qboolean SV_CheckSphereIntersection( edict_t *ent, const vec3_t start, const vec3_t end )
{
int i, sequence;
float radiusSquared;
vec3_t traceOrg, traceDir;
studiohdr_t *pstudiohdr;
mstudioseqdesc_t *pseqdesc;
model_t *mod;
if( !FBitSet( ent->v.flags, FL_CLIENT|FL_FAKECLIENT ))
return true;
if(( mod = SV_ModelHandle( ent->v.modelindex )) == NULL )
return true;
if(( pstudiohdr = (studiohdr_t *)Mod_StudioExtradata( mod )) == NULL )
return true;
sequence = ent->v.sequence;
if( sequence < 0 || sequence >= pstudiohdr->numseq )
sequence = 0;
pseqdesc = (mstudioseqdesc_t *)((byte *)pstudiohdr + pstudiohdr->seqindex) + sequence;
VectorCopy( start, traceOrg );
VectorSubtract( end, start, traceDir );
radiusSquared = 0.0f;
for ( i = 0; i < 3; i++ )
radiusSquared += Q_max( fabs( pseqdesc->bbmin[i] ), fabs( pseqdesc->bbmax[i] ));
return SphereIntersect( ent->v.origin, radiusSquared, traceOrg, traceDir );
}
/*
===================
SV_HullForBox
To keep everything totally uniform, bounding boxes are turned into small
BSP trees instead of being compared directly.
===================
*/
hull_t *SV_HullForBox( const vec3_t mins, const vec3_t maxs )
{
box_planes[0].dist = maxs[0];
box_planes[1].dist = mins[0];
box_planes[2].dist = maxs[1];
box_planes[3].dist = mins[1];
box_planes[4].dist = maxs[2];
box_planes[5].dist = mins[2];
return &box_hull;
}
/*
==================
SV_HullForBsp
forcing to select BSP hull
==================
*/
hull_t *SV_HullForBsp( edict_t *ent, const vec3_t mins, const vec3_t maxs, vec3_t offset )
{
hull_t *hull;
model_t *model;
vec3_t size;
if( svgame.physFuncs.SV_HullForBsp != NULL )
{
hull = svgame.physFuncs.SV_HullForBsp( ent, mins, maxs, offset );
if( hull ) return hull;
}
// decide which clipping hull to use, based on the size
model = SV_ModelHandle( ent->v.modelindex );
if( !model || model->type != mod_brush )
Host_Error( "Entity %i (%s) SOLID_BSP with a non bsp model %s\n", NUM_FOR_EDICT( ent ), SV_ClassName( ent ), STRING( ent->v.model ));
VectorSubtract( maxs, mins, size );
#ifdef RANDOM_HULL_NULLIZATION
// author: The FiEctro
hull = &model->hulls[COM_RandomLong( 0, 0 )];
#endif
// g-cont: find a better method to detect quake-maps?
if( FBitSet( world.flags, FWORLD_SKYSPHERE ))
{
// alternate hull select for quake maps
if( size[0] < 3.0f || ent->v.solid == SOLID_PORTAL )
hull = &model->hulls[0];
else if( size[0] <= 32.0f )
hull = &model->hulls[1];
else hull = &model->hulls[2];
VectorSubtract( hull->clip_mins, mins, offset );
}
else
{
if( size[0] <= 8.0f || ent->v.solid == SOLID_PORTAL )
{
hull = &model->hulls[0];
VectorCopy( hull->clip_mins, offset );
}
else
{
if( size[0] <= 36.0f )
{
if( size[2] <= 36.0f )
hull = &model->hulls[3];
else hull = &model->hulls[1];
}
else hull = &model->hulls[2];
VectorSubtract( hull->clip_mins, mins, offset );
}
}
VectorAdd( offset, ent->v.origin, offset );
return hull;
}
/*
================
SV_HullForEntity
Returns a hull that can be used for testing or clipping an object of mins/maxs
size.
Offset is filled in to contain the adjustment that must be added to the
testing object's origin to get a point to use with the returned hull.
================
*/
hull_t *SV_HullForEntity( edict_t *ent, vec3_t mins, vec3_t maxs, vec3_t offset )
{
hull_t *hull;
vec3_t hullmins, hullmaxs;
if( ent->v.solid == SOLID_BSP || ent->v.solid == SOLID_PORTAL )
{
if( ent->v.solid != SOLID_PORTAL )
{
if( ent->v.movetype != MOVETYPE_PUSH && ent->v.movetype != MOVETYPE_PUSHSTEP )
Host_Error( "'%s' has SOLID_BSP without MOVETYPE_PUSH or MOVETYPE_PUSHSTEP\n", SV_ClassName( ent ));
}
hull = SV_HullForBsp( ent, mins, maxs, offset );
}
else
{
// create a temp hull from bounding box sizes
VectorSubtract( ent->v.mins, maxs, hullmins );
VectorSubtract( ent->v.maxs, mins, hullmaxs );
hull = SV_HullForBox( hullmins, hullmaxs );
VectorCopy( ent->v.origin, offset );
}
return hull;
}
/*
====================
SV_HullForStudioModel
====================
*/
hull_t *SV_HullForStudioModel( edict_t *ent, vec3_t mins, vec3_t maxs, vec3_t offset, int *numhitboxes )
{
qboolean useComplexHull;
float scale = 0.5f;
hull_t *hull = NULL;
vec3_t size;
model_t *mod;
if(( mod = SV_ModelHandle( ent->v.modelindex )) == NULL )
{
*numhitboxes = 1;
return SV_HullForEntity( ent, mins, maxs, offset );
}
VectorSubtract( maxs, mins, size );
useComplexHull = false;
if( VectorIsNull( size ) && !FBitSet( svgame.globals->trace_flags, FTRACE_SIMPLEBOX ))
{
useComplexHull = true;
if( FBitSet( ent->v.flags, FL_CLIENT|FL_FAKECLIENT ))
{
if( sv_clienttrace.value == 0.0f )
{
// so no way to trace studiomodels by hitboxes
// use bbox instead
useComplexHull = false;
}
else
{
scale = sv_clienttrace.value * 0.5f;
VectorSet( size, 1.0f, 1.0f, 1.0f );
}
}
}
if( FBitSet( mod->flags, STUDIO_TRACE_HITBOX ) || useComplexHull )
{
VectorScale( size, scale, size );
VectorClear( offset );
if( FBitSet( ent->v.flags, FL_CLIENT|FL_FAKECLIENT ))
{
studiohdr_t *pstudio;
mstudioseqdesc_t *pseqdesc;
byte controller[4];
byte blending[2];
vec3_t angles;
int iBlend;
pstudio = Mod_StudioExtradata( mod );
pseqdesc = (mstudioseqdesc_t *)((byte *)pstudio + pstudio->seqindex) + ent->v.sequence;
VectorCopy( ent->v.angles, angles );
SV_StudioPlayerBlend( pseqdesc, &iBlend, &angles[PITCH] );
controller[0] = controller[1] = 0x7F;
controller[2] = controller[3] = 0x7F;
blending[0] = (byte)iBlend;
blending[1] = 0;
hull = Mod_HullForStudio( mod, ent->v.frame, ent->v.sequence, angles, ent->v.origin, size, controller, blending, numhitboxes, ent );
}
else
{
hull = Mod_HullForStudio( mod, ent->v.frame, ent->v.sequence, ent->v.angles, ent->v.origin, size, ent->v.controller, ent->v.blending, numhitboxes, ent );
}
}
if( hull ) return hull;
*numhitboxes = 1;
return SV_HullForEntity( ent, mins, maxs, offset );
}
/*
===============================================================================
ENTITY AREA CHECKING
===============================================================================
*/
static int iTouchLinkSemaphore = 0; // prevent recursion when SV_TouchLinks is active
areanode_t sv_areanodes[AREA_NODES];
static int sv_numareanodes;
/*
===============
SV_CreateAreaNode
builds a uniformly subdivided tree for the given world size
===============
*/
areanode_t *SV_CreateAreaNode( int depth, vec3_t mins, vec3_t maxs )
{
areanode_t *anode;
vec3_t size;
vec3_t mins1, maxs1;
vec3_t mins2, maxs2;
anode = &sv_areanodes[sv_numareanodes++];
ClearLink( &anode->trigger_edicts );
ClearLink( &anode->solid_edicts );
ClearLink( &anode->portal_edicts );
if( depth == AREA_DEPTH )
{
anode->axis = -1;
anode->children[0] = anode->children[1] = NULL;
return anode;
}
VectorSubtract( maxs, mins, size );
if( size[0] > size[1] )
anode->axis = 0;
else anode->axis = 1;
anode->dist = 0.5f * ( maxs[anode->axis] + mins[anode->axis] );
VectorCopy( mins, mins1 );
VectorCopy( mins, mins2 );
VectorCopy( maxs, maxs1 );
VectorCopy( maxs, maxs2 );
maxs1[anode->axis] = mins2[anode->axis] = anode->dist;
anode->children[0] = SV_CreateAreaNode( depth+1, mins2, maxs2 );
anode->children[1] = SV_CreateAreaNode( depth+1, mins1, maxs1 );
return anode;
}
/*
===============
SV_ClearWorld
===============
*/
void SV_ClearWorld( void )
{
int i;
SV_InitBoxHull(); // for box testing
// clear lightstyles
for( i = 0; i < MAX_LIGHTSTYLES; i++ )
{
sv.lightstyles[i].value = 256.0f;
sv.lightstyles[i].time = 0.0f;
}
memset( sv_areanodes, 0, sizeof( sv_areanodes ));
iTouchLinkSemaphore = 0;
sv_numareanodes = 0;
SV_CreateAreaNode( 0, sv.worldmodel->mins, sv.worldmodel->maxs );
}
/*
===============
SV_UnlinkEdict
===============
*/
void SV_UnlinkEdict( edict_t *ent )
{
// not linked in anywhere
if( !ent->area.prev ) return;
RemoveLink( &ent->area );
ent->area.prev = NULL;
ent->area.next = NULL;
}
/*
====================
SV_TouchLinks
====================
*/
void SV_TouchLinks( edict_t *ent, areanode_t *node )
{
link_t *l, *next;
edict_t *touch;
hull_t *hull;
vec3_t test, offset;
model_t *mod;
// touch linked edicts
for( l = node->trigger_edicts.next; l != &node->trigger_edicts; l = next )
{
next = l->next;
touch = EDICT_FROM_AREA( l );
if( svgame.physFuncs.SV_TriggerTouch != NULL )
{
// user dll can override trigger checking (Xash3D extension)
if( !svgame.physFuncs.SV_TriggerTouch( ent, touch ))
continue;
}
else
{
if( touch == ent || touch->v.solid != SOLID_TRIGGER ) // disabled ?
continue;
if( touch->v.groupinfo && ent->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, ent->v.groupinfo ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, ent->v.groupinfo ))
continue;
}
if( !BoundsIntersect( ent->v.absmin, ent->v.absmax, touch->v.absmin, touch->v.absmax ))
continue;
mod = SV_ModelHandle( touch->v.modelindex );
// check brush triggers accuracy
if( mod && mod->type == mod_brush )
{
// force to select bsp-hull
hull = SV_HullForBsp( touch, ent->v.mins, ent->v.maxs, offset );
// support for rotational triggers
if( FBitSet( mod->flags, MODEL_HAS_ORIGIN ) && !VectorIsNull( touch->v.angles ))
{
matrix4x4 matrix;
Matrix4x4_CreateFromEntity( matrix, touch->v.angles, offset, 1.0f );
Matrix4x4_VectorITransform( matrix, ent->v.origin, test );
}
else
{
// offset the test point appropriately for this hull.
VectorSubtract( ent->v.origin, offset, test );
}
// test hull for intersection with this model
if( PM_HullPointContents( hull, hull->firstclipnode, test ) != CONTENTS_SOLID )
continue;
}
}
// never touch the triggers when "playersonly" is active
if( !sv.playersonly )
{
svgame.globals->time = sv.time;
svgame.dllFuncs.pfnTouch( touch, ent );
}
}
// recurse down both sides
if( node->axis == -1 ) return;
if( ent->v.absmax[node->axis] > node->dist )
SV_TouchLinks( ent, node->children[0] );
if( ent->v.absmin[node->axis] < node->dist )
SV_TouchLinks( ent, node->children[1] );
}
/*
===============
SV_FindTouchedLeafs
===============
*/
void SV_FindTouchedLeafs( edict_t *ent, mnode_t *node, int *headnode )
{
int sides;
mleaf_t *leaf;
if( node->contents == CONTENTS_SOLID )
return;
// add an efrag if the node is a leaf
if( node->contents < 0 )
{
if( ent->num_leafs > ( MAX_ENT_LEAFS - 1 ))
{
// continue counting leafs,
// so we know how many it's overrun
ent->num_leafs = (MAX_ENT_LEAFS + 1);
}
else
{
leaf = (mleaf_t *)node;
ent->leafnums[ent->num_leafs] = leaf->cluster;
ent->num_leafs++;
}
return;
}
// NODE_MIXED
sides = BOX_ON_PLANE_SIDE( ent->v.absmin, ent->v.absmax, node->plane );
if(( sides == 3 ) && ( *headnode == -1 ))
*headnode = node - sv.worldmodel->nodes;
// recurse down the contacted sides
if( sides & 1 ) SV_FindTouchedLeafs( ent, node->children[0], headnode );
if( sides & 2 ) SV_FindTouchedLeafs( ent, node->children[1], headnode );
}
/*
===============
SV_LinkEdict
===============
*/
void SV_LinkEdict( edict_t *ent, qboolean touch_triggers )
{
areanode_t *node;
int headnode;
if( ent->area.prev ) SV_UnlinkEdict( ent ); // unlink from old position
if( ent == svgame.edicts ) return; // don't add the world
if( !SV_IsValidEdict( ent )) return; // never add freed ents
// set the abs box
svgame.dllFuncs.pfnSetAbsBox( ent );
if( ent->v.movetype == MOVETYPE_FOLLOW && SV_IsValidEdict( ent->v.aiment ))
{
memcpy( ent->leafnums, ent->v.aiment->leafnums, sizeof( ent->leafnums ));
ent->num_leafs = ent->v.aiment->num_leafs;
ent->headnode = ent->v.aiment->headnode;
}
else
{
// link to PVS leafs
ent->num_leafs = 0;
ent->headnode = -1;
headnode = -1;
if( ent->v.modelindex )
SV_FindTouchedLeafs( ent, sv.worldmodel->nodes, &headnode );
if( ent->num_leafs > MAX_ENT_LEAFS )
{
memset( ent->leafnums, -1, sizeof( ent->leafnums ));
ent->num_leafs = 0; // so we use headnode instead
ent->headnode = headnode;
}
}
// ignore non-solid bodies
if( ent->v.solid == SOLID_NOT && ent->v.skin >= CONTENTS_EMPTY )
return;
// find the first node that the ent's box crosses
node = sv_areanodes;
while( 1 )
{
if( node->axis == -1 ) break;
if( ent->v.absmin[node->axis] > node->dist )
node = node->children[0];
else if( ent->v.absmax[node->axis] < node->dist )
node = node->children[1];
else break; // crosses the node
}
// link it in
if( ent->v.solid == SOLID_TRIGGER )
InsertLinkBefore( &ent->area, &node->trigger_edicts );
else if( ent->v.solid == SOLID_PORTAL )
InsertLinkBefore( &ent->area, &node->portal_edicts );
else InsertLinkBefore( &ent->area, &node->solid_edicts );
if( touch_triggers && !iTouchLinkSemaphore )
{
iTouchLinkSemaphore = true;
SV_TouchLinks( ent, sv_areanodes );
iTouchLinkSemaphore = false;
}
}
/*
===============================================================================
POINT TESTING IN HULLS
===============================================================================
*/
void SV_WaterLinks( const vec3_t origin, int *pCont, areanode_t *node )
{
link_t *l, *next;
edict_t *touch;
hull_t *hull;
vec3_t test, offset;
model_t *mod;
// get water edicts
for( l = node->solid_edicts.next; l != &node->solid_edicts; l = next )
{
next = l->next;
touch = EDICT_FROM_AREA( l );
if( touch->v.solid != SOLID_NOT ) // disabled ?
continue;
if( touch->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, svs.groupmask ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, svs.groupmask ))
continue;
}
mod = SV_ModelHandle( touch->v.modelindex );
// only brushes can have special contents
if( !mod || mod->type != mod_brush )
continue;
if( !BoundsIntersect( origin, origin, touch->v.absmin, touch->v.absmax ))
continue;
// check water brushes accuracy
hull = SV_HullForBsp( touch, vec3_origin, vec3_origin, offset );
// support for rotational water
if( FBitSet( mod->flags, MODEL_HAS_ORIGIN ) && !VectorIsNull( touch->v.angles ))
{
matrix4x4 matrix;
Matrix4x4_CreateFromEntity( matrix, touch->v.angles, offset, 1.0f );
Matrix4x4_VectorITransform( matrix, origin, test );
}
else
{
// offset the test point appropriately for this hull.
VectorSubtract( origin, offset, test );
}
// test hull for intersection with this model
if( PM_HullPointContents( hull, hull->firstclipnode, test ) == CONTENTS_EMPTY )
continue;
// compare contents ranking
if( RankForContents( touch->v.skin ) > RankForContents( *pCont ))
*pCont = touch->v.skin; // new content has more priority
}
// recurse down both sides
if( node->axis == -1 ) return;
if( origin[node->axis] > node->dist )
SV_WaterLinks( origin, pCont, node->children[0] );
if( origin[node->axis] < node->dist )
SV_WaterLinks( origin, pCont, node->children[1] );
}
/*
=============
SV_TruePointContents
=============
*/
int SV_TruePointContents( const vec3_t p )
{
int cont;
// sanity check
if( !p ) return CONTENTS_NONE;
// get base contents from world
cont = PM_HullPointContents( &sv.worldmodel->hulls[0], 0, p );
// check all water entities
SV_WaterLinks( p, &cont, sv_areanodes );
return cont;
}
/*
=============
SV_PointContents
=============
*/
int GAME_EXPORT SV_PointContents( const vec3_t p )
{
int cont = SV_TruePointContents( p );
if( cont <= CONTENTS_CURRENT_0 && cont >= CONTENTS_CURRENT_DOWN )
cont = CONTENTS_WATER;
return cont;
}
//===========================================================================
/*
============
SV_TestEntityPosition
returns true if the entity is in solid currently
============
*/
qboolean SV_TestEntityPosition( edict_t *ent, edict_t *blocker )
{
qboolean monsterClip = FBitSet( ent->v.flags, FL_MONSTERCLIP ) ? true : false;
trace_t trace;
if( FBitSet( ent->v.flags, FL_CLIENT|FL_FAKECLIENT ))
{
// to avoid falling through tracktrain update client mins\maxs here
if( FBitSet( ent->v.flags, FL_DUCKING ))
SV_SetMinMaxSize( ent, svgame.pmove->player_mins[1], svgame.pmove->player_maxs[1], true );
else SV_SetMinMaxSize( ent, svgame.pmove->player_mins[0], svgame.pmove->player_maxs[0], true );
}
trace = SV_Move( ent->v.origin, ent->v.mins, ent->v.maxs, ent->v.origin, MOVE_NORMAL, ent, monsterClip );
if( SV_IsValidEdict( blocker ) && SV_IsValidEdict( trace.ent ))
{
if( trace.ent->v.movetype == MOVETYPE_PUSH || trace.ent == blocker )
return trace.startsolid;
return false;
}
return trace.startsolid;
}
/*
===============================================================================
LINE TESTING IN HULLS
===============================================================================
*/
/*
==================
SV_ClipMoveToEntity
Handles selection or creation of a clipping hull, and offseting (and
eventually rotation) of the end points
==================
*/
void SV_ClipMoveToEntity( edict_t *ent, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, trace_t *trace )
{
hull_t *hull;
model_t *model;
vec3_t start_l, end_l;
vec3_t offset, temp;
int last_hitgroup;
trace_t trace_hitbox;
int i, j, hullcount;
qboolean rotated, transform_bbox;
matrix4x4 matrix;
PM_InitTrace( trace, end );
model = SV_ModelHandle( ent->v.modelindex );
if( model && model->type == mod_studio )
{
hull = SV_HullForStudioModel( ent, mins, maxs, offset, &hullcount );
}
else
{
hull = SV_HullForEntity( ent, mins, maxs, offset );
hullcount = 1;
}
// rotate start and end into the models frame of reference
if(( ent->v.solid == SOLID_BSP || ent->v.solid == SOLID_PORTAL ) && !VectorIsNull( ent->v.angles ))
rotated = true;
else rotated = false;
if( FBitSet( host.features, ENGINE_PHYSICS_PUSHER_EXT ))
{
// keep untransformed bbox less than 45 degress or train on subtransit.bsp will stop working
if(( check_angles( ent->v.angles[0] ) || check_angles( ent->v.angles[2] )) && !VectorIsNull( mins ))
transform_bbox = true;
else transform_bbox = false;
}
else transform_bbox = false;
if( rotated )
{
vec3_t out_mins, out_maxs;
if( transform_bbox )
Matrix4x4_CreateFromEntity( matrix, ent->v.angles, ent->v.origin, 1.0f );
else Matrix4x4_CreateFromEntity( matrix, ent->v.angles, offset, 1.0f );
Matrix4x4_VectorITransform( matrix, start, start_l );
Matrix4x4_VectorITransform( matrix, end, end_l );
if( transform_bbox )
{
World_TransformAABB( matrix, mins, maxs, out_mins, out_maxs );
VectorSubtract( hull->clip_mins, out_mins, offset ); // calc new local offset
for( j = 0; j < 3; j++ )
{
if( start_l[j] >= 0.0f )
start_l[j] -= offset[j];
else start_l[j] += offset[j];
if( end_l[j] >= 0.0f )
end_l[j] -= offset[j];
else end_l[j] += offset[j];
}
}
}
else
{
VectorSubtract( start, offset, start_l );
VectorSubtract( end, offset, end_l );
}
if( hullcount == 1 )
{
PM_RecursiveHullCheck( hull, hull->firstclipnode, 0.0f, 1.0f, start_l, end_l, (pmtrace_t *)trace );
}
else
{
last_hitgroup = 0;
for( i = 0; i < hullcount; i++ )
{
PM_InitTrace( &trace_hitbox, end );
PM_RecursiveHullCheck( &hull[i], hull[i].firstclipnode, 0.0f, 1.0f, start_l, end_l, (pmtrace_t *)&trace_hitbox );
if( i == 0 || trace_hitbox.allsolid || trace_hitbox.startsolid || trace_hitbox.fraction < trace->fraction )
{
if( trace->startsolid )
{
*trace = trace_hitbox;
trace->startsolid = true;
}
else *trace = trace_hitbox;
last_hitgroup = i;
}
}
trace->hitgroup = Mod_HitgroupForStudioHull( last_hitgroup );
}
if( trace->fraction != 1.0f )
{
// compute endpos (generic case)
VectorLerp( start, trace->fraction, end, trace->endpos );
if( rotated )
{
// transform plane
VectorCopy( trace->plane.normal, temp );
Matrix4x4_TransformPositivePlane( matrix, temp, trace->plane.dist, trace->plane.normal, &trace->plane.dist );
}
else
{
trace->plane.dist = DotProduct( trace->endpos, trace->plane.normal );
}
}
if( trace->fraction < 1.0f || trace->startsolid )
trace->ent = ent;
}
/*
==================
SV_PortalCSG
a portal is flush with a world surface behind it. this causes problems. namely that we can't pass through the portal plane
if the bsp behind it prevents out origin from getting through. so if the trace was clipped and ended infront of the portal,
continue the trace to the edges of the portal cutout instead.
==================
*/
void SV_PortalCSG( edict_t *portal, const vec3_t trace_mins, const vec3_t trace_maxs, const vec3_t start, const vec3_t end, trace_t *trace )
{
vec4_t planes[6]; //far, near, right, left, up, down
int plane, k;
vec3_t worldpos;
float bestfrac;
int hitplane;
model_t *model;
float portalradius;
// only run this code if we impacted on the portal's parent.
if( trace->fraction == 1.0f && !trace->startsolid )
return;
// decide which clipping hull to use, based on the size
model = SV_ModelHandle( portal->v.modelindex );
if( !model || model->type != mod_brush )
return;
// make sure we use a sane valid position.
if( trace->startsolid ) VectorCopy( start, worldpos );
else VectorCopy( trace->endpos, worldpos );
// determine the csg area. normals should be facing in
AngleVectors( portal->v.angles, planes[1], planes[3], planes[5] );
VectorNegate(planes[1], planes[0]);
VectorNegate(planes[3], planes[2]);
VectorNegate(planes[5], planes[4]);
portalradius = model->radius * 0.5f;
planes[0][3] = DotProduct( portal->v.origin, planes[0] ) - (4.0f / 32.0f);
planes[1][3] = DotProduct( portal->v.origin, planes[1] ) - (4.0f / 32.0f); //an epsilon beyond the portal
planes[2][3] = DotProduct( portal->v.origin, planes[2] ) - portalradius;
planes[3][3] = DotProduct( portal->v.origin, planes[3] ) - portalradius;
planes[4][3] = DotProduct( portal->v.origin, planes[4] ) - portalradius;
planes[5][3] = DotProduct( portal->v.origin, planes[5] ) - portalradius;
// if we're actually inside the csg region
for( plane = 0; plane < 6; plane++ )
{
float d = DotProduct( worldpos, planes[plane] );
vec3_t nearest;
for( k = 0; k < 3; k++ )
nearest[k] = (planes[plane][k]>=0) ? trace_maxs[k] : trace_mins[k];
// front plane gets further away with side
if( !plane )
{
planes[plane][3] -= DotProduct( nearest, planes[plane] );
}
else if( plane > 1 )
{
// side planes get nearer with size
planes[plane][3] += 24; // DotProduct( nearest, planes[plane] );
}
if( d - planes[plane][3] >= 0 )
continue; // endpos is inside
else return; // end is already outside
}
// yup, we're inside, the trace shouldn't end where it actually did
bestfrac = 1;
hitplane = -1;
for( plane = 0; plane < 6; plane++ )
{
float ds = DotProduct( start, planes[plane] ) - planes[plane][3];
float de = DotProduct( end, planes[plane] ) - planes[plane][3];
float frac;
if( ds >= 0 && de < 0 )
{
frac = (ds) / (ds - de);
if( frac < bestfrac )
{
if( frac < 0 )
frac = 0;
bestfrac = frac;
hitplane = plane;
}
}
}
trace->startsolid = trace->allsolid = false;
// if we cross the front of the portal, don't shorten the trace,
// that will artificially clip us
if( hitplane == 0 && trace->fraction > bestfrac )
return;
// okay, elongate to clip to the portal hole properly.
VectorLerp( start, bestfrac, end, trace->endpos );
trace->fraction = bestfrac;
if( hitplane >= 0 )
{
VectorCopy( planes[hitplane], trace->plane.normal );
trace->plane.dist = planes[hitplane][3];
if( hitplane == 1 ) trace->ent = portal;
}
}
/*
==================
SV_CustomClipMoveToEntity
A part of physics engine implementation
or custom physics implementation
==================
*/
void SV_CustomClipMoveToEntity( edict_t *ent, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, trace_t *trace )
{
// initialize custom trace
PM_InitTrace( trace, end );
if( svgame.physFuncs.ClipMoveToEntity != NULL )
{
// do custom sweep test
svgame.physFuncs.ClipMoveToEntity( ent, start, mins, maxs, end, trace );
}
else
{
// function is missed, so we didn't hit anything
trace->allsolid = false;
}
}
/*
====================
SV_ClipToEntity
generic clip function
====================
*/
static qboolean SV_ClipToEntity( edict_t *touch, moveclip_t *clip )
{
trace_t trace;
model_t *mod;
if( touch->v.groupinfo && SV_IsValidEdict( clip->passedict ) && clip->passedict->v.groupinfo != 0 )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, clip->passedict->v.groupinfo ))
return true;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, clip->passedict->v.groupinfo ))
return true;
}
if( touch == clip->passedict || touch->v.solid == SOLID_NOT )
return true;
if( touch->v.solid == SOLID_TRIGGER )
Host_Error( "trigger in clipping list\n" );
// custom user filter
if( svgame.dllFuncs2.pfnShouldCollide )
{
if( !svgame.dllFuncs2.pfnShouldCollide( touch, clip->passedict ))
return true;
}
// monsterclip filter (solid custom is a static or dynamic bodies)
if( touch->v.solid == SOLID_BSP || touch->v.solid == SOLID_CUSTOM )
{
// func_monsterclip works only with monsters that have same flag!
if( FBitSet( touch->v.flags, FL_MONSTERCLIP ) && !clip->monsterclip )
return true;
}
else
{
// ignore all monsters but pushables
if( clip->type == MOVE_NOMONSTERS && touch->v.movetype != MOVETYPE_PUSHSTEP )
return true;
}
mod = SV_ModelHandle( touch->v.modelindex );
if( mod && mod->type == mod_brush && clip->ignoretrans )
{
// we ignore brushes with rendermode != kRenderNormal and without FL_WORLDBRUSH set
if( touch->v.rendermode != kRenderNormal && !FBitSet( touch->v.flags, FL_WORLDBRUSH ))
return true;
}
if( !BoundsIntersect( clip->boxmins, clip->boxmaxs, touch->v.absmin, touch->v.absmax ))
return true;
// aditional check to intersects clients with sphere
if( touch->v.solid != SOLID_SLIDEBOX && !SV_CheckSphereIntersection( touch, clip->start, clip->end ))
return true;
// Xash3D extension
if( SV_IsValidEdict( clip->passedict ) && clip->passedict->v.solid == SOLID_TRIGGER )
{
// never collide items and player (because call "give" always stuck item in player
// and total trace returns fail (old half-life bug)
// items touch should be done in SV_TouchLinks not here
if( FBitSet( touch->v.flags, FL_CLIENT|FL_FAKECLIENT ))
return true;
}
// g-cont. make sure what size is really zero - check all the components
if( SV_IsValidEdict( clip->passedict ) && !VectorIsNull( clip->passedict->v.size ) && VectorIsNull( touch->v.size ))
return true; // points never interact
// might intersect, so do an exact clip
if( clip->trace.allsolid ) return false;
if( SV_IsValidEdict( clip->passedict ))
{
if( touch->v.owner == clip->passedict )
return true; // don't clip against own missiles
if( clip->passedict->v.owner == touch )
return true; // don't clip against owner
}
// make sure we don't hit the world if we're inside the portal
if( touch->v.solid == SOLID_PORTAL )
SV_PortalCSG( touch, clip->mins, clip->maxs, clip->start, clip->end, &clip->trace );
if( touch->v.solid == SOLID_CUSTOM )
SV_CustomClipMoveToEntity( touch, clip->start, clip->mins, clip->maxs, clip->end, &trace );
else if( FBitSet( touch->v.flags, FL_MONSTER ))
SV_ClipMoveToEntity( touch, clip->start, clip->mins2, clip->maxs2, clip->end, &trace );
else SV_ClipMoveToEntity( touch, clip->start, clip->mins, clip->maxs, clip->end, &trace );
clip->trace = World_CombineTraces( &clip->trace, &trace, touch );
return true;
}
/*
====================
SV_ClipToLinks
Mins and maxs enclose the entire area swept by the move
====================
*/
static void SV_ClipToLinks( areanode_t *node, moveclip_t *clip )
{
link_t *l, *next;
edict_t *touch;
// touch linked edicts
for( l = node->solid_edicts.next; l != &node->solid_edicts; l = next )
{
next = l->next;
touch = EDICT_FROM_AREA( l );
if( !SV_ClipToEntity( touch, clip ))
return; // trace.allsoild
}
// recurse down both sides
if( node->axis == -1 ) return;
if( clip->boxmaxs[node->axis] > node->dist )
SV_ClipToLinks( node->children[0], clip );
if( clip->boxmins[node->axis] < node->dist )
SV_ClipToLinks( node->children[1], clip );
}
/*
====================
SV_ClipToPortals
Mins and maxs enclose the entire area swept by the move
====================
*/
static void SV_ClipToPortals( areanode_t *node, moveclip_t *clip )
{
link_t *l, *next;
edict_t *touch;
// touch linked edicts
for( l = node->portal_edicts.next; l != &node->portal_edicts; l = next )
{
next = l->next;
touch = EDICT_FROM_AREA( l );
if( !SV_ClipToEntity( touch, clip ))
return; // trace.allsoild
}
// recurse down both sides
if( node->axis == -1 ) return;
if( clip->boxmaxs[node->axis] > node->dist )
SV_ClipToPortals( node->children[0], clip );
if( clip->boxmins[node->axis] < node->dist )
SV_ClipToPortals( node->children[1], clip );
}
/*
====================
SV_ClipToWorldBrush
Mins and maxs enclose the entire area swept by the move
====================
*/
void SV_ClipToWorldBrush( areanode_t *node, moveclip_t *clip )
{
link_t *l, *next;
edict_t *touch;
trace_t trace;
for( l = node->solid_edicts.next; l != &node->solid_edicts; l = next )
{
next = l->next;
touch = EDICT_FROM_AREA( l );
if( touch->v.solid != SOLID_BSP || touch == clip->passedict || !( touch->v.flags & FL_WORLDBRUSH ))
continue;
if( !BoundsIntersect( clip->boxmins, clip->boxmaxs, touch->v.absmin, touch->v.absmax ))
continue;
if( clip->trace.allsolid ) return;
SV_ClipMoveToEntity( touch, clip->start, clip->mins, clip->maxs, clip->end, &trace );
clip->trace = World_CombineTraces( &clip->trace, &trace, touch );
}
// recurse down both sides
if( node->axis == -1 ) return;
if( clip->boxmaxs[node->axis] > node->dist )
SV_ClipToWorldBrush( node->children[0], clip );
if( clip->boxmins[node->axis] < node->dist )
SV_ClipToWorldBrush( node->children[1], clip );
}
/*
==================
SV_Move
==================
*/
trace_t SV_Move( const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int type, edict_t *e, qboolean monsterclip )
{
moveclip_t clip;
vec3_t trace_endpos;
float trace_fraction;
memset( &clip, 0, sizeof( moveclip_t ));
SV_ClipMoveToEntity( EDICT_NUM( 0 ), start, mins, maxs, end, &clip.trace );
if( clip.trace.fraction != 0.0f )
{
VectorCopy( clip.trace.endpos, trace_endpos );
trace_fraction = clip.trace.fraction;
clip.trace.fraction = 1.0f;
clip.start = start;
clip.end = trace_endpos;
clip.type = (type & 0xFF);
clip.ignoretrans = type >> 8;
clip.monsterclip = false;
clip.passedict = (e) ? e : EDICT_NUM( 0 );
clip.mins = mins;
clip.maxs = maxs;
if( monsterclip && !FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE ))
clip.monsterclip = true;
if( clip.type == MOVE_MISSILE )
{
VectorSet( clip.mins2, -15.0f, -15.0f, -15.0f );
VectorSet( clip.maxs2, 15.0f, 15.0f, 15.0f );
}
else
{
VectorCopy( mins, clip.mins2 );
VectorCopy( maxs, clip.maxs2 );
}
World_MoveBounds( start, clip.mins2, clip.maxs2, trace_endpos, clip.boxmins, clip.boxmaxs );
SV_ClipToLinks( sv_areanodes, &clip );
SV_ClipToPortals( sv_areanodes, &clip );
clip.trace.fraction *= trace_fraction;
svgame.globals->trace_ent = clip.trace.ent;
}
SV_CopyTraceToGlobal( &clip.trace );
return clip.trace;
}
trace_t SV_MoveNormal( const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int type, edict_t *e )
{
return SV_Move( start, mins, maxs, end, type, e, false );
}
/*
==================
SV_MoveNoEnts
==================
*/
trace_t SV_MoveNoEnts( const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int type, edict_t *e )
{
moveclip_t clip;
vec3_t trace_endpos;
float trace_fraction;
memset( &clip, 0, sizeof( moveclip_t ));
SV_ClipMoveToEntity( EDICT_NUM( 0 ), start, mins, maxs, end, &clip.trace );
if( clip.trace.fraction != 0.0f )
{
VectorCopy( clip.trace.endpos, trace_endpos );
trace_fraction = clip.trace.fraction;
clip.trace.fraction = 1.0f;
clip.start = start;
clip.end = trace_endpos;
clip.type = (type & 0xFF);
clip.ignoretrans = type >> 8;
clip.monsterclip = false;
clip.passedict = (e) ? e : EDICT_NUM( 0 );
clip.mins = mins;
clip.maxs = maxs;
VectorCopy( mins, clip.mins2 );
VectorCopy( maxs, clip.maxs2 );
World_MoveBounds( start, clip.mins2, clip.maxs2, trace_endpos, clip.boxmins, clip.boxmaxs );
SV_ClipToWorldBrush( sv_areanodes, &clip );
SV_ClipToPortals( sv_areanodes, &clip );
clip.trace.fraction *= trace_fraction;
svgame.globals->trace_ent = clip.trace.ent;
}
SV_CopyTraceToGlobal( &clip.trace );
return clip.trace;
}
/*
==================
SV_TraceSurface
find the face where the traceline hit
assume pTextureEntity is valid
==================
*/
msurface_t *SV_TraceSurface( edict_t *ent, const vec3_t start, const vec3_t end )
{
matrix4x4 matrix;
model_t *bmodel;
hull_t *hull;
vec3_t start_l, end_l;
vec3_t offset;
bmodel = SV_ModelHandle( ent->v.modelindex );
if( !bmodel || bmodel->type != mod_brush )
return NULL;
hull = SV_HullForBsp( ent, vec3_origin, vec3_origin, offset );
VectorSubtract( start, offset, start_l );
VectorSubtract( end, offset, end_l );
// rotate start and end into the models frame of reference
if( !VectorIsNull( ent->v.angles ))
{
Matrix4x4_CreateFromEntity( matrix, ent->v.angles, offset, 1.0f );
Matrix4x4_VectorITransform( matrix, start, start_l );
Matrix4x4_VectorITransform( matrix, end, end_l );
}
return PM_RecursiveSurfCheck( bmodel, &bmodel->nodes[hull->firstclipnode], start_l, end_l );
}
/*
==================
SV_TraceTexture
find the face where the traceline hit
assume pTextureEntity is valid
==================
*/
const char *SV_TraceTexture( edict_t *ent, const vec3_t start, const vec3_t end )
{
msurface_t *surf = SV_TraceSurface( ent, start, end );
if( !surf || !surf->texinfo || !surf->texinfo->texture )
return NULL;
return surf->texinfo->texture->name;
}
/*
==================
SV_MoveToss
==================
*/
trace_t SV_MoveToss( edict_t *tossent, edict_t *ignore )
{
float gravity;
vec3_t move, end;
vec3_t original_origin;
vec3_t original_velocity;
vec3_t original_angles;
vec3_t original_avelocity;
trace_t trace;
int i;
VectorCopy( tossent->v.origin, original_origin );
VectorCopy( tossent->v.velocity, original_velocity );
VectorCopy( tossent->v.angles, original_angles );
VectorCopy( tossent->v.avelocity, original_avelocity );
gravity = tossent->v.gravity * svgame.movevars.gravity * 0.05f;
for( i = 0; i < 200; i++ )
{
SV_CheckVelocity( tossent );
tossent->v.velocity[2] -= gravity;
VectorMA( tossent->v.angles, 0.05f, tossent->v.avelocity, tossent->v.angles );
VectorScale( tossent->v.velocity, 0.05f, move );
VectorAdd( tossent->v.origin, move, end );
trace = SV_Move( tossent->v.origin, tossent->v.mins, tossent->v.maxs, end, MOVE_NORMAL, tossent, false );
VectorCopy( trace.endpos, tossent->v.origin );
if( trace.fraction < 1.0f ) break;
}
VectorCopy( original_origin, tossent->v.origin );
VectorCopy( original_velocity, tossent->v.velocity );
VectorCopy( original_angles, tossent->v.angles );
VectorCopy( original_avelocity, tossent->v.avelocity );
return trace;
}
/*
===============================================================================
LIGHTING INFO
===============================================================================
*/
static vec3_t sv_pointColor;
/*
=================
SV_RecursiveLightPoint
=================
*/
static qboolean SV_RecursiveLightPoint( model_t *model, mnode_t *node, const vec3_t start, const vec3_t end )
{
float front, back, scale, frac;
int i, map, side, size;
float ds, dt, s, t;
int sample_size;
msurface_t *surf;
mtexinfo_t *tex;
mextrasurf_t *info;
color24 *lm;
vec3_t mid;
// didn't hit anything
if( !node || node->contents < 0 )
return false;
// calculate mid point
front = PlaneDiff( start, node->plane );
back = PlaneDiff( end, node->plane );
side = front < 0.0f;
if(( back < 0.0f ) == side )
return SV_RecursiveLightPoint( model, node->children[side], start, end );
frac = front / ( front - back );
VectorLerp( start, frac, end, mid );
// co down front side
if( SV_RecursiveLightPoint( model, node->children[side], start, mid ))
return true; // hit something
if(( back < 0.0f ) == side )
return false;// didn't hit anything
// check for impact on this node
surf = model->surfaces + node->firstsurface;
for( i = 0; i < node->numsurfaces; i++, surf++ )
{
int smax, tmax;
tex = surf->texinfo;
info = surf->info;
if( FBitSet( surf->flags, SURF_DRAWTILED ))
continue; // no lightmaps
s = DotProduct( mid, info->lmvecs[0] ) + info->lmvecs[0][3];
t = DotProduct( mid, info->lmvecs[1] ) + info->lmvecs[1][3];
if( s < info->lightmapmins[0] || t < info->lightmapmins[1] )
continue;
ds = s - info->lightmapmins[0];
dt = t - info->lightmapmins[1];
if ( ds > info->lightextents[0] || dt > info->lightextents[1] )
continue;
if( !surf->samples )
return true;
sample_size = Mod_SampleSizeForFace( surf );
smax = (info->lightextents[0] / sample_size) + 1;
tmax = (info->lightextents[1] / sample_size) + 1;
ds /= sample_size;
dt /= sample_size;
VectorClear( sv_pointColor );
lm = surf->samples + Q_rint( dt ) * smax + Q_rint( ds );
size = smax * tmax;
for( map = 0; map < MAXLIGHTMAPS && surf->styles[map] != 255; map++ )
{
scale = sv.lightstyles[surf->styles[map]].value;
sv_pointColor[0] += lm->r * scale;
sv_pointColor[1] += lm->g * scale;
sv_pointColor[2] += lm->b * scale;
lm += size; // skip to next lightmap
}
return true;
}
// go down back side
return SV_RecursiveLightPoint( model, node->children[!side], mid, end );
}
void SV_RunLightStyles( void )
{
int i, ofs;
lightstyle_t *ls;
float scale;
scale = sv_lighting_modulate.value;
// run lightstyles animation
for( i = 0, ls = sv.lightstyles; i < MAX_LIGHTSTYLES; i++, ls++ )
{
ls->time += sv.frametime;
ofs = (ls->time * 10);
if( ls->length == 0 ) ls->value = scale; // disable this light
else if( ls->length == 1 ) ls->value = ( ls->map[0] / 12.0f ) * scale;
else ls->value = ( ls->map[ofs % ls->length] / 12.0f ) * scale;
}
}
/*
==================
SV_SetLightStyle
needs to get correct working SV_LightPoint
==================
*/
void SV_SetLightStyle( int style, const char* s, float f )
{
int j, k;
Q_strncpy( sv.lightstyles[style].pattern, s, sizeof( sv.lightstyles[0].pattern ));
sv.lightstyles[style].time = f;
j = Q_strlen( s );
sv.lightstyles[style].length = j;
for( k = 0; k < j; k++ )
sv.lightstyles[style].map[k] = (float)(s[k] - 'a');
if( sv.state != ss_active ) return;
// tell the clients about changed lightstyle
MSG_BeginServerCmd( &sv.reliable_datagram, svc_lightstyle );
MSG_WriteByte( &sv.reliable_datagram, style );
MSG_WriteString( &sv.reliable_datagram, sv.lightstyles[style].pattern );
MSG_WriteFloat( &sv.reliable_datagram, sv.lightstyles[style].time );
}
/*
==================
SV_GetLightStyle
needs to get correct working SV_LightPoint
==================
*/
const char *SV_GetLightStyle( int style )
{
if( style < 0 ) style = 0;
if( style >= MAX_LIGHTSTYLES )
Host_Error( "SV_GetLightStyle: style: %i >= %d", style, MAX_LIGHTSTYLES );
return sv.lightstyles[style].pattern;
}
/*
==================
SV_LightForEntity
grab the ambient lighting color for current point
==================
*/
int SV_LightForEntity( edict_t *pEdict )
{
vec3_t start, end;
if( FBitSet( pEdict->v.effects, EF_FULLBRIGHT ) || !sv.worldmodel->lightdata )
return 255;
// player has more precision light level that come from client-side
if( FBitSet( pEdict->v.flags, FL_CLIENT ))
return pEdict->v.light_level;
VectorCopy( pEdict->v.origin, start );
VectorCopy( pEdict->v.origin, end );
if( FBitSet( pEdict->v.effects, EF_INVLIGHT ))
end[2] = start[2] + world.size[2];
else end[2] = start[2] - world.size[2];
VectorSet( sv_pointColor, 1.0f, 1.0f, 1.0f );
SV_RecursiveLightPoint( sv.worldmodel, sv.worldmodel->nodes, start, end );
return VectorAvg( sv_pointColor );
}