use an optional<uint8_t> to store hull number, which gets rid of special -1 collision hull number.

fixed no-hull always chopping even with chop off
This commit is contained in:
Jonathan 2022-08-15 06:13:30 -04:00
parent 20f5d73a3c
commit f7b8f85ece
14 changed files with 146 additions and 133 deletions

View File

@ -464,12 +464,12 @@ static int BSP_FindClipnodeAtPoint_r(const mbsp_t *bsp, const int clipnodenum, c
}
}
int BSP_FindContentsAtPoint(const mbsp_t *bsp, int hull, const dmodelh2_t *model, const qvec3d &point)
int BSP_FindContentsAtPoint(const mbsp_t *bsp, hull_index_t hullnum, const dmodelh2_t *model, const qvec3d &point)
{
if (hull == 0) {
if (!hullnum.value_or(0)) {
return BSP_FindLeafAtPoint_r(bsp, model->headnode[0], point)->contents;
}
return BSP_FindClipnodeAtPoint_r(bsp, model->headnode.at(hull), point);
return BSP_FindClipnodeAtPoint_r(bsp, model->headnode.at(hullnum.value()), point);
}
std::vector<const mface_t *> Leaf_Markfaces(const mbsp_t *bsp, const mleaf_t *leaf)

View File

@ -421,6 +421,9 @@ struct fmt::formatter<texvec<T>> : formatter<qmat<T, 2, 4>>
using texvecf = texvec<float>;
// type to store a hull index; max 256 hulls, zero is valid.
using hull_index_t = std::optional<uint8_t>;
#include "bspfile_generic.hh"
#include "bspfile_q1.hh"
#include "bspfile_q2.hh"

View File

@ -71,7 +71,7 @@ const bsp2_dnode_t *BSP_FindNodeAtPoint(
const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point, const qvec3d &wanted_normal);
const mleaf_t *BSP_FindLeafAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, const qvec3d &point);
int BSP_FindContentsAtPoint(const mbsp_t *bsp, int hull, const dmodelh2_t *model, const qvec3d &point);
int BSP_FindContentsAtPoint(const mbsp_t *bsp, hull_index_t hullnum, const dmodelh2_t *model, const qvec3d &point);
std::vector<const mface_t *> Leaf_Markfaces(const mbsp_t *bsp, const mleaf_t *leaf);
std::vector<const dbrush_t *> Leaf_Brushes(const mbsp_t *bsp, const mleaf_t *leaf);

View File

@ -595,12 +595,14 @@ std::tuple<qvec<T, 3>, qvec<T, 3>> MakeTangentAndBitangentUnnormalized(const qve
}
// debug test
#ifdef PARANOID
if (0) {
auto n = qv::normalize(qv::cross(tangent, bitangent));
double d = qv::distance(n, normal);
assert(d < 0.0001);
}
#endif
return {tangent, bitangent};
}

View File

@ -93,5 +93,5 @@ struct bspbrush_t
bspbrush_t clone() const;
};
std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush, const contentflags_t &contents, const int hullnum, std::optional<std::reference_wrapper<size_t>> num_clipped);
std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush, const contentflags_t &contents, hull_index_t hullnum, std::optional<std::reference_wrapper<size_t>> num_clipped);
bool CreateBrushWindings(bspbrush_t *brush);

View File

@ -139,7 +139,7 @@ struct maptexdata_t
surfflags_t flags;
int32_t value;
std::string animation;
int32_t animation_miptex = -1;
std::optional<int32_t> animation_miptex = std::nullopt;
};
#include <common/imglib.hh>
@ -342,18 +342,15 @@ void WriteEntitiesToString();
qvec3d FixRotateOrigin(mapentity_t *entity);
/** Special ID for the collision-only hull; used for wrbrushes/Q2 */
constexpr int HULL_COLLISION = -1;
/* Create BSP brushes from map brushes */
void Brush_LoadEntity(mapentity_t *entity, const int hullnum, bspbrush_t::container &brushes, size_t &num_clipped);
void Brush_LoadEntity(mapentity_t *entity, hull_index_t hullnum, bspbrush_t::container &brushes, size_t &num_clipped);
std::list<face_t *> CSGFace(
face_t *srcface, const mapentity_t *srcentity, const bspbrush_t *srcbrush, const node_t *srcnode);
void TJunc(node_t *headnode);
int MakeFaceEdges(node_t *headnode);
void EmitVertices(node_t *headnode);
void ExportClipNodes(mapentity_t *entity, node_t *headnode, const int hullnum);
void ExportClipNodes(mapentity_t *entity, node_t *headnode, hull_index_t::value_type hullnum);
void ExportDrawNodes(mapentity_t *entity, node_t *headnode, int firstface);
struct bspxbrushes_s

View File

@ -28,7 +28,7 @@ class mapentity_t;
struct node_t;
struct tree_t;
bool FillOutside(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_t::container &brushes);
bool FillOutside(mapentity_t *entity, tree_t *tree, hull_index_t hullnum, bspbrush_t::container &brushes);
std::vector<node_t *> FindOccupiedClusters(node_t *headnode);
void FillBrushEntity(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_t::container &brushes);
void FillBrushEntity(mapentity_t *entity, tree_t *tree, hull_index_t hullnum, bspbrush_t::container &brushes);

View File

@ -407,7 +407,7 @@ struct maptexinfo_t
int32_t miptex = 0;
surfflags_t flags = {};
int32_t value = 0; // Q2-specific
int32_t next = -1; // Q2-specific
std::optional<int32_t> next = std::nullopt; // Q2-specific
std::optional<size_t> outputnum = std::nullopt; // nullopt until added to bsp
constexpr auto as_tuple() const { return std::tie(vecs, miptex, flags, value, next); }

View File

@ -284,7 +284,7 @@ Converts a mapbrush to a bsp brush
===============
*/
std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush, const contentflags_t &contents,
const int hullnum, std::optional<std::reference_wrapper<size_t>> num_clipped)
hull_index_t hullnum, std::optional<std::reference_wrapper<size_t>> num_clipped)
{
// create the brush
bspbrush_t brush{};
@ -295,7 +295,7 @@ std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush
for (size_t i = 0; i < mapbrush->faces.size(); i++) {
auto &src = mapbrush->faces[i];
if (hullnum <= 0 && mapbrush->is_hint) {
if (!hullnum.value_or(0) && mapbrush->is_hint) {
/* Don't generate hintskip faces */
const maptexinfo_t &texinfo = map.mtexinfos.at(src.texinfo);
@ -306,23 +306,22 @@ std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush
}
// don't add bevels for the point hull
if (hullnum <= 0 && src.bevel) {
if (!hullnum.value_or(0) && src.bevel) {
continue;
}
auto &dst = brush.sides.emplace_back();
dst.texinfo = hullnum > 0 ? 0 : src.texinfo;
dst.texinfo = hullnum.value_or(0) ? 0 : src.texinfo;
dst.planenum = src.planenum;
dst.bevel = src.bevel;
dst.source = &src;
}
// expand the brushes for the hull
if (hullnum > 0) {
if (hullnum.value_or(0)) {
auto &hulls = qbsp_options.target_game->get_hull_sizes();
Q_assert(hullnum < hulls.size());
auto &hull = *(hulls.begin() + hullnum);
auto &hull = *(hulls.begin() + hullnum.value());
for (auto &mapface : brush.sides) {
if (mapface.get_texinfo().flags.no_expand) {
@ -365,8 +364,8 @@ std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush
// The idea behind the bounds expansion was to avoid incorrect vis culling (AFAIK).
const bool shouldExpand = (src->origin[0] != 0.0 || src->origin[1] != 0.0 || src->origin[2] != 0.0) &&
src->rotation == rotation_t::hipnotic &&
(hullnum >= 0) // hullnum < 0 corresponds to -wrbrushes clipping hulls
&& qbsp_options.target_game->id != GAME_HEXEN_II; // never do this in Hexen 2
hullnum.has_value() &&
qbsp_options.target_game->id != GAME_HEXEN_II; // never do this in Hexen 2
if (shouldExpand) {
vec_t max = -std::numeric_limits<vec_t>::infinity(), min = std::numeric_limits<vec_t>::infinity();
@ -389,7 +388,7 @@ std::optional<bspbrush_t> LoadBrush(const mapentity_t *src, mapbrush_t *mapbrush
//=============================================================================
static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hullnum, content_stats_base_t &stats, bspbrush_t::container &brushes, logging::percent_clock &clock, size_t &num_clipped)
static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, hull_index_t hullnum, content_stats_base_t &stats, bspbrush_t::container &brushes, logging::percent_clock &clock, size_t &num_clipped)
{
// _omitbrushes 1 just discards all brushes in the entity.
// could be useful for geometry guides, selective compilation, etc.
@ -477,7 +476,7 @@ static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hulln
/* func_detail_illusionary don't exist in the collision hull
* (or bspx export) except for Q2, who needs them in there */
if (hullnum > 0 && detail_illusionary) {
if (hullnum.value_or(0) && detail_illusionary) {
continue;
}
@ -486,8 +485,8 @@ static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hulln
* include them in the model bounds so collision detection works
* correctly.
*/
if (hullnum != HULL_COLLISION && contents.is_clip(qbsp_options.target_game)) {
if (hullnum == 0) {
if (hullnum.has_value() && contents.is_clip(qbsp_options.target_game)) {
if (hullnum.value() == 0) {
if (auto brush = LoadBrush(src, &mapbrush, contents, hullnum, num_clipped)) {
dst->bounds += brush->bounds;
}
@ -500,8 +499,9 @@ static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hulln
/* "hint" brushes don't affect the collision hulls */
if (mapbrush.is_hint) {
if (hullnum > 0)
if (hullnum.value_or(0)) {
continue;
}
contents = qbsp_options.target_game->create_empty_contents();
}
@ -517,18 +517,20 @@ static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hulln
before writing the bsp, and bmodels normally have CONTENTS_SOLID as their
contents type.
*/
if (hullnum <= 0 && mirrorinside.value_or(false)) {
if (!hullnum.value_or(0) && mirrorinside.value_or(false)) {
contents = qbsp_options.target_game->create_detail_fence_contents(contents);
}
}
/* nonsolid brushes don't show up in clipping hulls */
if (hullnum > 0 && !contents.is_any_solid(qbsp_options.target_game) && !contents.is_sky(qbsp_options.target_game))
continue;
if (hullnum.value_or(0)) {
/* nonsolid brushes don't show up in clipping hulls */
if (!contents.is_any_solid(qbsp_options.target_game) && !contents.is_sky(qbsp_options.target_game))
continue;
/* sky brushes are solid in the collision hulls */
if (hullnum > 0 && contents.is_sky(qbsp_options.target_game))
contents = qbsp_options.target_game->create_solid_contents();
/* sky brushes are solid in the collision hulls */
if (contents.is_sky(qbsp_options.target_game))
contents = qbsp_options.target_game->create_solid_contents();
}
// apply extended flags
contents.set_mirrored(mirrorinside);
@ -552,11 +554,11 @@ static void Brush_LoadEntity(mapentity_t *dst, mapentity_t *src, const int hulln
============
Brush_LoadEntity
hullnum HULL_COLLISION should contain ALL brushes. (used by BSPX_CreateBrushList())
hullnum nullopt should contain ALL brushes; BSPX and Quake II, etc.
hullnum 0 does not contain clip brushes.
============
*/
void Brush_LoadEntity(mapentity_t *entity, const int hullnum, bspbrush_t::container &brushes, size_t &num_clipped)
void Brush_LoadEntity(mapentity_t *entity, hull_index_t hullnum, bspbrush_t::container &brushes, size_t &num_clipped)
{
logging::funcheader();

View File

@ -395,10 +395,10 @@ int FindTexinfo(const maptexinfo_t &texinfo)
assert(map.mtexinfo_lookup.find(texinfo) != map.mtexinfo_lookup.end());
// create a copy of the miptex for animation chains
if (map.miptex[texinfo.miptex].animation_miptex != -1) {
if (map.miptex[texinfo.miptex].animation_miptex.has_value()) {
maptexinfo_t anim_next = texinfo;
anim_next.miptex = map.miptex[texinfo.miptex].animation_miptex;
anim_next.miptex = map.miptex[texinfo.miptex].animation_miptex.value();
map.mtexinfos[num_texinfo].next = FindTexinfo(anim_next);
}
@ -2869,7 +2869,7 @@ static void TestExpandBrushes(mapentity_t *src)
for (auto &mapbrush : src->mapbrushes) {
auto hull1brush = LoadBrush(src, &mapbrush, {CONTENTS_SOLID},
qbsp_options.target_game->id == GAME_QUAKE_II ? HULL_COLLISION : 1, std::nullopt);
qbsp_options.target_game->id == GAME_QUAKE_II ? std::nullopt : std::optional<int32_t>(1), std::nullopt);
if (hull1brush) {
hull1brushes.emplace_back(bspbrush_t::make_ptr(std::move(*hull1brush)));

View File

@ -584,7 +584,7 @@ get incorrectly marked as "invisible").
Special cases: structural fully covered by detail still needs to be marked "visible".
===========
*/
bool FillOutside(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_t::container &brushes)
bool FillOutside(mapentity_t *entity, tree_t *tree, hull_index_t hullnum, bspbrush_t::container &brushes)
{
node_t *node = tree->headnode;
@ -604,7 +604,7 @@ bool FillOutside(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_
}
if (occupied_clusters.empty()) {
logging::print("WARNING: No entities in empty space -- no filling performed (hull {})\n", hullnum);
logging::print("WARNING: No entities in empty space -- no filling performed (hull {})\n", hullnum.value_or(0));
return false;
}
@ -705,7 +705,7 @@ bool FillOutside(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_
return true;
}
void FillBrushEntity(mapentity_t *entity, tree_t *tree, const int hullnum, bspbrush_t::container &brushes)
void FillBrushEntity(mapentity_t *entity, tree_t *tree, hull_index_t hullnum, bspbrush_t::container &brushes)
{
logging::funcheader();

View File

@ -393,7 +393,7 @@ static void CountLeafs(node_t *headnode)
ProcessEntity
===============
*/
static void ProcessEntity(mapentity_t *entity, const int hullnum)
static void ProcessEntity(mapentity_t *entity, hull_index_t hullnum)
{
/* No map brushes means non-bmodel entity.
We need to handle worldspawn containing no brushes, though. */
@ -426,8 +426,9 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
if (qbsp_options.verbose.value())
PrintEntity(entity);
if (hullnum <= 0)
if (!hullnum.value_or(0) || qbsp_options.loghulls.value()) {
logging::print(logging::flag::STAT, " MODEL: {}\n", mod);
}
entity->epairs.set("model", mod);
}
}
@ -461,8 +462,8 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
logging::print(logging::flag::STAT, "INFO: calculating BSP for {} brushes with {} sides\n", brushes.size(), num_sides);
// always chop the other hulls
if (qbsp_options.chop.value() || hullnum != 0) {
// always chop the other hulls to reduce brush tests
if (qbsp_options.chop.value() || hullnum.value_or(0)) {
ChopBrushes(brushes);
}
@ -474,7 +475,9 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
}
std::unique_ptr<tree_t> tree = nullptr;
if (hullnum > 0) {
// simpler operation for hulls
if (hullnum.value_or(0)) {
tree = BrushBSP(entity, brushes, true);
if (entity == map.world_entity() && !qbsp_options.nofill.value()) {
// assume non-world bmodels are simple
@ -490,88 +493,89 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
PruneNodes(tree->headnode);
}
}
ExportClipNodes(entity, tree->headnode, hullnum);
ExportClipNodes(entity, tree->headnode, hullnum.value());
return;
}
// fixme-brushbsp: return here?
// full operation for collision (or main hull)
if (qbsp_options.forcegoodtree.value()) {
tree = BrushBSP(entity, brushes, false);
} else {
tree = BrushBSP(entity, brushes, entity == map.world_entity() ? std::nullopt : std::optional<bool>(false));
}
if (qbsp_options.forcegoodtree.value()) {
tree = BrushBSP(entity, brushes, false);
} else {
tree = BrushBSP(entity, brushes, entity == map.world_entity() ? std::nullopt : std::optional<bool>(false));
}
// build all the portals in the bsp tree
// some portals are solid polygons, and some are paths to other leafs
MakeTreePortals(tree.get());
// build all the portals in the bsp tree
// some portals are solid polygons, and some are paths to other leafs
MakeTreePortals(tree.get());
if (entity == map.world_entity()) {
// flood fills from the void.
// marks brush sides which are *only* touching void;
// we can skip using them as BSP splitters on the "really good tree"
// (effectively expanding those brush sides outwards).
if (!qbsp_options.nofill.value() && FillOutside(entity, tree.get(), hullnum, brushes)) {
// make a really good tree
tree.reset();
tree = BrushBSP(entity, brushes, false);
// make the real portals for vis tracing
MakeTreePortals(tree.get());
// fill again so PruneNodes works
FillOutside(entity, tree.get(), hullnum, brushes);
}
// Area portals
if (qbsp_options.target_game->id == GAME_QUAKE_II) {
FloodAreas(entity, tree->headnode);
EmitAreaPortals(tree->headnode);
}
} else {
FillBrushEntity(entity, tree.get(), hullnum, brushes);
// rebuild BSP now that we've marked invisible brush sides
if (entity == map.world_entity()) {
// flood fills from the void.
// marks brush sides which are *only* touching void;
// we can skip using them as BSP splitters on the "really good tree"
// (effectively expanding those brush sides outwards).
if (!qbsp_options.nofill.value() && FillOutside(entity, tree.get(), hullnum, brushes)) {
// make a really good tree
tree.reset();
tree = BrushBSP(entity, brushes, false);
// make the real portals for vis tracing
MakeTreePortals(tree.get());
// fill again so PruneNodes works
FillOutside(entity, tree.get(), hullnum, brushes);
}
MakeTreePortals(tree.get());
MarkVisibleSides(tree.get(), entity, brushes);
MakeFaces(tree->headnode);
FreeTreePortals(tree.get());
PruneNodes(tree->headnode);
if (hullnum <= 0 && entity == map.world_entity() && (!map.leakfile || qbsp_options.keepprt.value())) {
WritePortalFile(tree.get());
}
// needs to come after any face creation
MakeMarkFaces(tree->headnode);
CountLeafs(tree->headnode);
// output vertices first, since TJunc needs it
EmitVertices(tree->headnode);
TJunc(tree->headnode);
if (qbsp_options.objexport.value() && entity == map.world_entity()) {
ExportObj_Nodes("pre_makefaceedges_plane_faces", tree->headnode);
ExportObj_Marksurfaces("pre_makefaceedges_marksurfaces", tree->headnode);
}
Q_assert(!entity->firstoutputfacenumber.has_value());
entity->firstoutputfacenumber = MakeFaceEdges(tree->headnode);
// Area portals
if (qbsp_options.target_game->id == GAME_QUAKE_II) {
ExportBrushList(entity, tree->headnode);
FloodAreas(entity, tree->headnode);
EmitAreaPortals(tree->headnode);
}
} else {
FillBrushEntity(entity, tree.get(), hullnum, brushes);
ExportDrawNodes(entity, tree->headnode, entity->firstoutputfacenumber.value());
// rebuild BSP now that we've marked invisible brush sides
tree.reset();
tree = BrushBSP(entity, brushes, false);
}
MakeTreePortals(tree.get());
MarkVisibleSides(tree.get(), entity, brushes);
MakeFaces(tree->headnode);
FreeTreePortals(tree.get());
PruneNodes(tree->headnode);
// write out .prt for main hull
if (!hullnum.value_or(0) && entity == map.world_entity() && (!map.leakfile || qbsp_options.keepprt.value())) {
WritePortalFile(tree.get());
}
// needs to come after any face creation
MakeMarkFaces(tree->headnode);
CountLeafs(tree->headnode);
// output vertices first, since TJunc needs it
EmitVertices(tree->headnode);
TJunc(tree->headnode);
if (qbsp_options.objexport.value() && entity == map.world_entity()) {
ExportObj_Nodes("pre_makefaceedges_plane_faces", tree->headnode);
ExportObj_Marksurfaces("pre_makefaceedges_marksurfaces", tree->headnode);
}
Q_assert(!entity->firstoutputfacenumber.has_value());
entity->firstoutputfacenumber = MakeFaceEdges(tree->headnode);
if (qbsp_options.target_game->id == GAME_QUAKE_II) {
ExportBrushList(entity, tree->headnode);
}
ExportDrawNodes(entity, tree->headnode, entity->firstoutputfacenumber.value());
}
/*
@ -767,10 +771,12 @@ static void BSPX_CreateBrushList(void)
CreateSingleHull
=================
*/
static void CreateSingleHull(const int hullnum)
static void CreateSingleHull(hull_index_t hullnum)
{
if (hullnum >= 0) {
logging::print("Processing hull {}...\n", hullnum);
if (hullnum.has_value()) {
logging::print("Processing hull {}...\n", hullnum.value());
} else {
logging::print("Processing map...\n");
}
// for each entity in the map file that has geometry
@ -781,7 +787,7 @@ static void CreateSingleHull(const int hullnum)
if (&entity != map.world_entity()) {
wants_logging = wants_logging && qbsp_options.logbmodels.value();
}
if (hullnum > 0) {
if (hullnum.value_or(0)) {
wants_logging = wants_logging && qbsp_options.loghulls.value();
}
@ -818,14 +824,17 @@ static void CreateHulls(void)
// game has no hulls, so we have to export brush lists and stuff.
if (!hulls.size()) {
CreateSingleHull(HULL_COLLISION);
CreateSingleHull(std::nullopt);
return;
}
// all the hulls
for (size_t i = 0; i < hulls.size(); i++) {
CreateSingleHull(i);
// only create hull 0 if fNoclip is set
} else if (qbsp_options.noclip.value()) {
CreateSingleHull(0);
// do all the hulls
} else {
for (size_t i = 0; i < hulls.size(); i++) {
CreateSingleHull(i);
if (qbsp_options.noclip.value()) {
break;
}
}
}

View File

@ -79,8 +79,8 @@ size_t ExportMapTexinfo(size_t texinfonum)
src.outputnum = i;
if (src.next != -1) {
map.bsp.texinfo[i].nexttexinfo = ExportMapTexinfo(src.next);
if (src.next.has_value()) {
map.bsp.texinfo[i].nexttexinfo = ExportMapTexinfo(src.next.value());
}
return i;
@ -127,7 +127,7 @@ First time just store away data, second time fix up reference points to
accomodate new data interleaved with old.
==================
*/
void ExportClipNodes(mapentity_t *entity, node_t *nodes, const int hullnum)
void ExportClipNodes(mapentity_t *entity, node_t *nodes, hull_index_t::value_type hullnum)
{
auto &model = map.bsp.dmodels.at(entity->outputmodelnumber.value());
model.headnode[hullnum] = ExportClipNodes(nodes);

View File

@ -250,7 +250,7 @@ static std::tuple<mbsp_t, bspxentries_t, std::optional<prtfile_t>> LoadTestmapQ1
#endif
}
static void CheckFilled(const mbsp_t &bsp, int hullnum)
static void CheckFilled(const mbsp_t &bsp, hull_index_t hullnum)
{
int32_t contents = BSP_FindContentsAtPoint(&bsp, hullnum, &bsp.dmodels[0], qvec3d{8192, 8192, 8192});