/* pm_trace.c - pmove player trace code Copyright (C) 2010 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 "xash3d_mathlib.h" #include "mod_local.h" #include "pm_local.h" #include "pm_movevars.h" #include "enginefeatures.h" #include "studio.h" #include "world.h" #define PM_AllowHitBoxTrace( model, hull ) ( model && model->type == mod_studio && ( FBitSet( model->flags, STUDIO_TRACE_HITBOX ) || hull == 2 )) static mplane_t pm_boxplanes[6]; static mclipnode_t pm_boxclipnodes[6]; static hull_t pm_boxhull; // default hullmins static const vec3_t pm_hullmins[MAX_MAP_HULLS] = { { -16, -16, -36 }, { -16, -16, -18 }, { 0, 0, 0 }, { -32, -32, -32 }, }; // defualt hullmaxs static const vec3_t pm_hullmaxs[MAX_MAP_HULLS] = { { 16, 16, 36 }, { 16, 16, 18 }, { 0, 0, 0 }, { 32, 32, 32 }, }; void Pmove_Init( void ) { PM_InitBoxHull (); // init default hull sizes memcpy( host.player_mins, pm_hullmins, sizeof( pm_hullmins )); memcpy( host.player_maxs, pm_hullmaxs, sizeof( pm_hullmaxs )); } /* =================== PM_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 PM_InitBoxHull( void ) { int i, side; pm_boxhull.clipnodes = pm_boxclipnodes; pm_boxhull.planes = pm_boxplanes; pm_boxhull.firstclipnode = 0; pm_boxhull.lastclipnode = 5; for( i = 0; i < 6; i++ ) { pm_boxclipnodes[i].planenum = i; side = i & 1; pm_boxclipnodes[i].children[side] = CONTENTS_EMPTY; if( i != 5 ) pm_boxclipnodes[i].children[side^1] = i + 1; else pm_boxclipnodes[i].children[side^1] = CONTENTS_SOLID; pm_boxplanes[i].type = i>>1; pm_boxplanes[i].normal[i>>1] = 1.0f; pm_boxplanes[i].signbits = 0; } } /* =================== PM_HullForBox To keep everything totally uniform, bounding boxes are turned into small BSP trees instead of being compared directly. =================== */ hull_t *PM_HullForBox( const vec3_t mins, const vec3_t maxs ) { pm_boxplanes[0].dist = maxs[0]; pm_boxplanes[1].dist = mins[0]; pm_boxplanes[2].dist = maxs[1]; pm_boxplanes[3].dist = mins[1]; pm_boxplanes[4].dist = maxs[2]; pm_boxplanes[5].dist = mins[2]; return &pm_boxhull; } void PM_ConvertTrace( trace_t *out, pmtrace_t *in, edict_t *ent ) { memcpy( out, in, 48 ); // matched out->hitgroup = in->hitgroup; out->ent = ent; } /* ================== PM_HullPointContents ================== */ int PM_HullPointContents( hull_t *hull, int num, const vec3_t p ) { mplane_t *plane; if( !hull || !hull->planes ) // fantom bmodels? return CONTENTS_NONE; while( num >= 0 ) { plane = &hull->planes[hull->clipnodes[num].planenum]; num = hull->clipnodes[num].children[PlaneDiff( p, plane ) < 0]; } return num; } /* ================== PM_HullForBsp assume physent is valid ================== */ hull_t *PM_HullForBsp( physent_t *pe, playermove_t *pmove, float *offset ) { hull_t *hull; Assert( pe != NULL ); Assert( pe->model != NULL ); switch( pmove->usehull ) { case 1: hull = &pe->model->hulls[3]; break; case 2: hull = &pe->model->hulls[0]; break; case 3: hull = &pe->model->hulls[2]; break; default: hull = &pe->model->hulls[1]; break; } Assert( hull != NULL ); // calculate an offset value to center the origin VectorSubtract( hull->clip_mins, pmove->player_mins[pmove->usehull], offset ); VectorAdd( offset, pe->origin, offset ); return hull; } /* ================== PM_HullForStudio generate multiple hulls as hitboxes ================== */ hull_t *PM_HullForStudio( physent_t *pe, playermove_t *pmove, int *numhitboxes ) { vec3_t size; VectorSubtract( pmove->player_maxs[pmove->usehull], pmove->player_mins[pmove->usehull], size ); VectorScale( size, 0.5f, size ); return Mod_HullForStudio( pe->studiomodel, pe->frame, pe->sequence, pe->angles, pe->origin, size, pe->controller, pe->blending, numhitboxes, NULL ); } /* ================== PM_RecursiveHullCheck ================== */ qboolean PM_RecursiveHullCheck( hull_t *hull, int num, float p1f, float p2f, vec3_t p1, vec3_t p2, pmtrace_t *trace ) { mclipnode_t *node; mplane_t *plane; float t1, t2; float frac, midf; int side; vec3_t mid; loc0: // check for empty if( num < 0 ) { if( num != CONTENTS_SOLID ) { trace->allsolid = false; if( num == CONTENTS_EMPTY ) trace->inopen = true; else trace->inwater = true; } else trace->startsolid = true; return true; // empty } if( hull->firstclipnode >= hull->lastclipnode ) { // empty hull? trace->allsolid = false; trace->inopen = true; return true; } if( num < hull->firstclipnode || num > hull->lastclipnode ) Host_Error( "PM_RecursiveHullCheck: bad node number %i\n", num ); // find the point distances node = hull->clipnodes + num; plane = hull->planes + node->planenum; t1 = PlaneDiff( p1, plane ); t2 = PlaneDiff( p2, plane ); if( t1 >= 0.0f && t2 >= 0.0f ) { num = node->children[0]; goto loc0; } if( t1 < 0.0f && t2 < 0.0f ) { num = node->children[1]; goto loc0; } // put the crosspoint DIST_EPSILON pixels on the near side side = (t1 < 0.0f); if( side ) frac = ( t1 + DIST_EPSILON ) / ( t1 - t2 ); else frac = ( t1 - DIST_EPSILON ) / ( t1 - t2 ); if( frac < 0.0f ) frac = 0.0f; if( frac > 1.0f ) frac = 1.0f; midf = p1f + ( p2f - p1f ) * frac; VectorLerp( p1, frac, p2, mid ); // move up to the node if( !PM_RecursiveHullCheck( hull, node->children[side], p1f, midf, p1, mid, trace )) return false; // this recursion can not be optimized because mid would need to be duplicated on a stack if( PM_HullPointContents( hull, node->children[side^1], mid ) != CONTENTS_SOLID ) { // go past the node return PM_RecursiveHullCheck( hull, node->children[side^1], midf, p2f, mid, p2, trace ); } // never got out of the solid area if( trace->allsolid ) return false; // the other side of the node is solid, this is the impact point if( !side ) { VectorCopy( plane->normal, trace->plane.normal ); trace->plane.dist = plane->dist; } else { VectorNegate( plane->normal, trace->plane.normal ); trace->plane.dist = -plane->dist; } while( PM_HullPointContents( hull, hull->firstclipnode, mid ) == CONTENTS_SOLID ) { // shouldn't really happen, but does occasionally frac -= 0.1f; if( frac < 0.0f ) { trace->fraction = midf; VectorCopy( mid, trace->endpos ); Con_Reportf( S_WARN "trace backed up past 0.0\n" ); return false; } midf = p1f + ( p2f - p1f ) * frac; VectorLerp( p1, frac, p2, mid ); } trace->fraction = midf; VectorCopy( mid, trace->endpos ); return false; } pmtrace_t PM_PlayerTraceExt( playermove_t *pmove, vec3_t start, vec3_t end, int flags, int numents, physent_t *ents, int ignore_pe, pfnIgnore pmFilter ) { physent_t *pe; matrix4x4 matrix; pmtrace_t trace_bbox; pmtrace_t trace_hitbox; pmtrace_t trace_total; vec3_t offset, start_l, end_l; vec3_t temp, mins, maxs; int i, j, hullcount; qboolean rotated, transform_bbox; hull_t *hull = NULL; memset( &trace_total, 0, sizeof( trace_total )); VectorCopy( end, trace_total.endpos ); trace_total.fraction = 1.0f; trace_total.ent = -1; for( i = 0; i < numents; i++ ) { pe = &ents[i]; if( i != 0 && ( flags & PM_WORLD_ONLY )) break; // run custom user filter if( pmFilter != NULL ) { if( pmFilter( pe )) continue; } else if( ignore_pe != -1 ) { if( i == ignore_pe ) continue; } if( pe->model != NULL && pe->solid == SOLID_NOT && pe->skin != CONTENTS_NONE ) continue; if(( flags & PM_GLASS_IGNORE ) && pe->rendermode != kRenderNormal ) continue; if(( flags & PM_CUSTOM_IGNORE ) && pe->solid == SOLID_CUSTOM ) continue; hullcount = 1; if( pe->solid == SOLID_CUSTOM ) { VectorCopy( pmove->player_mins[pmove->usehull], mins ); VectorCopy( pmove->player_maxs[pmove->usehull], maxs ); VectorClear( offset ); } else if( pe->model ) { hull = PM_HullForBsp( pe, pmove, offset ); } else { if( pe->studiomodel ) { if( FBitSet( flags, PM_STUDIO_IGNORE )) continue; if( PM_AllowHitBoxTrace( pe->studiomodel, pmove->usehull ) && !FBitSet( flags, PM_STUDIO_BOX )) { hull = PM_HullForStudio( pe, pmove, &hullcount ); VectorClear( offset ); } else { VectorSubtract( pe->mins, pmove->player_maxs[pmove->usehull], mins ); VectorSubtract( pe->maxs, pmove->player_mins[pmove->usehull], maxs ); hull = PM_HullForBox( mins, maxs ); VectorCopy( pe->origin, offset ); } } else { VectorSubtract( pe->mins, pmove->player_maxs[pmove->usehull], mins ); VectorSubtract( pe->maxs, pmove->player_mins[pmove->usehull], maxs ); hull = PM_HullForBox( mins, maxs ); VectorCopy( pe->origin, offset ); } } if( pe->solid == SOLID_BSP && !VectorIsNull( pe->angles )) rotated = true; else rotated = false; if( FBitSet( host.features, ENGINE_PHYSICS_PUSHER_EXT )) { if(( check_angles( pe->angles[0] ) || check_angles( pe->angles[2] )) && pmove->usehull != 2 ) transform_bbox = true; else transform_bbox = false; } else transform_bbox = false; if( rotated ) { if( transform_bbox ) Matrix4x4_CreateFromEntity( matrix, pe->angles, pe->origin, 1.0f ); else Matrix4x4_CreateFromEntity( matrix, pe->angles, offset, 1.0f ); Matrix4x4_VectorITransform( matrix, start, start_l ); Matrix4x4_VectorITransform( matrix, end, end_l ); if( transform_bbox ) { World_TransformAABB( matrix, pmove->player_mins[pmove->usehull], pmove->player_maxs[pmove->usehull], mins, maxs ); VectorSubtract( hull->clip_mins, 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 ); } memset( &trace_bbox, 0, sizeof( trace_bbox )); VectorCopy( end, trace_bbox.endpos ); trace_bbox.allsolid = true; trace_bbox.fraction = 1.0f; if( hullcount < 1 ) { // g-cont. probably this never happens trace_bbox.allsolid = false; } else if( pe->solid == SOLID_CUSTOM ) { // run custom sweep callback if( pmove->server || Host_IsLocalClient( )) SV_ClipPMoveToEntity( pe, start, mins, maxs, end, &trace_bbox ); #if !XASH_DEDICATED else CL_ClipPMoveToEntity( pe, start, mins, maxs, end, &trace_bbox ); #endif } else if( hullcount == 1 ) { PM_RecursiveHullCheck( hull, hull->firstclipnode, 0, 1, start_l, end_l, &trace_bbox ); } else { int last_hitgroup; for( last_hitgroup = 0, j = 0; j < hullcount; j++ ) { memset( &trace_hitbox, 0, sizeof( trace_hitbox )); VectorCopy( end, trace_hitbox.endpos ); trace_hitbox.allsolid = true; trace_hitbox.fraction = 1.0f; PM_RecursiveHullCheck( &hull[j], hull[j].firstclipnode, 0, 1, start_l, end_l, &trace_hitbox ); if( j == 0 || trace_hitbox.allsolid || trace_hitbox.startsolid || trace_hitbox.fraction < trace_bbox.fraction ) { if( trace_bbox.startsolid ) { trace_bbox = trace_hitbox; trace_bbox.startsolid = true; } else trace_bbox = trace_hitbox; last_hitgroup = j; } } trace_bbox.hitgroup = Mod_HitgroupForStudioHull( last_hitgroup ); } if( trace_bbox.allsolid ) trace_bbox.startsolid = true; if( trace_bbox.startsolid ) trace_bbox.fraction = 0.0f; if( !trace_bbox.startsolid ) { VectorLerp( start, trace_bbox.fraction, end, trace_bbox.endpos ); if( rotated ) { VectorCopy( trace_bbox.plane.normal, temp ); Matrix4x4_TransformPositivePlane( matrix, temp, trace_bbox.plane.dist, trace_bbox.plane.normal, &trace_bbox.plane.dist ); } else { trace_bbox.plane.dist = DotProduct( trace_bbox.endpos, trace_bbox.plane.normal ); } } if( trace_bbox.fraction < trace_total.fraction ) { trace_total = trace_bbox; trace_total.ent = i; } } return trace_total; } int PM_TestPlayerPosition( playermove_t *pmove, vec3_t pos, pmtrace_t *ptrace, pfnIgnore pmFilter ) { int i, j, hullcount; vec3_t pos_l, offset; hull_t *hull = NULL; vec3_t mins, maxs; pmtrace_t trace; physent_t *pe; trace = PM_PlayerTraceExt( pmove, pmove->origin, pmove->origin, 0, pmove->numphysent, pmove->physents, -1, pmFilter ); if( ptrace ) *ptrace = trace; for( i = 0; i < pmove->numphysent; i++ ) { pe = &pmove->physents[i]; // run custom user filter if( pmFilter != NULL ) { if( pmFilter( pe )) continue; } if( pe->model != NULL && pe->solid == SOLID_NOT && pe->skin != CONTENTS_NONE ) continue; hullcount = 1; if( pe->solid == SOLID_CUSTOM ) { VectorCopy( pmove->player_mins[pmove->usehull], mins ); VectorCopy( pmove->player_maxs[pmove->usehull], maxs ); VectorClear( offset ); } else if( pe->model ) { hull = PM_HullForBsp( pe, pmove, offset ); } else if( PM_AllowHitBoxTrace( pe->studiomodel, pmove->usehull )) { hull = PM_HullForStudio( pe, pmove, &hullcount ); VectorClear( offset ); } else { VectorSubtract( pe->mins, pmove->player_maxs[pmove->usehull], mins ); VectorSubtract( pe->maxs, pmove->player_mins[pmove->usehull], maxs ); hull = PM_HullForBox( mins, maxs ); VectorCopy( pe->origin, offset ); } // CM_TransformedPointContents :-) if( pe->solid == SOLID_BSP && !VectorIsNull( pe->angles )) { qboolean transform_bbox = false; matrix4x4 matrix; if( FBitSet( host.features, ENGINE_PHYSICS_PUSHER_EXT )) { if(( check_angles( pe->angles[0] ) || check_angles( pe->angles[2] )) && pmove->usehull != 2 ) transform_bbox = true; } if( transform_bbox ) Matrix4x4_CreateFromEntity( matrix, pe->angles, pe->origin, 1.0f ); else Matrix4x4_CreateFromEntity( matrix, pe->angles, offset, 1.0f ); Matrix4x4_VectorITransform( matrix, pos, pos_l ); if( transform_bbox ) { World_TransformAABB( matrix, pmove->player_mins[pmove->usehull], pmove->player_maxs[pmove->usehull], mins, maxs ); VectorSubtract( hull->clip_mins, mins, offset ); // calc new local offset for( j = 0; j < 3; j++ ) { if( pos_l[j] >= 0.0f ) pos_l[j] -= offset[j]; else pos_l[j] += offset[j]; } } } else { // offset the test point appropriately for this hull. VectorSubtract( pos, offset, pos_l ); } if( pe->solid == SOLID_CUSTOM ) { pmtrace_t trace; memset( &trace, 0, sizeof( trace )); VectorCopy( pos, trace.endpos ); trace.allsolid = true; trace.fraction = 1.0f; // run custom sweep callback if( pmove->server || Host_IsLocalClient( )) SV_ClipPMoveToEntity( pe, pos, mins, maxs, pos, &trace ); #if !XASH_DEDICATED else CL_ClipPMoveToEntity( pe, pos, mins, maxs, pos, &trace ); #endif // if we inside the custom hull if( trace.allsolid ) return i; } else if( hullcount == 1 ) { if( PM_HullPointContents( hull, hull->firstclipnode, pos_l ) == CONTENTS_SOLID ) return i; } else { for( j = 0; j < hullcount; j++ ) { if( PM_HullPointContents( &hull[j], hull[j].firstclipnode, pos_l ) == CONTENTS_SOLID ) return i; } } } return -1; // didn't hit anything } /* ============= PM_TruePointContents ============= */ int PM_TruePointContents( playermove_t *pmove, const vec3_t p ) { hull_t *hull = &pmove->physents[0].model->hulls[0]; if( hull ) { return PM_HullPointContents( hull, hull->firstclipnode, p ); } else { return CONTENTS_EMPTY; } } /* ============= PM_PointContents ============= */ int PM_PointContents( playermove_t *pmove, const vec3_t p ) { int i, contents; hull_t *hull; vec3_t test; physent_t *pe; // sanity check if( !p || !pmove->physents[0].model ) return CONTENTS_NONE; // get base contents from world contents = PM_HullPointContents( &pmove->physents[0].model->hulls[0], 0, p ); for( i = 1; i < pmove->numphysent; i++ ) { pe = &pmove->physents[i]; if( pe->solid != SOLID_NOT ) // disabled ? continue; // only brushes can have special contents if( !pe->model ) continue; // check water brushes accuracy hull = &pe->model->hulls[0]; if( FBitSet( pe->model->flags, MODEL_HAS_ORIGIN ) && !VectorIsNull( pe->angles )) { matrix4x4 matrix; Matrix4x4_CreateFromEntity( matrix, pe->angles, pe->origin, 1.0f ); Matrix4x4_VectorITransform( matrix, p, test ); } else { // offset the test point appropriately for this hull. VectorSubtract( p, pe->origin, test ); } // test hull for intersection with this model if( PM_HullPointContents( hull, hull->firstclipnode, test ) == CONTENTS_EMPTY ) continue; // compare contents ranking if( RankForContents( pe->skin ) > RankForContents( contents )) contents = pe->skin; // new content has more priority } return contents; }