533 lines
15 KiB
C++
533 lines
15 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/outside.hh>
|
|
#include <qbsp/brush.hh>
|
|
#include <qbsp/map.hh>
|
|
#include <qbsp/portals.hh>
|
|
#include <qbsp/qbsp.hh>
|
|
|
|
#include <climits>
|
|
#include <vector>
|
|
#include <set>
|
|
#include <list>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <fstream>
|
|
#include <fmt/ostream.h>
|
|
|
|
/*
|
|
===========
|
|
PointInLeaf
|
|
|
|
If the point is exactly on a node plane, prefer to return the
|
|
opaque leaf.
|
|
|
|
This avoids spurious leaks if a point entity is on the outside
|
|
of the map (exactly on a brush faces) - happens in base1.map.
|
|
===========
|
|
*/
|
|
static node_t *PointInLeaf(node_t *node, const qvec3d &point)
|
|
{
|
|
if (node->planenum == PLANENUM_LEAF || node->detail_separator) {
|
|
return node;
|
|
}
|
|
|
|
auto &plane = map.planes[node->planenum];
|
|
vec_t dist = plane.distance_to(point);
|
|
|
|
if (dist > 0) {
|
|
// point is on the front of the node plane
|
|
return PointInLeaf(node->children[0], point);
|
|
} else if (dist < 0) {
|
|
// point is on the back of the node plane
|
|
return PointInLeaf(node->children[1], point);
|
|
} else {
|
|
// point is exactly on the node plane
|
|
|
|
node_t *front = PointInLeaf(node->children[0], point);
|
|
node_t *back = PointInLeaf(node->children[1], point);
|
|
|
|
// prefer the opaque one
|
|
if (front->opaque()) {
|
|
return front;
|
|
}
|
|
return back;
|
|
}
|
|
}
|
|
|
|
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->outside_distance = -1;
|
|
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;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
FloodFillFromOutsideNode
|
|
|
|
Sets outside_distance on leafs reachable from the void
|
|
|
|
preconditions:
|
|
- all leafs have outside_distance set to -1
|
|
==================
|
|
*/
|
|
static void FloodFillFromVoid()
|
|
{
|
|
// breadth-first search
|
|
std::list<std::pair<node_t *, int>> queue;
|
|
std::unordered_set<node_t *> visited_nodes;
|
|
|
|
// push a node onto the queue which is in the void, but has a portal to outside_node
|
|
// NOTE: remember, the headnode has no relationship to the outside of the map.
|
|
{
|
|
const int side = (outside_node.portals->nodes[0] == &outside_node);
|
|
node_t *fillnode = outside_node.portals->nodes[side];
|
|
|
|
Q_assert(fillnode != &outside_node);
|
|
|
|
// this must be true because the map is made from closed brushes, beyion which is void
|
|
Q_assert(!fillnode->opaque());
|
|
queue.emplace_back(fillnode, 0);
|
|
}
|
|
|
|
while (!queue.empty()) {
|
|
const auto front = queue.front();
|
|
queue.pop_front();
|
|
|
|
auto [node, outside_distance] = front;
|
|
|
|
if (visited_nodes.find(node) != visited_nodes.end()) {
|
|
// have already visited this node
|
|
continue;
|
|
}
|
|
|
|
// visit node
|
|
visited_nodes.insert(node);
|
|
node->outside_distance = outside_distance;
|
|
|
|
// 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.emplace_back(neighbour, outside_distance + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=============
|
|
FindPortalsToVoid
|
|
|
|
Given an occupied leaf, returns a list of porals leading to the void
|
|
=============
|
|
*/
|
|
static std::vector<portal_t *> FindPortalsToVoid(node_t *occupied_leaf)
|
|
{
|
|
Q_assert(occupied_leaf->occupant != nullptr);
|
|
Q_assert(occupied_leaf->outside_distance >= 0);
|
|
|
|
std::vector<portal_t *> result;
|
|
|
|
node_t *node = occupied_leaf;
|
|
while (1) {
|
|
// exit?
|
|
if (node->outside_distance == 0)
|
|
break; // this is the void leaf where we started the flood fill in FloodFillFromVoid()
|
|
|
|
// find the next node...
|
|
|
|
node_t *bestneighbour = nullptr;
|
|
portal_t *bestportal = nullptr;
|
|
int bestdist = node->outside_distance;
|
|
|
|
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->outside_distance >= 0);
|
|
|
|
if (neighbour->outside_distance < bestdist) {
|
|
bestneighbour = neighbour;
|
|
bestportal = portal;
|
|
bestdist = neighbour->outside_distance;
|
|
}
|
|
}
|
|
|
|
Q_assert(bestneighbour != nullptr);
|
|
Q_assert(bestdist < node->outside_distance);
|
|
|
|
// go through bestportal
|
|
result.push_back(bestportal);
|
|
node = bestneighbour;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
WriteLeakTrail
|
|
===============
|
|
*/
|
|
static void WriteLeakTrail(std::ofstream &leakfile, qvec3d point1, const qvec3d &point2)
|
|
{
|
|
qvec3d vector = point2 - point1;
|
|
vec_t dist = qv::normalizeInPlace(vector);
|
|
|
|
while (dist > options.leakdist.value()) {
|
|
fmt::print(leakfile, "{}\n", point1);
|
|
point1 += vector * options.leakdist.value();
|
|
dist -= options.leakdist.value();
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
WriteLeakLine
|
|
|
|
leakline should be a sequence of portals leading from leakentity to the void
|
|
===============
|
|
*/
|
|
static void WriteLeakLine(const mapentity_t *leakentity, const std::vector<portal_t *> &leakline)
|
|
{
|
|
fs::path name = options.bsp_path;
|
|
name.replace_extension("pts");
|
|
|
|
std::ofstream ptsfile(name);
|
|
|
|
if (!ptsfile)
|
|
FError("Failed to open {}: {}", name, strerror(errno));
|
|
|
|
qvec3d prevpt = leakentity->origin;
|
|
|
|
for (portal_t *portal : leakline) {
|
|
qvec3d currpt = portal->winding->center();
|
|
|
|
// draw dots from prevpt to currpt
|
|
WriteLeakTrail(ptsfile, prevpt, currpt);
|
|
|
|
prevpt = currpt;
|
|
}
|
|
|
|
logging::print("Leak file written to {}\n", name);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
FindOccupiedLeafs
|
|
|
|
sets node->occupant
|
|
==================
|
|
*/
|
|
std::vector<node_t *> FindOccupiedClusters(node_t *headnode)
|
|
{
|
|
std::vector<node_t *> result;
|
|
|
|
for (int i = 1; i < map.entities.size(); i++) {
|
|
mapentity_t *entity = &map.entities.at(i);
|
|
|
|
/* skip entities at (0 0 0) (bmodels) */
|
|
if (qv::epsilonEmpty(entity->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->facelist) {
|
|
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->outside_distance == -1) {
|
|
// This is an occupied leaf, so we need to keep all of the faces touching it.
|
|
for (auto &markface : node->markfaces) {
|
|
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 (auto &markface : node->markfaces) {
|
|
// NOTE: This is how faces are deleted here, kind of ugly
|
|
markface->w.clear();
|
|
}
|
|
|
|
// FIXME: Shouldn't be needed here
|
|
node->facelist = {};
|
|
}
|
|
|
|
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->outside_distance == -1)
|
|
return;
|
|
|
|
// Don't fill sky, or count solids as outleafs
|
|
if (node->contents.is_solid(options.target_game) || node->contents.is_sky(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 (auto &markface : node->markfaces) {
|
|
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)
|
|
{
|
|
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
|
|
|
|
/* Clear the outside filling state on all nodes */
|
|
ClearOccupied_r(node);
|
|
|
|
// Sets leaf->occupant
|
|
const std::vector<node_t *> occupied_leafs = FindOccupiedClusters(node);
|
|
|
|
if (occupied_leafs.empty()) {
|
|
logging::print("WARNING: No entities in empty space -- no filling performed (hull {})\n", hullnum);
|
|
return false;
|
|
}
|
|
|
|
// Flood fill from outside -> in.
|
|
//
|
|
// We tried inside -> out and it leads to things like monster boxes getting inadvertently sealed,
|
|
// or even whole sections of the map with no point entities - problems compounded by hull expansion.
|
|
FloodFillFromVoid();
|
|
|
|
// check for the occupied leaf closest to the void
|
|
int best_leak_dist = INT_MAX;
|
|
node_t *best_leak = nullptr;
|
|
|
|
for (node_t *leaf : occupied_leafs) {
|
|
if (leaf->outside_distance == -1)
|
|
continue;
|
|
|
|
if (leaf->outside_distance < best_leak_dist) {
|
|
best_leak_dist = leaf->outside_distance;
|
|
best_leak = leaf;
|
|
}
|
|
}
|
|
|
|
if (best_leak) {
|
|
const auto leakline = FindPortalsToVoid(best_leak);
|
|
|
|
mapentity_t *leakentity = best_leak->occupant;
|
|
Q_assert(leakentity != nullptr);
|
|
|
|
logging::print("WARNING: Reached occupant \"{}\" at ({}), no filling performed.\n",
|
|
ValueForKey(leakentity, "classname"), leakentity->origin);
|
|
if (map.leakfile)
|
|
return false;
|
|
|
|
WriteLeakLine(leakentity, leakline);
|
|
map.leakfile = true;
|
|
|
|
/* Get rid of the .prt file since the map has a leak */
|
|
if (!options.keepprt.value()) {
|
|
fs::path name = options.bsp_path;
|
|
name.replace_extension("prt");
|
|
remove(name);
|
|
}
|
|
|
|
if (options.leaktest.value()) {
|
|
logging::print("Aborting because -leaktest was used.\n");
|
|
exit(1);
|
|
}
|
|
|
|
// clear occupied state, so areas can be flooded in Q2
|
|
ClearOccupied_r(node);
|
|
|
|
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);
|
|
|
|
logging::print(logging::flag::STAT, " {:8} outleafs\n", outleafs);
|
|
return true;
|
|
}
|