#include "quakedef.h" #include "pr_common.h" #include "shader.h" #include "com_bih.h" extern cvar_t r_decal_noperpendicular; extern cvar_t mod_loadsurfenvmaps; extern cvar_t mod_loadmappackages; /* Decal functions */ #define MAXFRAGMENTVERTS 360 int Fragment_ClipPolyToPlane(float *inverts, float *outverts, int incount, float *plane, float planedist) { #define C (sizeof(vecV_t)/sizeof(vec_t)) float dotv[MAXFRAGMENTVERTS+1]; char keep[MAXFRAGMENTVERTS+1]; #define KEEP_KILL 0 #define KEEP_KEEP 1 #define KEEP_BORDER 2 int i; int outcount = 0; int clippedcount = 0; float d, *p1, *p2, *out; #define FRAG_EPSILON (1.0/32) //0.5 for (i = 0; i < incount; i++) { dotv[i] = DotProduct((inverts+i*C), plane) - planedist; if (dotv[i]<-FRAG_EPSILON) { keep[i] = KEEP_KILL; clippedcount++; } else if (dotv[i] > FRAG_EPSILON) keep[i] = KEEP_KEEP; else keep[i] = KEEP_BORDER; } dotv[i] = dotv[0]; keep[i] = keep[0]; if (clippedcount == incount) return 0; //all were clipped if (clippedcount == 0) { //none were clipped for (i = 0; i < incount; i++) VectorCopy((inverts+i*C), (outverts+i*C)); return incount; } for (i = 0; i < incount; i++) { p1 = inverts+i*C; if (keep[i] == KEEP_BORDER) { out = outverts+outcount++*C; VectorCopy(p1, out); continue; } if (keep[i] == KEEP_KEEP) { out = outverts+outcount++*C; VectorCopy(p1, out); } if (keep[i+1] == KEEP_BORDER || keep[i] == keep[i+1]) continue; p2 = inverts+((i+1)%incount)*C; d = dotv[i] - dotv[i+1]; if (d) d = dotv[i] / d; out = outverts+outcount++*C; VectorInterpolate(p1, d, p2, out); } return outcount; } //the plane itself must be a vec4_t, but can have other data packed between size_t Fragment_ClipPlaneToBrush(vecV_t *points, size_t maxpoints, void *planes, size_t planestride, size_t numplanes, vec4_t face) { int p, a; vecV_t verts[MAXFRAGMENTVERTS]; vecV_t verts2[MAXFRAGMENTVERTS]; vecV_t *cverts; int flip; // vec3_t d1, d2, n; size_t numverts; //generate some huge quad/poly aligned with the plane vec3_t tmp; vec3_t right, forward; double t; float *plane; // if (face[2] != 1) // return 0; t = fabs(face[2]); if (t > fabs(face[0]) && t > fabs(face[1])) VectorSet(tmp, 1, 0, 0); else VectorSet(tmp, 0, 0, 1); CrossProduct(face, tmp, right); VectorNormalize(right); CrossProduct(face, right, forward); VectorNormalize(forward); VectorScale(face, face[3], verts[0]); VectorMA(verts[0], 32767, right, verts[0]); VectorMA(verts[0], 32767, forward, verts[0]); VectorScale(face, face[3], verts[1]); VectorMA(verts[1], 32767, right, verts[1]); VectorMA(verts[1], -32767, forward, verts[1]); VectorScale(face, face[3], verts[2]); VectorMA(verts[2], -32767, right, verts[2]); VectorMA(verts[2], -32767, forward, verts[2]); VectorScale(face, face[3], verts[3]); VectorMA(verts[3], -32767, right, verts[3]); VectorMA(verts[3], 32767, forward, verts[3]); numverts = 4; //clip the quad to the various other planes flip = 0; for (p = 0; p < numplanes; p++) { plane = (float*)((qbyte*)planes + p*planestride); if (plane != face) { vec3_t norm; flip^=1; VectorNegate(plane, norm); if (flip) numverts = Fragment_ClipPolyToPlane((float*)verts, (float*)verts2, numverts, norm, -plane[3]); else numverts = Fragment_ClipPolyToPlane((float*)verts2, (float*)verts, numverts, norm, -plane[3]); if (numverts < 3) //totally clipped. return 0; } } if (numverts > maxpoints) return 0; if (flip) cverts = verts2; else cverts = verts; for (p = 0; p < numverts; p++) { for (a = 0; a < 3; a++) { float f = cverts[p][a]; int rounded = floor(f + 0.5); //if its within 1/1000th of a qu, just round it. if (fabs(f - rounded) < 0.001) points[p][a] = rounded; else points[p][a] = f; } } return numverts; } #ifdef HAVE_CLIENT #define MAXFRAGMENTTRIS 256 vec3_t decalfragmentverts[MAXFRAGMENTTRIS*3]; struct fragmentdecal_s { vec3_t center; vec3_t normal; // vec3_t tangent1; // vec3_t tangent2; vec3_t planenorm[6]; float planedist[6]; int numplanes; vec_t radius; //will only appear on surfaces with the matching surfaceflag unsigned int surfflagmask; unsigned int surfflagmatch; void (*callback)(void *ctx, vec3_t *fte_restrict points, size_t numpoints, shader_t *shader); void *ctx; }; //#define SHOWCLIPS //#define FRAGMENTASTRIANGLES //works, but produces more fragments. #ifdef FRAGMENTASTRIANGLES //if the triangle is clipped away, go recursive if there are tris left. static void Fragment_ClipTriToPlane(int trinum, float *plane, float planedist, fragmentdecal_t *dec) { float *point[3]; float dotv[3]; vec3_t impact1, impact2; float t; int i, i2, i3; int clippedverts = 0; for (i = 0; i < 3; i++) { point[i] = decalfragmentverts[trinum*3+i]; dotv[i] = DotProduct(point[i], plane)-planedist; clippedverts += dotv[i] < 0; } //if they're all clipped away, scrap the tri switch (clippedverts) { case 0: return; //plane does not clip the triangle. case 1: //split into 3, disregard the clipped vert for (i = 0; i < 3; i++) { if (dotv[i] < 0) { //This is the vertex that's getting clipped. if (dotv[i] > -DIST_EPSILON) return; //it's only over the line by a tiny ammount. i2 = (i+1)%3; i3 = (i+2)%3; if (dotv[i2] < DIST_EPSILON) return; if (dotv[i3] < DIST_EPSILON) return; //work out where the two lines impact the plane t = (dotv[i]) / (dotv[i]-dotv[i2]); VectorInterpolate(point[i], t, point[i2], impact1); t = (dotv[i]) / (dotv[i]-dotv[i3]); VectorInterpolate(point[i], t, point[i3], impact2); #ifdef SHOWCLIPS if (dec->numtris != MAXFRAGMENTTRIS) { VectorCopy(impact2, decalfragmentverts[dec->numtris*3+0]); VectorCopy(decalfragmentverts[trinum*3+i], decalfragmentverts[dec->numtris*3+1]); VectorCopy(impact1, decalfragmentverts[dec->numtris*3+2]); dec->numtris++; } #endif //shrink the tri, putting the impact into the killed vertex. VectorCopy(impact2, point[i]); if (dec->numtris == MAXFRAGMENTTRIS) return; //:( //build the second tri VectorCopy(impact1, decalfragmentverts[dec->numtris*3+0]); VectorCopy(decalfragmentverts[trinum*3+i2], decalfragmentverts[dec->numtris*3+1]); VectorCopy(impact2, decalfragmentverts[dec->numtris*3+2]); dec->numtris++; return; } } Sys_Error("Fragment_ClipTriToPlane: Clipped vertex not founc\n"); return; //can't handle it case 2: //split into 3, disregarding both the clipped. for (i = 0; i < 3; i++) { if (!(dotv[i] < 0)) { //This is the vertex that's staying. if (dotv[i] < DIST_EPSILON) break; //only just inside i2 = (i+1)%3; i3 = (i+2)%3; //work out where the two lines impact the plane t = (dotv[i]) / (dotv[i]-dotv[i2]); VectorInterpolate(point[i], t, point[i2], impact1); t = (dotv[i]) / (dotv[i]-dotv[i3]); VectorInterpolate(point[i], t, point[i3], impact2); //shrink the tri, putting the impact into the killed vertex. #ifdef SHOWCLIPS if (dec->numtris != MAXFRAGMENTTRIS) { VectorCopy(impact1, decalfragmentverts[dec->numtris*3+0]); VectorCopy(point[i2], decalfragmentverts[dec->numtris*3+1]); VectorCopy(point[i3], decalfragmentverts[dec->numtris*3+2]); dec->numtris++; } if (dec->numtris != MAXFRAGMENTTRIS) { VectorCopy(impact1, decalfragmentverts[dec->numtris*3+0]); VectorCopy(point[i3], decalfragmentverts[dec->numtris*3+1]); VectorCopy(impact2, decalfragmentverts[dec->numtris*3+2]); dec->numtris++; } #endif VectorCopy(impact1, point[i2]); VectorCopy(impact2, point[i3]); return; } } case 3://scrap it //fill the verts with the verts of the last and go recursive (due to the nature of Fragment_ClipTriangle, which doesn't actually know if we clip them away) #ifndef SHOWCLIPS dec->numtris--; VectorCopy(decalfragmentverts[dec->numtris*3+0], decalfragmentverts[trinum*3+0]); VectorCopy(decalfragmentverts[dec->numtris*3+1], decalfragmentverts[trinum*3+1]); VectorCopy(decalfragmentverts[dec->numtris*3+2], decalfragmentverts[trinum*3+2]); if (trinum < dec->numtris) Fragment_ClipTriToPlane(trinum, plane, planedist, dec); #endif return; } } static void Fragment_ClipTriangle(fragmentdecal_t *dec, float *a, float *b, float *c) { //emit the triangle, and clip it's fragments. int start, i; int p; if (dec->numtris == MAXFRAGMENTTRIS) return; //:( start = dec->numtris; VectorCopy(a, decalfragmentverts[dec->numtris*3+0]); VectorCopy(b, decalfragmentverts[dec->numtris*3+1]); VectorCopy(c, decalfragmentverts[dec->numtris*3+2]); dec->numtris++; //clip all the fragments to all of the planes. //This will produce a quad if the source triangle was big enough. for (p = 0; p < 6; p++) { for (i = start; i < dec->numtris; i++) Fragment_ClipTriToPlane(i, dec->planenorm[p], dec->plantdist[p], dec); } } #else void Fragment_ClipPoly(fragmentdecal_t *dec, int numverts, float *inverts, shader_t *surfshader) { //emit the triangle, and clip it's fragments. int p; float verts[MAXFRAGMENTVERTS*C]; float verts2[MAXFRAGMENTVERTS*C]; float *cverts; int flip; vec3_t d1, d2, n; size_t numtris; if (numverts > MAXFRAGMENTTRIS) return; if (r_decal_noperpendicular.ival) { VectorSubtract(inverts+C*1, inverts+C*0, d1); for (p = 2; ; p++) { if (p >= numverts) return; VectorSubtract(inverts+C*p, inverts+C*0, d2); CrossProduct(d1, d2, n); if (DotProduct(n,n)>.1) break; } VectorNormalizeFast(n); if (DotProduct(n, dec->normal) < 0.1) return; //faces too far way from the normal } //clip to the first plane specially, so we don't have extra copys numverts = Fragment_ClipPolyToPlane(inverts, verts, numverts, dec->planenorm[0], dec->planedist[0]); //clip the triangle to the 6 planes. flip = 0; for (p = 1; p < dec->numplanes; p++) { flip^=1; if (flip) numverts = Fragment_ClipPolyToPlane(verts, verts2, numverts, dec->planenorm[p], dec->planedist[p]); else numverts = Fragment_ClipPolyToPlane(verts2, verts, numverts, dec->planenorm[p], dec->planedist[p]); if (numverts < 3) //totally clipped. return; } if (flip) cverts = verts2; else cverts = verts; //decompose the resultant polygon into triangles. numtris = 0; while(numverts-->2) { if (numtris == MAXFRAGMENTTRIS) { dec->callback(dec->ctx, decalfragmentverts, numtris, NULL); numtris = 0; break; } VectorCopy((cverts+C*0), decalfragmentverts[numtris*3+0]); VectorCopy((cverts+C*(numverts-1)), decalfragmentverts[numtris*3+1]); VectorCopy((cverts+C*numverts), decalfragmentverts[numtris*3+2]); numtris++; } if (numtris) dec->callback(dec->ctx, decalfragmentverts, numtris, surfshader); } #endif //this could be inlined, but I'm lazy. static void Fragment_Mesh (fragmentdecal_t *dec, mesh_t *mesh, mtexinfo_t *texinfo) { int i; vecV_t verts[3]; shader_t *surfshader = texinfo->texture->shader; if ((surfshader->flags & SHADER_NOMARKS) || !mesh) return; if (dec->surfflagmask) { if ((texinfo->flags & dec->surfflagmask) != dec->surfflagmatch) return; } /*if its a triangle fan/poly/quad then we can just submit the entire thing without generating extra fragments*/ if (mesh->istrifan) { Fragment_ClipPoly(dec, mesh->numvertexes, mesh->xyz_array[0], surfshader); return; } //Fixme: optimise q3 patches (quad strips with bends between each strip) /*otherwise it goes in and out in weird places*/ for (i = 0; i < mesh->numindexes; i+=3) { VectorCopy(mesh->xyz_array[mesh->indexes[i+0]], verts[0]); VectorCopy(mesh->xyz_array[mesh->indexes[i+1]], verts[1]); VectorCopy(mesh->xyz_array[mesh->indexes[i+2]], verts[2]); Fragment_ClipPoly(dec, 3, verts[0], surfshader); } } #ifdef Q1BSPS static void Q1BSP_ClipDecalToNodes (model_t *mod, fragmentdecal_t *dec, mnode_t *node) { mplane_t *splitplane; float dist; msurface_t *surf; int i; if (node->contents < 0) return; splitplane = node->plane; dist = DotProduct (dec->center, splitplane->normal) - splitplane->dist; if (dist > dec->radius) { Q1BSP_ClipDecalToNodes (mod, dec, node->children[0]); return; } if (dist < -dec->radius) { Q1BSP_ClipDecalToNodes (mod, dec, node->children[1]); return; } // mark the polygons surf = mod->surfaces + node->firstsurface; if (r_decal_noperpendicular.ival) { for (i=0 ; inumsurfaces ; i++, surf++) { if (surf->flags & SURF_PLANEBACK) { if (-DotProduct(surf->plane->normal, dec->normal) > -0.5) continue; } else { if (DotProduct(surf->plane->normal, dec->normal) > -0.5) continue; } Fragment_Mesh(dec, surf->mesh, surf->texinfo); } } else { for (i=0 ; inumsurfaces ; i++, surf++) Fragment_Mesh(dec, surf->mesh, surf->texinfo); } Q1BSP_ClipDecalToNodes (mod, dec, node->children[0]); Q1BSP_ClipDecalToNodes (mod, dec, node->children[1]); } #endif #ifdef RTLIGHTS extern int sh_shadowframe; #else static int sh_shadowframe; #endif #ifdef Q3BSPS static void Q3BSP_ClipDecalToNodes (fragmentdecal_t *dec, mnode_t *node) { mplane_t *splitplane; float dist; msurface_t **msurf; msurface_t *surf; mleaf_t *leaf; int i; if (node->contents != -1) { leaf = (mleaf_t *)node; // mark the polygons msurf = leaf->firstmarksurface; for (i=0 ; inummarksurfaces ; i++, msurf++) { surf = *msurf; #ifdef RTLIGHTS //only check each surface once. it can appear in multiple leafs. if (surf->shadowframe == sh_shadowframe) continue; surf->shadowframe = sh_shadowframe; #endif Fragment_Mesh(dec, surf->mesh, surf->texinfo); } return; } splitplane = node->plane; dist = DotProduct (dec->center, splitplane->normal) - splitplane->dist; if (dist > dec->radius) { Q3BSP_ClipDecalToNodes (dec, node->children[0]); return; } if (dist < -dec->radius) { Q3BSP_ClipDecalToNodes (dec, node->children[1]); return; } Q3BSP_ClipDecalToNodes (dec, node->children[0]); Q3BSP_ClipDecalToNodes (dec, node->children[1]); } #endif void Mod_ClipDecal(struct model_s *mod, vec3_t center, vec3_t normal, vec3_t tangent1, vec3_t tangent2, float size, unsigned int surfflagmask, unsigned int surfflagmatch, void (*callback)(void *ctx, vec3_t *fte_restrict points, size_t numpoints, shader_t *shader), void *ctx) { //quad marks a full, independant quad int p; float r; fragmentdecal_t dec; VectorCopy(center, dec.center); VectorCopy(normal, dec.normal); dec.radius = 0; dec.callback = callback; dec.ctx = ctx; dec.surfflagmask = surfflagmask; dec.surfflagmatch = surfflagmatch; VectorCopy(tangent1, dec.planenorm[0]); VectorNegate(tangent1, dec.planenorm[1]); VectorCopy(tangent2, dec.planenorm[2]); VectorNegate(tangent2, dec.planenorm[3]); VectorCopy(dec.normal, dec.planenorm[4]); VectorNegate(dec.normal, dec.planenorm[5]); for (p = 0; p < 6; p++) { r = sqrt(DotProduct(dec.planenorm[p], dec.planenorm[p])); VectorScale(dec.planenorm[p], 1/r, dec.planenorm[p]); r*= size/2; if (r > dec.radius) dec.radius = r; dec.planedist[p] = -(r - DotProduct(dec.center, dec.planenorm[p])); } dec.numplanes = 6; sh_shadowframe++; if (!mod || mod->loadstate != MLS_LOADED) return; else if (mod->type != mod_brush) ; #ifdef Q1BSPS else if (mod->fromgame == fg_quake || mod->fromgame == fg_halflife) Q1BSP_ClipDecalToNodes(mod, &dec, mod->rootnode); #endif #ifdef Q3BSPS else if (mod->fromgame == fg_quake3 || mod->fromgame == fg_quake2) { if (mod->submodelof) { msurface_t *surf; for (surf = mod->surfaces+mod->firstmodelsurface, p = 0; p < mod->nummodelsurfaces; p++, surf++) Fragment_Mesh(&dec, surf->mesh, surf->texinfo); } else Q3BSP_ClipDecalToNodes(&dec, mod->rootnode); } #endif #ifdef TERRAIN if (mod->terrain) Terrain_ClipDecal(&dec, center, dec.radius, mod); #endif } #endif /* Decal functions ============================================================================ Physics functions (common) */ #ifdef Q1BSPS void Q1BSP_CheckHullNodes(hull_t *hull) { int num, c; mclipnode_t *node; for (num = hull->firstclipnode; num < hull->lastclipnode; num++) { node = hull->clipnodes + num; for (c = 0; c < 2; c++) if (node->children[c] >= 0) if (node->children[c] < hull->firstclipnode || node->children[c] > hull->lastclipnode) Sys_Error ("Q1BSP_CheckHull: bad node number"); } } static int Q1_ModelPointContents (mnode_t *node, const vec3_t p) { float d; mplane_t *plane; while(node->contents >= 0) { plane = node->plane; if (plane->type < 3) d = p[plane->type] - plane->dist; else d = DotProduct(plane->normal, p) - plane->dist; node = node->children[d<0]; } return node->contents; } #endif//Q1BSPS /* ================== SV_HullPointContents ================== */ static int Q1_HullPointContents (hull_t *hull, int num, const vec3_t p) { float d; mclipnode_t *node; mplane_t *plane; while (num >= 0) { node = hull->clipnodes + num; plane = hull->planes + node->planenum; if (plane->type < 3) d = p[plane->type] - plane->dist; else d = DotProduct (plane->normal, p) - plane->dist; if (d < 0) num = node->children[1]; else num = node->children[0]; } return num; } #define DIST_EPSILON (0.03125) #if 1 static const unsigned int q1toftecontents[] = { 0,//EMPTY FTECONTENTS_SOLID,//SOLID FTECONTENTS_WATER,//WATER FTECONTENTS_SLIME,//SLIME FTECONTENTS_LAVA,//LAVA FTECONTENTS_SKY,//SKY FTECONTENTS_SOLID,//STRIPPED FTECONTENTS_PLAYERCLIP,//CLIP Q2CONTENTS_CURRENT_0,//FLOW_1 Q2CONTENTS_CURRENT_90,//FLOW_2 Q2CONTENTS_CURRENT_180,//FLOW_3 Q2CONTENTS_CURRENT_270,//FLOW_4 Q2CONTENTS_CURRENT_UP,//FLOW_5 Q2CONTENTS_CURRENT_DOWN,//FLOW_6 Q2CONTENTS_WINDOW,//TRANS FTECONTENTS_LADDER,//LADDER }; enum { rht_solid, rht_empty, rht_impact }; struct rhtctx_s { unsigned int checkcontents; vec3_t start, end; mclipnode_t *clipnodes; mplane_t *planes; }; static int Q1BSP_RecursiveHullTrace (struct rhtctx_s *ctx, int num, float p1f, float p2f, const vec3_t p1, const vec3_t p2, trace_t *trace) { mclipnode_t *node; mplane_t *plane; float t1, t2; vec3_t mid; int side; float midf; int rht; reenter: if (num < 0) { unsigned int c = q1toftecontents[-1-num]; /*hit a leaf*/ if (c & ctx->checkcontents) { trace->contents = c; if (trace->allsolid) trace->startsolid = true; return rht_solid; } else { trace->allsolid = false; if (c & FTECONTENTS_FLUID) trace->inwater = true; else trace->inopen = true; return rht_empty; } } /*its a node*/ /*get the node info*/ node = ctx->clipnodes + num; plane = ctx->planes + node->planenum; if (plane->type < 3) { t1 = p1[plane->type] - plane->dist; t2 = p2[plane->type] - plane->dist; } else { t1 = DotProduct (plane->normal, p1) - plane->dist; t2 = DotProduct (plane->normal, p2) - plane->dist; } /*if its completely on one side, resume on that side*/ if (t1 >= 0 && t2 >= 0) { //return Q1BSP_RecursiveHullTrace (hull, node->children[0], p1f, p2f, p1, p2, trace); num = node->children[0]; goto reenter; } if (t1 < 0 && t2 < 0) { //return Q1BSP_RecursiveHullTrace (hull, node->children[1], p1f, p2f, p1, p2, trace); num = node->children[1]; goto reenter; } if (plane->type < 3) { t1 = ctx->start[plane->type] - plane->dist; t2 = ctx->end[plane->type] - plane->dist; } else { t1 = DotProduct (plane->normal, ctx->start) - plane->dist; t2 = DotProduct (plane->normal, ctx->end) - plane->dist; } side = t1 < 0; midf = t1 / (t1 - t2); if (midf < p1f) midf = p1f; if (midf > p2f) midf = p2f; VectorInterpolate(ctx->start, midf, ctx->end, mid); //check the near side rht = Q1BSP_RecursiveHullTrace(ctx, node->children[side], p1f, midf, p1, mid, trace); if (rht != rht_empty && !trace->allsolid) return rht; //check the far side rht = Q1BSP_RecursiveHullTrace(ctx, node->children[side^1], midf, p2f, mid, p2, trace); if (rht != rht_solid) return rht; if (side) { /*we impacted the back of the node, so flip the plane*/ trace->plane.dist = -plane->dist; VectorNegate(plane->normal, trace->plane.normal); midf = (t1 + DIST_EPSILON) / (t1 - t2); } else { /*we impacted the front of the node*/ trace->plane.dist = plane->dist; VectorCopy(plane->normal, trace->plane.normal); midf = (t1 - DIST_EPSILON) / (t1 - t2); } t1 = DotProduct (trace->plane.normal, ctx->start) - trace->plane.dist; t2 = DotProduct (trace->plane.normal, ctx->end) - trace->plane.dist; midf = (t1 - DIST_EPSILON) / (t1 - t2); midf = bound(0, midf, 1); trace->fraction = midf; VectorCopy (mid, trace->endpos); VectorInterpolate(ctx->start, midf, ctx->end, trace->endpos); return rht_impact; } qboolean Q1BSP_RecursiveHullCheck (hull_t *hull, int num, const vec3_t p1, const vec3_t p2, unsigned int hitcontents, trace_t *trace) { if (VectorEquals(p1, p2)) { /*points cannot cross planes, so do it faster*/ int q1 = Q1_HullPointContents(hull, num, p1); unsigned int c = q1toftecontents[-1-q1]; trace->contents = c; if (c & hitcontents) trace->startsolid = true; else if (c & FTECONTENTS_FLUID) { trace->allsolid = false; trace->inwater = true; } else { trace->allsolid = false; trace->inopen = true; } return true; } else { struct rhtctx_s ctx; VectorCopy(p1, ctx.start); VectorCopy(p2, ctx.end); ctx.clipnodes = hull->clipnodes; ctx.planes = hull->planes; ctx.checkcontents = hitcontents; return Q1BSP_RecursiveHullTrace(&ctx, num, 0, 1, p1, p2, trace) != rht_impact; } } #else qboolean Q1BSP_RecursiveHullCheck (hull_t *hull, int num, float p1f, float p2f, vec3_t p1, vec3_t p2, trace_t *trace) { mclipnode_t *node; mplane_t *plane; float t1, t2; float frac; int i; vec3_t mid; int side; float midf; // check for empty if (num < 0) { if (num != Q1CONTENTS_SOLID) { trace->allsolid = false; if (num == Q1CONTENTS_EMPTY) trace->inopen = true; else trace->inwater = true; } else trace->startsolid = true; return true; // empty } // // find the point distances // node = hull->clipnodes + num; plane = hull->planes + node->planenum; if (plane->type < 3) { t1 = p1[plane->type] - plane->dist; t2 = p2[plane->type] - plane->dist; } else { t1 = DotProduct (plane->normal, p1) - plane->dist; t2 = DotProduct (plane->normal, p2) - plane->dist; } #if 1 if (t1 >= 0 && t2 >= 0) return Q1BSP_RecursiveHullCheck (hull, node->children[0], p1f, p2f, p1, p2, trace); if (t1 < 0 && t2 < 0) return Q1BSP_RecursiveHullCheck (hull, node->children[1], p1f, p2f, p1, p2, trace); #else if ( (t1 >= DIST_EPSILON && t2 >= DIST_EPSILON) || (t2 > t1 && t1 >= 0) ) return Q1BSP_RecursiveHullCheck (hull, node->children[0], p1f, p2f, p1, p2, trace); if ( (t1 <= -DIST_EPSILON && t2 <= -DIST_EPSILON) || (t2 < t1 && t1 <= 0) ) return Q1BSP_RecursiveHullCheck (hull, node->children[1], p1f, p2f, p1, p2, trace); #endif // put the crosspoint DIST_EPSILON pixels on the near side if (t1 < 0) frac = (t1 + DIST_EPSILON)/(t1-t2); else frac = (t1 - DIST_EPSILON)/(t1-t2); if (frac < 0) frac = 0; if (frac > 1) frac = 1; midf = p1f + (p2f - p1f)*frac; for (i=0 ; i<3 ; i++) mid[i] = p1[i] + frac*(p2[i] - p1[i]); side = (t1 < 0); // move up to the node if (!Q1BSP_RecursiveHullCheck (hull, node->children[side], p1f, midf, p1, mid, trace) ) return false; #ifdef PARANOID if (Q1BSP_RecursiveHullCheck (sv_hullmodel, mid, node->children[side]) == Q1CONTENTS_SOLID) { Con_Printf ("mid PointInHullSolid\n"); return false; } #endif if (Q1_HullPointContents (hull, node->children[side^1], mid) != Q1CONTENTS_SOLID) // go past the node return Q1BSP_RecursiveHullCheck (hull, node->children[side^1], midf, p2f, mid, p2, trace); if (trace->allsolid) return false; // never got out of the solid area //================== // 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 (Q1_HullPointContents (hull, hull->firstclipnode, mid) == Q1CONTENTS_SOLID) { // shouldn't really happen, but does occasionally if (!(frac < 10000000) && !(frac > -10000000)) { trace->fraction = 0; VectorClear (trace->endpos); Con_Printf ("nan in traceline\n"); return false; } frac -= 0.1; if (frac < 0) { trace->fraction = midf; VectorCopy (mid, trace->endpos); Con_DPrintf ("backup past 0\n"); return false; } midf = p1f + (p2f - p1f)*frac; for (i=0 ; i<3 ; i++) mid[i] = p1[i] + frac*(p2[i] - p1[i]); } trace->fraction = midf; VectorCopy (mid, trace->endpos); return false; } #endif #if 0//def Q1BSPS /* the bsp tree we're walking through is the renderable hull we need to trace a box through the world. by its very nature, this will reach more nodes than we really want, and as we can follow a node sideways, the underlying bsp structure is no longer 100% reliable (meaning we cross planes that are entirely to one side, and follow its children too) so all contents and solidity must come from the brushes and ONLY the brushes. */ struct traceinfo_s { unsigned int solidcontents; trace_t trace; qboolean capsule; float radius; /*set even for sphere traces (used for bbox tests)*/ vec3_t mins; vec3_t maxs; vec3_t start; vec3_t end; vec3_t up; vec3_t capsulesize; vec3_t extents; }; static void Q1BSP_ClipToBrushes(struct traceinfo_s *traceinfo, mbrush_t *brush) { struct mbrushplane_s *plane; struct mbrushplane_s *enterplane; int i, j; vec3_t ofs; qboolean startout, endout; float d1,d2,dist,enterdist=0; float f, enterfrac, exitfrac; for (; brush; brush = brush->next) { /*ignore if its not solid to us*/ if (!(traceinfo->solidcontents & brush->contents)) continue; startout = false; endout = false; enterplane= NULL; enterfrac = -1; exitfrac = 10; for (i = brush->numplanes, plane = brush->planes; i; i--, plane++) { /*calculate the distance based upon the shape of the object we're tracing for*/ if (traceinfo->capsule) { dist = DotProduct(traceinfo->up, plane->normal); dist = dist*(traceinfo->capsulesize[(dist<0)?1:2]) - traceinfo->capsulesize[0]; dist = plane->dist - dist; //dist = plane->dist + traceinfo->radius; } else { for (j=0 ; j<3 ; j++) { if (plane->normal[j] < 0) ofs[j] = traceinfo->maxs[j]; else ofs[j] = traceinfo->mins[j]; } dist = DotProduct (ofs, plane->normal); dist = plane->dist - dist; } d1 = DotProduct (traceinfo->start, plane->normal) - dist; d2 = DotProduct (traceinfo->end, plane->normal) - dist; if (d1 >= 0) startout = true; if (d2 > 0) endout = true; //if we're fully outside any plane, then we cannot possibly enter the brush, skip to the next one if (d1 > 0 && d2 >= 0) goto nextbrush; //if we're fully inside the plane, then whatever is happening is not relevent for this plane if (d1 < 0 && d2 <= 0) continue; f = d1 / (d1-d2); if (d1 > d2) { //entered the brush. favour the furthest fraction to avoid extended edges (yay for convex shapes) if (enterfrac < f) { enterfrac = f; enterplane = plane; enterdist = dist; } } else { //left the brush, favour the nearest plane (smallest frac) if (exitfrac > f) { exitfrac = f; } } } if (!startout) { traceinfo->trace.startsolid = true; if (!endout) traceinfo->trace.allsolid = true; traceinfo->trace.contents |= brush->contents; return; } if (enterfrac != -1 && enterfrac < exitfrac) { //impact! if (enterfrac < traceinfo->trace.fraction) { traceinfo->trace.fraction = enterfrac; traceinfo->trace.plane.dist = enterdist; VectorCopy(enterplane->normal, traceinfo->trace.plane.normal); traceinfo->trace.contents = brush->contents; } } nextbrush: ; } } static void Q1BSP_InsertBrush(mnode_t *node, mbrush_t *brush, vec3_t bmins, vec3_t bmaxs) { vec3_t nearp, farp; float nd, fd; int i; while(1) { if (node->contents < 0) /*leaf, so no smaller node to put it in (I'd be surprised if it got this far)*/ { brush->next = node->brushes; node->brushes = brush; return; } for (i = 0; i < 3; i++) { if (node->plane->normal[i] > 0) { nearp[i] = bmins[i]; farp[i] = bmaxs[i]; } else { nearp[i] = bmaxs[i]; farp[i] = bmins[i]; } } nd = DotProduct(node->plane->normal, nearp) - node->plane->dist; fd = DotProduct(node->plane->normal, farp) - node->plane->dist; /*if its fully on either side, continue walking*/ if (nd < 0 && fd < 0) node = node->children[1]; else if (nd > 0 && fd > 0) node = node->children[0]; else { /*plane crosses bbox, so insert here*/ brush->next = node->brushes; node->brushes = brush; return; } } } static void Q1BSP_RecursiveBrushCheck (struct traceinfo_s *traceinfo, mnode_t *node, float p1f, float p2f, const vec3_t p1, const vec3_t p2) { mplane_t *plane; float t1, t2; float frac; int i; vec3_t mid; int side; float midf; float offset; if (node->brushes) { Q1BSP_ClipToBrushes(traceinfo, node->brushes); } if (traceinfo->trace.fraction < p1f) { //already hit something closer than this node return; } if (node->contents < 0) { //we're in a leaf return; } // // find the point distances // plane = node->plane; if (plane->type < 3) { t1 = p1[plane->type] - plane->dist; t2 = p2[plane->type] - plane->dist; if (plane->normal[plane->type] < 0) offset = -traceinfo->mins[plane->type]; else offset = traceinfo->maxs[plane->type]; } else { t1 = DotProduct (plane->normal, p1) - plane->dist; t2 = DotProduct (plane->normal, p2) - plane->dist; offset = 0; for (i = 0; i < 3; i++) { if (plane->normal[i] < 0) offset += plane->normal[i] * -traceinfo->mins[i]; else offset += plane->normal[i] * traceinfo->maxs[i]; } } /*if we're fully on one side of the trace, go only down that side*/ if (t1 >= offset && t2 >= offset) { Q1BSP_RecursiveBrushCheck (traceinfo, node->children[0], p1f, p2f, p1, p2); return; } if (t1 < -offset && t2 < -offset) { Q1BSP_RecursiveBrushCheck (traceinfo, node->children[1], p1f, p2f, p1, p2); return; } // put the crosspoint DIST_EPSILON pixels on the near side if (t1 == t2) { side = 0; frac = 0; } else if (t1 < 0) { frac = (t1 + DIST_EPSILON)/(t1-t2); side = 1; } else { frac = (t1 - DIST_EPSILON)/(t1-t2); side = 0; } if (frac < 0) frac = 0; if (frac > 1) frac = 1; midf = p1f + (p2f - p1f)*frac; for (i=0 ; i<3 ; i++) mid[i] = p1[i] + frac*(p2[i] - p1[i]); // move up to the node Q1BSP_RecursiveBrushCheck (traceinfo, node->children[side], p1f, midf, p1, mid); // go past the node Q1BSP_RecursiveBrushCheck (traceinfo, node->children[side^1], midf, p2f, mid, p2); } #endif //Q1BSPS static unsigned int Q1BSP_TranslateContents(enum q1contents_e contents) { safeswitch(contents) { case Q1CONTENTS_EMPTY: return FTECONTENTS_EMPTY; case Q1CONTENTS_SOLID: return FTECONTENTS_SOLID; case Q1CONTENTS_WATER: return FTECONTENTS_WATER; case Q1CONTENTS_SLIME: return FTECONTENTS_SLIME; case Q1CONTENTS_LAVA: return FTECONTENTS_LAVA; case Q1CONTENTS_SKY: return FTECONTENTS_SKY|FTECONTENTS_PLAYERCLIP|FTECONTENTS_MONSTERCLIP; case Q1CONTENTS_LADDER: return FTECONTENTS_LADDER; case HLCONTENTS_CLIP: return FTECONTENTS_PLAYERCLIP|FTECONTENTS_MONSTERCLIP; case HLCONTENTS_CURRENT_0: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_0; //q2 is better than nothing, right? case HLCONTENTS_CURRENT_90: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_90; case HLCONTENTS_CURRENT_180: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_180; case HLCONTENTS_CURRENT_270: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_270; case HLCONTENTS_CURRENT_UP: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_UP; case HLCONTENTS_CURRENT_DOWN: return FTECONTENTS_WATER|Q2CONTENTS_CURRENT_DOWN; case HLCONTENTS_TRANS: return FTECONTENTS_EMPTY; case Q1CONTENTS_MONSTERCLIP: return FTECONTENTS_MONSTERCLIP; case Q1CONTENTS_PLAYERCLIP: return FTECONTENTS_PLAYERCLIP; case Q1CONTENTS_CORPSE: return FTECONTENTS_CORPSE; safedefault: Con_Printf("Q1BSP_TranslateContents: Unknown contents type - %i", contents); return FTECONTENTS_SOLID; } } int Q1BSP_HullPointContents(hull_t *hull, const vec3_t p) { return Q1BSP_TranslateContents(Q1_HullPointContents(hull, hull->firstclipnode, p)); } #ifdef Q1BSPS unsigned int Q1BSP_PointContents(model_t *model, const vec3_t axis[3], const vec3_t point) { int contents; if (axis) { vec3_t transformed; transformed[0] = DotProduct(point, axis[0]); transformed[1] = DotProduct(point, axis[1]); transformed[2] = DotProduct(point, axis[2]); return Q1BSP_PointContents(model, NULL, transformed); } else { if (!model->firstmodelsurface) { contents = Q1BSP_TranslateContents(Q1_ModelPointContents(model->nodes, point)); } else contents = Q1BSP_HullPointContents(&model->hulls[0], point); } #ifdef TERRAIN if (model->terrain) contents |= Heightmap_PointContents(model, NULL, point); #endif return contents; } void Q1BSP_LoadBrushes(model_t *model, bspx_header_t *bspx, void *mod_base) { const struct { unsigned int ver; unsigned int modelnum; unsigned int numbrushes; unsigned int numplanes; } *srcmodel; const struct { float mins[3]; float maxs[3]; signed short contents; unsigned short numplanes; } *srcbrush; /* Note to implementors: a pointy brush with angles pointier than 90 degrees will extend further than any adjacent brush, thus creating invisible walls with larger expansions. the engine inserts 6 axial planes acording to the bbox, thus the qbsp need not write any axial planes note that doing it this way probably isn't good if you want to query textures... */ const struct{ vec3_t normal; float dist; } *srcplane; static vec3_t axis[3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; unsigned int br, pl; q2cbrush_t *brush; q2cbrushside_t *sides; //grr! mplane_t *planes; //bulky? size_t lumpsizeremaining; unsigned int numplanes; unsigned int srcver, srcmodelidx, modbrushes, modplanes; srcmodel = BSPX_FindLump(bspx, mod_base, "BRUSHLIST", &lumpsizeremaining); if (!srcmodel) return; while (lumpsizeremaining) { if (lumpsizeremaining < sizeof(*srcmodel)) return; srcver = LittleLong(srcmodel->ver); srcmodelidx = LittleLong(srcmodel->modelnum); modbrushes = LittleLong(srcmodel->numbrushes); modplanes = LittleLong(srcmodel->numplanes); if (srcver != 1 || lumpsizeremaining < sizeof(*srcmodel) + modbrushes*sizeof(*srcmodel) + modplanes*sizeof(*srcplane)) return; lumpsizeremaining -= ((const char*)(srcmodel+1) + modbrushes*sizeof(*srcbrush) + modplanes*sizeof(*srcplane)) - (const char*)srcmodel; if (srcmodelidx > model->numsubmodels) return; brush = ZG_Malloc(&model->memgroup, sizeof(*brush)*modbrushes + sizeof(*sides)*(modbrushes*6+modplanes) + sizeof(*planes)*(modbrushes*6+modplanes)); sides = (void*)(brush + modbrushes); planes = (void*)(sides + modbrushes*6+modplanes); model->submodels[srcmodelidx].brushes = brush; srcbrush = (const void*)(srcmodel+1); for (br = 0; br < modbrushes; br++, srcbrush = (const void*)srcplane) { /*byteswap it all in place*/ brush->absmins[0] = LittleFloat(srcbrush->mins[0]); brush->absmins[1] = LittleFloat(srcbrush->mins[1]); brush->absmins[2] = LittleFloat(srcbrush->mins[2]); brush->absmaxs[0] = LittleFloat(srcbrush->maxs[0]); brush->absmaxs[1] = LittleFloat(srcbrush->maxs[1]); brush->absmaxs[2] = LittleFloat(srcbrush->maxs[2]); numplanes = (unsigned short)LittleShort(srcbrush->numplanes); /*make sure planes don't overflow*/ if (numplanes > modplanes) return; modplanes-=numplanes; /*set up the mbrush from the file*/ brush->contents = Q1BSP_TranslateContents(LittleShort(srcbrush->contents)); brush->brushside = sides; for (srcplane = (const void*)(srcbrush+1); numplanes --> 0; srcplane++) { planes->normal[0] = LittleFloat(srcplane->normal[0]); planes->normal[1] = LittleFloat(srcplane->normal[1]); planes->normal[2] = LittleFloat(srcplane->normal[2]); planes->dist = LittleFloat(srcplane->dist); CategorizePlane(planes); sides->surface = NULL; sides++->plane = planes++; } /*and add axial planes acording to the brush's bbox*/ for (pl = 0; pl < 3; pl++) { VectorCopy(axis[pl], planes->normal); planes->dist = brush->absmaxs[pl]; CategorizePlane(planes); sides->surface = NULL; sides++->plane = planes++; } for (pl = 0; pl < 3; pl++) { VectorNegate(axis[pl], planes->normal); planes->dist = -brush->absmins[pl]; CategorizePlane(planes); sides->surface = NULL; sides++->plane = planes++; } brush->numsides = sides - brush->brushside; brush++; } model->submodels[srcmodelidx].numbrushes = brush-model->submodels[srcmodelidx].brushes; /*move on to the next model*/ srcmodel = (const void*)srcbrush; } } hull_t *Q1BSP_ChooseHull(model_t *model, int forcehullnum, const vec3_t mins, const vec3_t maxs, vec3_t offset) { hull_t *hull; vec3_t size; VectorSubtract (maxs, mins, size); if (forcehullnum >= 1 && forcehullnum <= MAX_MAP_HULLSM && model->hulls[forcehullnum-1].available) hull = &model->hulls[forcehullnum-1]; else { #ifdef HEXEN2 if (model->hulls[5].available) { //choose based on hexen2 sizes. if (size[0] < 3) // Point hull = &model->hulls[0]; else if (size[0] <= 8.1 && model->hulls[4].available) hull = &model->hulls[4]; //Pentacles else if (size[0] <= 32.1 && size[2] <= 28.1) // Half Player hull = &model->hulls[3]; else if (size[0] <= 32.1) // Full Player hull = &model->hulls[1]; else // Golumn hull = &model->hulls[5]; } else #endif { if (size[0] < 3 || !model->hulls[1].available) hull = &model->hulls[0]; else if (size[0] <= 32.1 || !model->hulls[2].available) { if (size[2] < 54.1 && model->hulls[3].available) hull = &model->hulls[3]; // 32x32x36 (half-life's crouch) else if (model->hulls[1].available) hull = &model->hulls[1]; else hull = &model->hulls[0]; } else hull = &model->hulls[2]; } } VectorSubtract (hull->clip_mins, mins, offset); return hull; } qboolean Q1BSP_Trace(model_t *model, int forcehullnum, const framestate_t *framestate, const vec3_t axis[3], const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, qboolean capsule, unsigned int hitcontentsmask, trace_t *trace) { hull_t *hull; vec3_t start_l, end_l; vec3_t offset; memset (trace, 0, sizeof(trace_t)); trace->fraction = 1; trace->allsolid = true; hull = Q1BSP_ChooseHull(model, forcehullnum, mins, maxs, offset); //fix for hexen2 monsters half-walking into walls. // if (forent.flags & FL_MONSTER) // { // offset[0] = 0; // offset[1] = 0; // } if (axis) { vec3_t tmp; VectorSubtract(start, offset, tmp); start_l[0] = DotProduct(tmp, axis[0]); start_l[1] = DotProduct(tmp, axis[1]); start_l[2] = DotProduct(tmp, axis[2]); VectorSubtract(end, offset, tmp); end_l[0] = DotProduct(tmp, axis[0]); end_l[1] = DotProduct(tmp, axis[1]); end_l[2] = DotProduct(tmp, axis[2]); Q1BSP_RecursiveHullCheck(hull, hull->firstclipnode, start_l, end_l, hitcontentsmask, trace); if (trace->fraction == 1) { VectorCopy (end, trace->endpos); } else { vec3_t iaxis[3]; vec3_t norm; Matrix3x3_RM_Invert_Simple((void *)axis, iaxis); VectorCopy(trace->plane.normal, norm); trace->plane.normal[0] = DotProduct(norm, iaxis[0]); trace->plane.normal[1] = DotProduct(norm, iaxis[1]); trace->plane.normal[2] = DotProduct(norm, iaxis[2]); /*just interpolate it, its easier than inverse matrix rotations*/ VectorInterpolate(start, trace->fraction, end, trace->endpos); } } else { VectorSubtract(start, offset, start_l); VectorSubtract(end, offset, end_l); Q1BSP_RecursiveHullCheck(hull, hull->firstclipnode, start_l, end_l, hitcontentsmask, trace); if (trace->fraction == 1) { VectorCopy (end, trace->endpos); } else { VectorAdd (trace->endpos, offset, trace->endpos); } } #ifdef TERRAIN if (model->terrain && trace->fraction) { trace_t hmt; Heightmap_Trace(model, forcehullnum, framestate, axis, start, end, mins, maxs, capsule, hitcontentsmask, &hmt); if (hmt.fraction < trace->fraction) *trace = hmt; } #endif return trace->fraction != 1; } /* Physics functions (common) ============================================================================ Rendering functions (Client only) */ #ifdef HAVE_CLIENT extern int r_dlightframecount; //goes through the nodes marking the surfaces near the dynamic light as lit. void Q1BSP_MarkLights (dlight_t *light, dlightbitmask_t bit, mnode_t *node) { mplane_t *splitplane; float dist; msurface_t *surf; int i; float l, maxdist; int j, s, t; vec3_t impact; vec4_t *lmvecs; float *lmvecscale; if (node->contents < 0) return; splitplane = node->plane; if (splitplane->type < 3) dist = light->origin[splitplane->type] - splitplane->dist; else dist = DotProduct (light->origin, splitplane->normal) - splitplane->dist; if (dist > light->radius) { Q1BSP_MarkLights (light, bit, node->children[0]); return; } if (dist < -light->radius) { Q1BSP_MarkLights (light, bit, node->children[1]); return; } maxdist = light->radius*light->radius; // mark the polygons surf = currentmodel->surfaces + node->firstsurface; for (i=0 ; inumsurfaces ; i++, surf++) { //Yeah, you can blame LordHavoc for this alternate code here. for (j=0 ; j<3 ; j++) impact[j] = light->origin[j] - surf->plane->normal[j]*dist; if (currentmodel->facelmvecs) lmvecs = currentmodel->facelmvecs[surf-currentmodel->surfaces].lmvecs, lmvecscale = currentmodel->facelmvecs[surf-currentmodel->surfaces].lmvecscale; else lmvecs = surf->texinfo->vecs, lmvecscale = surf->texinfo->vecscale; // clamp center of light to corner and check brightness l = DotProduct (impact, lmvecs[0]) + lmvecs[0][3] - surf->texturemins[0]; s = l+0.5;if (s < 0) s = 0;else if (s > surf->extents[0]) s = surf->extents[0]; s = (l - s)*lmvecscale[0]; //FIXME l = DotProduct (impact, lmvecs[1]) + lmvecs[1][3] - surf->texturemins[1]; t = l+0.5;if (t < 0) t = 0;else if (t > surf->extents[1]) t = surf->extents[1]; t = (l - t)*lmvecscale[1]; // compare to minimum light if ((s*s+t*t+dist*dist) < maxdist) { if (surf->dlightframe != r_dlightframecount) { surf->dlightbits = bit; surf->dlightframe = r_dlightframecount; } else surf->dlightbits |= bit; } } Q1BSP_MarkLights (light, bit, node->children[0]); Q1BSP_MarkLights (light, bit, node->children[1]); } //combination of R_AddDynamicLights and R_MarkLights static void Q1BSP_StainNode_r (model_t *model, mnode_t *node, float *parms) { mplane_t *splitplane; float dist; msurface_t *surf; int i; if (node->contents < 0) return; splitplane = node->plane; dist = DotProduct ((parms+1), splitplane->normal) - splitplane->dist; if (dist > (*parms)) { Q1BSP_StainNode_r (model, node->children[0], parms); return; } if (dist < (-*parms)) { Q1BSP_StainNode_r (model, node->children[1], parms); return; } // mark the polygons surf = model->surfaces + node->firstsurface; for (i=0 ; inumsurfaces ; i++, surf++) { if (surf->flags&~(SURF_DRAWALPHA|SURF_DONTWARP|SURF_PLANEBACK)) continue; Surf_StainSurf(model, surf, parms); } Q1BSP_StainNode_r (model, node->children[0], parms); Q1BSP_StainNode_r (model, node->children[1], parms); } static void Q1BSP_StainNode (model_t *model, float *parms) { Q1BSP_StainNode_r(model, model->rootnode, parms); } static qbyte *q1frustumvis; //temp use static int q1_visframecount;//temp use static int q1_framecount; //temp use struct q1bspprv_s { int visframecount; int framecount; int oldviewclusters[2]; }; #define BACKFACE_EPSILON 0.01 static void Q1BSP_RecursiveWorldNode (mnode_t *node, unsigned int clipflags) { int c, side, clipped; mplane_t *plane, *clipplane; msurface_t *surf, **mark; mleaf_t *pleaf; double dot; start: if (node->contents == Q1CONTENTS_SOLID) return; // solid if (node->visframe != q1_visframecount) return; for (c = 0, clipplane = r_refdef.frustum; c < r_refdef.frustum_numworldplanes; c++, clipplane++) { if (!(clipflags & (1 << c))) continue; // don't need to clip against it clipped = BOX_ON_PLANE_SIDE (node->minmaxs, node->minmaxs + 3, clipplane); if (clipped == 2) return; else if (clipped == 1) clipflags -= (1<contents < 0) { pleaf = (mleaf_t *)node; c = (pleaf - cl.worldmodel->leafs)-1; q1frustumvis[c>>3] |= 1<<(c&7); mark = pleaf->firstmarksurface; c = pleaf->nummarksurfaces; if (c) { do { (*mark++)->visframe = q1_framecount; } while (--c); } return; } // node is just a decision point, so go down the apropriate sides // find which side of the node we are on plane = node->plane; switch (plane->type) { case PLANE_X: dot = r_origin[0] - plane->dist; break; case PLANE_Y: dot = r_origin[1] - plane->dist; break; case PLANE_Z: dot = r_origin[2] - plane->dist; break; default: dot = DotProduct (r_origin, plane->normal) - plane->dist; break; } if (dot >= 0) side = 0; else side = 1; // recurse down the children, front side first Q1BSP_RecursiveWorldNode (node->children[side], clipflags); // draw stuff c = node->numsurfaces; if (c) { surf = cl.worldmodel->surfaces + node->firstsurface; if (dot < 0 -BACKFACE_EPSILON) side = SURF_PLANEBACK; else if (dot > BACKFACE_EPSILON) side = 0; { for ( ; c ; c--, surf++) { if (surf->visframe != q1_framecount) continue; if (((dot < 0) ^ !!(surf->flags & SURF_PLANEBACK))) continue; // wrong side Surf_RenderDynamicLightmaps (surf); surf->sbatch->mesh[surf->sbatch->meshes++] = surf->mesh; } } } // recurse down the back side //GLR_RecursiveWorldNode (node->children[!side], clipflags); node = node->children[!side]; goto start; } static void Q1BSP_OrthoRecursiveWorldNode (mnode_t *node, unsigned int clipflags) { //when rendering as ortho the front and back sides are technically equal. the only culling comes from frustum culling. int c, clipped; mplane_t *clipplane; msurface_t *surf, **mark; mleaf_t *pleaf; if (node->contents == Q1CONTENTS_SOLID) return; // solid if (node->visframe != q1_visframecount) return; for (c = 0, clipplane = r_refdef.frustum; c < r_refdef.frustum_numworldplanes; c++, clipplane++) { if (!(clipflags & (1 << c))) continue; // don't need to clip against it clipped = BOX_ON_PLANE_SIDE (node->minmaxs, node->minmaxs + 3, clipplane); if (clipped == 2) return; else if (clipped == 1) clipflags -= (1<contents < 0) { pleaf = (mleaf_t *)node; mark = pleaf->firstmarksurface; c = pleaf->nummarksurfaces; if (c) { do { (*mark++)->visframe = q1_framecount; } while (--c); } return; } // recurse down the children Q1BSP_OrthoRecursiveWorldNode (node->children[0], clipflags); Q1BSP_OrthoRecursiveWorldNode (node->children[1], clipflags); // draw stuff c = node->numsurfaces; if (c) { surf = cl.worldmodel->surfaces + node->firstsurface; for ( ; c ; c--, surf++) { if (surf->visframe != q1_framecount) continue; Surf_RenderDynamicLightmaps (surf); surf->sbatch->mesh[surf->sbatch->meshes++] = surf->mesh; } } return; } static qbyte *Q1BSP_MarkLeaves (model_t *model, int clusters[2]) { static qbyte *cvis; qbyte *vis; mnode_t *node; int i; int portal = r_refdef.recurse; static pvsbuffer_t pvsbuf; struct q1bspprv_s *prv = model->meshinfo; q1_framecount = ++prv->framecount; //for portals to work, we need two sets of any pvs caches //this means lights can still check pvs at the end of the frame despite recursing in the mean time //however, we still need to invalidate the cache because we only have one 'visframe' field in nodes. if (r_refdef.forcevis) { vis = r_refdef.forcedvis; prv->oldviewclusters[0] = -1; prv->oldviewclusters[1] = -2; cvis = NULL; } else { if (!portal) { if (((prv->oldviewclusters[0] == clusters[0] && prv->oldviewclusters[1] == clusters[1]) && !r_novis.ival) || r_novis.ival & 2) if (cvis) { q1_visframecount = prv->visframecount; return cvis; } prv->oldviewclusters[0] = clusters[0]; prv->oldviewclusters[1] = clusters[1]; } else { prv->oldviewclusters[0] = -1; prv->oldviewclusters[1] = -2; cvis = NULL; } if (r_novis.ival) { if (pvsbuf.buffersize < model->pvsbytes) pvsbuf.buffer = BZ_Realloc(pvsbuf.buffer, pvsbuf.buffersize=model->pvsbytes); vis = cvis = pvsbuf.buffer; memset (pvsbuf.buffer, 0xff, pvsbuf.buffersize); prv->oldviewclusters[0] = -1; prv->oldviewclusters[1] = -2; } else { if (clusters[1] >= 0 && clusters[1] != clusters[0]) { vis = cvis = model->funcs.ClusterPVS(model, clusters[0], &pvsbuf, PVM_REPLACE); vis = cvis = model->funcs.ClusterPVS(model, clusters[1], &pvsbuf, PVM_MERGE); } else vis = cvis = model->funcs.ClusterPVS(model, clusters[0], &pvsbuf, PVM_FAST); } } prv->visframecount++; q1_visframecount = prv->visframecount; if (clusters[0] < 0) { //to improve spectating, when the camera is in a wall, we ignore any sky leafs. //this prevents seeing the upwards-facing sky surfaces within the sky volumes. //this will not affect inwards facing sky, so sky will basically appear as though it is identical to solid brushes. for (i=0 ; inumclusters ; i++) { if (vis[i>>3] & (1<<(i&7))) { if (model->leafs[i+1].contents == Q1CONTENTS_SKY) continue; node = (mnode_t *)&model->leafs[i+1]; do { if (node->visframe == q1_visframecount) break; node->visframe = q1_visframecount; node = node->parent; } while (node); } } } else { for (i=0 ; inumclusters ; i++) { if (vis[i>>3] & (1<<(i&7))) { node = (mnode_t *)&model->leafs[i+1]; do { if (node->visframe == q1_visframecount) break; node->visframe = q1_visframecount; node = node->parent; } while (node); } } } return vis; } static void Q1BSP_PrepareFrame(model_t *model, refdef_t *refdef, int area, int clusters[2], pvsbuffer_t *vis, qbyte **entvis_out, qbyte **surfvis_out) { *entvis_out = Q1BSP_MarkLeaves (model, clusters); if (vis->buffersize < model->pvsbytes) vis->buffer = BZ_Realloc(vis->buffer, vis->buffersize=model->pvsbytes); q1frustumvis = vis->buffer; memset(q1frustumvis, 0, model->pvsbytes); if (model != cl.worldmodel) ; //global abuse... else if (r_refdef.useperspective) Q1BSP_RecursiveWorldNode (model->nodes, 0x1f); else Q1BSP_OrthoRecursiveWorldNode (model->nodes, 0x1f); *surfvis_out = q1frustumvis; } #endif /* Rendering functions (Client only) ============================================================================== Server only functions */ #ifdef HAVE_SERVER static qbyte *Q1BSP_ClusterPVS (model_t *model, int cluster, pvsbuffer_t *buffer, pvsmerge_t merge); //does the recursive work of Q1BSP_FatPVS static void SV_Q1BSP_AddToFatPVS (model_t *mod, const vec3_t org, mnode_t *node, pvsbuffer_t *pvsbuffer) { mplane_t *plane; float d; while (1) { // if this is a leaf, accumulate the pvs bits if (node->contents < 0) { if (node->contents != Q1CONTENTS_SOLID) { Q1BSP_ClusterPVS(mod, ((mleaf_t *)node - mod->leafs)-1, pvsbuffer, PVM_MERGE); } return; } plane = node->plane; d = DotProduct (org, plane->normal) - plane->dist; if (d > 8) node = node->children[0]; else if (d < -8) node = node->children[1]; else { // go down both SV_Q1BSP_AddToFatPVS (mod, org, node->children[0], pvsbuffer); node = node->children[1]; } } } /* ============= Q1BSP_FatPVS Calculates a PVS that is the inclusive or of all leafs within 8 pixels of the given point. ============= */ static unsigned int Q1BSP_FatPVS (model_t *mod, const vec3_t org, pvsbuffer_t *pvsbuffer, qboolean add) { if (pvsbuffer->buffersize < mod->pvsbytes) pvsbuffer->buffer = BZ_Realloc(pvsbuffer->buffer, pvsbuffer->buffersize=mod->pvsbytes); if (!add) Q_memset (pvsbuffer->buffer, 0, mod->pvsbytes); SV_Q1BSP_AddToFatPVS (mod, org, mod->nodes, pvsbuffer); return mod->pvsbytes; } #endif static qboolean Q1BSP_EdictInFatPVS(model_t *mod, const struct pvscache_s *ent, const qbyte *pvs, const int *areas) { int i; //if (areas)areas[0] is the area count... but q1bsp has no areas so we ignore it entirely. if (ent->num_leafs < 0) return true; //it's in too many leafs for us to cope with. Just trivially accept it. for (i=0 ; i < ent->num_leafs ; i++) if (pvs[ent->leafnums[i] >> 3] & (1 << (ent->leafnums[i]&7) )) return true; //we might be able to see this one. return false; //none of this ents leafs were visible, so neither is the ent. } /* =============== SV_FindTouchedLeafs Links the edict to the right leafs so we can get it's potential visability. =============== */ static void Q1BSP_RFindTouchedLeafs (model_t *wm, struct pvscache_s *ent, mnode_t *node, const float *mins, const float *maxs) { mplane_t *splitplane; mleaf_t *leaf; int sides; int leafnum; // add an efrag if the node is a leaf if (node->contents < 0) { //ignore solid leafs. this should include leaf 0 (which has no pvs info) if (node->contents == Q1CONTENTS_SOLID) return; if ((unsigned)ent->num_leafs >= MAX_ENT_LEAFS) { ent->num_leafs = -1; //too many. mark it as such so we can trivially accept huge mega-big brush models. return; } leaf = (mleaf_t *)node; leafnum = leaf - wm->leafs - 1; ent->leafnums[ent->num_leafs] = leafnum; ent->num_leafs++; return; } // NODE_MIXED splitplane = node->plane; sides = BOX_ON_PLANE_SIDE(mins, maxs, splitplane); // recurse down the contacted sides if (sides & 1) Q1BSP_RFindTouchedLeafs (wm, ent, node->children[0], mins, maxs); if (sides & 2) Q1BSP_RFindTouchedLeafs (wm, ent, node->children[1], mins, maxs); } static void Q1BSP_FindTouchedLeafs(model_t *mod, struct pvscache_s *ent, const float *mins, const float *maxs) { ent->num_leafs = 0; if (mins && maxs) Q1BSP_RFindTouchedLeafs (mod, ent, mod->nodes, mins, maxs); } /* Server only functions ============================================================================== PVS type stuff */ /* =================== Mod_DecompressVis =================== */ static qbyte *Q1BSP_DecompressVis (qbyte *in, model_t *model, qbyte *decompressed, unsigned int buffersize, qboolean merge) { int c; qbyte *out; int row; row = (model->numclusters+7)>>3; out = decompressed; if (buffersize < row) row = buffersize; if (!in) { // no vis info, so make all visible while (row) { *out++ = 0xff; row--; } return decompressed; } if (merge) { do { if (*in) { *out++ |= *in++; continue; } out += in[1]; in += 2; } while (out - decompressed < row); } else { do { if (*in) { *out++ = *in++; continue; } c = in[1]; in += 2; if ((out - decompressed) + c > row) { c = row - (out - decompressed); Con_DPrintf ("warning: Vis decompression overrun\n"); } while (c) { *out++ = 0; c--; } } while (out - decompressed < row); } return decompressed; } static pvsbuffer_t mod_novis; static pvsbuffer_t mod_tempvis; void Q1BSP_Shutdown(void) { Z_Free(mod_novis.buffer); memset(&mod_novis, 0, sizeof(mod_novis)); Z_Free(mod_tempvis.buffer); memset(&mod_tempvis, 0, sizeof(mod_tempvis)); } //pvs is 1-based. clusters are 0-based. otherwise, q1bsp has a 1:1 mapping. static qbyte *Q1BSP_ClusterPVS (model_t *model, int cluster, pvsbuffer_t *buffer, pvsmerge_t merge) { if (cluster == -1) { if (merge == PVM_FAST) { if (mod_novis.buffersize < model->pvsbytes) { mod_novis.buffer = BZ_Realloc(mod_novis.buffer, mod_novis.buffersize=model->pvsbytes); memset(mod_novis.buffer, 0xff, mod_novis.buffersize); } return mod_novis.buffer; } if (buffer->buffersize < model->pvsbytes) buffer->buffer = BZ_Realloc(buffer->buffer, buffer->buffersize=model->pvsbytes); memset(buffer->buffer, 0xff, model->pvsbytes); return buffer->buffer; } if (merge == PVM_FAST && model->pvs) return model->pvs + cluster * model->pvsbytes; cluster++; if (!buffer) buffer = &mod_tempvis; if (buffer->buffersize < model->pvsbytes) buffer->buffer = BZ_Realloc(buffer->buffer, buffer->buffersize=model->pvsbytes); return Q1BSP_DecompressVis (model->leafs[cluster].compressed_vis, model, buffer->buffer, buffer->buffersize, merge==PVM_MERGE); } static qbyte *Q1BSP_ClusterPHS (model_t *model, int cluster, pvsbuffer_t *buffer) { return model->phs + cluster*model->pvsbytes; } //returns the leaf number, which is used as a bit index into the pvs. static int Q1BSP_LeafnumForPoint (model_t *model, vec3_t p) { mnode_t *node; float d; mplane_t *plane; if (!model) { Sys_Error ("Q1BSP_LeafnumForPoint: bad model"); } if (!model->nodes) return 0; node = model->nodes; while (1) { if (node->contents < 0) return (mleaf_t *)node - model->leafs; plane = node->plane; d = DotProduct (p,plane->normal) - plane->dist; if (d > 0) node = node->children[0]; else node = node->children[1]; } return 0; // never reached } mleaf_t *Q1BSP_LeafForPoint (model_t *model, vec3_t p) { return model->leafs + Q1BSP_LeafnumForPoint(model, p); } static void Q1BSP_ClustersInSphere_Union(mleaf_t *firstleaf, const vec3_t center, float radius, mnode_t *node, qbyte *out, qbyte *unionwith) { //this is really for rtlights. float t1, t2; mplane_t *plane; while (1) { if (node->contents < 0) { //leaf! mark/merge it. size_t c = (mleaf_t *)node - firstleaf; if (c == -1) return; if (unionwith) out[c>>3] |= (1<<(c&7)) & unionwith[c>>3]; else out[c>>3] |= (1<<(c&7)); return; } plane = node->plane; if (plane->type < 3) t1 = center[plane->type] - plane->dist; else t1 = DotProduct (plane->normal, center) - plane->dist; t2 = t1 - radius; t1 = t1 + radius; //if the sphere is fully to one side, only walk that side. if (t1 > 0 && t2 > 0) { node = node->children[0]; continue; } if (t1 < 0 && t2 < 0) { node = node->children[1]; continue; } //both sides are within the sphere Q1BSP_ClustersInSphere_Union(firstleaf, center, radius, node->children[0], out, unionwith); node = node->children[1]; continue; } } static qbyte *Q1BSP_ClustersInSphere(model_t *mod, const vec3_t center, float radius, pvsbuffer_t *fte_restrict pvsbuffer, const qbyte *fte_restrict unionwith) { if (!mod) Sys_Error ("Q1BSP_ClustersInSphere: bad model"); if (!mod->nodes) return NULL; if (pvsbuffer->buffersize < mod->pvsbytes) pvsbuffer->buffer = BZ_Realloc(pvsbuffer->buffer, pvsbuffer->buffersize=mod->pvsbytes); Q_memset (pvsbuffer->buffer, 0, mod->pvsbytes); Q1BSP_ClustersInSphere_Union(mod->leafs+1, center, radius, mod->nodes, pvsbuffer->buffer, NULL);//unionwith); return pvsbuffer->buffer; } //returns the leaf number, which is used as a direct bit index into the pvs. //-1 for invalid static int Q1BSP_ClusterForPoint (model_t *model, const vec3_t p, int *area) { mnode_t *node; float d; mplane_t *plane; if (!model) { Sys_Error ("Q1BSP_ClusterForPoint: bad model"); } if (area) *area = 0; //no areas with q1bsp. if (!model->nodes) return -1; node = model->nodes; while (1) { if (node->contents < 0) return ((mleaf_t *)node - model->leafs) - 1; plane = node->plane; d = DotProduct (p,plane->normal) - plane->dist; if (d > 0) node = node->children[0]; else node = node->children[1]; } return -1; // never reached } static void Q1BSP_InfoForPoint (struct model_s *mod, vec3_t pos, int *area, int *cluster, unsigned int *contentbits) { mnode_t *node; float d; mplane_t *plane; *area = 0; //no areas with q1bsp. *cluster = -1; *contentbits = FTECONTENTS_SOLID; if (!mod->nodes) return; node = mod->nodes; while (1) { if (node->contents < 0) { *cluster = ((mleaf_t *)node - mod->leafs) - 1; *contentbits = Q1BSP_TranslateContents(((mleaf_t *)node)->contents); return; //we're done } plane = node->plane; d = DotProduct (pos,plane->normal) - plane->dist; if (d > 0) node = node->children[0]; else node = node->children[1]; } } /* PVS type stuff ============================================================================== Init stuff */ void Q1BSP_Init(void) { } //sets up the functions a server needs. //fills in bspfuncs_t void Q1BSP_SetModelFuncs(model_t *mod) { #ifdef HAVE_SERVER mod->funcs.FatPVS = Q1BSP_FatPVS; #endif mod->funcs.EdictInFatPVS = Q1BSP_EdictInFatPVS; mod->funcs.FindTouchedLeafs = Q1BSP_FindTouchedLeafs; mod->funcs.ClustersInSphere = Q1BSP_ClustersInSphere; mod->funcs.ClusterForPoint = Q1BSP_ClusterForPoint; mod->funcs.ClusterPVS = Q1BSP_ClusterPVS; mod->funcs.ClusterPHS = Q1BSP_ClusterPHS; mod->funcs.NativeTrace = Q1BSP_Trace; mod->funcs.PointContents = Q1BSP_PointContents; mod->funcs.InfoForPoint = Q1BSP_InfoForPoint; #ifdef HAVE_CLIENT mod->funcs.LightPointValues = GLQ1BSP_LightPointValues; mod->funcs.MarkLights = Q1BSP_MarkLights; mod->funcs.StainNode = Q1BSP_StainNode; #ifdef RTLIGHTS mod->funcs.GenerateShadowMesh = Q1BSP_GenerateShadowMesh; #endif mod->funcs.PrepareFrame = Q1BSP_PrepareFrame; { struct q1bspprv_s *prv = mod->meshinfo = ZG_Malloc(&mod->memgroup, sizeof(struct q1bspprv_s)); prv->oldviewclusters[0] = -1; //make sure its reset properly. prv->oldviewclusters[1] = -1; } #endif } #endif /* Init stuff ============================================================================== BSPX Stuff */ #include "fs.h" typedef struct { char lumpname[24]; // up to 23 chars, zero-padded unsigned int fileofs; // from file start unsigned int filelen; } bspx_lump_t; struct bspx_header_s { char id[4]; // 'BSPX' unsigned int numlumps; bspx_lump_t lumps[1]; }; //supported lumps (read specs/bspx.txt for more details): //RGBLIGHTING (.lit) //LIGHTING_E5BGR9 (hdr lit) //LIGHTINGDIR (.lux) //LMSHIFT (lightmap scaling, obsoleted bby DECOUPLED_LM) //LMOFFSET (lightmap scaling, redundant without LMSHIFT) //LMSTYLE (for when 4 styles per face are not enough) //LMSTYLE16 (for when you need more than 256 different lightswitches) //VERTEXNORMALS (smooth specular) //BRUSHLIST (no hull size issues) //ENVMAP (cubemaps) //SURFENVMAP (cubemaps) //FACENORMALS (because Quetoo's normals were rejected by ericw for some reason) //DECOUPLED_LM (upgraded alternative to LM_SHIFT with float scaling and explicit lm sizes) //LIGHTGRID_OCTREE (lightgrid alternative to floor-based model lighting, but still stuck with 4 8bit ldr styles) void *BSPX_FindLump(bspx_header_t *bspxheader, void *mod_base, char *lumpname, size_t *lumpsize) { int i; *lumpsize = 0; if (!bspxheader) return NULL; for (i = 0; i < bspxheader->numlumps; i++) { if (!strncmp(bspxheader->lumps[i].lumpname, lumpname, 24)) { *lumpsize = bspxheader->lumps[i].filelen; return (char*)mod_base + bspxheader->lumps[i].fileofs; } } return NULL; } bspx_header_t *BSPX_Setup(model_t *mod, char *filebase, size_t filelen, lump_t *lumps, size_t numlumps) { size_t i; size_t offs = 0; bspx_header_t *h; for (i = 0; i < numlumps; i++, lumps++) { if (offs < lumps->fileofs + lumps->filelen) offs = lumps->fileofs + lumps->filelen; } offs = (offs + 3) & ~3; if (offs + sizeof(*h) > filelen) h = NULL; /*no space for it*/ else { h = (bspx_header_t*)(filebase + offs); i = LittleLong(h->numlumps); /*verify the header*/ if (*(int*)h->id != (('B'<<0)|('S'<<8)|('P'<<16)|('X'<<24)) || i < 0 || offs + sizeof(*h) + sizeof(h->lumps[0])*(i-1) > filelen) h = NULL; else { h->numlumps = i; while(i-->0) { h->lumps[i].fileofs = LittleLong(h->lumps[i].fileofs); h->lumps[i].filelen = LittleLong(h->lumps[i].filelen); if (h->lumps[i].fileofs + h->lumps[i].filelen > filelen) return NULL; //some sort of corruption/truncation. if (offs < h->lumps[i].fileofs + h->lumps[i].filelen) offs = h->lumps[i].fileofs + h->lumps[i].filelen; } } } if (offs < filelen && mod && !mod->archive && mod_loadmappackages.ival && filelen-offs > 22)//end-of-central-dir being 22 bytes sets a minimum zip size, which should slightly reduce false-positives. { //we have some sort of trailing junk... is it a zip?... Mod_LoadMapArchive(mod, filebase+offs, filelen-offs); } return h; } #ifndef HAVE_CLIENT void BSPX_LoadEnvmaps(model_t *mod, bspx_header_t *bspx, void *mod_base) { } #else /* void *SCR_ScreenShot_Capture(int fbwidth, int fbheight, int *stride, enum uploadfmt *fmt); void BSPX_RenderEnvmaps(model_t *mod) { int c, i; void *buffer; int stride, cubesize; uploadfmt_t fmt; char filename[MAX_QPATH]; char olddrawviewmodel[64]; //hack, so we can set r_drawviewmodel to 0 so that it doesn't appear in screenshots even if the csqc is generating new data. vec3_t oldangles; const struct { vec3_t angle; const char *postfix; qboolean verticalflip; qboolean horizontalflip; } sides[] = { {{0, 0, 90}, "_px", true}, {{0, 180, -90}, "_nx", true}, {{0, 90, 0}, "_py", true}, //upside down {{0, 270, 0}, "_ny", false, true}, {{-90, 0, 90}, "_pz", true}, {{90, 0, 90}, "_nz", true}, }; char base[MAX_QPATH]; COM_FileBase(cl.worldmodel->name, base, sizeof(base)); r_refdef.stereomethod = STEREO_OFF; Q_strncpyz(olddrawviewmodel, r_drawviewmodel.string, sizeof(olddrawviewmodel)); Cvar_Set(&r_drawviewmodel, "0"); VectorCopy(cl.playerview->viewangles, oldangles); for (c = 0; c < mod->numenvmaps; c++) { cubesize = mod->envmaps[c].cubesize; if (cubesize < 1) cubesize = 32; VectorCopy(mod->envmaps[c].origin, r_refdef.vieworg); for (i = 0; i < 6; i++) { Q_snprintfz(filename, sizeof(filename), "%s/%i_%i_%i%s.tga", base, (int)mod->envmaps[c].origin[0], (int)mod->envmaps[c].origin[1], (int)mod->envmaps[c].origin[2], sides[i].postfix); VectorCopy(sides[i].angle, cl.playerview->simangles); VectorCopy(cl.playerview->simangles, cl.playerview->viewangles); buffer = SCR_ScreenShot_Capture(cubesize, cubesize, &stride, &fmt); if (buffer) { char sysname[1024]; if (sides[i].horizontalflip) { int y, x, p; int pxsize; char *bad = buffer; char *in = buffer, *out; switch(fmt) { case TF_RGBA32: case TF_BGRA32: case TF_RGBX32: case TF_BGRX32: pxsize = 4; break; case TF_RGB24: case TF_BGR24: pxsize = 3; break; case PTI_RGBA16F: pxsize = 8; break; case PTI_RGBA32F: pxsize = 16; break; default: //erk! pxsize = 1; break; } buffer = out = BZ_Malloc(cubesize*cubesize*pxsize); for (y = 0; y < cubesize; y++, in += abs(stride), out += cubesize*pxsize) { for (x = 0; x < cubesize*pxsize; x+=pxsize) { for (p = 0; p < pxsize; p++) out[x+p] = in[(cubesize-1)*pxsize-x+p]; } } BZ_Free(bad); if (stride < 0) stride = -cubesize*pxsize; else stride = cubesize*pxsize; } if (sides[i].verticalflip) stride = -stride; if (SCR_ScreenShot(filename, FS_GAMEONLY, &buffer, 1, stride, cubesize, cubesize, fmt)) { FS_SystemPath(filename, FS_GAMEONLY, sysname, sizeof(sysname)); Con_Printf ("Wrote %s\n", sysname); } else { FS_SystemPath(filename, FS_GAMEONLY, sysname, sizeof(sysname)); Con_Printf ("Failed to write %s\n", sysname); } BZ_Free(buffer); } } } Cvar_Set(&r_drawviewmodel, olddrawviewmodel); VectorCopy(oldangles, cl.playerview->viewangles); } */ void BSPX_LoadEnvmaps(model_t *mod, bspx_header_t *bspx, void *mod_base) { unsigned int *envidx, idx; size_t i; char base[MAX_QPATH]; char imagename[MAX_QPATH]; menvmap_t *out; size_t count; denvmap_t *in = BSPX_FindLump(bspx, mod_base, "ENVMAP", &count); mod->envmaps = NULL; mod->numenvmaps = 0; if (!mod_loadsurfenvmaps.ival) return; if (count%sizeof(*in)) return; //erk count /= sizeof(*in); if (!count) return; out = ZG_Malloc(&mod->memgroup, sizeof(*out)*count); mod->envmaps = out; mod->numenvmaps = count; COM_FileBase(mod->name, base, sizeof(base)); for (i = 0; i < count; i++) { out[i].origin[0] = LittleFloat(in[i].origin[0]); out[i].origin[1] = LittleFloat(in[i].origin[1]); out[i].origin[2] = LittleFloat(in[i].origin[2]); out[i].cubesize = LittleLong(in[i].cubesize); Q_snprintfz(imagename, sizeof(imagename), "textures/env/%s_%i_%i_%i", base, (int)mod->envmaps[i].origin[0], (int)mod->envmaps[i].origin[1], (int)mod->envmaps[i].origin[2]); out[i].image = Image_GetTexture(imagename, NULL, IF_TEXTYPE_CUBE|IF_NOREPLACE, NULL, NULL, out[i].cubesize, out[i].cubesize, PTI_INVALID); } //now update surface lists. envidx = BSPX_FindLump(bspx, mod_base, "SURFENVMAP", &i); if (i/sizeof(*envidx) == mod->numsurfaces) { for (i = 0; i < mod->numsurfaces; i++) { idx = LittleLong(envidx[i]); if (idx < (unsigned int)count) mod->surfaces[i].envmap = out[idx].image; } } } struct bspxrw { const char *fname; char *origfile; qofs_t origsize; int lumpofs; fromgame_t fg; size_t corelumps; size_t totallumps; void *archivedata; qofs_t archivesize; struct { char lumpname[24]; // up to 23 chars, zero-padded void *data; // from file start qofs_t filelen; } *lumps; }; static void Mod_BSPXRW_Free(struct bspxrw *ctx) { FS_FreeFile(ctx->origfile); Z_Free(ctx->lumps); ctx->corelumps = ctx->totallumps = 0; ctx->origfile = NULL; } static void Mod_BSPXRW_Write(struct bspxrw *ctx) { #if 1 vfsfile_t *f = FS_OpenVFS(ctx->fname, "wb", FS_GAMEONLY); if (f) { qofs_t bspxofs; size_t i, j; int pad, paddata = 0; int nxlumps = ctx->totallumps-ctx->corelumps; lump_t *lumps = alloca(sizeof(*lumps)*ctx->corelumps); bspx_lump_t *xlumps = alloca(sizeof(*xlumps)*(ctx->totallumps-ctx->corelumps)); //bsp header info VFS_WRITE(f, ctx->origfile, ctx->lumpofs); VFS_WRITE(f, lumps, sizeof(lumps[0])*ctx->corelumps); //placeholder //orig lumps for (i = 0; i < ctx->corelumps; i++) { lumps[i].fileofs = VFS_TELL(f); lumps[i].filelen = ctx->lumps[i].filelen; VFS_WRITE(f, ctx->lumps[i].data, ctx->lumps[i].filelen); //ALL lumps must be 4-aligned, so pad if needed. pad = ((ctx->lumps[i].filelen+3)&~3)-ctx->lumps[i].filelen; VFS_WRITE(f, &paddata, pad); } //bspx header VFS_WRITE(f, "BSPX", 4); VFS_WRITE(f, &nxlumps, sizeof(nxlumps)); bspxofs = VFS_TELL(f); VFS_WRITE(f, xlumps, sizeof(xlumps[0])*(ctx->totallumps-ctx->corelumps)); //placeholder //bspx data for (i = 0; i < nxlumps; i++) { j = ctx->corelumps+i; xlumps[i].fileofs = VFS_TELL(f); xlumps[i].filelen = ctx->lumps[j].filelen; memcpy(xlumps[i].lumpname, ctx->lumps[j].lumpname, sizeof(xlumps[i].lumpname)); VFS_WRITE(f, ctx->lumps[j].data, ctx->lumps[j].filelen); //ALL lumps must be 4-aligned, so pad if needed. pad = ((ctx->lumps[j].filelen+3)&~3)-ctx->lumps[j].filelen; VFS_WRITE(f, &paddata, pad); } //now reinsert any concatenated zip. VFS_WRITE(f, ctx->archivedata, ctx->archivesize); //now rewrite both sets of offsets. VFS_SEEK(f, ctx->lumpofs); VFS_WRITE(f, lumps, sizeof(lumps[0])*ctx->corelumps); VFS_SEEK(f, bspxofs); VFS_WRITE(f, xlumps, sizeof(xlumps[0])*(ctx->totallumps-ctx->corelumps)); VFS_CLOSE(f); } #endif Mod_BSPXRW_Free(ctx); } static qboolean Mod_BSPXRW_SetLump(struct bspxrw *ctx, const char *lumpname, void *data, size_t datasize) { int i; for (i = 0; i < ctx->totallumps; i++) { if (!strcmp(ctx->lumps[i].lumpname, lumpname)) { //replace the existing lump if (ctx->lumps[i].filelen == datasize && !memcmp(ctx->lumps[i].data, data, datasize)) return false; //nothing changed. ctx->lumps[i].data = data; ctx->lumps[i].filelen = datasize; return true; } } Z_ReallocElements((void**)&ctx->lumps, &ctx->totallumps, ctx->totallumps+1, sizeof(*ctx->lumps)); Q_strncpyz(ctx->lumps[i].lumpname, lumpname, sizeof(ctx->lumps[i].lumpname)); ctx->lumps[i].data = data; ctx->lumps[i].filelen = datasize; return true; } static qboolean Mod_BSPXRW_Read(struct bspxrw *ctx, const char *fname) { int i; lump_t *l; const char **corelumpnames = NULL; bspx_header_t *bspxheader; #ifdef Q3BSPS static const char *q3corelumpnames[Q3LUMPS_TOTAL] = {"entities","shaders","planes","nodes","leafs","leafsurfs","leafbrushes","submodels","brushes","brushsides","verts","indexes","fogs","surfaces","lightmaps","lightgrid","visibility" #ifdef RFBSPS ,"lightgrididx" #endif }; #endif #ifdef Q2BSPS static const char *q2corelumpnames[Q2HEADER_LUMPS] = {"entities","planes","vertexes","visibility","nodes","texinfo","faces","lighting","leafs","leaffaces","leafbrushes","edges","surfedges","models","brushes","brushsides","pop","areas","areaportals"}; #endif static const char *q1corelumpnames[HEADER_LUMPS] = {"entities","planes","textures","vertexes","visibility","nodes","texinfo","faces","lighting","clipnodes","leafs","marksurfaces","edges","surfedges","models"}; ctx->archivedata = NULL; ctx->archivesize = 0; ctx->fname = fname; ctx->origfile = FS_MallocFile(ctx->fname, FS_GAME, &ctx->origsize); if (!ctx->origfile) return false; ctx->lumps = 0; ctx->totallumps = 0; if (ctx->origsize < 4) i = 0; else i = LittleLong(*(int*)ctx->origfile); switch(i) { case 29: case 30: ctx->fg = ((i==30)?fg_halflife:fg_quake); ctx->lumpofs = 4; ctx->corelumps = HEADER_LUMPS; corelumpnames = q1corelumpnames; break; case ('I'<<0)+('B'<<8)+('S'<<16)+('P'<<24): //starting with q2. i = LittleLong(*(int*)(ctx->origfile+4)); ctx->lumpofs = 8; switch(i) { #ifdef Q2BSPS case BSPVERSION_Q2: // case BSPVERSION_Q2W: ctx->fg = fg_quake2; ctx->corelumps = Q2HEADER_LUMPS; corelumpnames = q2corelumpnames; break; #endif #ifdef Q3BSPS case BSPVERSION_Q3: case BSPVERSION_RTCW: ctx->fg = fg_quake3; ctx->corelumps = 17; corelumpnames = q3corelumpnames; break; #endif default: Mod_BSPXRW_Free(ctx); Con_Printf(CON_ERROR"%s: Unknown 'IBSP' revision\n", fname); return false; } break; #ifdef RFBSPS case ('R'<<0)+('B'<<8)+('S'<<16)+('P'<<24): case ('F'<<0)+('B'<<8)+('S'<<16)+('P'<<24): i = LittleLong(*(int*)(ctx->origfile+4)); ctx->lumpofs = 8; switch(i) { case BSPVERSION_RBSP: //both rbsp+fbsp are version 1, with the only difference being lightmap sizes. ctx->fg = fg_quake3; ctx->corelumps = 18; corelumpnames = q3corelumpnames; break; default: Mod_BSPXRW_Free(ctx); Con_Printf(CON_ERROR"%s: Unknown 'RBSP'/'FBSP' revision\n", fname); return false; } break; #endif default: Mod_BSPXRW_Free(ctx); Con_Printf(CON_ERROR"%s: Unknown file magic\n", fname); return false; } l = (lump_t*)(ctx->origfile+ctx->lumpofs); for (i = 0; i < ctx->corelumps; i++) { Z_ReallocElements((void**)&ctx->lumps, &ctx->totallumps, ctx->totallumps+1, sizeof(*ctx->lumps)); ctx->lumps[ctx->totallumps-1].data = ctx->origfile+l[i].fileofs; ctx->lumps[ctx->totallumps-1].filelen = l[i].filelen; if (corelumpnames) Q_snprintfz(ctx->lumps[ctx->totallumps-1].lumpname, sizeof(ctx->lumps[0].lumpname), "%s", corelumpnames[i]); else Q_snprintfz(ctx->lumps[ctx->totallumps-1].lumpname, sizeof(ctx->lumps[0].lumpname), "lump%u", i); } bspxheader = BSPX_Setup(NULL, ctx->origfile, ctx->origsize, l, ctx->corelumps); if (bspxheader) { for (i = 0; i < bspxheader->numlumps; i++) { Z_ReallocElements((void**)&ctx->lumps, &ctx->totallumps, ctx->totallumps+1, sizeof(*ctx->lumps)); ctx->lumps[ctx->totallumps-1].data = ctx->origfile+bspxheader->lumps[i].fileofs; ctx->lumps[ctx->totallumps-1].filelen = bspxheader->lumps[i].filelen; memcpy(ctx->lumps[ctx->totallumps-1].lumpname, bspxheader->lumps[i].lumpname, sizeof(ctx->lumps[0].lumpname)); } } return true; } static unsigned int Mod_NearestCubeForSurf(msurface_t *surf, denvmap_t *envmap, size_t nenvmap) { //this is slow, yes. size_t n, v; unsigned int best = ~0; float bestdist = FLT_MAX, dist; vec3_t diff, mins, maxs, mid; if (surf->mesh && surf->mesh->numvertexes) { VectorCopy(surf->mesh->xyz_array[0], mins); VectorCopy(surf->mesh->xyz_array[0], maxs); for (v = 1; v < surf->mesh->numvertexes; v++) AddPointToBounds(surf->mesh->xyz_array[v], mins, maxs); VectorAvg(mins, maxs, mid); for (n = 0; n < nenvmap; n++) { VectorSubtract(envmap[n].origin, mid, diff); #if 0 //axial distance dist = fabs(diff[0]) + fabs(diff[1]) + fabs(diff[2]); #else //radial distance (squared) dist = DotProduct(diff,diff); #endif if (bestdist > dist) { best = n; bestdist = dist; } } } return best; } static int QDECL envmapsort(const void *av, const void *bv) { //sorts cubemaps in order of size, to make texturearrays easier, if ever. The loader can then just make runs. const denvmap_t *a=av, *b=bv; if (a->cubesize == b->cubesize) return 0; if (a->cubesize > b->cubesize) return 1; return -1; } static void Mod_FindCubemaps_f(void) { struct bspxrw bspctx; if (Mod_BSPXRW_Read(&bspctx, cl.worldmodel->name)) { const char *entlump = Mod_GetEntitiesString(cl.worldmodel), *lmp; int nest; char key[1024]; char value[1024]; qboolean isenvmap; float size; vec3_t origin; denvmap_t *envmap = NULL; //*nenvmap size_t nenvmap = 0; unsigned int *envmapidx = NULL; //*numsurfaces size_t nenvmapidx = 0, i; //find targetnames, and store their origins so that we can deal with spotlights. for (lmp = entlump; ;) { lmp = COM_Parse(lmp); if (com_token[0] != '{') break; isenvmap = false; size = 128; VectorClear(origin); nest = 1; while (1) { lmp = COM_ParseOut(lmp, key, sizeof(key)); if (!lmp) break; // error if (key[0] == '{') { nest++; continue; } if (key[0] == '}') { nest--; if (!nest) break; // end of entity continue; } if (nest!=1) continue; if (key[0] == '_') memmove(key, key+1, strlen(key)); while (key[strlen(key)-1] == ' ') // remove trailing spaces key[strlen(key)-1] = 0; lmp = COM_ParseOut(lmp, value, sizeof(value)); if (!lmp) break; // error // now that we have the key pair worked out... if (!strcmp("classname", key) && !strcmp(value, "env_cubemap")) isenvmap = true; else if (!strcmp("origin", key)) sscanf(value, "%f %f %f", &origin[0], &origin[1], &origin[2]); else if (!strcmp("size", key)) sscanf(value, "%f", &size); } if (isenvmap) { int e = nenvmap; if (ZF_ReallocElements((void**)&envmap, &nenvmap, nenvmap+1, sizeof(*envmap))) { VectorCopy(origin, envmap[e].origin); envmap[e].cubesize = size; } } } if (nenvmap) { qsort(envmap, nenvmap, sizeof(*envmap), envmapsort); //sort them by size if (ZF_ReallocElements((void**)&envmapidx, &nenvmapidx, cl.worldmodel->numsurfaces, sizeof(*envmapidx))) { for(i = 0; i < cl.worldmodel->numsurfaces; i++) envmapidx[i] = Mod_NearestCubeForSurf(cl.worldmodel->surfaces+i, envmap, nenvmap); } Mod_BSPXRW_SetLump(&bspctx, "ENVMAP", envmap, nenvmap*sizeof(*envmap)); Mod_BSPXRW_SetLump(&bspctx, "SURFENVMAP", envmapidx, cl.worldmodel->numsurfaces*sizeof(*envmapidx)); Mod_BSPXRW_Write(&bspctx); } else { Con_Printf("No cubemaps found on map\n"); Mod_BSPXRW_Free(&bspctx); } Z_Free(envmapidx); Z_Free(envmap); } } static void Mod_Realign_f(void) { struct bspxrw bspctx; if (Mod_BSPXRW_Read(&bspctx, cl.worldmodel->name)) Mod_BSPXRW_Write(&bspctx); } static searchpathfuncs_t *Mod_BSPX_List_ArchiveFile_handle; static void QDECL Mod_BSPX_List_ArchiveFile(int depth, const char *fname, fsbucket_t *filehandle, void *pathhandle) { searchpathfuncs_t *archive = Mod_BSPX_List_ArchiveFile_handle; flocation_t loc; time_t mtime; char timestr[MAX_QPATH], sizestr[MAX_QPATH]; archive->FindFile(archive, &loc, fname, pathhandle); archive->FileStat(archive, &loc, &mtime); *timestr = 0; strftime(timestr,sizeof(timestr), "%Y-%m-%d %H:%M:%S", localtime(&mtime)); Con_Printf("\t\t%20s: %-12s %s\n", fname, FS_AbbreviateSize(sizestr,sizeof(sizestr), loc.len), timestr); } static void Mod_BSPX_List_f(void) { int i; struct bspxrw ctx; char *fname = Cmd_Argv(1); if (!*fname && cl.worldmodel) fname = cl.worldmodel->name; if (Mod_BSPXRW_Read(&ctx, fname)) { Con_Printf("%s:\n", fname); for (i = 0; i < ctx.totallumps; i++) Con_Printf("\t%20s: %-12"PRIuQOFS" csum:%08x\n", ctx.lumps[i].lumpname, ctx.lumps[i].filelen, LittleLong (CalcHashInt(&hash_md4, ctx.lumps[i].data, ctx.lumps[i].filelen))); if (ctx.archivesize) { searchpathfuncs_t *f; vfsfile_t *tmp = VFSPIPE_Open(1,true); if (tmp) { VFS_WRITE(tmp, ctx.archivedata, ctx.archivesize); f = FSZIP_LoadArchive(tmp, NULL, ctx.fname, ctx.fname, NULL); if (!f) VFS_CLOSE(tmp); else { Con_Printf("\t%20s: %-12"PRIuQOFS"\n", "archive", ctx.archivesize); Mod_BSPX_List_ArchiveFile_handle = f; f->BuildHash(f, 0, Mod_BSPX_List_ArchiveFile); f->ClosePath(f); } } } Mod_BSPXRW_Free(&ctx); } } void Mod_BSPX_Strip_f(void) { int i; struct bspxrw ctx; qboolean found = false; if (Cmd_Argc() != 3) Con_Printf("%s FILENAME NAME: removes an extended lump from the named bsp file\n", Cmd_Argv(0)); else if (Mod_BSPXRW_Read(&ctx, Cmd_Argv(1))) { for (i = ctx.corelumps; i < ctx.totallumps;) { if (!Q_strcasecmp(ctx.lumps[i].lumpname, Cmd_Argv(2))) { found = true; memmove(&ctx.lumps[i], &ctx.lumps[i+1], sizeof(ctx.lumps[0])*(ctx.totallumps-(i+1))); ctx.totallumps--; } else i++; } if (found) Mod_BSPXRW_Write(&ctx); else Mod_BSPXRW_Free(&ctx); } } static qbyte *Mod_BSPX_Injest_Read(qofs_t *sz, char *name, char *fnamefmt, ...) { va_list argptr; char fname[MAX_QPATH]; int i; va_start (argptr, fnamefmt); if (Q_vsnprintfz (fname,sizeof(fname), fnamefmt,argptr)) *fname = 0; va_end (argptr); if (!*fname) return NULL; for (i = 2; i < Cmd_Argc(); i++) { if (!Q_strcasecmp(Cmd_Argv(i), "all") || !Q_strcasecmp(Cmd_Argv(i), name)) return FS_MallocFile(fname, FS_GAME, sz); } Con_Printf("%s not requested\n", name); return NULL; } static void Mod_BSPX_Injest_f(void) { qbyte *tmp; struct bspxrw ctx; qboolean found = false; qofs_t sz; if (Cmd_Argc() <= 2) Con_Printf("%s FILENAME : inserts external supplementary resources into the bsp. "CON_WARNING"REMEMBER TO BACK-UP FIRST!\n", Cmd_Argv(0)); else if (Mod_BSPXRW_Read(&ctx, Cmd_Argv(1))) { char noext[MAX_QPATH]; COM_StripExtension(ctx.fname, noext,sizeof(noext)); if (ctx.fg == fg_quake) { tmp = Mod_BSPX_Injest_Read(&sz, "lit", "%s.lit", noext); if (tmp) { if (sz == 8+ctx.lumps[LUMP_LIGHTING].filelen*3 && !memcmp(tmp, "QLIT\0x01\x00\x00\x00", 8)) Con_Printf("Injesting sdr lit file\n"), found|=Mod_BSPXRW_SetLump(&ctx, "RGBLIGHTING", tmp+8, sz-8); //ldr else if (sz == 8+ctx.lumps[LUMP_LIGHTING].filelen*4 && !memcmp(tmp, "QLIT\0x01\x00\x01\x00", 8)) Con_Printf("Injesting hdr lit file\n"), found|=Mod_BSPXRW_SetLump(&ctx, "LIGHTING_E5BGR9", tmp+8, sz-8); //hdr FS_FreeFile(tmp); } tmp = Mod_BSPX_Injest_Read(&sz, "lux", "%s.lux", noext); if (tmp) { if (sz == 8+ctx.lumps[LUMP_LIGHTING].filelen*3 && !memcmp(tmp, "QLIT\0x01\x00\x00\x00", 8)) Con_Printf("Injesting lux file\n"), found|=Mod_BSPXRW_SetLump(&ctx, "LIGHTINGDIR", tmp+8, sz-8); FS_FreeFile(tmp); } tmp = Mod_BSPX_Injest_Read(&sz, "ent", "%s.ent", noext); if (tmp) { if (sz) Con_Printf("Injesting ent file\n"), found|=Mod_BSPXRW_SetLump(&ctx, "entities", tmp, sz); FS_FreeFile(tmp); } // tmp = Mod_BSPX_Injest_Read(&sz, "vis", "%s.vis", noext); // tmp = Mod_BSPX_Injest_Read(&sz, "rtlights", "%s.rtlights", noext); // tmp = Mod_BSPX_Injest_Read(&sz, "way", "%s.way", noext); //fixme: cubemaps } if (found) Mod_BSPXRW_Write(&ctx); else { Mod_BSPXRW_Free(&ctx); Con_Printf("%s \"%s\": nothing changed.\n", Cmd_Argv(0), Cmd_Argv(1)); } } } image_t *Mod_CubemapForOrigin(model_t *wmodel, vec3_t org) { int i; menvmap_t *e; float bestdist = FLT_MAX, dist; image_t *ret = NULL; vec3_t move; if (!wmodel || wmodel->loadstate != MLS_LOADED) return NULL; for ( i=0 , e=wmodel->envmaps ; inumenvmaps ; i++, e++) { VectorSubtract(org, e->origin, move); dist = DotProduct(move,move); if (bestdist > dist) { bestdist = dist; ret = e->image; } } return ret; } void Mod_BSPX_Init(void) { Cmd_AddCommandD("mod_findcubemaps", Mod_FindCubemaps_f, "Scans the entities of a map to find reflection env_cubemap sites and determines the nearest one to each surface."); Cmd_AddCommandD("mod_realign", Mod_Realign_f, "Reads the named bsp and writes it back out with only alignment changes."); Cmd_AddCommandD("mod_bspx_list", Mod_BSPX_List_f, "Lists all lumps (and their sizes) in the specified bsp."); Cmd_AddCommandD("mod_bspx_strip", Mod_BSPX_Strip_f, "Strips a named extension lump from a bsp file."); Cmd_AddCommandD("mod_bspx_injest", Mod_BSPX_Injest_f, "Injests supplemental files (like .lits) into a new bspx file. "CON_WARNING"REMEMBER TO BACK-UP FIRST!"); } #endif