/* 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 #include #include #include #include #include #include #include #include /* =========== 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) { return node; } auto &plane = map.planes[node->planenum]; vec_t dist = DotProduct(plane.normal, point) - plane.dist; 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 std::ofstream InitPtsFile(void) { options.szBSPName.replace_extension("pts"); std::ofstream ptsfile(options.szBSPName); if (!ptsfile) FError("Failed to open {}: {}", options.szBSPName, strerror(errno)); return ptsfile; } 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> queue; std::unordered_set 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 FindPortalsToVoid(node_t *occupied_leaf) { Q_assert(occupied_leaf->occupant != nullptr); Q_assert(occupied_leaf->outside_distance >= 0); std::vector 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, 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) { fmt::print(leakfile, "{} {} {}\n", trail[0], trail[1], trail[2]); VectorMA(trail, options.dxLeakDist, vector, trail); dist -= options.dxLeakDist; } } /* =============== WriteLeakLine leakline should be a sequence of portals leading from leakentity to the void =============== */ static void WriteLeakLine(const mapentity_t *leakentity, const std::vector &leakline) { std::ofstream ptsfile = InitPtsFile(); vec3_t prevpt, currpt; VectorCopy(leakentity->origin, prevpt); for (portal_t *portal : leakline) { VectorCopy(portal->winding->center(), currpt); // draw dots from prevpt to currpt WriteLeakTrail(ptsfile, prevpt, currpt); VectorCopy(currpt, prevpt); } LogPrint("Leak file written to {}\n", options.szBSPName); } /* ================== FindOccupiedLeafs sets node->occupant ================== */ static std::vector FindOccupiedLeafs(node_t *headnode) { std::vector 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->outside_distance == -1) { // 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.clear(); } // 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->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 (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) { LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__); if (options.fNofill) { LogPrint(LOG_STAT, " skipped\n"); return false; } /* Clear the outside filling state on all nodes */ ClearOccupied_r(node); // Sets leaf->occupant const std::vector occupied_leafs = FindOccupiedLeafs(node); if (occupied_leafs.empty()) { LogPrint("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); LogPrint("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 */ options.szBSPName.replace_extension("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); LogPrint(LOG_STAT, " {:8} outleafs\n", outleafs); return true; }