diff --git a/include/common/bspfile_generic.hh b/include/common/bspfile_generic.hh index f254749c..f5fd007e 100644 --- a/include/common/bspfile_generic.hh +++ b/include/common/bspfile_generic.hh @@ -318,10 +318,12 @@ struct mleaf_t bool operator==(const mleaf_t &other) const; }; +// index of darea_t in dareas *is* the "area number" (unlike for dareaportals). +// 0 is reserved struct darea_t { - int32_t numareaportals; - int32_t firstareaportal; + int32_t numareaportals; // number of entries in dareaportals (number of outgoing graph edges) + int32_t firstareaportal; // index into dareaportals of our first outgoing graph edge, *not* the "area portal number" // serialize for streams void stream_write(std::ostream &s) const; @@ -334,10 +336,13 @@ struct darea_t // each area has a list of portals that lead into other areas // when portals are closed, other areas may not be visible or // hearable even if the vis info says that it should be +// +// a dareaportal_t is a directed edge in the area graph. struct dareaportal_t { - int32_t portalnum; - int32_t otherarea; + int32_t portalnum; // our "area portal number". corresponds to the "style" key on the func_areaportal entity. + // multiple dareaportal_t entries (should be always 2) will have the same portalnum + int32_t otherarea; // area number (index in dareas array) of the other area. // serialize for streams void stream_write(std::ostream &s) const; diff --git a/testmaps/q2_areaportals.map b/testmaps/q2_areaportals.map new file mode 100644 index 00000000..43dcf981 --- /dev/null +++ b/testmaps/q2_areaportals.map @@ -0,0 +1,160 @@ +// Game: Quake 2 +// Format: Quake2 (Valve) +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"_tb_textures" "textures/e1u1" +// brush 0 +{ +( -176 -256 64 ) ( -176 -255 64 ) ( -176 -256 65 ) e1u1/ggrat4_2 [ 0 1.0000000000000002 0 -16 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +( -176 -432 64 ) ( -176 -432 65 ) ( -175 -432 64 ) e1u1/ggrat4_2 [ -1 0 0 -16 ] [ 0 0 -1 -32 ] 0 1 1 +( -176 -256 96 ) ( -175 -256 96 ) ( -176 -255 96 ) e1u1/ggrat4_2 [ -1.0000000000000002 0 0 -16 ] [ 0 1.0000000000000002 0 -48 ] 0 1 1 +( -160 192 352 ) ( -160 193 352 ) ( -159 192 352 ) e1u1/ggrat4_2 [ -1.0000000000000002 0 0 -16 ] [ 0 -1.0000000000000002 0 -80 ] 0 1 1 +( -160 176 80 ) ( -159 176 80 ) ( -160 176 81 ) e1u1/ggrat4_2 [ -1 0 0 -16 ] [ 0 0 -1 -32 ] 0 1 1 +( -160 192 80 ) ( -160 192 81 ) ( -160 193 80 ) e1u1/ggrat4_2 [ 0 -1.0000000000000002 0 64 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +} +// brush 1 +{ +( -160 176 88 ) ( -160 177 88 ) ( -160 176 89 ) e1u1/ggrat4_2 [ 0 1.0000000000000002 0 -32 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +( -160 176 88 ) ( -160 176 89 ) ( -159 176 88 ) e1u1/ggrat4_2 [ -1 0 0 -16 ] [ 0 0 -1 -32 ] 0 1 1 +( -160 176 96 ) ( -159 176 96 ) ( -160 177 96 ) e1u1/ggrat4_2 [ -1.0000000000000002 0 0 -16 ] [ 0 1.0000000000000002 0 -40 ] 0 1 1 +( 288 192 352 ) ( 288 193 352 ) ( 289 192 352 ) e1u1/ggrat4_2 [ -1.0000000000000002 0 0 -16 ] [ 0 -1.0000000000000002 0 48 ] 0 1 1 +( 288 192 96 ) ( 289 192 96 ) ( 288 192 97 ) e1u1/ggrat4_2 [ -1 0 0 -16 ] [ 0 0 -1 -32 ] 0 1 1 +( 496 192 96 ) ( 496 192 97 ) ( 496 193 96 ) e1u1/ggrat4_2 [ 0 -1.0000000000000002 0 0 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +} +// brush 2 +{ +( -160 -112 96 ) ( -160 -111 96 ) ( -160 -112 97 ) e1u1/florr1_8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 1 1 +( -80 -432 80 ) ( -81 -432 80 ) ( -80 -432 81 ) e1u1/florr1_8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 1 1 +( -80 -432 80 ) ( -80 -431 80 ) ( -81 -432 80 ) e1u1/florr1_8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 1 1 +( -160 -112 96 ) ( -161 -112 96 ) ( -160 -111 96 ) e1u1/florr1_8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 1 1 +( -160 176 96 ) ( -160 176 97 ) ( -161 176 96 ) e1u1/florr1_8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 1 1 +( 496 -432 80 ) ( 496 -432 81 ) ( 496 -431 80 ) e1u1/florr1_8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 3 +{ +( -160 -448 88 ) ( -160 -447 88 ) ( -160 -448 89 ) e1u1/ggrat4_2 [ 0 1 0 0 ] [ 0 0 -1 -32 ] 0 1 1 +( -160 -448 88 ) ( -160 -448 89 ) ( -159 -448 88 ) e1u1/ggrat4_2 [ -1.0000000000000002 0 0 -32 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +( -160 -448 96 ) ( -159 -448 96 ) ( -160 -447 96 ) e1u1/ggrat4_2 [ 0 1.0000000000000002 0 0 ] [ 1.0000000000000002 0 0 -32 ] 0 1 1 +( 288 -432 352 ) ( 288 -431 352 ) ( 289 -432 352 ) e1u1/ggrat4_2 [ 0 1.0000000000000002 0 0 ] [ -1.0000000000000002 0 0 -96 ] 0 1 1 +( 288 -432 96 ) ( 289 -432 96 ) ( 288 -432 97 ) e1u1/ggrat4_2 [ 1.0000000000000002 0 0 -80 ] [ 0 0 -1.0000000000000002 -32 ] 0 1 1 +( 496 -432 96 ) ( 496 -432 97 ) ( 496 -431 96 ) e1u1/ggrat4_2 [ 0 1 0 0 ] [ 0 0 -1 -32 ] 0 1 1 +} +// brush 4 +{ +( -160 -256 352 ) ( -160 -255 352 ) ( -160 -256 353 ) e1u1/florr1_8 [ 0 0 -1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 180 1 1 +( 288 -432 360 ) ( 288 -432 361 ) ( 289 -432 360 ) e1u1/florr1_8 [ -1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 -16 ] 180 1 1 +( -160 -256 352 ) ( -159 -256 352 ) ( -160 -255 352 ) e1u1/florr1_8 [ -1 0 0 0 ] [ 0 -1 0 0 ] 180 1 1 +( 288 176 368 ) ( 288 177 368 ) ( 289 176 368 ) e1u1/florr1_8 [ -1 0 0 0 ] [ 0 -1 0 0 ] 180 1 1 +( 288 176 360 ) ( 289 176 360 ) ( 288 176 361 ) e1u1/florr1_8 [ -1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 -16 ] 180 1 1 +( 496 176 360 ) ( 496 176 361 ) ( 496 177 360 ) e1u1/florr1_8 [ 0 0 1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 180 1 1 +} +// brush 5 +{ +( 496 192 80 ) ( 496 193 80 ) ( 496 192 81 ) e1u1/ggrat4_2 [ 0 1 0 0 ] [ 0 0 -1 -32 ] 0 1 1 +( 512 -432 64 ) ( 511 -432 64 ) ( 512 -432 65 ) e1u1/ggrat4_2 [ -1 0 0 96 ] [ 0 0 -1 -32 ] 180 1 1 +( 512 -256 96 ) ( 512 -255 96 ) ( 511 -256 96 ) e1u1/ggrat4_2 [ 1 0 0 -96 ] [ 0 -1 0 0 ] 180 1 1 +( 512 -256 352 ) ( 511 -256 352 ) ( 512 -255 352 ) e1u1/ggrat4_2 [ 1 0 0 -96 ] [ 0 -1 0 0 ] 180 1 1 +( 496 176 80 ) ( 496 176 81 ) ( 495 176 80 ) e1u1/ggrat4_2 [ 1 0 0 -96 ] [ 0 0 -1 -32 ] 180 1 1 +( 512 -256 64 ) ( 512 -256 65 ) ( 512 -255 64 ) e1u1/ggrat4_2 [ 0 -1 0 0 ] [ 0 0 -1 -32 ] 0 1 1 +} +// brush 6 +{ +( 16 -48 96 ) ( 16 -47 96 ) ( 16 -48 97 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 32 -48 96 ) ( 32 -48 97 ) ( 33 -48 96 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 32 -48 96 ) ( 33 -48 96 ) ( 32 -47 96 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 48 -16 192 ) ( 48 -15 192 ) ( 49 -16 192 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 48 176 112 ) ( 49 176 112 ) ( 48 176 113 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 48 -16 112 ) ( 48 -16 113 ) ( 48 -15 112 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 7 +{ +( 16 -240 96 ) ( 16 -239 96 ) ( 16 -240 97 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 32 -432 96 ) ( 32 -432 97 ) ( 33 -432 96 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 32 -240 96 ) ( 33 -240 96 ) ( 32 -239 96 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 -1 0 -64 ] 0 1 1 +( 48 -208 192 ) ( 48 -207 192 ) ( 49 -208 192 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 -1 0 -64 ] 0 1 1 +( 48 -208 112 ) ( 49 -208 112 ) ( 48 -208 113 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 48 -208 112 ) ( 48 -208 113 ) ( 48 -207 112 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 8 +{ +( 16 -48 192 ) ( 16 -47 192 ) ( 16 -48 193 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 96 ] 0 1 1 +( 32 -432 192 ) ( 32 -432 193 ) ( 33 -432 192 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 0 -1 96 ] 0 1 1 +( 32 -48 192 ) ( 33 -48 192 ) ( 32 -47 192 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 48 -16 352 ) ( 48 -15 352 ) ( 49 -16 352 ) e1u1/caution1_1 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 48 176 208 ) ( 49 176 208 ) ( 48 176 209 ) e1u1/caution1_1 [ -1 0 0 0 ] [ 0 0 -1 96 ] 0 1 1 +( 48 -16 208 ) ( 48 -16 209 ) ( 48 -15 208 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 96 ] 0 1 1 +} +// brush 9 +{ +( 288 -48 96 ) ( 288 -47 96 ) ( 288 -48 97 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 304 -48 96 ) ( 304 -48 97 ) ( 305 -48 96 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 0 -1 0 ] 0 1 1 +( 304 -48 96 ) ( 305 -48 96 ) ( 304 -47 96 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 -1 0 0 ] 0 1 1 +( 320 -16 192 ) ( 320 -15 192 ) ( 321 -16 192 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 -1 0 0 ] 0 1 1 +( 320 176 112 ) ( 321 176 112 ) ( 320 176 113 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 0 -1 0 ] 0 1 1 +( 320 -16 112 ) ( 320 -16 113 ) ( 320 -15 112 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 10 +{ +( 288 -48 192 ) ( 288 -47 192 ) ( 288 -48 193 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 96 ] 0 1 1 +( 304 -432 192 ) ( 304 -432 193 ) ( 305 -432 192 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 0 -1 96 ] 0 1 1 +( 304 -48 192 ) ( 305 -48 192 ) ( 304 -47 192 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 -1 0 0 ] 0 1 1 +( 320 -16 352 ) ( 320 -15 352 ) ( 321 -16 352 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 -1 0 0 ] 0 1 1 +( 320 176 208 ) ( 321 176 208 ) ( 320 176 209 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 0 -1 96 ] 0 1 1 +( 320 -16 208 ) ( 320 -16 209 ) ( 320 -15 208 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 96 ] 0 1 1 +} +// brush 11 +{ +( 288 -240 96 ) ( 288 -239 96 ) ( 288 -240 97 ) e1u1/caution1_1 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 304 -432 96 ) ( 304 -432 97 ) ( 305 -432 96 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 0 -1 0 ] 0 1 1 +( 304 -240 96 ) ( 305 -240 96 ) ( 304 -239 96 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 -1 0 -64 ] 0 1 1 +( 320 -208 192 ) ( 320 -207 192 ) ( 321 -208 192 ) e1u1/caution1_1 [ 1 0 0 16 ] [ 0 -1 0 -64 ] 0 1 1 +( 320 -208 112 ) ( 321 -208 112 ) ( 320 -208 113 ) e1u1/caution1_1 [ -1 0 0 -16 ] [ 0 0 -1 0 ] 0 1 1 +( 320 -208 112 ) ( 320 -208 113 ) ( 320 -207 112 ) e1u1/caution1_1 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "light" +"origin" "72 -136 168" +"light" "3000" +} +// entity 2 +{ +"classname" "info_player_start" +"origin" "-88 -112 120" +} +// entity 3 +{ +"classname" "func_areaportal" +"targetname" "playerstart_portal" +// brush 0 +{ +( 16 -136 96 ) ( 16 -135 96 ) ( 16 -136 97 ) e1u1/trigger [ 0 0 -1.0000000000000002 16 ] [ 0 -1.0000000000000002 0 -8 ] 180 1 1 65536 128 0 +( 80 -208 96 ) ( 80 -208 97 ) ( 81 -208 96 ) e1u1/trigger [ -1.0000000000000002 0 0 0 ] [ 0 0 -1.0000000000000002 16 ] 180 1 1 65536 128 0 +( 80 -136 96 ) ( 81 -136 96 ) ( 80 -135 96 ) e1u1/trigger [ -1 0 0 0 ] [ 0 -1 0 -8 ] 180 1 1 65536 128 0 +( 144 -56 192 ) ( 144 -55 192 ) ( 145 -56 192 ) e1u1/trigger [ -1 0 0 0 ] [ 0 -1 0 -8 ] 180 1 1 65536 128 0 +( 144 -48 112 ) ( 145 -48 112 ) ( 144 -48 113 ) e1u1/trigger [ -1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 -16 ] 180 1 1 65536 128 0 +( 48 -56 112 ) ( 48 -56 113 ) ( 48 -55 112 ) e1u1/trigger [ 0 0 1.0000000000000002 -16 ] [ 0 -1.0000000000000002 0 -8 ] 180 1 1 65536 128 0 +} +} +// entity 4 +{ +"classname" "func_areaportal" +"targetname" "grenades_portal" +// brush 0 +{ +( 288 -136 96 ) ( 288 -135 96 ) ( 288 -136 97 ) e1u1/trigger [ 0 0 -1.0000000000000002 16 ] [ 0 -1.0000000000000002 0 -8 ] 180 1 1 65536 128 0 +( 352 -208 96 ) ( 352 -208 97 ) ( 353 -208 96 ) e1u1/trigger [ -1.0000000000000002 0 0 -16 ] [ 0 0 -1.0000000000000002 16 ] 180 1 1 65536 128 0 +( 352 -136 96 ) ( 353 -136 96 ) ( 352 -135 96 ) e1u1/trigger [ -1 0 0 -16 ] [ 0 -1 0 -8 ] 180 1 1 65536 128 0 +( 416 -56 192 ) ( 416 -55 192 ) ( 417 -56 192 ) e1u1/trigger [ -1 0 0 -16 ] [ 0 -1 0 -8 ] 180 1 1 65536 128 0 +( 416 -48 112 ) ( 417 -48 112 ) ( 416 -48 113 ) e1u1/trigger [ -1.0000000000000002 0 0 -16 ] [ 0 0 1.0000000000000002 -16 ] 180 1 1 65536 128 0 +( 320 -56 112 ) ( 320 -56 113 ) ( 320 -55 112 ) e1u1/trigger [ 0 0 1.0000000000000002 -16 ] [ 0 -1.0000000000000002 0 -8 ] 180 1 1 65536 128 0 +} +} +// entity 5 +{ +"classname" "ammo_grenades" +"origin" "416 -128 112" +} diff --git a/tests/test_qbsp_q2.cc b/tests/test_qbsp_q2.cc index bd36a277..de9ccff7 100644 --- a/tests/test_qbsp_q2.cc +++ b/tests/test_qbsp_q2.cc @@ -256,6 +256,94 @@ TEST(testmapsQ2, areaportalWithDetail) EXPECT_THAT(bsp.dareas, testing::UnorderedElementsAreArray(std::vector{{0, 0}, {1, 1}, {1, 2}})); } +/** + * same as q2_areaportal.map but has 2 areaportals + * more clearly shows how areaportal indices work + * + * ap1 ap2 + * + * player | light | ammo + * start | | grenades + * + * area area area + * 3 2 1 + * + * -- +x --> + */ +TEST(testmapsQ2, areaportals) +{ + const auto [bsp, bspx, prt] = LoadTestmapQ2("q2_areaportals.map"); + + ASSERT_EQ(4, bsp.dareas.size()); // 1 reserved + 3 actual = 4 + ASSERT_EQ(5, bsp.dareaportals.size()); // 1 reserved + (2 portals * 2 directions) = 5 + + // check the areaportal numbers from the "style" keys of the func_areaportal entities + auto ents = EntData_Parse(bsp); + + auto playerstart_portal_it = std::ranges::find_if( + ents, [](const entdict_t &dict) { return dict.get("targetname") == "playerstart_portal"; }); + auto grenades_portal_it = + std::ranges::find_if(ents, [](const entdict_t &dict) { return dict.get("targetname") == "grenades_portal"; }); + + ASSERT_NE(playerstart_portal_it, ents.end()); + ASSERT_NE(grenades_portal_it, ents.end()); + + const int32_t playerstart_portal_num = playerstart_portal_it->get_int("style"); + const int32_t grenades_portal_num = grenades_portal_it->get_int("style"); + + // may need to be adjusted + ASSERT_EQ(1, playerstart_portal_num); + ASSERT_EQ(2, grenades_portal_num); + + // look up the leafs + const qvec3d player_start{-88, -112, 120}; + const qvec3d light_pos{72, -136, 168}; + const qvec3d grenades_pos{416, -128, 112}; + + auto *player_start_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_start); + auto *light_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], light_pos); + auto *grenades_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], grenades_pos); + + // check leaf areas (may need to be adjusted) + EXPECT_EQ(2, light_leaf->area); + EXPECT_EQ(3, player_start_leaf->area); + EXPECT_EQ(1, grenades_leaf->area); + + // inspect player_start_leaf area + { + const darea_t &area = bsp.dareas[player_start_leaf->area]; + ASSERT_EQ(area.numareaportals, 1); // to light area + + const dareaportal_t &portal = bsp.dareaportals[area.firstareaportal]; + EXPECT_EQ(portal.otherarea, light_leaf->area); + EXPECT_EQ(portal.portalnum, playerstart_portal_num); + } + + // inspect "light" leaf + { + const darea_t &area = bsp.dareas[light_leaf->area]; + ASSERT_EQ(area.numareaportals, 2); // to player start, grenades areas + + dareaportal_t portal_x = bsp.dareaportals[area.firstareaportal]; + dareaportal_t portal_y = bsp.dareaportals[area.firstareaportal + 1]; + + EXPECT_THAT((std::vector{portal_x, portal_y}), + testing::UnorderedElementsAre( + dareaportal_t{.portalnum = playerstart_portal_num, .otherarea = player_start_leaf->area}, + dareaportal_t{.portalnum = grenades_portal_num, .otherarea = grenades_leaf->area})); + } + + // inspect "grenades" leaf + { + const darea_t &area = bsp.dareas[grenades_leaf->area]; + ASSERT_EQ(area.numareaportals, 1); // to light leaf + + dareaportal_t portal = bsp.dareaportals[area.firstareaportal]; + + EXPECT_EQ(portal, (dareaportal_t{.portalnum = grenades_portal_num, .otherarea = light_leaf->area})); + } +} + TEST(testmapsQ2, nodrawLight) { const auto [bsp, bspx, prt] = LoadTestmapQ2("q2_nodraw_light.map", {"-includeskip"});