1759 lines
62 KiB
C++
1759 lines
62 KiB
C++
#include "test_qbsp.hh"
|
|
|
|
#include <qbsp/brush.hh>
|
|
#include <qbsp/brushbsp.hh>
|
|
#include <qbsp/qbsp.hh>
|
|
#include <qbsp/map.hh>
|
|
#include <qbsp/csg.hh>
|
|
#include <common/fs.hh>
|
|
#include <common/bsputils.hh>
|
|
#include <common/decompile.hh>
|
|
#include <common/prtfile.hh>
|
|
#include <common/qvec.hh>
|
|
#include <common/log.hh>
|
|
#include <testmaps.hh>
|
|
|
|
#include <subprocess.h>
|
|
#include <nanobench.h>
|
|
|
|
#include <fstream>
|
|
#include <cstring>
|
|
#include <stdexcept>
|
|
#include <tuple>
|
|
#include <map>
|
|
#include <doctest/doctest.h>
|
|
#include "testutils.hh"
|
|
|
|
// FIXME: Clear global data (planes, etc) between each test
|
|
|
|
const mapface_t *Mapbrush_FirstFaceWithTextureName(const mapbrush_t &brush, const std::string &texname)
|
|
{
|
|
for (auto &face : brush.faces) {
|
|
if (face.texname == texname) {
|
|
return &face;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
mapentity_t &LoadMap(const char *map, size_t length)
|
|
{
|
|
::map.reset();
|
|
qbsp_options.reset();
|
|
|
|
qbsp_options.target_version = &bspver_q1;
|
|
qbsp_options.target_game = qbsp_options.target_version->game;
|
|
|
|
parser_t parser(map, length, { doctest::getContextOptions()->currentTest->m_name });
|
|
|
|
mapentity_t &entity = ::map.entities.emplace_back();
|
|
texture_def_issues_t issue_stats;
|
|
|
|
// FIXME: adds the brush to the global map...
|
|
Q_assert(ParseEntity(parser, entity, issue_stats));
|
|
|
|
CalculateWorldExtent();
|
|
|
|
return entity;
|
|
}
|
|
|
|
mapentity_t &LoadMap(const char *map)
|
|
{
|
|
return LoadMap(map, strlen(map));
|
|
}
|
|
|
|
mapentity_t &LoadMapPath(const std::filesystem::path &name)
|
|
{
|
|
auto filename = std::filesystem::path(testmaps_dir) / name;
|
|
fs::data file_data = fs::load(filename);
|
|
return LoadMap(reinterpret_cast<const char *>(file_data->data()),
|
|
file_data->size());
|
|
}
|
|
|
|
#include <common/bspinfo.hh>
|
|
|
|
#if 0
|
|
std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmapRef(const std::filesystem::path &name)
|
|
{
|
|
const char *destdir = test_quake2_maps_dir;
|
|
if (strlen(destdir) == 0) {
|
|
return {};
|
|
}
|
|
|
|
auto testmap_path = std::filesystem::path(testmaps_dir) / name;
|
|
auto map_in_game_path = fs::path(destdir) / name.filename();
|
|
fs::copy(testmap_path, map_in_game_path, fs::copy_options::overwrite_existing);
|
|
|
|
std::string map_string = map_in_game_path.generic_string();
|
|
|
|
const char *command_line[] = {R"(C:\Users\Eric\Documents\q2tools-220\x64\Debug\4bsp.exe)",
|
|
map_string.c_str(),
|
|
NULL};
|
|
|
|
struct subprocess_s subprocess;
|
|
int result = subprocess_create(command_line, 0, &subprocess);
|
|
if (0 != result) {
|
|
throw std::runtime_error("error launching process");
|
|
}
|
|
|
|
// let the process write
|
|
FILE* p_stdout = subprocess_stdout(&subprocess);
|
|
char buf[32];
|
|
void *res;
|
|
do {
|
|
res = fgets(buf, 32, p_stdout);
|
|
} while (res != nullptr);
|
|
|
|
int retcode;
|
|
if (0 != subprocess_join(&subprocess, &retcode)) {
|
|
throw std::runtime_error("error joining");
|
|
}
|
|
|
|
// re-open the .bsp and return it
|
|
fs::path bsp_path = map_in_game_path;
|
|
bsp_path.replace_extension("bsp");
|
|
|
|
bspdata_t bspdata;
|
|
LoadBSPFile(bsp_path, &bspdata);
|
|
|
|
bspdata.version->game->init_filesystem(bsp_path, qbsp_options);
|
|
|
|
ConvertBSPFormat(&bspdata, &bspver_generic);
|
|
|
|
// write to .json for inspection
|
|
serialize_bsp(bspdata, std::get<mbsp_t>(bspdata.bsp), fs::path(bsp_path).replace_extension(".bsp.json"));
|
|
|
|
std::optional<prtfile_t> prtfile;
|
|
if (const auto prtpath = fs::path(bsp_path).replace_extension(".prt"); fs::exists(prtpath)) {
|
|
prtfile = {LoadPrtFile(prtpath, bspdata.loadversion)};
|
|
}
|
|
|
|
return std::make_tuple(std::move(std::get<mbsp_t>(bspdata.bsp)),
|
|
std::move(bspdata.bspx.entries),
|
|
std::move(prtfile));
|
|
}
|
|
|
|
std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmapRefQ1(const std::filesystem::path &name)
|
|
{
|
|
auto testmap_path = std::filesystem::path(testmaps_dir) / name;
|
|
std::string testmap_path_string = testmap_path.generic_string();
|
|
|
|
const char *command_line[] = {R"(C:\Users\Eric\Downloads\ericw-tools-v0.18.1-win64\bin\qbsp.exe)",
|
|
testmap_path_string.c_str(),
|
|
NULL};
|
|
|
|
struct subprocess_s subprocess;
|
|
int result = subprocess_create(command_line, 0, &subprocess);
|
|
if (0 != result) {
|
|
throw std::runtime_error("error launching process");
|
|
}
|
|
|
|
// let the process write
|
|
FILE* p_stdout = subprocess_stdout(&subprocess);
|
|
char buf[32];
|
|
void *res;
|
|
do {
|
|
res = fgets(buf, 32, p_stdout);
|
|
} while (res != nullptr);
|
|
|
|
int retcode;
|
|
if (0 != subprocess_join(&subprocess, &retcode)) {
|
|
throw std::runtime_error("error joining");
|
|
}
|
|
|
|
// re-open the .bsp and return it
|
|
fs::path bsp_path = testmap_path;
|
|
bsp_path.replace_extension("bsp");
|
|
|
|
bspdata_t bspdata;
|
|
LoadBSPFile(bsp_path, &bspdata);
|
|
|
|
bspdata.version->game->init_filesystem(bsp_path, qbsp_options);
|
|
|
|
ConvertBSPFormat(&bspdata, &bspver_generic);
|
|
|
|
// write to .json for inspection
|
|
serialize_bsp(bspdata, std::get<mbsp_t>(bspdata.bsp), fs::path(bsp_path).replace_extension(".bsp.json"));
|
|
|
|
std::optional<prtfile_t> prtfile;
|
|
if (const auto prtpath = fs::path(bsp_path).replace_extension(".prt"); fs::exists(prtpath)) {
|
|
prtfile = {LoadPrtFile(prtpath, bspdata.loadversion)};
|
|
}
|
|
|
|
return std::make_tuple(std::move(std::get<mbsp_t>(bspdata.bsp)),
|
|
std::move(bspdata.bspx.entries),
|
|
std::move(prtfile));
|
|
}
|
|
#endif
|
|
|
|
std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmap(const std::filesystem::path &name, std::vector<std::string> extra_args)
|
|
{
|
|
auto map_path = std::filesystem::path(testmaps_dir) / name;
|
|
auto bsp_path = map_path;
|
|
bsp_path.replace_extension(".bsp");
|
|
|
|
auto wal_metadata_path = std::filesystem::path(testmaps_dir) / "q2_wal_metadata";
|
|
|
|
std::vector<std::string> args{
|
|
"", // the exe path, which we're ignoring in this case
|
|
"-noverbose",
|
|
"-path",
|
|
wal_metadata_path.string()
|
|
};
|
|
for (auto &arg : extra_args) {
|
|
args.push_back(arg);
|
|
}
|
|
args.push_back(map_path.string());
|
|
args.push_back(bsp_path.string());
|
|
|
|
// run qbsp
|
|
|
|
InitQBSP(args);
|
|
ProcessFile();
|
|
|
|
const char *destdir = "";
|
|
|
|
// read cmake variables TEST_QUAKE_MAP_EXPORT_DIR / TEST_QUAKE2_MAP_EXPORT_DIR
|
|
if (qbsp_options.target_game->id == GAME_QUAKE_II) {
|
|
destdir = test_quake2_maps_dir;
|
|
} else if (qbsp_options.target_game->id == GAME_QUAKE) {
|
|
destdir = test_quake_maps_dir;
|
|
}
|
|
|
|
// copy .bsp to game's basedir/maps directory, for easy in-game testing
|
|
if (strlen(destdir) > 0) {
|
|
auto dest = fs::path(destdir) / name.filename();
|
|
dest.replace_extension(".bsp");
|
|
fs::copy(qbsp_options.bsp_path, dest, fs::copy_options::overwrite_existing);
|
|
}
|
|
|
|
// re-open the .bsp and return it
|
|
qbsp_options.bsp_path.replace_extension("bsp");
|
|
|
|
bspdata_t bspdata;
|
|
LoadBSPFile(qbsp_options.bsp_path, &bspdata);
|
|
|
|
bspdata.version->game->init_filesystem(qbsp_options.bsp_path, qbsp_options);
|
|
|
|
ConvertBSPFormat(&bspdata, &bspver_generic);
|
|
|
|
// write to .json for inspection
|
|
serialize_bsp(bspdata, std::get<mbsp_t>(bspdata.bsp), fs::path(qbsp_options.bsp_path).replace_extension(".bsp.json"));
|
|
|
|
std::optional<prtfile_t> prtfile;
|
|
if (const auto prtpath = fs::path(bsp_path).replace_extension(".prt"); fs::exists(prtpath)) {
|
|
prtfile = {LoadPrtFile(prtpath, bspdata.loadversion)};
|
|
}
|
|
|
|
// decompile .bsp hulls
|
|
if (qbsp_options.target_game->id == GAME_QUAKE) {
|
|
fs::path decompiled_map_path = qbsp_options.bsp_path;
|
|
decompiled_map_path.replace_extension("");
|
|
decompiled_map_path.replace_filename(decompiled_map_path.stem().string() + "-decompiled-hull1");
|
|
decompiled_map_path.replace_extension(".map");
|
|
|
|
std::ofstream f(decompiled_map_path);
|
|
|
|
if (!f)
|
|
Error("couldn't open {} for writing\n", decompiled_map_path);
|
|
|
|
decomp_options options;
|
|
options.hullnum = 1;
|
|
|
|
DecompileBSP(&std::get<mbsp_t>(bspdata.bsp), options, f);
|
|
}
|
|
|
|
return std::make_tuple(std::move(std::get<mbsp_t>(bspdata.bsp)),
|
|
std::move(bspdata.bspx.entries),
|
|
std::move(prtfile));
|
|
}
|
|
|
|
std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmapQ2(const std::filesystem::path &name, std::vector<std::string> extra_args)
|
|
{
|
|
#if 0
|
|
return LoadTestmapRef(name);
|
|
#else
|
|
extra_args.insert(extra_args.begin(), "-q2bsp");
|
|
return LoadTestmap(name, extra_args);
|
|
#endif
|
|
}
|
|
|
|
std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmapQ1(const std::filesystem::path &name, std::vector<std::string> extra_args)
|
|
{
|
|
#if 0
|
|
return LoadTestmapRefQ1(name);
|
|
#else
|
|
return LoadTestmap(name, extra_args);
|
|
#endif
|
|
}
|
|
|
|
void CheckFilled(const mbsp_t &bsp, hull_index_t hullnum)
|
|
{
|
|
int32_t contents = BSP_FindContentsAtPoint(&bsp, hullnum, &bsp.dmodels[0], qvec3d{8192, 8192, 8192});
|
|
|
|
if (bsp.loadversion->game->id == GAME_QUAKE_II) {
|
|
CHECK(contents == Q2_CONTENTS_SOLID);
|
|
} else {
|
|
CHECK(contents == CONTENTS_SOLID);
|
|
}
|
|
}
|
|
|
|
|
|
void CheckFilled(const mbsp_t &bsp)
|
|
{
|
|
if (bsp.loadversion->game->id == GAME_QUAKE_II) {
|
|
CheckFilled(bsp, 0);
|
|
} else {
|
|
auto hullsizes = bsp.loadversion->game->get_hull_sizes();
|
|
for (int i = 0; i < hullsizes.size(); ++i) {
|
|
CheckFilled(bsp, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
mbsp_t LoadBsp(const std::filesystem::path &path_in)
|
|
{
|
|
std::filesystem::path path = path_in;
|
|
|
|
bspdata_t bspdata;
|
|
LoadBSPFile(path, &bspdata);
|
|
|
|
ConvertBSPFormat(&bspdata, &bspver_generic);
|
|
|
|
return std::get<mbsp_t>(bspdata.bsp);
|
|
}
|
|
#endif
|
|
|
|
std::map<std::string, std::vector<const mface_t *>> MakeTextureToFaceMap(const mbsp_t &bsp)
|
|
{
|
|
std::map<std::string, std::vector<const mface_t *>> result;
|
|
|
|
for (auto &face : bsp.dfaces) {
|
|
result[Face_TextureName(&bsp, &face)].push_back(&face);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
const texvecf &GetTexvecs(const char *map, const char *texname)
|
|
{
|
|
mapentity_t &worldspawn = LoadMap(map);
|
|
|
|
const mapbrush_t &mapbrush = worldspawn.mapbrushes.front();
|
|
const mapface_t *mapface = Mapbrush_FirstFaceWithTextureName(mapbrush, "tech02_1");
|
|
Q_assert(nullptr != mapface);
|
|
|
|
return mapface->get_texvecs();
|
|
}
|
|
|
|
std::vector<std::string> TexNames(const mbsp_t &bsp, std::vector<const mface_t *> faces)
|
|
{
|
|
std::vector<std::string> result;
|
|
for (auto &face : faces) {
|
|
result.push_back(Face_TextureName(&bsp, face));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<const mface_t *> FacesWithTextureName(const mbsp_t &bsp, const std::string &name)
|
|
{
|
|
std::vector<const mface_t *> result;
|
|
for (auto &face : bsp.dfaces) {
|
|
if (Face_TextureName(&bsp, &face) == name) {
|
|
result.push_back(&face);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// https://github.com/ericwa/ericw-tools/issues/158
|
|
TEST_CASE("testTextureIssue" * doctest::test_suite("qbsp"))
|
|
{
|
|
const char *bufActual = R"(
|
|
{
|
|
"classname" "worldspawn"
|
|
"wad" "Q.wad"
|
|
{
|
|
( -104 -4 23.999998 ) ( -96.000252 -4 39.999489 ) ( -96.000252 4 39.999489 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -135.996902 4 80.001549 ) ( -152 4 72 ) ( -104 4 23.999998 ) skip 0 -11 -45 1.000000 -1.070000 0 0 0
|
|
( -152 -4 72 ) ( -135.996902 -4 80.001549 ) ( -95.998451 -4 40.003094 ) skip 0 -11 -45 1.000000 -1.070000 0 0 0
|
|
( -96.000633 -4 40.000637 ) ( -136 -4 80.000008 ) ( -136 4 80.000008 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -136 -4 80 ) ( -152 -4 72 ) ( -152 4 72 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -152 -4 72.000008 ) ( -104.000168 -4 24.000172 ) ( -104.000168 4 24.000172 ) tech02_1 0 -8 0 1.000000 0.750000 0 0 0
|
|
}
|
|
}
|
|
)";
|
|
|
|
const char *bufExpected = R"(
|
|
{
|
|
"classname" "worldspawn"
|
|
"wad" "Q.wad"
|
|
{
|
|
( -104 -4 23.999998 ) ( -96.000252 -4 39.999489 ) ( -96.000252 4 39.999489 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -135.996902 4 80.001549 ) ( -152 4 72 ) ( -104 4 23.999998 ) skip 0 -11 -45 1.000000 -1.070000 0 0 0
|
|
( -152 -4 72 ) ( -135.996902 -4 80.001549 ) ( -95.998451 -4 40.003094 ) skip 0 -11 -45 1.000000 -1.070000 0 0 0
|
|
( -96.000633 -4 40.000637 ) ( -136 -4 80.000008 ) ( -136 4 80.000008 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -136 -4 80 ) ( -152 -4 72 ) ( -152 4 72 ) skip 0 0 0 1.000000 1.000000 0 0 0
|
|
( -152 -4 72 ) ( -104 -4 24 ) ( -104 4 24 ) tech02_1 0 -8 0 1 0.75 0 0 0
|
|
}
|
|
}
|
|
)";
|
|
|
|
const auto texvecsExpected = GetTexvecs(bufExpected, "tech02_1");
|
|
const auto texvecsActual = GetTexvecs(bufActual, "tech02_1");
|
|
|
|
// not going to fix #158 for now
|
|
#if 0
|
|
for (int i=0; i<2; i++) {
|
|
for (int j=0; j<4; j++) {
|
|
CHECK(doctest::Approx(texvecsExpected[i][j]) == texvecsActual[i][j]);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TEST_CASE("duplicatePlanes" * doctest::test_suite("qbsp"))
|
|
{
|
|
// a brush from e1m4.map with 7 planes, only 6 unique.
|
|
const char *mapWithDuplicatePlanes = R"(
|
|
{
|
|
"classname" "worldspawn"
|
|
{
|
|
( 512 120 1184 ) ( 512 104 1184 ) ( 512 8 1088 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 1072 104 1184 ) ( 176 104 1184 ) ( 176 8 1088 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 896 56 1184 ) ( 896 72 1184 ) ( 896 -24 1088 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 176 88 1184 ) ( 1072 88 1184 ) ( 1072 -8 1088 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 176 88 1184 ) ( 176 104 1184 ) ( 1072 104 1184 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 1072 8 1088 ) ( 176 8 1088 ) ( 176 -8 1088 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
( 960 8 1088 ) ( 864 104 1184 ) ( 848 104 1184 ) WBRICK1_5 0 0 0 1.000000 1.000000
|
|
}
|
|
}
|
|
)";
|
|
|
|
mapentity_t &worldspawn = LoadMap(mapWithDuplicatePlanes);
|
|
REQUIRE(1 == worldspawn.mapbrushes.size());
|
|
CHECK(6 == worldspawn.mapbrushes.front().faces.size());
|
|
|
|
auto brush = LoadBrush(worldspawn, worldspawn.mapbrushes.front(), {CONTENTS_SOLID}, 0, std::nullopt);
|
|
CHECK(6 == brush->sides.size());
|
|
}
|
|
|
|
/**
|
|
* Test that this skip face gets auto-corrected.
|
|
*/
|
|
TEST_CASE("InvalidTextureProjection" * doctest::test_suite("qbsp"))
|
|
{
|
|
const char *map = R"(
|
|
// entity 0
|
|
{
|
|
"classname" "worldspawn"
|
|
// brush 0
|
|
{
|
|
( -64 -64 -16 ) ( -64 -63 -16 ) ( -64 -64 -15 ) +2butn [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 16 ) ( 64 64 17 ) ( 64 65 16 ) +2butn [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( -64 -64 -16 ) ( -64 -64 -15 ) ( -63 -64 -16 ) +2butn [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) +2butn [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 64 ) ( 64 65 64 ) ( 65 64 64 ) +2butn [ 1 0 0 -0 ] [ 0 -1 0 -0 ] -0 1 1
|
|
( -64 -64 -0 ) ( -63 -64 -0 ) ( -64 -63 -0 ) skip [ 0 0 0 0 ] [ 0 0 0 0 ] -0 1 1
|
|
}
|
|
}
|
|
)";
|
|
|
|
mapentity_t &worldspawn = LoadMap(map);
|
|
Q_assert(1 == worldspawn.mapbrushes.size());
|
|
|
|
const mapface_t *face = &worldspawn.mapbrushes.front().faces[5];
|
|
REQUIRE("skip" == face->texname);
|
|
const auto texvecs = face->get_texvecs();
|
|
CHECK(IsValidTextureProjection(face->get_plane().get_normal(), texvecs.row(0), texvecs.row(1)));
|
|
}
|
|
|
|
/**
|
|
* Same as above but the texture scales are 0
|
|
*/
|
|
TEST_CASE("InvalidTextureProjection2" * doctest::test_suite("qbsp"))
|
|
{
|
|
const char *map = R"(
|
|
// entity 0
|
|
{
|
|
"classname" "worldspawn"
|
|
// brush 0
|
|
{
|
|
( -64 -64 -16 ) ( -64 -63 -16 ) ( -64 -64 -15 ) +2butn [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 16 ) ( 64 64 17 ) ( 64 65 16 ) +2butn [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( -64 -64 -16 ) ( -64 -64 -15 ) ( -63 -64 -16 ) +2butn [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) +2butn [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
|
( 64 64 64 ) ( 64 65 64 ) ( 65 64 64 ) +2butn [ 1 0 0 -0 ] [ 0 -1 0 -0 ] -0 1 1
|
|
( -64 -64 -0 ) ( -63 -64 -0 ) ( -64 -63 -0 ) skip [ 0 0 0 0 ] [ 0 0 0 0 ] -0 0 0
|
|
}
|
|
}
|
|
)";
|
|
|
|
mapentity_t &worldspawn = LoadMap(map);
|
|
Q_assert(1 == worldspawn.mapbrushes.size());
|
|
|
|
const mapface_t *face = &worldspawn.mapbrushes.front().faces[5];
|
|
REQUIRE("skip" == face->texname);
|
|
const auto texvecs = face->get_texvecs();
|
|
CHECK(IsValidTextureProjection(face->get_plane().get_normal(), texvecs.row(0), texvecs.row(1)));
|
|
}
|
|
|
|
/**
|
|
* More realistic: *lava1 has tex vecs perpendicular to face
|
|
*/
|
|
TEST_CASE("InvalidTextureProjection3" * doctest::test_suite("qbsp"))
|
|
{
|
|
const char *map = R"(
|
|
// entity 0
|
|
{
|
|
"classname" "worldspawn"
|
|
"wad" "Q.wad"
|
|
// brush 0
|
|
{
|
|
( 512 512 64 ) ( 512 512 -0 ) ( 512 448 64 ) *04mwat1 [ 0 1 0 0 ] [ 0 0 -1 0 ] -0 1 1
|
|
( -0 448 -0 ) ( -0 512 -0 ) ( -0 448 64 ) *04mwat1 [ 0 -1 0 0 ] [ -0 -0 -1 0 ] -0 1 1
|
|
( 512 512 64 ) ( -0 512 64 ) ( 512 512 -0 ) *04mwat1 [ -1 0 0 0 ] [ 0 0 -1 0 ] -0 1 1
|
|
( -0 448 -0 ) ( -0 448 64 ) ( 512 448 -0 ) *lava1 [ 0 1 0 0 ] [ 0 0 -1 0 ] -0 1 1
|
|
( 512 512 64 ) ( 512 448 64 ) ( -0 512 64 ) *04mwat1 [ 1 0 0 0 ] [ 0 -1 0 0 ] -0 1 1
|
|
( -0 448 -0 ) ( 512 448 -0 ) ( -0 512 -0 ) *04mwat1 [ -1 0 0 0 ] [ -0 -1 -0 -0 ] -0 1 1
|
|
}
|
|
}
|
|
)";
|
|
|
|
mapentity_t &worldspawn = LoadMap(map);
|
|
Q_assert(1 == worldspawn.mapbrushes.size());
|
|
|
|
const mapface_t *face = &worldspawn.mapbrushes.front().faces[3];
|
|
REQUIRE("*lava1" == face->texname);
|
|
const auto texvecs = face->get_texvecs();
|
|
CHECK(IsValidTextureProjection(face->get_plane().get_normal(), texvecs.row(0), texvecs.row(1)));
|
|
}
|
|
|
|
TEST_SUITE("mathlib") {
|
|
TEST_CASE("WindingArea")
|
|
{
|
|
winding_t w(5);
|
|
|
|
// poor test.. but at least checks that the colinear point is treated correctly
|
|
w[0] = {0, 0, 0};
|
|
w[1] = {0, 32, 0}; // colinear
|
|
w[2] = {0, 64, 0};
|
|
w[3] = {64, 64, 0};
|
|
w[4] = {64, 0, 0};
|
|
|
|
CHECK(64.0f * 64.0f == w.area());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* checks that options are reset across tests.
|
|
* set two random options and check that they don't carry over.
|
|
*/
|
|
TEST_CASE("options_reset1" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
LoadTestmap("qbsp_simple_sealed.map", {"-transsky"});
|
|
|
|
CHECK_FALSE(qbsp_options.forcegoodtree.value());
|
|
CHECK(qbsp_options.transsky.value());
|
|
}
|
|
|
|
TEST_CASE("options_reset2" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
LoadTestmap("qbsp_simple_sealed.map", {"-forcegoodtree"});
|
|
|
|
CHECK(qbsp_options.forcegoodtree.value());
|
|
CHECK_FALSE(qbsp_options.transsky.value());
|
|
}
|
|
|
|
/**
|
|
* The brushes are touching but not intersecting, so ChopBrushes shouldn't change anything.
|
|
*/
|
|
TEST_CASE("chop_no_change" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
LoadTestmapQ1("qbsp_chop_no_change.map");
|
|
|
|
// TODO: ideally we should check we get back the same brush pointers from ChopBrushes
|
|
}
|
|
|
|
TEST_CASE("simple_sealed" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const std::vector<std::string> quake_maps{"qbsp_simple_sealed.map", "qbsp_simple_sealed_rotated.map"};
|
|
|
|
for (const auto& mapname : quake_maps) {
|
|
SUBCASE(fmt::format("testing {}", mapname).c_str()) {
|
|
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname);
|
|
|
|
REQUIRE(bsp.dleafs.size() == 2);
|
|
|
|
REQUIRE(bsp.dleafs[0].contents == CONTENTS_SOLID);
|
|
REQUIRE(bsp.dleafs[1].contents == CONTENTS_EMPTY);
|
|
|
|
// just a hollow box
|
|
REQUIRE(bsp.dfaces.size() == 6);
|
|
|
|
// no bspx lumps
|
|
CHECK(bspx.empty());
|
|
|
|
// check markfaces
|
|
CHECK(bsp.dleafs[0].nummarksurfaces == 0);
|
|
CHECK(bsp.dleafs[0].firstmarksurface == 0);
|
|
|
|
CHECK(bsp.dleafs[1].nummarksurfaces == 6);
|
|
CHECK(bsp.dleafs[1].firstmarksurface == 0);
|
|
CHECK_VECTORS_UNOREDERED_EQUAL(bsp.dleaffaces, std::vector<uint32_t>{0, 1, 2, 3, 4, 5});
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("simple_sealed2" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_sealed2.map");
|
|
|
|
CHECK(bsp.dleafs.size() == 3);
|
|
|
|
CHECK(bsp.dleafs[0].contents == CONTENTS_SOLID);
|
|
CHECK(bsp.dleafs[1].contents == CONTENTS_EMPTY);
|
|
CHECK(bsp.dleafs[2].contents == CONTENTS_EMPTY);
|
|
|
|
// L-shaped room
|
|
// 2 ceiling + 2 floor + 6 wall faces
|
|
CHECK(bsp.dfaces.size() == 10);
|
|
|
|
// get markfaces
|
|
const qvec3d player_pos{-56, -96, 120};
|
|
const qvec3d other_empty_leaf_pos{-71, -288, 102};
|
|
auto *player_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos);
|
|
auto *other_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], other_empty_leaf_pos);
|
|
|
|
auto player_markfaces = Leaf_Markfaces(&bsp, player_leaf);
|
|
auto other_markfaces = Leaf_Markfaces(&bsp, other_leaf);
|
|
|
|
// other room's expected markfaces
|
|
|
|
auto *other_floor = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-80, -272, 64), qvec3d(0, 0, 1));
|
|
auto *other_ceil = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-80, -272, 192), qvec3d(0, 0, -1));
|
|
auto *other_minus_x =
|
|
BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-16, -272, 128), qvec3d(-1, 0, 0));
|
|
auto *other_plus_x = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-128, -272, 128), qvec3d(1, 0, 0)); // +X normal wall (extends into player leaf)
|
|
auto *other_plus_y =
|
|
BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-64, -368, 128), qvec3d(0, 1, 0)); // back wall +Y normal
|
|
|
|
CHECK_VECTORS_UNOREDERED_EQUAL(other_markfaces, std::vector<const mface_t*>{
|
|
other_floor, other_ceil, other_minus_x, other_plus_x, other_plus_y
|
|
});
|
|
}
|
|
|
|
|
|
TEST_CASE("simple_worldspawn_worldspawn" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_worldspawn_worldspawn.map", {"-tjunc", "rotate"});
|
|
|
|
// 1 solid leaf
|
|
// 5 empty leafs around the button
|
|
REQUIRE(bsp.dleafs.size() == 6);
|
|
|
|
// 5 faces for the "button"
|
|
// 9 faces for the room (6 + 3 extra for the floor splits)
|
|
REQUIRE(bsp.dfaces.size() == 14);
|
|
|
|
int fan_faces = 0;
|
|
int room_faces = 0;
|
|
for (auto &face : bsp.dfaces) {
|
|
const char *texname = Face_TextureName(&bsp, &face);
|
|
if (!strcmp(texname, "orangestuff8")) {
|
|
++room_faces;
|
|
} else if (!strcmp(texname, "+0fan")) {
|
|
++fan_faces;
|
|
} else {
|
|
FAIL("");
|
|
}
|
|
}
|
|
REQUIRE(fan_faces == 5);
|
|
REQUIRE(room_faces == 9);
|
|
}
|
|
|
|
TEST_CASE("simple_worldspawn_detail_wall" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_worldspawn_detail_wall.map");
|
|
|
|
CHECK(prt.has_value());
|
|
|
|
// 5 faces for the "button"
|
|
// 6 faces for the room
|
|
CHECK(bsp.dfaces.size() == 11);
|
|
|
|
const qvec3d button_pos = {16, -48, 104};
|
|
auto *button_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], button_pos);
|
|
|
|
CHECK(button_leaf->contents == CONTENTS_SOLID);
|
|
CHECK(button_leaf == &bsp.dleafs[0]); // should be using shared solid leaf because it's func_detail_wall
|
|
}
|
|
|
|
TEST_CASE("simple_worldspawn_detail" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_worldspawn_detail.map", {"-tjunc", "rotate"});
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
// 5 faces for the "button"
|
|
// 9 faces for the room
|
|
REQUIRE(bsp.dfaces.size() == 14);
|
|
|
|
// 6 for the box room
|
|
// 5 for the "button"
|
|
CHECK(bsp.dnodes.size() == 11);
|
|
|
|
// this is how many we get with ericw-tools-v0.18.1-32-g6660c5f-win64
|
|
CHECK(bsp.dclipnodes.size() <= 22);
|
|
}
|
|
|
|
TEST_CASE("simple_worldspawn_detail_illusionary" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_worldspawn_detail_illusionary.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
// 5 faces for the "button"
|
|
// 6 faces for the room
|
|
CHECK(bsp.dfaces.size() == 11);
|
|
|
|
// leaf/node counts
|
|
CHECK(11 == bsp.dnodes.size()); // one node per face
|
|
CHECK(7 == bsp.dleafs.size()); // shared solid leaf + 6 empty leafs inside the room
|
|
|
|
// where the func_detail_illusionary sticks into the void
|
|
const qvec3d illusionary_in_void{8, -40, 72};
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], illusionary_in_void)->contents);
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
CHECK(prt->portalleafs == 1);
|
|
}
|
|
|
|
TEST_CASE("simple_worldspawn_sky" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple_worldspawn_sky.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
// just a box with sky on the ceiling
|
|
const auto textureToFace = MakeTextureToFaceMap(bsp);
|
|
CHECK(1 == textureToFace.at("sky3").size());
|
|
CHECK(5 == textureToFace.at("orangestuff8").size());
|
|
|
|
// leaf/node counts
|
|
// - we'd get 7 nodes if it's cut like a cube (solid outside), with 1 additional cut inside to divide sky / empty
|
|
// - we'd get 11 if it's cut as the sky plane (1), then two open cubes (5 nodes each)
|
|
// - can get in between values if it does some vertical cuts, then the sky plane, then other vertical cuts
|
|
//
|
|
// the 7 solution is better but the BSP heuristics won't help reach that one in this trivial test map
|
|
CHECK(bsp.dnodes.size() >= 7);
|
|
CHECK(bsp.dnodes.size() <= 11);
|
|
CHECK(3 == bsp.dleafs.size()); // shared solid leaf + empty + sky
|
|
|
|
// check contents
|
|
const qvec3d player_pos{-88, -64, 120};
|
|
const double inside_sky_z = 232;
|
|
|
|
CHECK(CONTENTS_EMPTY == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos)->contents);
|
|
|
|
// way above map is solid - sky should not fill outwards
|
|
// (otherwise, if you had sky with a floor further up above it, it's not clear where the leafs would be divided, or
|
|
// if the floor contents would turn to sky, etc.)
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d(0,0,500))->contents);
|
|
|
|
CHECK(CONTENTS_SKY == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], qvec3d(player_pos[0], player_pos[1], inside_sky_z))->contents);
|
|
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d( 500, 0, 0))->contents);
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d(-500, 0, 0))->contents);
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d( 0, 500, 0))->contents);
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d( 0, -500, 0))->contents);
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d( 0, 0, -500))->contents);
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
// FIXME: unsure what the expected number of visclusters is, does sky get one?
|
|
}
|
|
|
|
TEST_CASE("water_detail_illusionary" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
static const std::string basic_mapname = "qbsp_water_detail_illusionary.map";
|
|
static const std::string mirrorinside_mapname = "qbsp_water_detail_illusionary_mirrorinside.map";
|
|
|
|
for (const auto& mapname : {basic_mapname, mirrorinside_mapname}) {
|
|
SUBCASE(fmt::format("testing {}", mapname).c_str()) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname);
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
const qvec3d inside_water_and_fence{-20, -52, 124};
|
|
const qvec3d inside_fence{-20, -52, 172};
|
|
|
|
CHECK(BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], inside_water_and_fence)->contents == CONTENTS_WATER);
|
|
CHECK(BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], inside_fence)->contents == CONTENTS_EMPTY);
|
|
|
|
const qvec3d underwater_face_pos{-40, -52, 124};
|
|
const qvec3d above_face_pos{-40, -52, 172};
|
|
|
|
// make sure the detail_illusionary face underwater isn't clipped away
|
|
auto *underwater_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], underwater_face_pos, {-1, 0, 0});
|
|
auto *underwater_face_inner = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], underwater_face_pos, {1, 0, 0});
|
|
|
|
auto *above_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], above_face_pos, {-1, 0, 0});
|
|
auto *above_face_inner = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], above_face_pos, {1, 0, 0});
|
|
|
|
REQUIRE(nullptr != underwater_face);
|
|
REQUIRE(nullptr != above_face);
|
|
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, underwater_face));
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, above_face));
|
|
|
|
if (mapname == mirrorinside_mapname) {
|
|
REQUIRE(underwater_face_inner != nullptr);
|
|
REQUIRE(above_face_inner != nullptr);
|
|
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, underwater_face_inner));
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, above_face_inner));
|
|
} else {
|
|
CHECK(underwater_face_inner == nullptr);
|
|
CHECK(above_face_inner == nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("qbsp_bmodel_mirrorinside_with_liquid" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_bmodel_mirrorinside_with_liquid.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
const qvec3d model1_fenceface{-16, -56, 168};
|
|
const qvec3d model2_waterface{-16, -120, 168};
|
|
|
|
CHECK(2 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[1], model1_fenceface).size());
|
|
CHECK(2 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[2], model2_waterface).size());
|
|
|
|
// both bmodels should be CONTENTS_SOLID in all hulls
|
|
for (int model_idx = 1; model_idx <= 2; ++model_idx) {
|
|
for (int hull = 0; hull <= 2; ++hull) {
|
|
auto &model = bsp.dmodels[model_idx];
|
|
|
|
INFO("model: ", model_idx, " hull: ", hull);
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, {hull}, &model, (model.mins + model.maxs) / 2));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("noclipfaces" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_noclipfaces.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
REQUIRE(bsp.dfaces.size() == 2);
|
|
|
|
// TODO: contents should be empty in hull0 because it's func_detail_illusionary
|
|
|
|
for (auto &face : bsp.dfaces) {
|
|
REQUIRE(std::string("{trigger") == Face_TextureName(&bsp, &face));
|
|
}
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
CHECK(prt->portalleafs == 1);
|
|
}
|
|
|
|
/**
|
|
* _noclipfaces 1 detail_fence meeting a _noclipfaces 0 one.
|
|
*
|
|
* Currently, to simplify the implementation, we're treating that the same as if both had _noclipfaces 1
|
|
*/
|
|
TEST_CASE("noclipfaces_junction" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const std::vector<std::string> maps{
|
|
"qbsp_noclipfaces_junction.map",
|
|
"q2_noclipfaces_junction.map"
|
|
};
|
|
|
|
for (const auto& map : maps) {
|
|
const bool q2 = (map.find("q2") == 0);
|
|
|
|
SUBCASE(map.c_str()) {
|
|
const auto [bsp, bspx, prt] =
|
|
q2 ? LoadTestmapQ2(map) : LoadTestmapQ1(map);
|
|
|
|
CHECK(bsp.dfaces.size() == 12);
|
|
|
|
const qvec3d portal_pos {96, 56, 32};
|
|
|
|
auto *pos_x = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], portal_pos, {1, 0, 0});
|
|
auto *neg_x = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], portal_pos, {-1, 0, 0});
|
|
|
|
REQUIRE(pos_x != nullptr);
|
|
REQUIRE(neg_x != nullptr);
|
|
|
|
if (q2) {
|
|
CHECK(std::string("e1u1/wndow1_2") == Face_TextureName(&bsp, pos_x));
|
|
CHECK(std::string("e1u1/window1") == Face_TextureName(&bsp, neg_x));
|
|
} else {
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, pos_x));
|
|
CHECK(std::string("blood1") == Face_TextureName(&bsp, neg_x));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as previous test, but the T shaped brush entity has _mirrorinside
|
|
*/
|
|
TEST_CASE("noclipfaces_mirrorinside" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_noclipfaces_mirrorinside.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
REQUIRE(bsp.dfaces.size() == 4);
|
|
|
|
// TODO: contents should be empty in hull0 because it's func_detail_illusionary
|
|
|
|
for (auto &face : bsp.dfaces) {
|
|
REQUIRE(std::string("{trigger") == Face_TextureName(&bsp, &face));
|
|
}
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
CHECK(prt->portalleafs == 1);
|
|
}
|
|
|
|
TEST_CASE("detail_illusionary_intersecting" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_illusionary_intersecting.map", {"-tjunc", "rotate"});
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
// sides: 3*4 = 12
|
|
// top: 3 (4 with new tjunc code that prefers more faces over 0-area tris)
|
|
// bottom: 3 (4 with new tjunc code that prefers more faces over 0-area tris)
|
|
CHECK(bsp.dfaces.size() >= 18);
|
|
CHECK(bsp.dfaces.size() <= 20);
|
|
|
|
for (auto &face : bsp.dfaces) {
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, &face));
|
|
}
|
|
|
|
// top of cross
|
|
CHECK(1 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size());
|
|
|
|
// interior face that should be clipped away
|
|
CHECK(0 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -52, 116), qvec3d(0, -1, 0)).size());
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
CHECK(prt->portalleafs == 1);
|
|
}
|
|
|
|
TEST_CASE("detail_illusionary_noclipfaces_intersecting" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_illusionary_noclipfaces_intersecting.map", {"-tjunc", "rotate"});
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
for (auto &face : bsp.dfaces) {
|
|
CHECK(std::string("{trigger") == Face_TextureName(&bsp, &face));
|
|
}
|
|
|
|
// top of cross has 2 faces Z-fighting, because we disabled clipping
|
|
// (with qbsp3 method, there won't ever be z-fighting since we only ever generate 1 face per portal)
|
|
size_t faces_at_top = BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size();
|
|
CHECK(faces_at_top >= 1);
|
|
CHECK(faces_at_top <= 2);
|
|
|
|
// interior face not clipped away
|
|
CHECK(1 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -52, 116), qvec3d(0, -1, 0)).size());
|
|
|
|
CHECK(prt->portals.size() == 0);
|
|
CHECK(prt->portalleafs == 1);
|
|
}
|
|
|
|
/**
|
|
* Since moving to a qbsp3 codebase, detail seals by default.
|
|
*/
|
|
TEST_CASE("detail_seals" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_seals.map");
|
|
|
|
CHECK(prt.has_value());
|
|
}
|
|
|
|
TEST_CASE("detail_doesnt_remove_world_nodes" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_doesnt_remove_world_nodes.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
{
|
|
// check for a face under the start pos
|
|
const qvec3d floor_under_start{-56, -72, 64};
|
|
auto *floor_under_start_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], floor_under_start, {0, 0, 1});
|
|
CHECK(nullptr != floor_under_start_face);
|
|
}
|
|
|
|
{
|
|
// floor face should be clipped away by detail
|
|
const qvec3d floor_inside_detail{64, -72, 64};
|
|
auto *floor_inside_detail_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], floor_inside_detail, {0, 0, 1});
|
|
CHECK(nullptr == floor_inside_detail_face);
|
|
}
|
|
|
|
// make sure the detail face exists
|
|
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], {32, -72, 136}, {-1, 0, 0}));
|
|
|
|
#if 0
|
|
// fixme-brushbsp: with qbsp3 code, the strucutral node is actually clippped away.
|
|
// we could repurpose this test case to test func_detail_wall (q2 window) in which case it would not be clipped away.
|
|
{
|
|
// but the sturctural nodes/leafs should not be clipped away by detail
|
|
const qvec3d covered_by_detail{48, -88, 128};
|
|
auto *covered_by_detail_node = BSP_FindNodeAtPoint(&bsp, &bsp.dmodels[0], covered_by_detail, {-1, 0, 0});
|
|
CHECK(nullptr != covered_by_detail_node);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TEST_CASE("merge" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_merge.map");
|
|
|
|
REQUIRE_FALSE(prt.has_value());
|
|
REQUIRE(bsp.dfaces.size() >= 6);
|
|
|
|
// BrushBSP does a split through the middle first to keep the BSP balanced, which prevents
|
|
// two of the side face from being merged
|
|
REQUIRE(bsp.dfaces.size() <= 8);
|
|
|
|
const auto exp_bounds = aabb3d{{48,0,96}, {224,96,96}};
|
|
|
|
auto* top_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], {48,0,96}, {0,0,1});
|
|
const auto top_winding = Face_Winding(&bsp, top_face);
|
|
|
|
CHECK(top_winding.bounds().mins() == exp_bounds.mins());
|
|
CHECK(top_winding.bounds().maxs() == exp_bounds.maxs());
|
|
}
|
|
|
|
TEST_CASE("tjunc_many_sided_face" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_tjunc_many_sided_face.map", {"-tjunc", "rotate"});
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
std::map<qvec3d, std::vector<const mface_t *>> faces_by_normal;
|
|
for (auto &face : bsp.dfaces) {
|
|
faces_by_normal[Face_Normal(&bsp, &face)].push_back(&face);
|
|
}
|
|
|
|
REQUIRE(6 == faces_by_normal.size());
|
|
|
|
// the floor has a 0.1 texture scale, so it gets subdivided into many small faces
|
|
CHECK(15 * 15 == (faces_by_normal.at({0, 0, 1}).size()));
|
|
|
|
// the ceiling gets split into 2 faces because fixing T-Junctions with all of the
|
|
// wall sections exceeds the max vertices per face limit
|
|
CHECK(2 == (faces_by_normal.at({0, 0, -1}).size()));
|
|
}
|
|
|
|
TEST_CASE("tjunc_angled_face" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_tjunc_angled_face.map");
|
|
CheckFilled(bsp);
|
|
|
|
auto faces = FacesWithTextureName(bsp, "bolt6");
|
|
REQUIRE(faces.size() == 1);
|
|
|
|
auto *bolt6_face = faces.at(0);
|
|
CHECK(bolt6_face->numedges == 5);
|
|
}
|
|
|
|
/**
|
|
* Because it comes second, the sbutt2 brush should "win" in clipping against the floor,
|
|
* in both a worldspawn test case, as well as a func_wall.
|
|
*/
|
|
TEST_CASE("brush_clipping_order" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_brush_clipping_order.map", {"-tjunc", "rotate"});
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
const qvec3d world_button{-8, -8, 16};
|
|
const qvec3d func_wall_button{152, -8, 16};
|
|
|
|
// 0 = world, 1 = func_wall
|
|
REQUIRE(2 == bsp.dmodels.size());
|
|
|
|
REQUIRE(20 == bsp.dfaces.size());
|
|
|
|
REQUIRE(10 == bsp.dmodels[0].numfaces); // 5 faces for the sides + bottom, 5 faces for the top
|
|
REQUIRE(10 == bsp.dmodels[1].numfaces); // (same on worldspawn and func_wall)
|
|
|
|
auto *world_button_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], world_button, {0, 0, 1});
|
|
REQUIRE(nullptr != world_button_face);
|
|
REQUIRE(std::string("sbutt2") == Face_TextureName(&bsp, world_button_face));
|
|
|
|
auto *func_wall_button_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[1], func_wall_button, {0, 0, 1});
|
|
REQUIRE(nullptr != func_wall_button_face);
|
|
REQUIRE(std::string("sbutt2") == Face_TextureName(&bsp, func_wall_button_face));
|
|
}
|
|
|
|
/**
|
|
* Box room with a rotating fan (just a cube). Works in a mod with hiprotate - AD, Quoth, etc.
|
|
*/
|
|
TEST_CASE("origin" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_origin.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
// 0 = world, 1 = rotate_object
|
|
REQUIRE(2 == bsp.dmodels.size());
|
|
|
|
// check that the origin brush didn't clip away any solid faces, or generate faces
|
|
REQUIRE(6 == bsp.dmodels[1].numfaces);
|
|
|
|
// FIXME: should the origin brush update the dmodel's origin too?
|
|
REQUIRE(qvec3f(0, 0, 0) == bsp.dmodels[1].origin);
|
|
|
|
// check that the origin brush updated the entity lump
|
|
parser_t parser(bsp.dentdata, { "qbsp_origin.bsp" });
|
|
auto ents = EntData_Parse(parser);
|
|
auto it = std::find_if(ents.begin(), ents.end(),
|
|
[](const entdict_t &dict) -> bool { return dict.get("classname") == "rotate_object"; });
|
|
|
|
REQUIRE(it != ents.end());
|
|
CHECK(it->get("origin") == "216 -216 340");
|
|
}
|
|
|
|
TEST_CASE("simple" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple.map");
|
|
|
|
REQUIRE_FALSE(prt.has_value());
|
|
|
|
}
|
|
|
|
/**
|
|
* Just a solid cuboid
|
|
*/
|
|
TEST_CASE("q1_cube")
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_cube.map");
|
|
|
|
REQUIRE_FALSE(prt.has_value());
|
|
|
|
const aabb3f cube_bounds {
|
|
{32, -240, 80},
|
|
{80, -144, 112}
|
|
};
|
|
|
|
CHECK(bsp.dedges.size() == 13); // index 0 is reserved, and the cube has 12 edges
|
|
|
|
REQUIRE(7 == bsp.dleafs.size());
|
|
|
|
// check the solid leaf
|
|
auto& solid_leaf = bsp.dleafs[0];
|
|
CHECK(solid_leaf.mins == qvec3f(0,0,0));
|
|
CHECK(solid_leaf.maxs == qvec3f(0,0,0));
|
|
|
|
// check the empty leafs
|
|
for (int i = 1; i < 7; ++i) {
|
|
SUBCASE(fmt::format("leaf {}", i).c_str()) {
|
|
auto &leaf = bsp.dleafs[i];
|
|
CHECK(CONTENTS_EMPTY == leaf.contents);
|
|
|
|
CHECK(1 == leaf.nummarksurfaces);
|
|
}
|
|
}
|
|
|
|
REQUIRE(6 == bsp.dfaces.size());
|
|
|
|
// node bounds
|
|
auto cube_bounds_grown = cube_bounds.grow(24);
|
|
|
|
auto &headnode = bsp.dnodes[bsp.dmodels[0].headnode[0]];
|
|
CHECK(cube_bounds_grown.mins() == headnode.mins);
|
|
CHECK(cube_bounds_grown.maxs() == headnode.maxs);
|
|
|
|
// model bounds are shrunk by 1 unit on each side for some reason
|
|
CHECK(cube_bounds.grow(-1).mins() == bsp.dmodels[0].mins);
|
|
CHECK(cube_bounds.grow(-1).maxs() == bsp.dmodels[0].maxs);
|
|
|
|
CHECK(6 == bsp.dnodes.size());
|
|
|
|
CHECK(12 == bsp.dclipnodes.size());
|
|
}
|
|
|
|
/**
|
|
* Two solid cuboids touching along one edge
|
|
*/
|
|
TEST_CASE("q1_cubes" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_cubes.map");
|
|
|
|
// index 0 is reserved, and the first cube has 12 edges, the second can share one edge so only needs 11
|
|
CHECK(bsp.dedges.size() == 24);
|
|
}
|
|
|
|
/**
|
|
* Ensure submodels that are all "clip" get bounds set correctly
|
|
*/
|
|
TEST_CASE("q1_clip_func_wall" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_clip_func_wall.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
const aabb3f cube_bounds {
|
|
{64, 64, 48},
|
|
{128, 128, 80}
|
|
};
|
|
|
|
REQUIRE(2 == bsp.dmodels.size());
|
|
|
|
// node bounds
|
|
auto &headnode = bsp.dnodes[bsp.dmodels[1].headnode[0]];
|
|
CHECK(cube_bounds.grow(24).mins() == headnode.mins);
|
|
CHECK(cube_bounds.grow(24).maxs() == headnode.maxs);
|
|
|
|
// model bounds are shrunk by 1 unit on each side for some reason
|
|
CHECK(cube_bounds.grow(-1).mins() == bsp.dmodels[1].mins);
|
|
CHECK(cube_bounds.grow(-1).maxs() == bsp.dmodels[1].maxs);
|
|
}
|
|
|
|
/**
|
|
* Lots of features in one map, more for testing in game than automated testing
|
|
*/
|
|
TEST_CASE("features" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbspfeatures.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
|
|
CHECK(bsp.loadversion == &bspver_q1);
|
|
}
|
|
|
|
bool PortalMatcher(const prtfile_winding_t& a, const prtfile_winding_t &b)
|
|
{
|
|
return a.undirectional_equal(b);
|
|
}
|
|
|
|
TEST_CASE("qbsp_func_detail various types" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_func_detail.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
CHECK(1 == bsp.dmodels.size());
|
|
|
|
const qvec3d in_func_detail{56, -56, 120};
|
|
const qvec3d in_func_detail_wall{56, -136, 120};
|
|
const qvec3d in_func_detail_illusionary{56, -216, 120};
|
|
const qvec3d in_func_detail_illusionary_mirrorinside{56, -296, 120};
|
|
|
|
//const double floor_z = 96;
|
|
|
|
// detail clips away world faces, others don't
|
|
CHECK(nullptr == BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], in_func_detail - qvec3d(0,0,24), {0, 0, 1}));
|
|
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_wall - qvec3d(0,0,24), {0, 0, 1}));
|
|
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_illusionary - qvec3d(0,0,24), {0, 0, 1}));
|
|
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_illusionary_mirrorinside - qvec3d(0,0,24), {0, 0, 1}));
|
|
|
|
// check for correct contents
|
|
auto *detail_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_func_detail);
|
|
auto *detail_wall_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_wall);
|
|
auto *detail_illusionary_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_illusionary);
|
|
auto *detail_illusionary_mirrorinside_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_func_detail_illusionary_mirrorinside);
|
|
|
|
CHECK(CONTENTS_SOLID == detail_leaf->contents);
|
|
CHECK(CONTENTS_SOLID == detail_wall_leaf->contents);
|
|
CHECK(CONTENTS_EMPTY == detail_illusionary_leaf->contents);
|
|
CHECK(CONTENTS_EMPTY == detail_illusionary_mirrorinside_leaf->contents);
|
|
|
|
// portals
|
|
|
|
REQUIRE(2 == prt->portals.size());
|
|
|
|
const auto p0 = prtfile_winding_t{{-160, -8, 352}, {56, -8, 352}, {56, -8, 96}, {-160, -8, 96}};
|
|
const auto p1 = p0.translate({232, 0, 0});
|
|
|
|
CHECK(((PortalMatcher(prt->portals[0].winding, p0) && PortalMatcher(prt->portals[1].winding, p1))
|
|
|| (PortalMatcher(prt->portals[0].winding, p1) && PortalMatcher(prt->portals[1].winding, p0))));
|
|
|
|
CHECK(prt->portalleafs == 3);
|
|
CHECK(prt->portalleafs_real > 3);
|
|
}
|
|
|
|
TEST_CASE("qbsp_angled_brush" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_angled_brush.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
CHECK(1 == bsp.dmodels.size());
|
|
// tilted cuboid floating in a box room, so shared solid leaf + 6 empty leafs around the cube
|
|
CHECK(6 + 1 == bsp.dleafs.size());
|
|
}
|
|
|
|
TEST_CASE("qbsp_sealing_point_entity_on_outside" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_sealing_point_entity_on_outside.map");
|
|
|
|
REQUIRE(prt.has_value());
|
|
}
|
|
|
|
TEST_CASE("qbsp_q1_0125unit_faces" * doctest::test_suite("testmaps_q1") * doctest::may_fail())
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_0125unit_faces.map");
|
|
|
|
CHECK(bsp.loadversion == &bspver_q1);
|
|
CHECK(2 == bsp.dfaces.size());
|
|
}
|
|
|
|
TEST_CASE("quake maps" * doctest::test_suite("testmaps_q1") * doctest::skip())
|
|
{
|
|
const std::vector<std::string> quake_maps{"DM1-test.map", "DM2-test.map", "DM3-test.map", "DM4-test.map",
|
|
"DM5-test.map", "DM6-test.map", "DM7-test.map", "E1M1-test.map", "E1M2-test.map", "E1M3-test.map",
|
|
"E1M4-test.map", "E1M5-test.map", "E1M6-test.map", "E1M7-test.map", "E1M8-test.map", "E2M1-test.map",
|
|
"E2M2-test.map", "E2M3-test.map", "E2M4-test.map", "E2M5-test.map", "E2M6-test.map", "E2M7-test.map",
|
|
"E3M1-test.map", "E3M2-test.map", "E3M3-test.map", "E3M4-test.map", "E3M5-test.map", "E3M6-test.map",
|
|
"E3M7-test.map", "E4M1-test.map", "E4M2-test.map", "E4M3-test.map", "E4M4-test.map", "E4M5-test.map",
|
|
"E4M6-test.map", "E4M7-test.map", "E4M8-test.map", "END-test.map"};
|
|
|
|
for (const auto& map : quake_maps) {
|
|
SUBCASE(map.c_str()) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("quake_map_source/" + map);
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
CHECK(prt);
|
|
CheckFilled(bsp);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("chop" * doctest::test_suite("testmaps_q1") * doctest::skip())
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("quake_map_source/DM1-test.map", {"-chop", "-debugchop"});
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
CHECK(prt);
|
|
CheckFilled(bsp);
|
|
}
|
|
|
|
TEST_CASE("mountain" * doctest::test_suite("testmaps_q1") * doctest::skip() * doctest::may_fail())
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_mountain.map");
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
CHECK(prt);
|
|
CheckFilled(bsp);
|
|
}
|
|
|
|
/**
|
|
* Q1 sealing test:
|
|
* - hull0 can use Q2 method (fill inside)
|
|
* - hull1+ can't, because it would cause areas containing no entities but connected by a thin gap to the
|
|
* rest of the world to get sealed off as solid.
|
|
**/
|
|
TEST_CASE("qbsp_q1_sealing" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_sealing.map");
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
const qvec3d in_start_room {-192, 144, 104};
|
|
const qvec3d in_emptyroom {-168, 544, 104};
|
|
const qvec3d in_void {-16, -800, 56};
|
|
const qvec3d connected_by_thin_gap {72, 136, 104};
|
|
|
|
// check leaf contents in hull 0
|
|
CHECK(CONTENTS_EMPTY == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_start_room)->contents);
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_emptyroom)->contents); // can get sealed, since there are no entities
|
|
CHECK(CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], in_void)->contents);
|
|
CHECK(CONTENTS_EMPTY == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], connected_by_thin_gap)->contents);
|
|
|
|
// check leaf contents in hull 1
|
|
CHECK(CONTENTS_EMPTY == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], in_start_room));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], in_emptyroom));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], in_void));
|
|
// ideally this wouldn't get sealed, but we need to do the "inside filling" for compatibility with complex
|
|
// maps using e.g. obj2map geometry, otherwise the clipnodes count explodes
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], connected_by_thin_gap));
|
|
|
|
// check leaf contents in hull 2
|
|
CHECK(CONTENTS_EMPTY == BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], in_start_room));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], in_emptyroom));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], in_void));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], connected_by_thin_gap));
|
|
|
|
CHECK(prt->portals.size() == 2);
|
|
CHECK(prt->portalleafs == 3); // 2 connected rooms + gap (other room is filled in with solid)
|
|
CHECK(prt->portalleafs_real == 3); // no detail, so same as above
|
|
}
|
|
|
|
TEST_CASE("q1_csg" * doctest::test_suite("testmaps_q1")) {
|
|
auto &entity = LoadMapPath("qbsp_q1_csg.map");
|
|
|
|
REQUIRE(entity.mapbrushes.size() == 2);
|
|
|
|
bspbrush_t::container bspbrushes;
|
|
for (int i = 0; i < 2; ++i) {
|
|
auto b = LoadBrush(entity, entity.mapbrushes[i], {CONTENTS_SOLID}, 0, std::nullopt);
|
|
|
|
CHECK(6 == b->sides.size());
|
|
|
|
bspbrushes.push_back(bspbrush_t::make_ptr(std::move(*b)));
|
|
}
|
|
|
|
auto csged = CSGFaces(bspbrushes);
|
|
CHECK(2 == csged.size());
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
CHECK(5 == csged[i]->sides.size());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test for WAD internal textures
|
|
**/
|
|
TEST_CASE("q1_wad_internal" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple.map");
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
CHECK(bsp.dtex.textures.size() == 4);
|
|
// skip is only here because of the water
|
|
CHECK(bsp.dtex.textures[0].name == "skip");
|
|
|
|
CHECK(bsp.dtex.textures[1].name == "orangestuff8");
|
|
CHECK(bsp.dtex.textures[2].name == "*zwater1");
|
|
CHECK(bsp.dtex.textures[3].name == "brown_brick");
|
|
|
|
CHECK(!bsp.dtex.textures[1].data.empty());
|
|
CHECK(!bsp.dtex.textures[2].data.empty());
|
|
CHECK(!bsp.dtex.textures[3].data.empty());
|
|
|
|
CHECK(img::load_mip("orangestuff8", bsp.dtex.textures[1].data, false, bsp.loadversion->game));
|
|
CHECK(img::load_mip("*zwater1", bsp.dtex.textures[2].data, false, bsp.loadversion->game));
|
|
CHECK(img::load_mip("brown_brick", bsp.dtex.textures[3].data, false, bsp.loadversion->game));
|
|
}
|
|
|
|
/**
|
|
* Test for WAD internal textures
|
|
**/
|
|
TEST_CASE("q1_wad_external" * doctest::test_suite("testmaps_q1")) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_simple.map", { "-xwadpath", std::string(testmaps_dir) });
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
CHECK(bsp.dtex.textures.size() == 4);
|
|
// skip is only here because of the water
|
|
CHECK(bsp.dtex.textures[0].name == "skip");
|
|
|
|
CHECK(bsp.dtex.textures[1].name == "orangestuff8");
|
|
CHECK(bsp.dtex.textures[2].name == "*zwater1");
|
|
CHECK(bsp.dtex.textures[3].name == "brown_brick");
|
|
|
|
CHECK(bsp.dtex.textures[1].data.size() == sizeof(dmiptex_t));
|
|
CHECK(bsp.dtex.textures[2].data.size() == sizeof(dmiptex_t));
|
|
CHECK(bsp.dtex.textures[3].data.size() == sizeof(dmiptex_t));
|
|
}
|
|
|
|
/**
|
|
* Test that we automatically try to load X.wad when compiling X.map
|
|
**/
|
|
TEST_CASE("q1_wad_mapname" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_wad_mapname.map");
|
|
|
|
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[1].name == "{trigger");
|
|
CHECK(bsp.dtex.textures[1].data.size() > sizeof(dmiptex_t));
|
|
}
|
|
|
|
TEST_CASE("q1_merge_maps" * doctest::test_suite("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
|
|
parser_t parser(bsp.dentdata, { "q1_merge_maps_base.bsp" });
|
|
auto ents = EntData_Parse(parser);
|
|
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());
|
|
}
|
|
|
|
/**
|
|
* Tests that hollow obj2map style geometry (tetrahedrons) get filled in, in all hulls.
|
|
*/
|
|
TEST_CASE("q1_rocks" * doctest::test_suite("testmaps_q1") * doctest::may_fail())
|
|
{
|
|
constexpr auto* q1_rocks_structural_cube = "q1_rocks_structural_cube.map";
|
|
|
|
const auto mapnames = {
|
|
"q1_rocks.map", // box room with a func_detail "mountain" of tetrahedrons with a hollow inside
|
|
"q1_rocks_merged.map", // same as above but the mountain has been merged in the .map file into 1 brush
|
|
"q1_rocks_structural.map", // same as q1_rocks.map but without the use of func_detail
|
|
"q1_rocks_structural_merged.map",
|
|
q1_rocks_structural_cube // simpler version where the mountain is just a cube
|
|
};
|
|
for (auto *mapname : mapnames) {
|
|
SUBCASE(mapname) {
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname);
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
const qvec3d point{48, 320, 88};
|
|
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 0, &bsp.dmodels[0], point));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], point));
|
|
CHECK(CONTENTS_SOLID == BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], point));
|
|
|
|
for (int i = 1; i <= 2; ++i) {
|
|
INFO("hull " << i);
|
|
|
|
const auto clipnodes = CountClipnodeLeafsByContentType(bsp, i);
|
|
|
|
REQUIRE(clipnodes.size() == 2);
|
|
REQUIRE(clipnodes.find(CONTENTS_SOLID) != clipnodes.end());
|
|
REQUIRE(clipnodes.find(CONTENTS_EMPTY) != clipnodes.end());
|
|
|
|
// 6 for the walls of the box, and 1 for the rock structure, which is convex
|
|
CHECK(clipnodes.at(CONTENTS_SOLID) == 7);
|
|
|
|
if (std::string(q1_rocks_structural_cube) == mapname) {
|
|
CHECK((5 + 6) == CountClipnodeNodes(bsp, i));
|
|
}
|
|
}
|
|
|
|
// for completion's sake, check the nodes
|
|
if (std::string(q1_rocks_structural_cube) == mapname) {
|
|
CHECK((5 + 6) == bsp.dnodes.size());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CountClipnodeLeafsByContentType_r(const mbsp_t& bsp, int clipnode, std::map<int, int> &result)
|
|
{
|
|
if (clipnode < 0) {
|
|
// we're in a leaf node and `clipnode` is actually the content type
|
|
++result[clipnode];
|
|
return;
|
|
}
|
|
|
|
auto &node = bsp.dclipnodes.at(clipnode);
|
|
CountClipnodeLeafsByContentType_r(bsp, node.children[0], result);
|
|
CountClipnodeLeafsByContentType_r(bsp, node.children[1], result);
|
|
}
|
|
|
|
std::map<int, int> CountClipnodeLeafsByContentType(const mbsp_t& bsp, int hullnum)
|
|
{
|
|
Q_assert(hullnum > 0);
|
|
|
|
int headnode = bsp.dmodels.at(0).headnode.at(hullnum);
|
|
std::map<int, int> result;
|
|
CountClipnodeLeafsByContentType_r(bsp, headnode, result);
|
|
|
|
return result;
|
|
}
|
|
|
|
static int CountClipnodeNodes_r(const mbsp_t& bsp, int clipnode)
|
|
{
|
|
if (clipnode < 0) {
|
|
// we're in a leaf node and `clipnode` is actually the content type
|
|
return 0;
|
|
}
|
|
|
|
auto &node = bsp.dclipnodes.at(clipnode);
|
|
return 1 + CountClipnodeNodes_r(bsp, node.children[0])
|
|
+ CountClipnodeNodes_r(bsp, node.children[1]);
|
|
}
|
|
|
|
/**
|
|
* Count the non-leaf clipnodes of the worldmodel for the given hull's decision tree.
|
|
*/
|
|
int CountClipnodeNodes(const mbsp_t& bsp, int hullnum)
|
|
{
|
|
Q_assert(hullnum > 0);
|
|
|
|
int headnode = bsp.dmodels.at(0).headnode.at(hullnum);
|
|
return CountClipnodeNodes_r(bsp, headnode);
|
|
}
|
|
|
|
/**
|
|
* Tests a bad hull expansion
|
|
*/
|
|
TEST_CASE("q1_hull_expansion_lip" * doctest::test_suite("testmaps_q1") * doctest::may_fail())
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_hull_expansion_lip.map");
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
const qvec3d point {174, 308, 42};
|
|
CHECK(CONTENTS_EMPTY == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], point));
|
|
|
|
for (int i = 1; i <= 2; ++i) {
|
|
INFO("hull " << i);
|
|
|
|
const auto clipnodes = CountClipnodeLeafsByContentType(bsp, i);
|
|
|
|
REQUIRE(clipnodes.size() == 2);
|
|
REQUIRE(clipnodes.find(CONTENTS_SOLID) != clipnodes.end());
|
|
REQUIRE(clipnodes.find(CONTENTS_EMPTY) != clipnodes.end());
|
|
|
|
// room shaped like:
|
|
//
|
|
// |\ /|
|
|
// | \__/ |
|
|
// |______|
|
|
//
|
|
// 6 solid leafs for the walls/floor, 3 for the empty regions inside
|
|
CHECK(clipnodes.at(CONTENTS_SOLID) == 6);
|
|
CHECK(clipnodes.at(CONTENTS_EMPTY) == 3);
|
|
|
|
// 6 walls + 2 floors
|
|
CHECK(CountClipnodeNodes(bsp, i) == 8);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("q1_hull1_content_types" * doctest::test_suite("testmaps_q1"))
|
|
{
|
|
const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_hull1_content_types.map");
|
|
|
|
CHECK(GAME_QUAKE == bsp.loadversion->game->id);
|
|
|
|
enum leaf {
|
|
shared_leaf_0,
|
|
new_leaf
|
|
};
|
|
|
|
struct expected_types_t
|
|
{
|
|
int hull0_contenttype;
|
|
leaf hull0_leaf;
|
|
|
|
int hull1_contenttype;
|
|
};
|
|
|
|
const std::vector<std::tuple<qvec3d, expected_types_t>> expected{
|
|
// box center, hull0 contents, hull0 leaf, hull1 contents
|
|
{{0, 0, 0}, {CONTENTS_SOLID, shared_leaf_0, CONTENTS_SOLID}},
|
|
{{64, 0, 0}, {CONTENTS_WATER, new_leaf, CONTENTS_EMPTY}}, // liquids are absent in hull1
|
|
{{128, 0, 0}, {CONTENTS_SLIME, new_leaf, CONTENTS_EMPTY}},
|
|
{{192, 0, 0}, {CONTENTS_LAVA, new_leaf, CONTENTS_EMPTY}},
|
|
{{256, 0, 0}, {CONTENTS_SKY, new_leaf, CONTENTS_SOLID}}, // sky is solid in hull1
|
|
{{320, 0, 0}, {CONTENTS_SOLID, shared_leaf_0, CONTENTS_SOLID}}, // func_detail is solid in hull1
|
|
{{384, 0, 0}, {CONTENTS_SOLID, new_leaf, CONTENTS_SOLID}}, // func_detail_fence is solid in hull1. uses a new leaf in hull0 because it can be seen through
|
|
{{384, -64, 0}, {CONTENTS_SOLID, new_leaf, CONTENTS_SOLID}}, // func_detail_fence + _mirrorinside is solid in hull1
|
|
{{448, 0, 0}, {CONTENTS_EMPTY, new_leaf, CONTENTS_EMPTY}}, // func_detail_illusionary is empty in hull1
|
|
{{448, -64, 0}, {CONTENTS_EMPTY, new_leaf, CONTENTS_EMPTY}}, // func_detail_illusionary + _mirrorinside is empty in hull1
|
|
{{512, 0, 0}, {CONTENTS_SOLID, shared_leaf_0, CONTENTS_SOLID}}, // func_detail_wall is solid in hull1
|
|
};
|
|
|
|
for (const auto &[point, expected_types] : expected) {
|
|
std::string message = qv::to_string(point);
|
|
CAPTURE(message);
|
|
|
|
// hull 0
|
|
auto* hull0_leaf = BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], point);
|
|
|
|
CHECK(expected_types.hull0_contenttype == hull0_leaf->contents);
|
|
ptrdiff_t hull0_leaf_index = hull0_leaf - &bsp.dleafs[0];
|
|
|
|
if (expected_types.hull0_leaf == shared_leaf_0) {
|
|
CHECK(hull0_leaf_index == 0);
|
|
} else {
|
|
CHECK(hull0_leaf_index != 0);
|
|
}
|
|
|
|
// hull 1
|
|
CHECK(expected_types.hull1_contenttype == BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[0], point));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("winding" * doctest::test_suite("benchmark")
|
|
* doctest::skip()) {
|
|
ankerl::nanobench::Bench bench;
|
|
|
|
bench.run("std::vector<double> reserve(3*4*6)", [&] {
|
|
std::vector<double> temp;
|
|
temp.reserve(3 * 4 * 6);
|
|
ankerl::nanobench::doNotOptimizeAway(temp);
|
|
});
|
|
bench.run("std::vector<qvec3d> reserve(4*6)", [&] {
|
|
std::vector<qvec3d> temp;
|
|
temp.reserve(4 * 6);
|
|
ankerl::nanobench::doNotOptimizeAway(temp);
|
|
});
|
|
bench.run("std::array<double, 3*4*6>", [&] {
|
|
std::array<double, 3 * 4 * 6> temp;
|
|
ankerl::nanobench::doNotOptimizeAway(temp);
|
|
});
|
|
bench.run("std::array<qvec3d, 4*6>", [&] {
|
|
std::array<qvec3d, 4 * 6> temp;
|
|
ankerl::nanobench::doNotOptimizeAway(temp);
|
|
});
|
|
bench.run("polylib::winding_base_t<6> construct", [&] {
|
|
polylib::winding_base_t<polylib::winding_storage_hybrid_t<6>> temp;
|
|
ankerl::nanobench::doNotOptimizeAway(temp);
|
|
});
|
|
}
|
|
|
|
TEST_CASE("BrushFromBounds") {
|
|
map.reset();
|
|
qbsp_options.reset();
|
|
qbsp_options.worldextent.setValue(1024, settings::source::COMMANDLINE);
|
|
|
|
auto brush = BrushFromBounds({{2,2,2}, {32, 32, 32}});
|
|
|
|
CHECK(brush->sides.size() == 6);
|
|
|
|
const auto top_winding = winding_t{{2, 2, 32}, {2, 32, 32}, {32, 32, 32}, {32, 2, 32}};
|
|
const auto bottom_winding = winding_t{{32, 2, 2},{32, 32, 2}, {2, 32, 2}, {2, 2, 2}};
|
|
|
|
int found = 0;
|
|
|
|
for (auto &side : brush->sides) {
|
|
CHECK(side.w);
|
|
|
|
if (side.w.directional_equal(top_winding)) {
|
|
found++;
|
|
auto &plane = side.get_plane();
|
|
CHECK(plane.get_normal() == qvec3d{0,0,1});
|
|
CHECK(plane.get_dist() == 32);
|
|
}
|
|
|
|
if (side.w.directional_equal(bottom_winding)) {
|
|
found++;
|
|
auto plane = side.get_plane();
|
|
CHECK(plane.get_normal() == qvec3d{0,0,-1});
|
|
CHECK(plane.get_dist() == -2);
|
|
}
|
|
}
|
|
CHECK(found == 2);
|
|
}
|