diff --git a/CMakeLists.txt b/CMakeLists.txt index 01b66a5e..dfeaece7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,8 @@ find_package(TBB REQUIRED) set(TEST_QUAKE_MAP_EXPORT_DIR "" CACHE PATH "When running unit tests, export Quake maps to this directory (useful for testing in game)") set(TEST_QUAKE2_MAP_EXPORT_DIR "" CACHE PATH "When running unit tests, export Quake 2 maps to this directory (useful for testing in game)") +set(TEST_HEXEN2_MAP_EXPORT_DIR "" CACHE PATH "When running unit tests, export Hexen 2 maps to this directory (useful for testing in game)") +set(TEST_HALFLIFE_MAP_EXPORT_DIR "" CACHE PATH "When running unit tests, export Half-Life maps to this directory (useful for testing in game)") add_subdirectory(3rdparty) add_subdirectory(common) diff --git a/common/bspfile_generic.cc b/common/bspfile_generic.cc index 3c950032..0081c19f 100644 --- a/common/bspfile_generic.cc +++ b/common/bspfile_generic.cc @@ -150,6 +150,7 @@ void dmiptexlump_t::stream_read(std::istream &stream, const lump_t &lump) // dummy texture? if (offset < 0) { + tex.null_texture = true; continue; } @@ -159,12 +160,20 @@ void dmiptexlump_t::stream_read(std::istream &stream, const lump_t &lump) stream.seekg(lump.fileofs + offset); // calculate the length of the data used for the individual miptex. - int32_t next_offset; + int32_t next_offset = -1; - if (i == nummiptex - 1) { + // scan forward (skipping -1's) to find the next valid offset + for (int j = i + 1; j < nummiptex; ++j) { + // valid? + if (offsets[j] >= 0) { + next_offset = offsets[j]; + break; + } + } + if (next_offset == -1) { + // the remainder of the texures are missing, so read to the end + // of the overall lump next_offset = lump.filelen; - } else { - next_offset = offsets[i + 1]; } if (next_offset > offset) { @@ -185,7 +194,7 @@ void dmiptexlump_t::stream_write(std::ostream &stream) const // write out the miptex offsets for (auto &texture : textures) { - if (!texture.name[0]) { + if (!texture.name[0] || texture.width == 0 || texture.height == 0) { // dummy texture stream <= static_cast(-1); continue; @@ -203,7 +212,7 @@ void dmiptexlump_t::stream_write(std::ostream &stream) const } for (auto &texture : textures) { - if (texture.name[0]) { + if (texture.name[0] && texture.width && texture.height) { // fix up the padding to match the above conditions if (stream.tellp() % 4) { constexpr const char pad[4]{}; diff --git a/common/bspinfo.cc b/common/bspinfo.cc index 04c50805..7525c64b 100644 --- a/common/bspinfo.cc +++ b/common/bspinfo.cc @@ -765,6 +765,11 @@ void serialize_bsp(const bspdata_t &bspdata, const mbsp_t &bsp, const fs::path & json &textures = (j.emplace("textures", json::array())).first.value(); for (auto &src_tex : bsp.dtex.textures) { + if (src_tex.null_texture) { + // use json null to indicate offset -1 + textures.insert(textures.end(), json(nullptr)); + continue; + } json &tex = textures.insert(textures.end(), json::object()).value(); tex.push_back({"name", src_tex.name}); diff --git a/include/common/bspfile_generic.hh b/include/common/bspfile_generic.hh index cec995b7..454542b3 100644 --- a/include/common/bspfile_generic.hh +++ b/include/common/bspfile_generic.hh @@ -102,6 +102,10 @@ struct miptex_t std::string name; uint32_t width, height; std::vector data; + /** + * set at read time if the offset is -1 + */ + bool null_texture = false; size_t stream_size() const; diff --git a/testmaps.hh.in b/testmaps.hh.in index e5e7486a..267784dd 100644 --- a/testmaps.hh.in +++ b/testmaps.hh.in @@ -1,3 +1,5 @@ inline const char *testmaps_dir = "@CMAKE_CURRENT_SOURCE_DIR@/testmaps"; inline const char *test_quake_maps_dir = "@TEST_QUAKE_MAP_EXPORT_DIR@"; inline const char *test_quake2_maps_dir = "@TEST_QUAKE2_MAP_EXPORT_DIR@"; +inline const char *test_hexen2_maps_dir = "@TEST_HEXEN2_MAP_EXPORT_DIR@"; +inline const char *test_halflife_maps_dir = "@TEST_HALFLIFE_MAP_EXPORT_DIR@"; diff --git a/testmaps/deprecated/hlwad.wad b/testmaps/deprecated/hlwad.wad new file mode 100644 index 00000000..6d75867e Binary files /dev/null and b/testmaps/deprecated/hlwad.wad differ diff --git a/testmaps/hl_basic.map b/testmaps/hl_basic.map new file mode 100644 index 00000000..c5c5cd3a --- /dev/null +++ b/testmaps/hl_basic.map @@ -0,0 +1,79 @@ +// Game: Half-Life +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"wad" "deprecated/hlwad.wad" +// brush 0 +{ +( -192 -64 -16 ) ( -192 -63 -16 ) ( -192 -64 -15 ) hltest [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -128 -16 ) ( -64 -128 -15 ) ( -63 -128 -16 ) hltest [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -64 -16 ) ( -63 -64 -16 ) ( -64 -63 -16 ) hltest [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 64 16 ) ( 64 65 16 ) ( 65 64 16 ) hltest [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 192 16 ) ( 65 192 16 ) ( 64 192 17 ) hltest [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 256 64 16 ) ( 256 64 17 ) ( 256 65 16 ) hltest [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 1 +{ +( -192 -384 -16 ) ( -192 -383 -16 ) ( -192 -384 -15 ) hltest [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -144 -16 ) ( -64 -144 -15 ) ( -63 -144 -16 ) hltest [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 64 -256 16 ) ( 65 -256 16 ) ( 64 -255 16 ) hltest [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 -256 304 ) ( 64 -255 304 ) ( 65 -256 304 ) hltest [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 -128 16 ) ( 65 -128 16 ) ( 64 -128 17 ) hltest [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 256 -256 16 ) ( 256 -256 17 ) ( 256 -255 16 ) hltest [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 2 +{ +( -192 -48 -16 ) ( -192 -47 -16 ) ( -192 -48 -15 ) hltest [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 1 1 +( -64 192 -16 ) ( -64 192 -15 ) ( -63 192 -16 ) hltest [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 64 80 16 ) ( 65 80 16 ) ( 64 81 16 ) hltest [ 1 0 0 0 ] [ 0 -1 0 16 ] 0 1 1 +( 64 80 304 ) ( 64 81 304 ) ( 65 80 304 ) hltest [ 1 0 0 0 ] [ 0 -1 0 16 ] 0 1 1 +( 64 208 16 ) ( 65 208 16 ) ( 64 208 17 ) hltest [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 256 80 16 ) ( 256 80 17 ) ( 256 81 16 ) hltest [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 3 +{ +( -208 128 16 ) ( -208 129 16 ) ( -208 128 17 ) hltest [ -6.123233995736766e-17 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 48 -128 -16 ) ( 47 -128 -16 ) ( 48 -128 -15 ) hltest [ 1 -6.123233995736766e-17 0 16 ] [ 0 0 -1 0 ] 0 1 1 +( -80 128 16 ) ( -80 129 16 ) ( -81 128 16 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 1 -6.123233995736766e-17 0 16 ] 90 1 1 +( -80 128 304 ) ( -81 128 304 ) ( -80 129 304 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 1 -6.123233995736766e-17 0 16 ] 270 1 1 +( -80 192 16 ) ( -80 192 17 ) ( -81 192 16 ) hltest [ -1 6.123233995736766e-17 0 -16 ] [ 0 0 -1 0 ] 0 1 1 +( -192 0 -16 ) ( -192 0 -15 ) ( -192 1 -16 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 4 +{ +( 256 128 16 ) ( 256 129 16 ) ( 256 128 17 ) hltest [ -6.123233995736766e-17 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 512 -128 -16 ) ( 511 -128 -16 ) ( 512 -128 -15 ) hltest [ 1 -6.123233995736766e-17 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 384 128 16 ) ( 384 129 16 ) ( 383 128 16 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 1 -6.123233995736766e-17 0 0 ] 90 1 1 +( 384 128 304 ) ( 383 128 304 ) ( 384 129 304 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 1 -6.123233995736766e-17 0 0 ] 270 1 1 +( 384 192 16 ) ( 384 192 17 ) ( 383 192 16 ) hltest [ -1 6.123233995736766e-17 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 272 0 -16 ) ( 272 0 -15 ) ( 272 1 -16 ) hltest [ 6.123233995736766e-17 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 5 +{ +( -192 -64 304 ) ( -192 -63 304 ) ( -192 -64 305 ) hltest [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -128 304 ) ( -64 -128 305 ) ( -63 -128 304 ) hltest [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -64 -64 304 ) ( -63 -64 304 ) ( -64 -63 304 ) hltest [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 64 336 ) ( 64 65 336 ) ( 65 64 336 ) hltest [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( 64 192 336 ) ( 65 192 336 ) ( 64 192 337 ) hltest [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 256 64 336 ) ( 256 64 337 ) ( 256 65 336 ) hltest [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"spawnflags" "0" +"angles" "0 0 0" +"origin" "32 16 52" +} +// entity 2 +{ +"classname" "light" +"spawnflags" "0" +"_light" "255 255 128 200" +"style" "0" +"_fade" "1.0" +"_falloff" "0" +"origin" "40 8 200" +} diff --git a/testmaps/q1_missing_texture.map b/testmaps/q1_missing_texture.map new file mode 100644 index 00000000..6e9315fd --- /dev/null +++ b/testmaps/q1_missing_texture.map @@ -0,0 +1,69 @@ +// Game: Quake +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad" +"_wateralpha" "0.5" +"_tb_def" "builtin:Quake.fgd" +// brush 0 +{ +( 96 32 208 ) ( 96 -192 208 ) ( 96 32 48 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +( 112 -192 48 ) ( 96 -192 48 ) ( 112 -192 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( 112 32 48 ) ( 96 32 48 ) ( 112 -192 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( 112 -192 208 ) ( 96 -192 208 ) ( 112 32 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( 112 32 208 ) ( 96 32 208 ) ( 112 32 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( 112 -192 208 ) ( 112 32 208 ) ( 112 -192 48 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +} +// brush 1 +{ +( -144 -192 48 ) ( -144 32 48 ) ( -144 -192 208 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +( -144 -192 208 ) ( -128 -192 208 ) ( -144 -192 48 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( -144 -192 48 ) ( -128 -192 48 ) ( -144 32 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( -144 32 208 ) ( -128 32 208 ) ( -144 -192 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( -144 32 48 ) ( -128 32 48 ) ( -144 32 208 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( -128 -192 48 ) ( -128 -192 208 ) ( -128 32 48 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +} +// brush 2 +{ +( -128 32 208 ) ( -128 16 208 ) ( -128 32 48 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +( 96 16 208 ) ( 96 16 48 ) ( -128 16 208 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( -128 32 48 ) ( -128 16 48 ) ( 96 32 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( 96 32 208 ) ( 96 16 208 ) ( -128 32 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( 96 32 208 ) ( -128 32 208 ) ( 96 32 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 32 48 ) ( 96 16 48 ) ( 96 32 208 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +} +// brush 3 +{ +( -128 -192 48 ) ( -128 -176 48 ) ( -128 -192 208 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +( 96 -192 48 ) ( -128 -192 48 ) ( 96 -192 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 -192 48 ) ( 96 -176 48 ) ( -128 -192 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( -128 -192 208 ) ( -128 -176 208 ) ( 96 -192 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( -128 -176 48 ) ( 96 -176 48 ) ( -128 -176 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 -192 208 ) ( 96 -176 208 ) ( 96 -192 48 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +} +// brush 4 +{ +( -128 -176 208 ) ( -128 -176 192 ) ( -128 16 208 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +( 96 -176 208 ) ( 96 -176 192 ) ( -128 -176 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 16 192 ) ( -128 16 192 ) ( 96 -176 192 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( 96 16 208 ) ( 96 -176 208 ) ( -128 16 208 ) somemissingtexture [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 180 1 1 +( -128 16 208 ) ( -128 16 192 ) ( 96 16 208 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 16 208 ) ( 96 16 192 ) ( 96 -176 208 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +} +// brush 5 +{ +( -128 16 48 ) ( -128 16 64 ) ( -128 -176 48 ) somemissingtexture [ 0 1 0 16 ] [ 0 0 -1 48 ] 0 1 1 +( -128 -176 48 ) ( -128 -176 64 ) ( 96 -176 48 ) somemissingtexture [ -1 0 0 -16 ] [ 0 0 -1 48 ] 180 1 1 +( -128 16 48 ) ( -128 -176 48 ) ( 96 16 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( -128 -176 64 ) ( -128 16 64 ) ( 96 -176 64 ) somemissingtexture [ 1 0 0 16 ] [ 0 -1 0 -16 ] 180 1 1 +( 96 16 48 ) ( 96 16 64 ) ( -128 16 48 ) somemissingtexture [ 1 0 0 16 ] [ 0 0 -1 48 ] 180 1 1 +( 96 -176 48 ) ( 96 -176 64 ) ( 96 16 48 ) somemissingtexture [ 0 -1 0 -16 ] [ 0 0 -1 48 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "-56 -96 120" +} diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index 3d466a4f..b4f7d2da 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -97,6 +97,10 @@ std::tuple> LoadTestmap( destdir = test_quake2_maps_dir; } else if (qbsp_options.target_game->id == GAME_QUAKE) { destdir = test_quake_maps_dir; + } else if (qbsp_options.target_game->id == GAME_HEXEN_II) { + destdir = test_hexen2_maps_dir; + } else if (qbsp_options.target_game->id == GAME_HALF_LIFE) { + destdir = test_halflife_maps_dir; } // copy .bsp to game's basedir/maps directory, for easy in-game testing @@ -1449,8 +1453,9 @@ TEST_CASE("q1_wad_mapname" * doctest::test_suite("testmaps_q1")) CHECK(GAME_QUAKE == bsp.loadversion->game->id); CHECK(bsp.dtex.textures.size() == 2); - CHECK(bsp.dtex.textures[0].name == "skip"); - CHECK(bsp.dtex.textures[0].data.size() == sizeof(dmiptex_t)); // no texture data + CHECK(bsp.dtex.textures[0].name == ""); // skip + CHECK(bsp.dtex.textures[0].data.size() == 0); // no texture data + CHECK(bsp.dtex.textures[0].null_texture); // no texture data CHECK(bsp.dtex.textures[1].name == "{trigger"); CHECK(bsp.dtex.textures[1].data.size() > sizeof(dmiptex_t)); @@ -1763,7 +1768,7 @@ TEST_CASE("textures search relative to current directory") const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_cwd_relative_wad.map"); REQUIRE(2 == bsp.dtex.textures.size()); // FIXME: we shouldn't really be writing skip - CHECK("skip" == bsp.dtex.textures[0].name); + CHECK("" == bsp.dtex.textures[0].name); // make sure the texture was written CHECK("orangestuff8" == bsp.dtex.textures[1].name); @@ -1882,3 +1887,37 @@ TEST_CASE("q1_liquid_software") CHECK(inwater_undirected_edges.find(e) == inwater_undirected_edges.end()); } } + +TEST_CASE("q1_missing_texture") +{ + const auto [bsp, bspx, prt] = LoadTestmap("q1_missing_texture.map"); + + REQUIRE(2 == bsp.dtex.textures.size()); + + // FIXME: we shouldn't really be writing skip + // (our test data includes an actual "skip" texture, + // so that gets included in the bsp.) + CHECK("skip" == bsp.dtex.textures[0].name); + CHECK(!bsp.dtex.textures[0].null_texture); + CHECK(64 == bsp.dtex.textures[0].width); + CHECK(64 == bsp.dtex.textures[0].height); + + CHECK("" == bsp.dtex.textures[1].name); + CHECK(bsp.dtex.textures[1].null_texture); +} + +TEST_CASE("hl_basic") +{ + const auto [bsp, bspx, prt] = LoadTestmap("hl_basic.map", {"-hlbsp"}); + CHECK(prt); + + REQUIRE(2 == bsp.dtex.textures.size()); + + // FIXME: we shouldn't really be writing skip + CHECK(bsp.dtex.textures[0].null_texture); + + CHECK("hltest" == bsp.dtex.textures[1].name); + CHECK(!bsp.dtex.textures[1].null_texture); + CHECK(64 == bsp.dtex.textures[1].width); + CHECK(64 == bsp.dtex.textures[1].height); +}