490 lines
13 KiB
C++
490 lines
13 KiB
C++
/*
|
|
Copyright (C) 1996-1997 Id Software, Inc.
|
|
Copyright (C) 1997 Greg Lewis
|
|
|
|
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 2 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.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
See file, 'COPYING', for details.
|
|
*/
|
|
|
|
#include <qbsp/qbsp.hh>
|
|
|
|
#include <vector>
|
|
#include <set>
|
|
#include <list>
|
|
#include <utility>
|
|
|
|
/*
|
|
===========
|
|
PointInLeaf
|
|
===========
|
|
*/
|
|
node_t *
|
|
PointInLeaf(node_t *node, const vec3_t point)
|
|
{
|
|
vec_t dist;
|
|
const qbsp_plane_t *plane;
|
|
|
|
while (node->planenum != PLANENUM_LEAF) {
|
|
plane = &map.planes[node->planenum];
|
|
dist = DotProduct(plane->normal, point) - plane->dist;
|
|
node = (dist > 0) ? node->children[0] : node->children[1];
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
static FILE *
|
|
InitPtsFile(void)
|
|
{
|
|
FILE *ptsfile;
|
|
|
|
StripExtension(options.szBSPName);
|
|
strcat(options.szBSPName, ".pts");
|
|
ptsfile = fopen(options.szBSPName, "wt");
|
|
if (!ptsfile)
|
|
Error("Failed to open %s: %s", options.szBSPName, strerror(errno));
|
|
|
|
return ptsfile;
|
|
}
|
|
|
|
// new code
|
|
|
|
static void
|
|
ClearOccupied_r(node_t *node)
|
|
{
|
|
if (node->planenum != PLANENUM_LEAF) {
|
|
ClearOccupied_r(node->children[0]);
|
|
ClearOccupied_r(node->children[1]);
|
|
return;
|
|
}
|
|
|
|
/* leaf node */
|
|
node->occupied = 0;
|
|
node->occupant = nullptr;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
Portal_Passable
|
|
|
|
Returns true if the portal has non-opaque leafs on both sides
|
|
|
|
from q3map
|
|
=============
|
|
*/
|
|
static bool Portal_Passable(const portal_t *p)
|
|
{
|
|
if (p->nodes[0] == &outside_node
|
|
|| p->nodes[1] == &outside_node) {
|
|
// FIXME: need this because the outside_node doesn't have PLANENUM_LEAF set
|
|
return false;
|
|
}
|
|
|
|
Q_assert(p->nodes[0]->planenum == PLANENUM_LEAF);
|
|
Q_assert(p->nodes[1]->planenum == PLANENUM_LEAF);
|
|
|
|
if (p->nodes[0]->opaque()
|
|
|| p->nodes[1]->opaque())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
precondition: all leafs have occupied set to 0
|
|
==================
|
|
*/
|
|
static void
|
|
BFSFloodFillFromOccupiedLeafs(const std::vector<node_t *> &occupied_leafs)
|
|
{
|
|
std::list<std::pair<node_t *, int>> queue;
|
|
for (node_t *leaf : occupied_leafs) {
|
|
queue.push_back(std::make_pair(leaf, 1));
|
|
}
|
|
|
|
while (!queue.empty()) {
|
|
auto pair = queue.front();
|
|
queue.pop_front();
|
|
|
|
node_t *node = pair.first;
|
|
const int dist = pair.second;
|
|
|
|
if (node->occupied == 0) {
|
|
// we haven't visited this node yet
|
|
node->occupied = dist;
|
|
|
|
// push neighbouring nodes onto the back of the queue
|
|
int side;
|
|
for (portal_t *portal = node->portals; portal; portal = portal->next[!side]) {
|
|
side = (portal->nodes[0] == node);
|
|
|
|
if (!Portal_Passable(portal))
|
|
continue;
|
|
|
|
node_t *neighbour = portal->nodes[side];
|
|
queue.push_back(std::make_pair(neighbour, dist + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static std::pair<std::vector<portal_t *>, node_t*>
|
|
MakeLeakLine(node_t *outleaf)
|
|
{
|
|
std::vector<portal_t *> result;
|
|
|
|
Q_assert(outleaf->occupied > 0);
|
|
|
|
node_t *node = outleaf;
|
|
while (1)
|
|
{
|
|
// exit?
|
|
if (node->occupied == 1)
|
|
break; // this node contains an entity
|
|
|
|
// find the next node...
|
|
|
|
node_t *bestneighbour = nullptr;
|
|
portal_t *bestportal = nullptr;
|
|
int bestoccupied = node->occupied;
|
|
|
|
int side;
|
|
for (portal_t *portal = node->portals; portal; portal = portal->next[!side]) {
|
|
side = (portal->nodes[0] == node);
|
|
|
|
if (!Portal_Passable(portal))
|
|
continue;
|
|
|
|
node_t *neighbour = portal->nodes[side];
|
|
Q_assert(neighbour != node);
|
|
Q_assert(neighbour->occupied > 0);
|
|
|
|
if (neighbour->occupied < bestoccupied) {
|
|
bestneighbour = neighbour;
|
|
bestportal = portal;
|
|
bestoccupied = neighbour->occupied;
|
|
}
|
|
}
|
|
|
|
Q_assert(bestneighbour != nullptr);
|
|
Q_assert(bestoccupied < node->occupied);
|
|
|
|
// go through bestportal
|
|
result.push_back(bestportal);
|
|
node = bestneighbour;
|
|
}
|
|
|
|
Q_assert(node->occupant != nullptr);
|
|
Q_assert(node->occupied == 1);
|
|
return std::make_pair(result, node);
|
|
}
|
|
|
|
/*
|
|
===============
|
|
WriteLeakTrail
|
|
===============
|
|
*/
|
|
static void
|
|
WriteLeakTrail(FILE *leakfile, const vec3_t point1, const vec3_t point2)
|
|
{
|
|
vec3_t vector, trail;
|
|
vec_t dist;
|
|
|
|
VectorSubtract(point2, point1, vector);
|
|
dist = VectorNormalize(vector);
|
|
|
|
VectorCopy(point1, trail);
|
|
while (dist > options.dxLeakDist) {
|
|
fprintf(leakfile, "%f %f %f\n", trail[0], trail[1], trail[2]);
|
|
VectorMA(trail, options.dxLeakDist, vector, trail);
|
|
dist -= options.dxLeakDist;
|
|
}
|
|
}
|
|
|
|
static void
|
|
WriteLeakLine(const std::pair<std::vector<portal_t *>, node_t*> &leakline)
|
|
{
|
|
FILE *ptsfile = InitPtsFile();
|
|
|
|
vec3_t prevpt, currpt;
|
|
VectorCopy(leakline.second->occupant->origin, prevpt);
|
|
|
|
for (auto it = leakline.first.rbegin(); it != leakline.first.rend(); ++it) {
|
|
portal_t *portal = *it;
|
|
MidpointWinding(portal->winding, currpt);
|
|
|
|
// draw dots from prevpt to currpt
|
|
WriteLeakTrail(ptsfile, prevpt, currpt);
|
|
|
|
VectorCopy(currpt, prevpt);
|
|
}
|
|
|
|
fclose(ptsfile);
|
|
Message(msgLiteral, "Leak file written to %s\n", options.szBSPName);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
FindOccupiedLeafs
|
|
|
|
sets node->occupant
|
|
==================
|
|
*/
|
|
static std::vector<node_t *>
|
|
FindOccupiedLeafs(node_t *headnode)
|
|
{
|
|
std::vector<node_t *> result;
|
|
|
|
for (int i = 1; i < map.numentities(); i++) {
|
|
mapentity_t *entity = &map.entities.at(i);
|
|
|
|
/* skip entities at (0 0 0) (bmodels) */
|
|
if (VectorCompare(entity->origin, vec3_origin, EQUAL_EPSILON))
|
|
continue;
|
|
|
|
/* find the leaf it's in. Skip opqaue leafs */
|
|
node_t *leaf = PointInLeaf(headnode, entity->origin);
|
|
if (leaf->opaque())
|
|
continue;
|
|
|
|
/* did we already find an entity for this leaf? */
|
|
if (leaf->occupant != nullptr)
|
|
continue;
|
|
|
|
leaf->occupant = entity;
|
|
|
|
result.push_back(leaf);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
ResetFacesTouchingOccupiedLeafs
|
|
|
|
Set f->touchesOccupiedLeaf=false on all faces.
|
|
==================
|
|
*/
|
|
static void
|
|
ResetFacesTouchingOccupiedLeafs(node_t *node)
|
|
{
|
|
if (node->planenum == PLANENUM_LEAF) {
|
|
return;
|
|
}
|
|
|
|
for (face_t *face = node->faces; face; face = face->next) {
|
|
face->touchesOccupiedLeaf = false;
|
|
}
|
|
|
|
ResetFacesTouchingOccupiedLeafs(node->children[0]);
|
|
ResetFacesTouchingOccupiedLeafs(node->children[1]);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
MarkFacesTouchingOccupiedLeafs
|
|
|
|
Set f->touchesOccupiedLeaf=true on faces that are touching occupied leafs
|
|
==================
|
|
*/
|
|
static void
|
|
MarkFacesTouchingOccupiedLeafs(node_t *node)
|
|
{
|
|
if (node->planenum != PLANENUM_LEAF) {
|
|
MarkFacesTouchingOccupiedLeafs(node->children[0]);
|
|
MarkFacesTouchingOccupiedLeafs(node->children[1]);
|
|
return;
|
|
}
|
|
|
|
// visit the leaf
|
|
|
|
if (node->occupied > 0) {
|
|
// This is an occupied leaf, so we need to keep all of the faces touching it.
|
|
for (face_t **markface = node->markfaces; *markface; markface++) {
|
|
(*markface)->touchesOccupiedLeaf = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
ClearOutFaces
|
|
|
|
Deletes (by setting f->w.numpoints=0) faces in solid nodes
|
|
==================
|
|
*/
|
|
static void
|
|
ClearOutFaces(node_t *node)
|
|
{
|
|
if (node->planenum != PLANENUM_LEAF) {
|
|
ClearOutFaces(node->children[0]);
|
|
ClearOutFaces(node->children[1]);
|
|
return;
|
|
}
|
|
|
|
// visit the leaf
|
|
if (!node->contents.is_solid(options.target_game)) {
|
|
return;
|
|
}
|
|
|
|
for (face_t **markface = node->markfaces; *markface; markface++) {
|
|
// NOTE: This is how faces are deleted here, kind of ugly
|
|
(*markface)->w.numpoints = 0;
|
|
}
|
|
|
|
// FIXME: Shouldn't be needed here
|
|
node->faces = NULL;
|
|
}
|
|
|
|
static void
|
|
OutLeafsToSolid_r(node_t *node, int *outleafs_count)
|
|
{
|
|
if (node->planenum != PLANENUM_LEAF) {
|
|
OutLeafsToSolid_r(node->children[0], outleafs_count);
|
|
OutLeafsToSolid_r(node->children[1], outleafs_count);
|
|
return;
|
|
}
|
|
|
|
// skip leafs reachable from entities
|
|
if (node->occupied > 0)
|
|
return;
|
|
|
|
// Don't fill sky, or count solids as outleafs
|
|
if (node->contents.is_sky(options.target_game)
|
|
|| node->contents.is_solid(options.target_game))
|
|
return;
|
|
|
|
// Now check all faces touching the leaf. If any of them are partially going into the occupied part of the map,
|
|
// don't fill the leaf (see comment in FillOutside).
|
|
bool skipFill = false;
|
|
for (face_t **markface = node->markfaces; *markface; markface++) {
|
|
if ((*markface)->touchesOccupiedLeaf) {
|
|
skipFill = true;
|
|
break;
|
|
}
|
|
}
|
|
if (skipFill) {
|
|
return;
|
|
}
|
|
|
|
// Finally, we can fill it in as void.
|
|
node->contents = options.target_game->create_solid_contents();
|
|
*outleafs_count += 1;
|
|
}
|
|
|
|
static int
|
|
OutLeafsToSolid(node_t *node)
|
|
{
|
|
int count = 0;
|
|
OutLeafsToSolid_r(node, &count);
|
|
return count;
|
|
}
|
|
|
|
//=============================================================================
|
|
|
|
/*
|
|
===========
|
|
FillOutside
|
|
|
|
===========
|
|
*/
|
|
bool
|
|
FillOutside(node_t *node, const int hullnum)
|
|
{
|
|
Message(msgProgress, "FillOutside");
|
|
|
|
if (options.fNofill) {
|
|
Message(msgStat, "skipped");
|
|
return false;
|
|
}
|
|
|
|
/* Clear the node->occupied on all leafs to 0 */
|
|
ClearOccupied_r(node);
|
|
|
|
const std::vector<node_t *> occupied_leafs = FindOccupiedLeafs(node);
|
|
|
|
if (occupied_leafs.empty()) {
|
|
Message(msgWarning, warnNoFilling, hullnum);
|
|
return false;
|
|
}
|
|
|
|
BFSFloodFillFromOccupiedLeafs(occupied_leafs);
|
|
|
|
/* first check to see if an occupied leaf is hit */
|
|
const int side = (outside_node.portals->nodes[0] == &outside_node);
|
|
node_t *fillnode = outside_node.portals->nodes[side];
|
|
|
|
if (fillnode->occupied > 0) {
|
|
const auto leakline = MakeLeakLine(fillnode);
|
|
|
|
mapentity_t *leakentity = leakline.second->occupant;
|
|
Q_assert(leakentity != nullptr);
|
|
|
|
const vec_t *origin = leakentity->origin;
|
|
Message(msgWarning, warnMapLeak, ValueForKey(leakentity, "classname"), origin[0], origin[1], origin[2]);
|
|
if (map.leakfile)
|
|
return false;
|
|
|
|
WriteLeakLine(leakline);
|
|
map.leakfile = true;
|
|
|
|
/* Get rid of the .prt file since the map has a leak */
|
|
StripExtension(options.szBSPName);
|
|
strcat(options.szBSPName, ".prt");
|
|
remove(options.szBSPName);
|
|
|
|
if (options.fLeakTest) {
|
|
logprint("Aborting because -leaktest was used.\n");
|
|
exit(1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// At this point, leafs not reachable from entities have (node->occupied == 0).
|
|
// The two final tasks are:
|
|
// 1. Mark the leafs that are not reachable as CONTENTS_SOLID (i.e. filling them in as the void).
|
|
// 2. Delete faces in those leafs
|
|
|
|
|
|
// An annoying wrinkle here: there may be leafs with (node->occupied == 0), which means they should be filled in as void,
|
|
// but they have faces straddling between them and occupied leafs (i.e. leafs which will be CONTENTS_EMPTY because
|
|
// they're in playable space). See missing_face_simple.map for an example.
|
|
//
|
|
// The subtlety is, if we fill these leafs in as solid and delete the inward-facing faces, the only face left
|
|
// will be the void-and-non-void-straddling face. This face will mess up LinkConvexFaces, since we need to rebuild the
|
|
// BSP and recalculate the leaf contents, unaware of the fact that we wanted this leaf to be void (CONTENTS_SOLID),
|
|
// and this face will cause it to be marked as CONTENTS_EMPTY which will manifest as messed up hull0 collision in game
|
|
// (weapons shoot through the leaf.)
|
|
//
|
|
// In order to avoid this scenario, we need to detect those "void-and-non-void-straddling" faces and not fill those leafs
|
|
// in as solid. This will keep some extra faces around but keep the content types consistent.
|
|
|
|
ResetFacesTouchingOccupiedLeafs(node);
|
|
MarkFacesTouchingOccupiedLeafs(node);
|
|
|
|
/* now go back and fill outside with solid contents */
|
|
const int outleafs = OutLeafsToSolid(node);
|
|
|
|
/* remove faces from filled in leafs */
|
|
ClearOutFaces(node);
|
|
|
|
Message(msgStat, "%8d outleafs", outleafs);
|
|
return true;
|
|
}
|