From f12cefb2c9843133b0054a46b55a7c46f834b3f0 Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Sun, 26 Mar 2023 19:02:25 -0600 Subject: [PATCH 1/2] qbsp: if Q2_CONTENTS_SOLID bit is set, always assign invalid area --- qbsp/writebsp.cc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qbsp/writebsp.cc b/qbsp/writebsp.cc index b1506af4..528b7156 100644 --- a/qbsp/writebsp.cc +++ b/qbsp/writebsp.cc @@ -193,12 +193,14 @@ static void ExportLeaf(node_t *node) } dleaf.nummarksurfaces = static_cast(map.bsp.dleaffaces.size()) - dleaf.firstmarksurface; - if (map.leakfile) { - if (!(dleaf.contents & Q2_CONTENTS_SOLID)) { - dleaf.area = 1; - } + if (dleaf.contents & Q2_CONTENTS_SOLID) { + dleaf.area = AREA_INVALID; } else { - dleaf.area = node->area; + if (map.leakfile) { + dleaf.area = 1; + } else { + dleaf.area = node->area; + } } dleaf.cluster = node->viscluster; From d7db2bdae508cfa5f68ce6313f95008d76212afb Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Mon, 27 Mar 2023 01:20:53 -0600 Subject: [PATCH 2/2] qbsp: debug helper for finding areaportal leaks --- include/qbsp/outside.hh | 4 + qbsp/outside.cc | 2 +- qbsp/portals.cc | 178 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/include/qbsp/outside.hh b/include/qbsp/outside.hh index 6ef061a1..9a15dcd8 100644 --- a/include/qbsp/outside.hh +++ b/include/qbsp/outside.hh @@ -21,12 +21,16 @@ #pragma once +#include #include #include +#include struct node_t; struct tree_t; +void WriteLeakTrail(std::ofstream &leakfile, qvec3d point1, const qvec3d &point2); + bool FillOutside(tree_t &tree, hull_index_t hullnum, bspbrush_t::container &brushes); std::vector FindOccupiedClusters(node_t *headnode); void MarkBrushSidesInvisible(bspbrush_t::container &brushes); diff --git a/qbsp/outside.cc b/qbsp/outside.cc index fd8c72ff..0ec7e390 100644 --- a/qbsp/outside.cc +++ b/qbsp/outside.cc @@ -241,7 +241,7 @@ static std::vector FindPortalsToVoid(node_t *occupied_leaf) WriteLeakTrail =============== */ -static void WriteLeakTrail(std::ofstream &leakfile, qvec3d point1, const qvec3d &point2) +void WriteLeakTrail(std::ofstream &leakfile, qvec3d point1, const qvec3d &point2) { qvec3d vector = point2 - point1; vec_t dist = qv::normalizeInPlace(vector); diff --git a/qbsp/portals.cc b/qbsp/portals.cc index 65ae3dfc..f2a75f83 100644 --- a/qbsp/portals.cc +++ b/qbsp/portals.cc @@ -687,6 +687,183 @@ static void FindAreas_r(node_t *node) FloodAreas_r(node); } +/** + * Starting at `a`, find and return the shortest path to `b`. + * + * Reference: + * https://en.wikipedia.org/wiki/Breadth-first_search#Pseudocode + */ +static std::list FindShortestPath(node_t *a, node_t *b, const std::function &passable) +{ + std::list queue; + std::unordered_set queue_set; + std::unordered_map parent; + + queue.push_back(a); + queue_set.insert(a); + + while (!queue.empty()) { + node_t *node = queue.front(); + queue.pop_front(); + + if (node == b) { + // reached target. now we just need to extract the path we took from the `parent` map. + std::list result; + for (node_t *n = b;; n = parent.at(n)) { + result.push_front(n); + if (n == a) + break; + } + return result; + } + + // push neighbouring nodes onto the back of the queue, + // if they're not already enqueued, and if the portal is passable + int side; + for (portal_t *portal = node->portals; portal; portal = portal->next[!side]) { + side = (portal->nodes[0] == node); + + node_t *neighbour = portal->nodes[side]; + + if (!passable(portal)) + continue; + if (queue_set.find(neighbour) != queue_set.end()) + continue; + + // enqueue it + queue.push_back(neighbour); + queue_set.insert(neighbour); + parent[neighbour] = node; + } + } + + // couldn't find a path + return {}; +} + +using exit_t = std::tuple; + +static void FindAreaPortalExits_R(node_t *n, std::unordered_set &visited, std::vector &exits) +{ + Q_assert(n->is_leaf); + + visited.insert(n); + + int s; + for (portal_t *p = n->portals; p; p = p->next[!s]) { + s = (p->nodes[0] == n); + + node_t *neighbour = p->nodes[s]; + + // already visited? + if (visited.find(neighbour) != visited.end()) + continue; + + // is this an exit? + if (!(neighbour->contents.native & Q2_CONTENTS_AREAPORTAL) && + !neighbour->contents.is_solid(qbsp_options.target_game)) { + exits.emplace_back(p, neighbour); + continue; + } + + // valid edge to explore? + // if this isn't an exit, don't leave AREAPORTAL + if (!(neighbour->contents.native & Q2_CONTENTS_AREAPORTAL)) + continue; + + // continue exploding + return FindAreaPortalExits_R(neighbour, visited, exits); + } +} + +/** + * DFS to find all portals leading out of the Q2_CONTENTS_AREAPORTAL leaf `n`, into non-solid leafs. + * Returns all of the portals and corresponding "outside" leafs. + */ +static std::vector FindAreaPortalExits(node_t *n) +{ + std::unordered_set visited; + std::vector exits; + + FindAreaPortalExits_R(n, visited, exits); + + return exits; +} + +/** + * Attempts to write a leak line showing how the two sides of the areaportal are reachable. + */ +static void DebugAreaPortalBothSidesLeak(node_t *node) +{ + std::vector exits = FindAreaPortalExits(node); + + logging::print("found {} exits:\n", exits.size()); + for (auto [exit_portal, exit_leaf] : exits) { + logging::print( + " {} ({}):\n", exit_leaf->bounds.centroid(), exit_leaf->contents.to_string(qbsp_options.target_game)); + } + if (exits.size() < 2) + return; + + auto [exit_portal0, exit_leaf0] = exits[0]; + + // look for the other exit `i`, such that the shortest path between exit 0 and `i` is the longest. + // this is to avoid picking two exits on the same side of the areaportal, which would not help + // track down the leak. + size_t longest_length = 0; + std::list longest_path; + + for (size_t i = 1; i < exits.size(); ++i) { + auto [exit_portal_i, exit_leaf_i] = exits[i]; + + auto path = FindShortestPath(exit_leaf0, exit_leaf_i, [](portal_t *p) -> bool { + if (!Portal_EntityFlood(p, 0)) + return false; + + // don't go back into an areaportal + if ((p->nodes[0]->contents.native & Q2_CONTENTS_AREAPORTAL) || + (p->nodes[1]->contents.native & Q2_CONTENTS_AREAPORTAL)) + return false; + + return true; + }); + + logging::print("shortest path from exit 0 to {} is {} leafs long\n", i, path.size()); + + if (path.size() > longest_length) { + longest_length = path.size(); + longest_path = path; + } + } + + // write `longest_path` as the leak + + mapentity_t *entity = AreanodeEntityForLeaf(node); + + fs::path name = qbsp_options.bsp_path; + name.replace_extension(fmt::format("areaportal{}_leak.pts", entity - map.entities.data())); + + std::ofstream ptsfile(name); + + if (!ptsfile) + FError("Failed to open {}: {}", name, strerror(errno)); + + for (auto it = longest_path.begin();; ++it) { + if (it == longest_path.end()) + break; + + auto next_it = it; + next_it++; + + if (next_it == longest_path.end()) + break; + + WriteLeakTrail(ptsfile, (*it)->bounds.centroid(), (*next_it)->bounds.centroid()); + } + + logging::print("Wrote {}\n", name); +} + /* ============= SetAreaPortalAreas_r @@ -722,6 +899,7 @@ static void SetAreaPortalAreas_r(node_t *node) logging::print( "WARNING: areaportal entity {} with targetname {} doesn't touch two areas\n Node bounds: {} -> {}\n", entity - map.entities.data(), entity->epairs.get("targetname"), node->bounds.mins(), node->bounds.maxs()); + DebugAreaPortalBothSidesLeak(node); return; } }