From e05a2bdf750f65b6a15f13fbfb1141755303300d Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Mon, 1 Aug 2022 19:09:37 -0600 Subject: [PATCH] qbsp: "-add additional.map" feature --- common/bspfile.cc | 9 ++++++++- include/qbsp/qbsp.hh | 1 + qbsp/map.cc | 30 ++++++++++++++++++++++++++++ testmaps/q1_merge_maps_addition.map | 31 +++++++++++++++++++++++++++++ testmaps/q1_merge_maps_base.map | 23 +++++++++++++++++++++ tests/test_qbsp.cc | 27 +++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 testmaps/q1_merge_maps_addition.map create mode 100644 testmaps/q1_merge_maps_base.map diff --git a/common/bspfile.cc b/common/bspfile.cc index e20226fd..23dd1244 100644 --- a/common/bspfile.cc +++ b/common/bspfile.cc @@ -688,7 +688,7 @@ public: return create_solid_contents(); } - void init_filesystem(const fs::path &, const settings::common_settings &options) const override + void init_filesystem(const fs::path &map_or_bsp, const settings::common_settings &options) const override { // Q1-like games don't care about the local // filesystem. @@ -699,6 +699,13 @@ public: fs::addArchive(path, true); } + // certain features like '-add additional.map' search relative to the map we're compiling + // so add the map directory to the search path + auto map_or_bsp_dir = map_or_bsp.parent_path(); + if (!map_or_bsp_dir.empty()) { + fs::addArchive(map_or_bsp_dir); + } + img::init_palette(this); } diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index eac457ef..5b0052d2 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -352,6 +352,7 @@ public: // since the max world size in Q3 is {-65536, -65536, -65536, 65536, 65536, 65536}. should we dynamically change this? // should we automatically turn this on if the world gets too big but leave it off for smaller worlds? setting_blocksize blocksize{this, "blocksize", { 0, 0, 0 }, &common_format_group, "from q3map2; split the world by x/y/z sized chunks, speeding up split decisions"}; + setting_string add{this, "add", "", "", &common_format_group, "the given map file will be appended to the base map"}; void setParameters(int argc, const char **argv) override { diff --git a/qbsp/map.cc b/qbsp/map.cc index 5d0c07ab..0266c03f 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -2051,6 +2051,36 @@ void LoadMapFile(void) map.entities.pop_back(); } + // -add function + if (!qbsp_options.add.value().empty()) { + auto file = fs::load(qbsp_options.add.value()); + + if (!file) { + FError("Couldn't load map file \"{}\".\n", qbsp_options.add.value()); + return; + } + + parser_t parser(file->data(), file->size()); + + for (int i = 0;; i++) { + mapentity_t &entity = map.entities.emplace_back(); + + if (!ParseEntity(parser, &entity)) { + break; + } + + if (entity.epairs.get("classname") == "worldspawn") { + // The easiest way to get the additional map's worldspawn brushes + // into the base map's is to rename the additional map's worldspawn classname to func_group + entity.epairs.set("classname", "func_group"); + } + } + // Remove dummy entity inserted above + assert(!map.entities.back().epairs.size()); + assert(map.entities.back().brushes.empty()); + map.entities.pop_back(); + } + logging::print(logging::flag::STAT, " {:8} faces\n", map.faces.size()); logging::print(logging::flag::STAT, " {:8} brushes\n", map.brushes.size()); logging::print(logging::flag::STAT, " {:8} entities\n", map.entities.size()); diff --git a/testmaps/q1_merge_maps_addition.map b/testmaps/q1_merge_maps_addition.map new file mode 100644 index 00000000..ea9cac68 --- /dev/null +++ b/testmaps/q1_merge_maps_addition.map @@ -0,0 +1,31 @@ +// Game: Quake +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"message" "merge maps addition. The worldspawn keys here should be ignored" +"wad" "deprecated/free_wad.wad" +// brush 0 +{ +( 0 -64 -16 ) ( 0 -63 -16 ) ( 0 -64 -15 ) bolt11 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 0 -64 -16 ) ( 0 -64 -15 ) ( 1 -64 -16 ) bolt11 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 0 -64 -16 ) ( 1 -64 -16 ) ( 0 -63 -16 ) bolt11 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 128 64 16 ) ( 128 65 16 ) ( 129 64 16 ) bolt11 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 128 64 16 ) ( 129 64 16 ) ( 128 64 17 ) bolt11 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 64 64 16 ) ( 64 64 17 ) ( 64 65 16 ) bolt11 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "func_wall" +// brush 0 +{ +( 96 32 0 ) ( 96 33 0 ) ( 96 32 1 ) bolt11 [ 0 -1 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 96 32 0 ) ( 96 32 1 ) ( 97 32 0 ) bolt11 [ 1 0 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 96 32 0 ) ( 97 32 0 ) ( 96 33 0 ) bolt11 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 128 64 16 ) ( 128 65 16 ) ( 129 64 16 ) bolt11 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 128 64 16 ) ( 129 64 16 ) ( 128 64 17 ) bolt11 [ -1 0 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +( 128 64 16 ) ( 128 64 17 ) ( 128 65 16 ) bolt11 [ 0 1 0 0 ] [ 0 0 -1 -16 ] 0 1 1 +} +} diff --git a/testmaps/q1_merge_maps_base.map b/testmaps/q1_merge_maps_base.map new file mode 100644 index 00000000..485e0e36 --- /dev/null +++ b/testmaps/q1_merge_maps_base.map @@ -0,0 +1,23 @@ +// Game: Quake +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"message" "merge maps base" +"wad" "deprecated/free_wad.wad" +// brush 0 +{ +( -64 -64 -16 ) ( -64 -63 -16 ) ( -64 -64 -15 ) bolt11 [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -64 -16 ) ( -64 -64 -15 ) ( -63 -64 -16 ) bolt11 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -64 -16 ) ( -63 -64 -16 ) ( -64 -63 -16 ) bolt11 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 64 16 ) ( 64 65 16 ) ( 65 64 16 ) bolt11 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) bolt11 [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 0 64 16 ) ( 0 64 17 ) ( 0 65 16 ) bolt11 [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "-16 -16 40" +} diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index 30977e6b..c049062c 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -1766,6 +1766,33 @@ TEST_CASE("q1_wad_external", "[testmaps_q1]") { CHECK(bsp.dtex.textures[3].data.size() == sizeof(dmiptex_t)); } +TEST_CASE("q1_merge_maps", "[testmaps_q1]") { + const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_merge_maps_base.map", { "-add", "q1_merge_maps_addition.map" }); + + CHECK(GAME_QUAKE == bsp.loadversion->game->id); + + // check brushwork from the two maps is merged + REQUIRE(BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], {5,0,16}, {0, 0, 1})); + REQUIRE(BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], {-5,0,16}, {0, 0, 1})); + + // check that the worldspawn keys from the base map are used + auto ents = EntData_Parse(bsp.dentdata); + REQUIRE(ents.size() == 3); // worldspawn, info_player_start, func_wall + + REQUIRE(ents[0].get("classname") == "worldspawn"); + CHECK(ents[0].get("message") == "merge maps base"); + + // check info_player_start + auto it = std::find_if(ents.begin(), ents.end(), + [](const entdict_t &dict) -> bool { return dict.get("classname") == "info_player_start"; }); + REQUIRE(it != ents.end()); + + // check func_wall entity from addition map is included + it = std::find_if(ents.begin(), ents.end(), + [](const entdict_t &dict) -> bool { return dict.get("classname") == "func_wall"; }); + REQUIRE(it != ents.end()); +} + TEST_CASE("winding", "[benchmark][.releaseonly]") { ankerl::nanobench::Bench bench;