diff --git a/qbsp/portals.cc b/qbsp/portals.cc index 718e1d24..f5e137b2 100644 --- a/qbsp/portals.cc +++ b/qbsp/portals.cc @@ -641,32 +641,6 @@ static void FloodAreas_r(node_t *node) } } -static void FloodNode(node_t *node) -{ - if (node->area) - return; - - // area portals are always only flooded into, never - // out of - if (ClusterContents(node).native & Q2_CONTENTS_AREAPORTAL) - return; - - map.c_areas++; - FloodAreas_r(node); -} - -static void FloodNodes_R(node_t *node) -{ - if (!node) { - return; - } - - FloodNode(node); - - FloodNodes_R(node->children[0]); - FloodNodes_R(node->children[1]); -} - /* ============= FindAreas_r @@ -675,19 +649,30 @@ Just decend the tree, and for each node that hasn't had an area set, flood fill out from there ============= */ -static void FindAreas(node_t *node) +static void FindAreas_r(node_t *node) { - auto leaves = FindOccupiedClusters(node); - - if (leaves.empty()) { - // map leaked, just flood entire map - FloodNodes_R(node); + if (!node->is_leaf) { + FindAreas_r(node->children[0]); + FindAreas_r(node->children[1]); return; } + + if (node->area) + return; // already got it - for (auto *leaf : leaves) { - FloodNode(leaf); - } + if (node->contents.is_any_solid(qbsp_options.target_game)) + return; + + if (!node->occupied) + return; // not reachable from an entity + + // area portals are always only flooded into, never + // out of + if (node->contents.native & Q2_CONTENTS_AREAPORTAL) + return; + + map.c_areas++; + FloodAreas_r(node); } /* @@ -740,7 +725,7 @@ void EmitAreaPortals(node_t *headnode) { logging::funcheader(); - FindAreas(headnode); + FindAreas_r(headnode); SetAreaPortalAreas_r(headnode); map.bsp.dareaportals.emplace_back(); diff --git a/testmaps/q2_areaportal_split.map b/testmaps/q2_areaportal_split.map new file mode 100644 index 00000000..da50f418 --- /dev/null +++ b/testmaps/q2_areaportal_split.map @@ -0,0 +1,88 @@ +// Game: Quake 2 +// Format: Quake2 +// entity 0 +{ +"classname" "worldspawn" +"_tb_textures" "textures/e1u1" +// brush 0 +{ +( 48 64 112 ) ( 48 -64 112 ) ( 48 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 64 -64 -16 ) ( 48 -64 -16 ) ( 64 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 64 64 -16 ) ( 48 64 -16 ) ( 64 -64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 64 -64 112 ) ( 48 -64 112 ) ( 64 64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 64 64 112 ) ( 48 64 112 ) ( 64 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 64 -64 112 ) ( 64 64 112 ) ( 64 -64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 1 +{ +( -64 -64 -16 ) ( -64 64 -16 ) ( -64 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -64 -64 112 ) ( -48 -64 112 ) ( -64 -64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -64 -64 -16 ) ( -48 -64 -16 ) ( -64 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -64 64 112 ) ( -48 64 112 ) ( -64 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -64 64 -16 ) ( -48 64 -16 ) ( -64 64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 -64 -16 ) ( -48 -64 112 ) ( -48 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 2 +{ +( -48 64 112 ) ( -48 48 112 ) ( -48 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 48 112 ) ( 48 48 -16 ) ( -48 48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 64 -16 ) ( -48 48 -16 ) ( 48 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 64 112 ) ( 48 48 112 ) ( -48 64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 64 112 ) ( -48 64 112 ) ( 48 64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 64 -16 ) ( 48 48 -16 ) ( 48 64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 3 +{ +( -48 -64 -16 ) ( -48 -48 -16 ) ( -48 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 -64 -16 ) ( -48 -64 -16 ) ( 48 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 -64 -16 ) ( 48 -48 -16 ) ( -48 -64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 -64 112 ) ( -48 -48 112 ) ( 48 -64 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 -48 -16 ) ( 48 -48 -16 ) ( -48 -48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 -64 112 ) ( 48 -48 112 ) ( 48 -64 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 4 +{ +( -48 -48 112 ) ( -48 -48 96 ) ( -48 48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 -48 112 ) ( 48 -48 96 ) ( -48 -48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 48 96 ) ( -48 48 96 ) ( 48 -48 96 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 48 112 ) ( 48 -48 112 ) ( -48 48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 48 112 ) ( -48 48 96 ) ( 48 48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 48 112 ) ( 48 48 96 ) ( 48 -48 112 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 5 +{ +( -48 48 -16 ) ( -48 48 0 ) ( -48 -48 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 -48 -16 ) ( -48 -48 0 ) ( 48 -48 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 48 -16 ) ( -48 -48 -16 ) ( 48 48 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( -48 -48 0 ) ( -48 48 0 ) ( 48 -48 0 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 48 -16 ) ( 48 48 0 ) ( -48 48 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +( 48 -48 -16 ) ( 48 -48 0 ) ( 48 48 -16 ) e1u1/skip 0 0 0 1 1 0 128 0 +} +// brush 6 +{ +( -48 0 80 ) ( -48 1 80 ) ( -48 0 81 ) e1u1/skip 0 16 0 1 1 +( -48 -48 80 ) ( -48 -48 81 ) ( -47 -48 80 ) e1u1/skip 0 16 0 1 1 +( -48 0 80 ) ( -47 0 80 ) ( -48 1 80 ) e1u1/box3_4 0 0 0 1 1 +( 48 16 96 ) ( 48 17 96 ) ( 49 16 96 ) e1u1/skip 0 0 0 1 1 +( 48 48 96 ) ( 49 48 96 ) ( 48 48 97 ) e1u1/skip 0 16 0 1 1 +( 48 16 96 ) ( 48 16 97 ) ( 48 17 96 ) e1u1/skip 0 16 0 1 1 +} +} +// entity 1 +{ +"classname" "func_areaportal" +// brush 0 +{ +( -48 0 0 ) ( -48 1 0 ) ( -48 0 1 ) e1u1/trigger 0 0 0 1 1 +( -16 0 0 ) ( -16 0 1 ) ( -15 0 0 ) e1u1/trigger 0 0 0 1 1 +( -16 0 0 ) ( -15 0 0 ) ( -16 1 0 ) e1u1/trigger 0 0 0 1 1 +( 48 16 80 ) ( 48 17 80 ) ( 49 16 80 ) e1u1/trigger 0 0 0 1 1 +( 48 16 16 ) ( 49 16 16 ) ( 48 16 17 ) e1u1/trigger 0 0 0 1 1 +( 48 16 16 ) ( 48 16 17 ) ( 48 17 16 ) e1u1/trigger 0 0 0 1 1 +} +} +// entity 2 +{ +"classname" "info_player_start" +"origin" "16 -32 24" +} diff --git a/testmaps/q2_double_areaportal.map b/testmaps/q2_double_areaportal.map new file mode 100644 index 00000000..fc85e92c --- /dev/null +++ b/testmaps/q2_double_areaportal.map @@ -0,0 +1,117 @@ +// Game: Quake 2 +// Format: Quake2 +// entity 0 +{ +"classname" "worldspawn" +"_tb_textures" "textures/e1u1" +// brush 0 +{ +( 60 128 112 ) ( 60 -128 112 ) ( 60 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 64 -128 -16 ) ( 60 -128 -16 ) ( 64 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 64 128 -16 ) ( 60 128 -16 ) ( 64 -128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 64 -128 112 ) ( 60 -128 112 ) ( 64 128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 64 128 112 ) ( 60 128 112 ) ( 64 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 64 -128 112 ) ( 64 128 112 ) ( 64 -128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 1 +{ +( -64 -128 -16 ) ( -64 128 -16 ) ( -64 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -64 -128 112 ) ( -60 -128 112 ) ( -64 -128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -64 -128 -16 ) ( -60 -128 -16 ) ( -64 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -64 128 112 ) ( -60 128 112 ) ( -64 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -64 128 -16 ) ( -60 128 -16 ) ( -64 128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 -128 -16 ) ( -60 -128 112 ) ( -60 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 2 +{ +( -60 128 112 ) ( -60 124 112 ) ( -60 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 124 112 ) ( 60 124 -16 ) ( -60 124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 128 -16 ) ( -60 124 -16 ) ( 60 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 128 112 ) ( 60 124 112 ) ( -60 128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 128 112 ) ( -60 128 112 ) ( 60 128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 128 -16 ) ( 60 124 -16 ) ( 60 128 112 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 3 +{ +( -60 -128 -16 ) ( -60 -124 -16 ) ( -60 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 -128 -16 ) ( -60 -128 -16 ) ( 60 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 -128 -16 ) ( 60 -124 -16 ) ( -60 -128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 -128 112 ) ( -60 -124 112 ) ( 60 -128 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 -124 -16 ) ( 60 -124 -16 ) ( -60 -124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 -128 112 ) ( 60 -124 112 ) ( 60 -128 -16 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 4 +{ +( -60 -124 112 ) ( -60 -124 108 ) ( -60 124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 -124 112 ) ( 60 -124 108 ) ( -60 -124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 124 108 ) ( -60 124 108 ) ( 60 -124 108 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 124 112 ) ( 60 -124 112 ) ( -60 124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 124 112 ) ( -60 124 108 ) ( 60 124 112 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 124 112 ) ( 60 124 108 ) ( 60 -124 112 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 5 +{ +( -60 124 -16 ) ( -60 124 -12 ) ( -60 -124 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 -124 -16 ) ( -60 -124 -12 ) ( 60 -124 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 124 -16 ) ( -60 -124 -16 ) ( 60 124 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( -60 -124 -12 ) ( -60 124 -12 ) ( 60 -124 -12 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 124 -16 ) ( 60 124 -12 ) ( -60 124 -16 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 -124 -16 ) ( 60 -124 -12 ) ( 60 124 -16 ) e1u1/c_met11_2 0 0 0 1 1 +} +// brush 6 +{ +( 0 48 -12 ) ( 0 49 -12 ) ( 0 48 -11 ) e1u1/c_met11_2 0 0 0 1 1 +( 52 32 -12 ) ( 52 32 -11 ) ( 53 32 -12 ) e1u1/c_met11_2 0 0 0 1 1 +( 52 48 -12 ) ( 53 48 -12 ) ( 52 49 -12 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 56 108 ) ( 60 57 108 ) ( 61 56 108 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 56 -8 ) ( 61 56 -8 ) ( 60 56 -7 ) e1u1/c_met11_2 0 0 0 1 1 +( 60 56 -8 ) ( 60 56 -7 ) ( 60 57 -8 ) e1u1/c_met11_2 0 0 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "-4 -88 12" +} +// entity 2 +{ +"classname" "func_areaportal" +"targetname" "a1" +// brush 0 +{ +( -60 48 -12 ) ( -60 49 -12 ) ( -60 48 -11 ) e1u1/trigger 4 0 0 1 1 +( -60 48 -12 ) ( -60 48 -11 ) ( -59 48 -12 ) e1u1/trigger 0 0 0 1 1 +( -60 48 -12 ) ( -59 48 -12 ) ( -60 49 -12 ) e1u1/trigger 0 -4 0 1 1 +( 0 52 108 ) ( 0 53 108 ) ( 1 52 108 ) e1u1/trigger 0 -4 0 1 1 +( 0 52 -8 ) ( 1 52 -8 ) ( 0 52 -7 ) e1u1/trigger 0 0 0 1 1 +( 0 52 -8 ) ( 0 52 -7 ) ( 0 53 -8 ) e1u1/trigger 4 0 0 1 1 +} +} +// entity 3 +{ +"classname" "func_areaportal" +"targetname" "a1" +// brush 0 +{ +( -60 36 -12 ) ( -60 37 -12 ) ( -60 36 -11 ) e1u1/trigger 16 0 0 1 1 +( -60 36 -12 ) ( -60 36 -11 ) ( -59 36 -12 ) e1u1/trigger 0 0 0 1 1 +( -60 36 -12 ) ( -59 36 -12 ) ( -60 37 -12 ) e1u1/trigger 0 -16 0 1 1 +( 0 40 108 ) ( 0 41 108 ) ( 1 40 108 ) e1u1/trigger 0 -16 0 1 1 +( 0 40 -8 ) ( 1 40 -8 ) ( 0 40 -7 ) e1u1/trigger 0 0 0 1 1 +( 0 40 -8 ) ( 0 40 -7 ) ( 0 41 -8 ) e1u1/trigger 16 0 0 1 1 +} +} +// entity 4 +{ +"classname" "func_door" +"target" "a1" +// brush 0 +{ +( -60 32 -12 ) ( -60 33 -12 ) ( -60 32 -11 ) e1u1/grndoor1 0 0 0 1 1 +( -60 32 -12 ) ( -60 32 -11 ) ( -59 32 -12 ) e1u1/grndoor1 0 0 0 1 1 +( -60 32 -12 ) ( -59 32 -12 ) ( -60 33 -12 ) e1u1/grndoor1 0 0 0 1 1 +( 0 48 108 ) ( 0 49 108 ) ( 1 48 108 ) e1u1/grndoor1 0 0 0 1 1 +( 0 56 -8 ) ( 1 56 -8 ) ( 0 56 -7 ) e1u1/grndoor1 0 0 0 1 1 +( 0 48 -8 ) ( 0 48 -7 ) ( 0 49 -8 ) e1u1/grndoor1 0 0 0 1 1 +} +} diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index e0c7502d..db8319ff 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -1740,6 +1740,34 @@ TEST_CASE("qbsp_q2_detail_seals", "[testmaps_q2]") { CHECK(Q2_CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_void)->contents); } +/** + * Two areaportals with a small gap in between creating another area. + * + * Also, the faces on the ceiling/floor cross the areaportal + * (due to our aggressive face merging). + */ +TEST_CASE("q2_double_areaportal", "[testmaps_q2]") +{ + const auto [bsp, bspx, prt] = LoadTestmapQ2("q2_double_areaportal.map"); + + CHECK(GAME_QUAKE_II == bsp.loadversion->game->id); + CheckFilled(bsp); + + CHECK(4 == bsp.dareas.size()); + CHECK(5 == bsp.dareaportals.size()); +} + +TEST_CASE("q2_areaportal_split", "[testmaps_q2]") +{ + const auto [bsp, bspx, prt] = LoadTestmapQ2("q2_areaportal_split.map"); + + CHECK(GAME_QUAKE_II == bsp.loadversion->game->id); + CheckFilled(bsp); + + CHECK(3 == bsp.dareas.size()); // 1 invalid index zero reserved + 2 areas + CHECK(2 == bsp.dareaportals.size()); // 1 invalid index zero reserved + 1 portal +} + /** * Q1 sealing test: * - hull0 can use Q2 method (fill inside)