From f93a36c29504a7aacfdb8cfdab2ec321bdfd2d98 Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Mon, 17 Jul 2023 23:32:38 -0600 Subject: [PATCH 1/3] qbsp: don't write 0 size textures write them as offset -1 in dmiptexlump_t::stream_write() --- common/bspfile_generic.cc | 4 ++-- tests/test_qbsp.cc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/bspfile_generic.cc b/common/bspfile_generic.cc index 3c950032..a027891b 100644 --- a/common/bspfile_generic.cc +++ b/common/bspfile_generic.cc @@ -185,7 +185,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 +203,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/tests/test_qbsp.cc b/tests/test_qbsp.cc index 3d466a4f..9bf32bf4 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -1449,8 +1449,8 @@ 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[1].name == "{trigger"); CHECK(bsp.dtex.textures[1].data.size() > sizeof(dmiptex_t)); @@ -1763,7 +1763,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); From 40a10b2c52c8908e2cefc1bc3a6851395907825b Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Mon, 17 Jul 2023 23:58:44 -0600 Subject: [PATCH 2/3] bspinfo: serialize missing textures (offset -1) to json as null previously, we were writing a json dictionary which was confusing - bspfile: fix reading a valid texture followed by a missing texture (offset -1) - add missing texture testcase --- common/bspfile_generic.cc | 17 ++++++-- common/bspinfo.cc | 5 +++ include/common/bspfile_generic.hh | 4 ++ testmaps/q1_missing_texture.map | 69 +++++++++++++++++++++++++++++++ tests/test_qbsp.cc | 19 +++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 testmaps/q1_missing_texture.map diff --git a/common/bspfile_generic.cc b/common/bspfile_generic.cc index a027891b..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) { 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/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 9bf32bf4..74840fff 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -1451,6 +1451,7 @@ TEST_CASE("q1_wad_mapname" * doctest::test_suite("testmaps_q1")) CHECK(bsp.dtex.textures.size() == 2); 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)); @@ -1882,3 +1883,21 @@ 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); +} From a02a4ec3161e7481041c824f4e20c84e811a05c5 Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Tue, 18 Jul 2023 17:57:51 -0600 Subject: [PATCH 3/3] imglib: fix wad3 loading (thanks to erysdren for the fix) add a basic half-life test case (box map) --- CMakeLists.txt | 2 + common/imglib.cc | 2 +- testmaps.hh.in | 2 + testmaps/deprecated/hlwad.wad | Bin 0 -> 6296 bytes testmaps/hl_basic.map | 79 ++++++++++++++++++++++++++++++++++ tests/test_qbsp.cc | 20 +++++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 testmaps/deprecated/hlwad.wad create mode 100644 testmaps/hl_basic.map 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/imglib.cc b/common/imglib.cc index aec33786..dc6484fe 100644 --- a/common/imglib.cc +++ b/common/imglib.cc @@ -220,7 +220,7 @@ std::optional load_mip( if (game->id == GAME_HALF_LIFE) { bool valid_mip_palette = true; - int32_t mip3_size = (header.width >> 3) + (header.height >> 3); + int32_t mip3_size = (header.width >> 3) * (header.height >> 3); size_t palette_size = sizeof(uint16_t) + (sizeof(qvec3b) * 256); if (header.offsets[3] <= 0) { 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 0000000000000000000000000000000000000000..6d75867e79a537894597d3961f2a1b39a226e051 GIT binary patch literal 6296 zcmeI1e>7iZ9LHaiBuSEFGD#*$l9?ovNs^f)nItnIvu~1plbK91NkWp$gd|ClBuVz2 zBuSEFW+q9JB+1N7O82Yv&;FRxxo2nRbnfSz&;9kB=bYzxo%j2mbKiH%M%Q(#fH-}K zkjcY#%Gnj@y-gM-m*XrvSGKvbwT0Fd`f^a5^GXm^3WccY3DSItpBMs?e#FpAko+gT zzeEX0@}DStlK&+Ci4u_HKT-H3|4IH6B_PRvqVP%nTj~3M1-vWSR~kPvK#7mU=S&37 zwmzzhPpi`wQ~W=_-=B4=PVF=6Kb}zrt|*Txa2}|_`Je_DfI3_V8gLP4!o{Ekmw*tz zUJ5#J83-|~kizQ06(Gd6tHA)S1VdN@MsO7v!_{B{*MKRk1v9vIIm^8cEZ};ugd4yL zZUk$%32a~;*ur|SgPXw~ZUG0l6&zs$IKgepx$s7Cf!o0q?f^Hq6Wrl0@PJL=37f$S z?gnqT2YldO@P+%p54M0m+z$cp00hE=5CmHx7`8zOJOrT-2I26qkWs^Sh=d&=gGV3= z9))Ol3}Rp>$l-B_g(rku8+JiF?1lt*5)xq#B&n!8rK*~&ruMYDdWwd|UQNwZEv;v? zwbOKTp4HV&*VB7WUq8dZ;CVyCOe3Q#W8-WSlNU@)bIiNCk3S%!Ux|qalaiW~lRr*LX-Q4}BrUBqJ^j;+jJC|o&$6@Ib8h2!z>G`?0ccQQFm;V0A zfq`EK2d9RHej6U19vS(4baZBH?AZAD?8L> 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 @@ -1901,3 +1905,19 @@ TEST_CASE("q1_missing_texture") 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); +}