diff --git a/common/bspfile.cc b/common/bspfile.cc index 98db9226..d5b00472 100644 --- a/common/bspfile.cc +++ b/common/bspfile.cc @@ -28,6 +28,7 @@ #include #include +#include static std::vector make_palette(std::initializer_list bytes) { @@ -47,35 +48,47 @@ template struct gamedef_q1_like_t : public gamedef_t { private: - enum class detail_type_t - { - STRUCTURAL, - DETAIL, - ILLUSIONARY, - FENCE - }; - // extra data for contentflags_t for Quake-like + // todo: remove this and contentflags_t::native, and just use q1_contentflags_bits struct q1_contentflags_data { - // detail type - detail_type_t detail = detail_type_t::STRUCTURAL; + // extended content types. can be combined with native content types + // (e.g. a fence, or mist dipping into water needs to have + // both water and is_fence/is_mist) + bool is_origin = false; + bool is_clip = false; + bool is_fence = false; + bool is_mist = false; - bool origin = false; // is an origin brush + // can be combined with any content type including native ones + bool is_detail = false; + + constexpr bool operator==(const q1_contentflags_data &other) const + { + return is_origin == other.is_origin + && is_clip == other.is_clip + && is_fence == other.is_fence + && is_mist == other.is_mist + && is_detail == other.is_detail; + } - bool clip = false; // is a clip brush - - constexpr bool operator==(const q1_contentflags_data &other) const { return detail == other.detail && origin == other.origin && clip == other.clip; } constexpr bool operator!=(const q1_contentflags_data &other) const { return !(*this == other); } - constexpr explicit operator bool() const { return detail != detail_type_t::STRUCTURAL || origin || clip; } + constexpr explicit operator bool() const + { + return is_origin + || is_clip + || is_fence + || is_mist + || is_detail; + } }; // returns a blank entry if the given contents don't have // any game data inline const q1_contentflags_data &get_data(const contentflags_t &contents) const { - static const q1_contentflags_data blank_data; + static constexpr q1_contentflags_data blank_data; if (!contents.game_data.has_value()) { return blank_data; @@ -83,9 +96,231 @@ private: return std::any_cast(contents.game_data); } + + // representation of q1 native contents and compiler extended contents, as well as flags, as bit flags + // todo: this should be the only state inside a contentflags_t in q1 mode. + struct q1_contentflags_bits + { + using bitset_t = std::bitset<13>; + + // visible contents + bool solid = false; + bool sky = false; + bool fence = false; // compiler-internal + bool lava = false; + bool slime = false; + bool water = false; + bool mist = false; // compiler-internal + + // non-visible contents contents + bool origin = false; + bool clip = false; + bool illusionary_visblocker = false; + + // content flags + bool detail = false; + bool mirror_inside = false; + bool suppress_clipping_same_type = false; + + constexpr size_t size() const { return 11; } + constexpr size_t last_visible_contents() const { return 6; } + constexpr bitset_t bitset() const { + bitset_t result; + result[0] = solid; + result[1] = sky; + result[2] = fence; + result[3] = lava; + result[4] = slime; + result[5] = water; + result[6] = mist; + result[7] = origin; + result[8] = clip; + result[9] = illusionary_visblocker; + result[10] = detail; + result[11] = mirror_inside; + result[12] = suppress_clipping_same_type; + return result; + } + + q1_contentflags_bits() = default; + explicit q1_contentflags_bits(const bitset_t& bitset) : + solid(bitset[0]), + sky(bitset[1]), + fence(bitset[2]), + lava(bitset[3]), + slime(bitset[4]), + water(bitset[5]), + mist(bitset[6]), + origin(bitset[7]), + clip(bitset[8]), + illusionary_visblocker(bitset[9]), + detail(bitset[10]), + mirror_inside(bitset[11]), + suppress_clipping_same_type(bitset[12]) {} + + static constexpr const char *bitflag_names[] = {"SOLID", "SKY", "FENCE", "LAVA", + "SLIME", "WATER", "MIST", "ORIGIN", "CLIP", "ILLUSIONARY_VISBLOCKER", "DETAIL", + "MIRROR_INSIDE", "SUPPRESS_CLIPPING_SAME_TYPE" + }; + + constexpr bool operator[](size_t index) const { + switch (index) { + case 0: return solid; + case 1: return sky; + case 2: return fence; + case 3: return lava; + case 4: return slime; + case 5: return water; + case 6: return mist; + case 7: return origin; + case 8: return clip; + case 9: return illusionary_visblocker; + case 10: return detail; + case 11: return mirror_inside; + case 12: return suppress_clipping_same_type; + } + } + + constexpr bool& operator[](size_t index) { + switch (index) { + case 0: return solid; + case 1: return sky; + case 2: return fence; + case 3: return lava; + case 4: return slime; + case 5: return water; + case 6: return mist; + case 7: return origin; + case 8: return clip; + case 9: return illusionary_visblocker; + case 10: return detail; + case 11: return mirror_inside; + case 12: return suppress_clipping_same_type; + } + } + + constexpr q1_contentflags_bits operator|(const q1_contentflags_bits &other) const { + return q1_contentflags_bits(bitset() | other.bitset()); + } + + constexpr q1_contentflags_bits operator^(const q1_contentflags_bits &other) const { + return q1_contentflags_bits(bitset() ^ other.bitset()); + } + + constexpr bool operator==(const q1_contentflags_bits &other) const + { + return bitset() == other.bitset(); + } + + constexpr bool operator!=(const q1_contentflags_bits &other) const { return !(*this == other); } + + constexpr int32_t visible_contents_index() const { + for (size_t i = 0; i <= last_visible_contents(); ++i) { + if ((*this)[i]) { + return static_cast(i); + } + } + + return -1; + } + + constexpr q1_contentflags_bits visible_contents() const { + q1_contentflags_bits result; + + int32_t index = visible_contents_index(); + if (index != -1) { + result[index] = true; + } + + return result; + } + + constexpr bool empty() const { + // fixme-brushbsp: what should we do with empty, but the detail flag set? + q1_contentflags_bits empty_test; + return (*this) == empty_test; + } + }; + + inline q1_contentflags_bits contentflags_to_bits(const contentflags_t &contents) const + { + q1_contentflags_bits result; + + // set bit for native contents + switch (contents.native) { + case CONTENTS_SOLID: + result.solid = true; + break; + case CONTENTS_WATER: + result.water = true; + break; + case CONTENTS_SLIME: + result.slime = true; + break; + case CONTENTS_LAVA: + result.lava = true; + break; + case CONTENTS_SKY: + result.sky = true; + break; + } + + // copy in extra flags + auto &data = get_data(contents); + result.origin = data.is_origin; + result.clip = data.is_clip; + result.fence = data.is_fence; + result.mist = data.is_mist; + result.detail = data.is_detail; + + result.illusionary_visblocker = contents.illusionary_visblocker; + result.mirror_inside = contents.mirror_inside.value_or(false); + result.suppress_clipping_same_type = !contents.clips_same_type.value_or(true); + + return result; + } + + inline contentflags_t contentflags_from_bits(const q1_contentflags_bits &bits) const + { + contentflags_t result; + + // set native contents + if (bits.solid) { + result.native = CONTENTS_SOLID; + } else if (bits.sky) { + result.native = CONTENTS_SKY; + } else if (bits.water) { + result.native = CONTENTS_WATER; + } else if (bits.slime) { + result.native = CONTENTS_SLIME; + } else if (bits.lava) { + result.native = CONTENTS_LAVA; + } else { + result.native = CONTENTS_EMPTY; + } + + // copy in extra flags + q1_contentflags_data data; + data.is_origin = bits.origin; + data.is_clip = bits.clip; + data.is_fence = bits.fence; + data.is_mist = bits.mist; + data.is_detail = bits.detail; + + if (data) { + result.game_data = data; + } + + result.illusionary_visblocker = bits.illusionary_visblocker; + result.mirror_inside = bits.mirror_inside; + result.clips_same_type = !bits.suppress_clipping_same_type; + + return result; + } + public: - gamedef_q1_like_t(const char *base_dir = "ID1") : gamedef_t(base_dir) { this->id = ID; } + explicit gamedef_q1_like_t(const char *base_dir = "ID1") : gamedef_t(base_dir) { this->id = ID; } bool surf_is_lightmapped(const surfflags_t &flags) const override { return !(flags.native & TEX_SPECIAL); } @@ -112,152 +347,127 @@ public: return !string_iequals(name, "hint"); } - contentflags_t create_sky_contents() const - { - return {CONTENTS_SKY}; - } - - contentflags_t create_liquid_contents(const int32_t &liquid_type) const - { - return {liquid_type}; - } - contentflags_t cluster_contents(const contentflags_t &contents0, const contentflags_t &contents1) const override { - if (contents0.equals(this, contents1)) - return contents0; + const auto bits0 = contentflags_to_bits(contents0); + const auto bits1 = contentflags_to_bits(contents1); - /* - * Clusters may be partially solid but still be seen into - * ?? - Should we do something more explicit with mixed liquid contents? - */ - if (contents0.native == CONTENTS_EMPTY || contents1.native == CONTENTS_EMPTY) - return create_empty_contents(); + auto combined = bits0 | bits1; - if (contents0.native >= CONTENTS_LAVA && contents0.native <= CONTENTS_WATER) - return create_liquid_contents(contents0.native); - if (contents1.native >= CONTENTS_LAVA && contents1.native <= CONTENTS_WATER) - return create_liquid_contents(contents1.native); - if (contents0.native == CONTENTS_SKY || contents1.native == CONTENTS_SKY) - return create_sky_contents(); - - return create_solid_contents(); - } - - int32_t contents_priority(const contentflags_t &contents) const override - { - switch (get_data(contents).detail) { - case detail_type_t::DETAIL: return 5; - case detail_type_t::FENCE: return 4; - case detail_type_t::ILLUSIONARY: return 2; + // a cluster may include some solid detail areas, but + // still be seen into + if (!bits0.solid || !bits1.solid) { + combined.solid = false; } - if (contents.illusionary_visblocker) { - return 2; - } - - switch (contents.native) { - case CONTENTS_SOLID: return 7; - - case CONTENTS_SKY: return 6; - - case CONTENTS_WATER: return 3; - case CONTENTS_SLIME: return 3; - case CONTENTS_LAVA: return 3; - - case CONTENTS_EMPTY: return 1; - case 0: return 0; - - default: FError("Bad contents in face"); return 0; - } + return contentflags_from_bits(combined); } - bool chops(const contentflags_t &contents) const override { - return contents_are_solid(contents) || contents_are_sky(contents) || get_data(contents).detail != detail_type_t::STRUCTURAL; - } - - inline contentflags_t create_extended_contents(const q1_contentflags_data &data) const { return {0, data}; } - contentflags_t create_empty_contents() const override { - return {CONTENTS_EMPTY}; + q1_contentflags_bits result; + return contentflags_from_bits(result); } contentflags_t create_solid_contents() const override { - return {CONTENTS_SOLID}; + q1_contentflags_bits result; + result.solid = true; + return contentflags_from_bits(result); } - contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const override { - // ignore the original contents in Q1 - return create_extended_contents({detail_type_t::ILLUSIONARY}); + contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const override + { + q1_contentflags_bits result; + result.mist = true; + result.detail = true; + return contentflags_from_bits(result); } - contentflags_t create_detail_fence_contents(const contentflags_t &original) const override { - return create_extended_contents({detail_type_t::FENCE}); + contentflags_t create_detail_fence_contents(const contentflags_t &original) const override + { + q1_contentflags_bits result; + result.fence = true; + result.detail = true; + return contentflags_from_bits(result); } - contentflags_t create_detail_solid_contents(const contentflags_t &original) const override { - return create_extended_contents({detail_type_t::DETAIL}); + contentflags_t create_detail_solid_contents(const contentflags_t &original) const override + { + q1_contentflags_bits result; + result.solid = true; + result.detail = true; + return contentflags_from_bits(result); } bool contents_are_type_equal(const contentflags_t &self, const contentflags_t &other) const override { - if (get_data(self) != get_data(other)) { - return false; - } + // fixme-brushbsp: document what this is supposed to do, remove if unneeded? + // is it checking for equality of visible content bits (in q2 terminology)? + // same highest-priority visible content bit? - return self.illusionary_visblocker == other.illusionary_visblocker && - self.native == other.native; + return contentflags_to_bits(self) == contentflags_to_bits(other); } bool contents_are_equal(const contentflags_t &self, const contentflags_t &other) const override { + // fixme-brushbsp: document what this is supposed to do, remove if unneeded? return contents_are_type_equal(self, other); } bool contents_are_any_detail(const contentflags_t &contents) const override { - // in Q1, there are only CFLAGS_DETAIL, CFLAGS_DETAIL_ILLUSIONARY, or CFLAGS_DETAIL_FENCE - return get_data(contents).detail != detail_type_t::STRUCTURAL; + return contentflags_to_bits(contents).detail; } bool contents_are_detail_solid(const contentflags_t &contents) const override { - return get_data(contents).detail == detail_type_t::DETAIL; + // fixme-brushbsp: document whether this is an exclusive test (i.e. what does it return for water|solid|detail) + const auto bits = contentflags_to_bits(contents); + return bits.detail && bits.solid; } bool contents_are_detail_fence(const contentflags_t &contents) const override { - return get_data(contents).detail == detail_type_t::FENCE; + // fixme-brushbsp: document whether this is an exclusive test (i.e. what does it return for water|fence|detail) + const auto bits = contentflags_to_bits(contents); + return bits.detail && bits.fence; } bool contents_are_detail_illusionary(const contentflags_t &contents) const override { - return get_data(contents).detail == detail_type_t::ILLUSIONARY; + // fixme-brushbsp: document whether this is an exclusive test (i.e. what does it return for water|mist|detail) + const auto bits = contentflags_to_bits(contents); + return bits.detail && bits.mist; } bool contents_are_mirrored(const contentflags_t &contents) const override { - // if we have mirrorinside set, go ahead - if (contents.mirror_inside.has_value()) { - return contents.mirror_inside.value(); - } + const auto bits = contentflags_to_bits(contents); + if (bits.mirror_inside) + return true; + + const auto visible = bits.visible_contents(); + // If the brush is non-solid, mirror faces for the inside view - return (contents.native == CONTENTS_WATER) - || (contents.native == CONTENTS_SLIME) - || (contents.native == CONTENTS_LAVA); + return visible.water + || visible.slime + || visible.lava; } bool contents_are_origin(const contentflags_t &contents) const override { - return get_data(contents).origin; + // fixme-brushbsp: document whether this is an exclusive test (i.e. what does it return for water|origin) + const auto bits = contentflags_to_bits(contents); + return bits.origin; } bool contents_are_clip(const contentflags_t &contents) const override { - return get_data(contents).clip; + // fixme-brushbsp: document whether this is an exclusive test (i.e. what does it return for water|clip) + const auto bits = contentflags_to_bits(contents); + return bits.clip; } bool contents_clip_same_type(const contentflags_t &self, const contentflags_t &other) const override @@ -265,45 +475,40 @@ public: return self.equals(this, other) && self.clips_same_type.value_or(true); } - inline bool contents_has_extended(const contentflags_t &contents) const - { - if (get_data(contents).detail != detail_type_t::STRUCTURAL) - return true; - else if (contents.illusionary_visblocker) - return true; - else if (get_data(contents)) - return true; - - return false; - } - bool contents_are_empty(const contentflags_t &contents) const override { - return !contents_has_extended(contents) && contents.native == CONTENTS_EMPTY; + const auto bits = contentflags_to_bits(contents); + return bits.empty(); } bool contents_are_any_solid(const contentflags_t &contents) const override { - return contents_are_solid(contents) || contents_are_detail_solid(contents); + const auto bits = contentflags_to_bits(contents); + return bits.solid; } + // fixme-brushbsp: this is a leftover from q1 tools, and not really used in qbsp3, remove if possible bool contents_are_solid(const contentflags_t &contents) const override { - return !contents_has_extended(contents) && contents.native == CONTENTS_SOLID; + const auto bits = contentflags_to_bits(contents); + return bits.solid && !bits.detail; } bool contents_are_sky(const contentflags_t &contents) const override { - return !contents_has_extended(contents) && contents.native == CONTENTS_SKY; + const auto bits = contentflags_to_bits(contents); + return bits.sky; } bool contents_are_liquid(const contentflags_t &contents) const override { - return !contents_has_extended(contents) && contents.native <= CONTENTS_WATER && contents.native >= CONTENTS_LAVA; + const auto bits = contentflags_to_bits(contents); + return bits.water || bits.lava || bits.slime; } bool contents_are_valid(const contentflags_t &contents, bool strict) const override { + // fixme-brushbsp: document exactly what this is supposed to do if (!contents.native && !strict) { return true; } @@ -326,24 +531,29 @@ public: } bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const override { - /* If water is transparent, liquids are like empty space */ - if (transwater) { - if (contents_are_liquid(contents0) && contents_are_empty(contents1)) - return true; - if (contents_are_liquid(contents1) && contents_are_empty(contents0)) - return true; - } + auto bits_a = contentflags_to_bits(contents0); + auto bits_b = contentflags_to_bits(contents1); - /* If sky is transparent, then sky is like empty space */ - if (transsky) { - if (contents_are_sky(contents0) && contents_are_empty(contents1)) - return true; - if (contents_are_empty(contents0) && contents_are_sky(contents1)) - return true; - } + bool a_translucent = transwater ? (bits_a.water || bits_a.slime || bits_a.lava) : false; + bool b_translucent = transwater ? (bits_b.water || bits_b.slime || bits_b.lava) : false; - /* If contents values are the same and not solid, can see through */ - return !(contents0.is_solid(this) || contents1.is_solid(this)) && contents0.equals(this, contents1); + if ((bits_a ^ bits_b).visible_contents().empty()) + return true; + + if (bits_a.detail || a_translucent) + bits_a = q1_contentflags_bits(); + if (bits_b.detail || b_translucent) + bits_b = q1_contentflags_bits(); + + if (bits_a.solid || bits_b.solid) + return false; // can't see through solid + + if ((bits_a ^ bits_b).empty()) + return true; // identical on both sides + + if ((bits_a ^ bits_b).visible_contents().empty()) + return true; + return false; } bool contents_seals_map(const contentflags_t &contents) const override @@ -368,104 +578,91 @@ public: contentflags_t combine_contents(const contentflags_t &a, const contentflags_t &b) const override { - int32_t a_pri = contents_priority(a); - int32_t b_pri = contents_priority(b); + auto bits_a = contentflags_to_bits(a); + auto bits_b = contentflags_to_bits(b); - if (a_pri > b_pri) { - return a; - } else { - return b; + if (bits_a.solid || bits_b.solid) { + // qbsp3 behaviour: clear any other set content flags + return create_solid_contents(); } + + return contentflags_from_bits(bits_a | bits_b); } - contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const override + contentflags_t portal_visible_contents(const contentflags_t &a, const contentflags_t &b) const override { - if (a.equals(this, b)) { - if (contents_clip_same_type(a, b)) { - return create_empty_contents(); - } else { - return a; - } - } + auto bits_a = contentflags_to_bits(a); + auto bits_b = contentflags_to_bits(b); - int32_t a_pri = contents_priority(a); - int32_t b_pri = contents_priority(b); + q1_contentflags_bits result; - if (a_pri > b_pri) { - return a; + if (bits_a.suppress_clipping_same_type + || bits_b.suppress_clipping_same_type) { + result = bits_a | bits_b; } else { - return b; + result = bits_a ^ bits_b; } - // fixme-brushbsp: support detail-illusionary intersecting liquids + + auto strongest_contents_change = result.visible_contents(); + + return contentflags_from_bits(strongest_contents_change); } - bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const override + bool portal_generates_face(const contentflags_t &portal_visible_contents, const contentflags_t &brushcontents, planeside_t brushside_side) const override { - if (contents_priority(b) > contents_priority(a)) { - return true; - } + auto bits_portal = contentflags_to_bits(portal_visible_contents); + auto bits_brush = contentflags_to_bits(brushcontents); - if (a.is_empty(this)) { - // empty can always see whatever is in `b` - return true; - } - - if (!a.will_clip_same_type(this) && contents_are_type_equal(a, b)) { - // _noclipfaces - return true; - } - - if (!a.is_mirrored(this)) { + // find the highest visible content bit set in portal + int32_t index = bits_portal.visible_contents_index(); + if (index == -1) { return false; } + // check if it's also set in the brush + if (!bits_brush[index]) { + return false; + } + + if (brushside_side == SIDE_BACK) { + return bits_brush.mirror_inside + || bits_brush.water + || bits_brush.slime + || bits_brush.lava; + } return true; } - bool contents_contains(const contentflags_t &a, const contentflags_t &b) const override + inline std::string get_contents_display(const q1_contentflags_bits &bits) const { - return a.equals(this, b); + if (bits.empty()) { + return "EMPTY"; + } + + std::string s; + + for (int32_t i = 0; i < std::size(q1_contentflags_bits::bitflag_names); i++) { + if (bits[i]) { + if (!s.empty()) { + s += " | "; + } + + s += q1_contentflags_bits::bitflag_names[i]; + } + } + + return s; } std::string get_contents_display(const contentflags_t &contents) const override { - std::string base; - - switch (contents.native) { - case 0: base = "UNSET"; break; - case CONTENTS_EMPTY: base = "EMPTY"; break; - case CONTENTS_SOLID: base = "SOLID"; break; - case CONTENTS_SKY: base = "SKY"; break; - case CONTENTS_WATER: base = "WATER"; break; - case CONTENTS_SLIME: base = "SLIME"; break; - case CONTENTS_LAVA: base = "LAVA"; break; - default: base = fmt::to_string(contents.native); break; - } - - if (contents_are_clip(contents)) { - base += " | CLIP"; - } - if (contents_are_origin(contents)) { - base += " | ORIGIN"; - } - - switch (get_data(contents).detail) { - case detail_type_t::DETAIL: - base += " | DETAIL"; - break; - case detail_type_t::ILLUSIONARY: - base += " | DETAIL[ILLUSIONARY]"; - break; - case detail_type_t::FENCE: - base += " | DETAIL[FENCE]"; - break; - } - - return base; + const auto bits = contentflags_to_bits(contents); + return get_contents_display(bits); } void contents_make_valid(contentflags_t &contents) const override { + // fixme-brushbsp: probably wrong? // todo: anything smarter we can do here? // think this can't even happen in Q1 anyways if (!contents_are_valid(contents, false)) { @@ -485,21 +682,33 @@ public: { // check for strong content indicators if (!Q_strcasecmp(texname.data(), "origin")) { - return create_extended_contents({ detail_type_t::STRUCTURAL, true, false }); + q1_contentflags_bits result; + result.origin = true; + return contentflags_from_bits(result); } else if (!Q_strcasecmp(texname.data(), "hint") || !Q_strcasecmp(texname.data(), "hintskip")) { return create_empty_contents(); } else if (!Q_strcasecmp(texname.data(), "clip")) { - return create_extended_contents({ detail_type_t::STRUCTURAL, false, true }); + q1_contentflags_bits result; + result.clip = true; + return contentflags_from_bits(result); } else if (texname[0] == '*') { if (!Q_strncasecmp(texname.data() + 1, "lava", 4)) { - return create_liquid_contents(CONTENTS_LAVA); + q1_contentflags_bits result; + result.lava = true; + return contentflags_from_bits(result); } else if (!Q_strncasecmp(texname.data() + 1, "slime", 5)) { - return create_liquid_contents(CONTENTS_SLIME); + q1_contentflags_bits result; + result.slime = true; + return contentflags_from_bits(result); } else { - return create_liquid_contents(CONTENTS_WATER); + q1_contentflags_bits result; + result.water = true; + return contentflags_from_bits(result); } } else if (!Q_strncasecmp(texname.data(), "sky", 3)) { - return create_sky_contents(); + q1_contentflags_bits result; + result.sky = true; + return contentflags_from_bits(result); } // and anything else is assumed to be a regular solid. @@ -561,14 +770,10 @@ public: private: struct content_stats_t : public content_stats_base_t { - std::atomic solid; - std::atomic empty; - std::atomic liquid; - std::atomic detail; - std::atomic detail_illusionary; - std::atomic detail_fence; - std::atomic sky; - std::atomic illusionary_visblocker; + std::mutex stat_mutex; + std::unordered_map native_types; + + std::atomic total_brushes; }; public: @@ -580,54 +785,27 @@ public: void count_contents_in_stats(const contentflags_t &contents, content_stats_base_t &stats_any) const override { content_stats_t &stats = dynamic_cast(stats_any); - - if (contents_are_solid(contents)) { - stats.solid++; - } else if (contents_are_sky(contents)) { - stats.sky++; - } else if (contents_are_detail_solid(contents)) { - stats.detail++; - } else if (contents_are_detail_illusionary(contents)) { - stats.detail_illusionary++; - } else if (contents_are_detail_fence(contents)) { - stats.detail_fence++; - } else if (contents.illusionary_visblocker) { - stats.illusionary_visblocker++; - } else if (contents_are_liquid(contents)) { - stats.liquid++; - } else { - stats.empty++; + + // convert to std::bitset so we can use it as an unordered_map key + const auto bitset = contentflags_to_bits(contents).bitset(); + + { + std::unique_lock lock(stats.stat_mutex); + stats.native_types[bitset]++; } + + stats.total_brushes++; } void print_content_stats(const content_stats_base_t &stats_any, const char *what) const override { const content_stats_t &stats = dynamic_cast(stats_any); - if (stats.empty) { - logging::print(logging::flag::STAT, " {:8} empty {}\n", stats.empty, what); - } - if (stats.solid) { - logging::print(logging::flag::STAT, " {:8} solid {}\n", stats.solid, what); - } - if (stats.sky) { - logging::print(logging::flag::STAT, " {:8} sky {}\n", stats.sky, what); - } - if (stats.detail) { - logging::print(logging::flag::STAT, " {:8} detail {}\n", stats.detail, what); - } - if (stats.detail_illusionary) { - logging::print(logging::flag::STAT, " {:8} detail illusionary {}\n", stats.detail_illusionary, what); - } - if (stats.detail_fence) { - logging::print(logging::flag::STAT, " {:8} detail fence {}\n", stats.detail_fence, what); - } - if (stats.liquid) { - logging::print(logging::flag::STAT, " {:8} liquid {}\n", stats.liquid, what); - } - if (stats.illusionary_visblocker) { - logging::print(logging::flag::STAT, " {:8} illusionary visblocker {}\n", stats.illusionary_visblocker, what); + for (auto [bits, count] : stats.native_types) { + logging::print(logging::flag::STAT, " {:8} {} {}\n", count, get_contents_display(q1_contentflags_bits(bits)), what); } + + logging::print(logging::flag::STAT, " {:8} {} total\n", stats.total_brushes, what); } }; @@ -766,35 +944,6 @@ struct gamedef_q2_t : public gamedef_t Q2_CONTENTS_TRANSLUCENT | Q2_CONTENTS_AREAPORTAL)); } - int32_t contents_priority(const contentflags_t &contents) const override - { - if (contents_are_detail_solid(contents)) { - return 8; - } else if (contents_are_detail_illusionary(contents)) { - return 6; - } else if (contents_are_detail_fence(contents)) { - return 7; - } else if (contents.illusionary_visblocker) { - return 2; - } else { - switch (contents.native & Q2_ALL_VISIBLE_CONTENTS) { - case Q2_CONTENTS_SOLID: return 10; - case Q2_CONTENTS_WINDOW: return 9; - case Q2_CONTENTS_AUX: return 5; - case Q2_CONTENTS_LAVA: return 4; - case Q2_CONTENTS_SLIME: return 3; - case Q2_CONTENTS_WATER: return 2; - case Q2_CONTENTS_MIST: return 1; - default: return 0; - } - } - } - - bool chops(const contentflags_t &contents) const override - { - return !!(contents.native & Q2_CONTENTS_SOLID); - } - contentflags_t create_empty_contents() const override { return {Q2_CONTENTS_EMPTY}; } contentflags_t create_solid_contents() const override { return {Q2_CONTENTS_SOLID}; } @@ -972,15 +1121,17 @@ struct gamedef_q2_t : public gamedef_t return 0; } - /** - * For a portal from `a` to `b`, should the viewer on side `a` see a face? - */ - bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const override + contentflags_t portal_visible_contents(const contentflags_t &a, const contentflags_t &b) const override { - if ((a.native & Q2_CONTENTS_WINDOW) && visible_contents(a, b).native == Q2_CONTENTS_WINDOW) - return false; // don't show insides of windows + contentflags_t result; - return true; + if (!a.clips_same_type.value_or(true) + || !b.clips_same_type.value_or(true)) { + result.native = visible_contents(a.native | b.native); + } else { + result.native = visible_contents(a.native ^ b.native); + } + return result; } bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool, bool) const override @@ -1026,19 +1177,28 @@ struct gamedef_q2_t : public gamedef_t return {Q2_CONTENTS_SOLID}; } - return {a.native | b.native}; + contentflags_t result; + result.native = a.native | b.native; + result.clips_same_type = (a.clips_same_type.value_or(true) && b.clips_same_type.value_or(true)); + result.mirror_inside = (a.mirror_inside.value_or(true) && b.mirror_inside.value_or(true)); + result.illusionary_visblocker == a.illusionary_visblocker || b.illusionary_visblocker; + return result; } - contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const override + bool portal_generates_face(const contentflags_t &portal_visible_contents, const contentflags_t &brushcontents, planeside_t brushside_side) const override { - int viscontents = visible_contents(a.native ^ b.native); + if ((portal_visible_contents.native & brushcontents.native) == 0) { + return false; + } - return {viscontents}; - } - - bool contents_contains(const contentflags_t &a, const contentflags_t &b) const override - { - return (a.native & b.native) != 0; + if (brushside_side == SIDE_BACK) { + if (portal_visible_contents.native & Q2_CONTENTS_WINDOW) { + // windows don't generate inside faces + return false; + } + return true; + } + return true; } std::string get_contents_display(const contentflags_t &contents) const override @@ -1280,6 +1440,7 @@ public: private: struct content_stats_t : public content_stats_base_t { + std::mutex stat_mutex; //std::array, 32> native_types; std::unordered_map native_types; std::atomic total_brushes; @@ -1301,9 +1462,8 @@ public: stats.native_types[i]++; } }*/ - static std::mutex stat_mutex; { - std::unique_lock lock(stat_mutex); + std::unique_lock lock(stats.stat_mutex); stats.native_types[contents.native]++; } @@ -1522,16 +1682,6 @@ bool contentflags_t::types_equal(const contentflags_t &other, const gamedef_t *g return game->contents_are_type_equal(*this, other); } -int32_t contentflags_t::priority(const gamedef_t *game) const -{ - return game->contents_priority(*this); -} - -bool contentflags_t::chops(const gamedef_t* game) const -{ - return game->chops(*this); -} - bool contentflags_t::is_any_detail(const gamedef_t *game) const { return game->contents_are_any_detail(*this); diff --git a/include/common/bspfile.hh b/include/common/bspfile.hh index 8b34b090..4caa2604 100644 --- a/include/common/bspfile.hh +++ b/include/common/bspfile.hh @@ -268,8 +268,6 @@ struct gamedef_t // FIXME: fix so that we don't have to pass a name here virtual bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const = 0; virtual contentflags_t cluster_contents(const contentflags_t &contents0, const contentflags_t &contents1) const = 0; - virtual int32_t contents_priority(const contentflags_t &contents) const = 0; - virtual bool chops(const contentflags_t &) const = 0; virtual contentflags_t create_empty_contents() const = 0; virtual contentflags_t create_solid_contents() const = 0; virtual contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const = 0; @@ -296,11 +294,13 @@ struct gamedef_t virtual bool contents_seals_map(const contentflags_t &contents) const = 0; virtual contentflags_t contents_remap_for_export(const contentflags_t &contents) const = 0; virtual contentflags_t combine_contents(const contentflags_t &a, const contentflags_t &b) const = 0; - virtual contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const = 0; - // counterpart to visible_contents. for a portal with contents from `a` to `b`, returns whether a viewer in `a` - // should see a face - virtual bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const = 0; - virtual bool contents_contains(const contentflags_t &a, const contentflags_t &b) const = 0; + // for a portal with contents from `a` to `b`, returns what type of face should be rendered facing `a` and `b` + virtual contentflags_t portal_visible_contents(const contentflags_t &a, const contentflags_t &b) const = 0; + // for a brush with the given contents touching a portal with the required `portal_visible_contents`, as determined by + // portal_visible_contents, should the `brushside_side` of the brushside generate a face? + // e.g. liquids generate front and back sides by default, but for q1 detail_wall/detail_illusionary the back side is opt-in + // with _mirrorinside + virtual bool portal_generates_face(const contentflags_t &portal_visible_contents, const contentflags_t &brushcontents, planeside_t brushside_side) const = 0; virtual std::string get_contents_display(const contentflags_t &contents) const = 0; virtual void contents_make_valid(contentflags_t &contents) const = 0; virtual const std::initializer_list &get_hull_sizes() const = 0; diff --git a/include/qbsp/portals.hh b/include/qbsp/portals.hh index cf1c4c5b..00ea8427 100644 --- a/include/qbsp/portals.hh +++ b/include/qbsp/portals.hh @@ -40,7 +40,7 @@ struct portal_t std::optional winding; bool sidefound; // false if ->side hasn't been checked - side_t *side; // NULL = non-visible + side_t *sides[2]; // [0] = the brush side visible on nodes[0] - it could come from a brush in nodes[1]. NULL = non-visible face_t *face[2]; // output face in bsp file }; diff --git a/qbsp/brushbsp.cc b/qbsp/brushbsp.cc index 04f8ffcf..eafdb214 100644 --- a/qbsp/brushbsp.cc +++ b/qbsp/brushbsp.cc @@ -931,9 +931,9 @@ static std::unique_ptr BrushBSP(mapentity_t *entity, std::vector(); @@ -945,9 +945,10 @@ static std::unique_ptr BrushBSP(mapentity_t *entity, std::vectorcreate_content_stats(); BuildTree_r(tree->headnode.get(), std::move(brushlist), stats); - logging::print(logging::flag::STAT, "{:5} visible nodes\n", stats.c_nodes - stats.c_nonvis); - logging::print(logging::flag::STAT, "{:5} nonvis nodes\n", stats.c_nonvis); - logging::print(logging::flag::STAT, "{:5} leafs\n", stats.c_leafs); + logging::print(logging::flag::STAT, " {:8} visible nodes\n", stats.c_nodes - stats.c_nonvis); + logging::print(logging::flag::STAT, " {:8} nonvis nodes\n", stats.c_nonvis); + logging::print(logging::flag::STAT, " {:8} leafs\n", stats.c_leafs); + qbsp_options.target_game->print_content_stats(*stats.leafstats, "leafs"); return tree; } diff --git a/qbsp/faces.cc b/qbsp/faces.cc index b60b8fb2..b65e615c 100644 --- a/qbsp/faces.cc +++ b/qbsp/faces.cc @@ -477,7 +477,7 @@ see also FindPortalSide which populates p->side */ static std::unique_ptr FaceFromPortal(portal_t *p, int pside) { - side_t *side = p->side; + side_t *side = p->sides[pside]; if (!side) return nullptr; // portal does not bridge different visible contents @@ -489,6 +489,7 @@ static std::unique_ptr FaceFromPortal(portal_t *p, int pside) f->portal = p; f->lmshift = side->lmshift; +#if 0 bool make_face = qbsp_options.target_game->directional_visible_contents(p->nodes[pside]->contents, p->nodes[!pside]->contents); if (!make_face) { @@ -505,7 +506,7 @@ static std::unique_ptr FaceFromPortal(portal_t *p, int pside) } } } - +#endif if (pside) { @@ -590,7 +591,7 @@ void MakeFaces(node_t *node) MakeFaces_r(node, stats); - logging::print(logging::flag::STAT, "{} makefaces\n", stats.c_nodefaces); - logging::print(logging::flag::STAT, "{} merged\n", stats.c_merge); - logging::print(logging::flag::STAT, "{} subdivided\n", stats.c_subdivide); + logging::print(logging::flag::STAT, " {:8} makefaces\n", stats.c_nodefaces); + logging::print(logging::flag::STAT, " {:8} merged\n", stats.c_merge); + logging::print(logging::flag::STAT, " {:8} subdivided\n", stats.c_subdivide); } diff --git a/qbsp/portals.cc b/qbsp/portals.cc index 87649bd9..6989bc47 100644 --- a/qbsp/portals.cc +++ b/qbsp/portals.cc @@ -720,16 +720,20 @@ static void FindPortalSide(portal_t *p) { // decide which content change is strongest // solid > lava > water, etc + + // if either is "_noclipfaces" then we don't require a content change contentflags_t viscontents = - qbsp_options.target_game->visible_contents(p->nodes[0]->contents, p->nodes[1]->contents); + qbsp_options.target_game->portal_visible_contents(p->nodes[0]->contents, p->nodes[1]->contents); if (viscontents.is_empty(qbsp_options.target_game)) return; int planenum = p->onnode->planenum; - side_t *bestside = nullptr; + // bestside[0] is the brushside visible on portal side[0] which is the positive side of the plane, always + side_t *bestside[2] = {nullptr, nullptr}; float bestdot = 0; qbsp_plane_t p1 = map.get_plane(p->onnode->planenum); + // check brushes on both sides of the portal for (int j = 0; j < 2; j++) { node_t *n = p->nodes[j]; @@ -739,38 +743,61 @@ static void FindPortalSide(portal_t *p) for (auto it = n->original_brushes.rbegin(); it != n->original_brushes.rend(); ++it) { auto *brush = *it; - if (!qbsp_options.target_game->contents_contains(brush->contents, viscontents)) + const bool generate_outside_face = qbsp_options.target_game->portal_generates_face(viscontents, brush->contents, SIDE_FRONT); + const bool generate_inside_face = qbsp_options.target_game->portal_generates_face(viscontents, brush->contents, SIDE_BACK); + + if (!(generate_outside_face || generate_inside_face)) { continue; + } for (auto &side : brush->sides) { - // fixme-brushbsp: port these -// if (side.bevel) -// continue; -// if (side.texinfo == TEXINFO_NODE) + if (side.bevel) + continue; + // fixme-brushbsp: restore +// if (!side.visible) // continue; // non-visible - if (side.planenum == planenum) - { // exact match - bestside = &side; - goto gotit; + if (side.planenum == planenum) { + // exact match (undirectional) + + // because the brush is on j of the positive plane, the brushside must be facing away from j + Q_assert(side.planeside == !j); + + // see which way(s) we want to generate faces - we could be a brush on either side of + // the portal, generating either a outward face (common case) or an inward face (liquids) or both. + if (generate_outside_face) { + if (!bestside[!j]) { + bestside[!j] = &side; + } + } + if (generate_inside_face) { + if (!bestside[j]) { + bestside[j] = &side; + } + } + + break; } // see how close the match is - auto p2 = map.planes.at(side.planenum); - float dot = qv::dot(p1.normal, p2.normal); - if (dot > bestdot) - { - bestdot = dot; - bestside = &side; - } + // fixme-brushbsp: verify that this actually works, restore it +// auto p2 = map.planes.at(side.planenum); +// double dot = qv::dot(p1.normal, p2.normal); +// if (dot > bestdot) +// { +// bestdot = dot; +// bestside[j] = &side; +// } } } } -gotit: - if (!bestside) + if (!bestside[0] && !bestside[1]) logging::print("WARNING: side not found for portal\n"); p->sidefound = true; - p->side = bestside; + + for (int i = 0; i < 2; ++i) { + p->sides[i] = bestside[i]; + } } /* @@ -801,8 +828,11 @@ static void MarkVisibleSides_r(node_t *node) continue; // edge of world if (!p->sidefound) FindPortalSide(p); - if (p->side) - p->side->visible = true; + for (int i = 0; i < 2; ++i) { + if (p->sides[i]) { + p->sides[i]->visible = true; + } + } } } diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 18d7d1b6..19a5fb28 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -495,6 +495,25 @@ static bool IsTrigger(const mapentity_t *entity) return trigger_pos == (tex.size() - strlen("trigger")); } +static void CountLeafs_r(node_t *node, content_stats_base_t& stats) +{ + if (node->planenum == PLANENUM_LEAF) { + qbsp_options.target_game->count_contents_in_stats(node->contents, stats); + return; + } + CountLeafs_r(node->children[0].get(), stats); + CountLeafs_r(node->children[1].get(), stats); +} + +static void CountLeafs(node_t *headnode) +{ + logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__); + + auto stats = qbsp_options.target_game->create_content_stats(); + CountLeafs_r(headnode, *stats); + qbsp_options.target_game->print_content_stats(*stats, "leafs"); +} + /* =============== ProcessEntity @@ -596,7 +615,6 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) MakeTreePortals(tree.get()); FillOutside(entity, tree.get(), hullnum); PruneNodes(tree->headnode.get()); - DetailToSolid(tree->headnode.get()); } } ExportClipNodes(entity, tree->headnode.get(), hullnum); @@ -652,10 +670,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) // needs to come after any face creation MakeMarkFaces(tree->headnode.get()); - // convert detail leafs to solid (in case we didn't make the call above) - DetailToSolid(tree->headnode.get()); - - // fixme-brushbsp: prune nodes + CountLeafs(tree->headnode.get()); // output vertices first, since TJunc needs it EmitVertices(tree->headnode.get()); diff --git a/qbsp/tree.cc b/qbsp/tree.cc index a2b92863..7a01dd47 100644 --- a/qbsp/tree.cc +++ b/qbsp/tree.cc @@ -79,39 +79,6 @@ static void ConvertNodeToLeaf(node_t *node, const contentflags_t &contents) Q_assert(node->markfaces.empty()); } -void DetailToSolid(node_t *node) -{ - if (node->planenum == PLANENUM_LEAF) { - if (qbsp_options.target_game->id == GAME_QUAKE_II) { - return; - } - - // We need to remap CONTENTS_DETAIL to a standard quake content type - if (node->contents.is_detail_solid(qbsp_options.target_game)) { - node->contents = qbsp_options.target_game->create_solid_contents(); - } else if (node->contents.is_detail_illusionary(qbsp_options.target_game)) { - node->contents = qbsp_options.target_game->create_empty_contents(); - } - /* N.B.: CONTENTS_DETAIL_FENCE is not remapped to CONTENTS_SOLID until the very last moment, - * because we want to generate a leaf (if we set it to CONTENTS_SOLID now it would use leaf 0). - */ - return; - } else { - DetailToSolid(node->children[0].get()); - DetailToSolid(node->children[1].get()); - - // If both children are solid, we can merge the two leafs into one. - // DarkPlaces has an assertion that fails if both children are - // solid. - if (node->children[0]->contents.is_solid(qbsp_options.target_game) && - node->children[1]->contents.is_solid(qbsp_options.target_game)) { - // This discards any faces on-node. Should be safe (?) - ConvertNodeToLeaf(node, qbsp_options.target_game->create_solid_contents()); - } - // fixme-brushbsp: merge with PruneNodes - } -} - static void PruneNodes_R(node_t *node, int &count_pruned) { if (node->planenum == PLANENUM_LEAF) { @@ -128,6 +95,13 @@ static void PruneNodes_R(node_t *node, int &count_pruned) ++count_pruned; } + // DarkPlaces has an assertion that fails if both children are + // solid. + + /* N.B.: CONTENTS_DETAIL_FENCE is not remapped to CONTENTS_SOLID until the very last moment, + * because we want to generate a leaf (if we set it to CONTENTS_SOLID now it would use leaf 0). + */ + // fixme-brushbsp: corner case where two solid leafs shouldn't merge is two noclipfaces fence brushes touching // fixme-brushbsp: also merge other content types // fixme-brushbsp: maybe merge if same content type, and all faces on node are invisible? diff --git a/testmaps/q2_noclipfaces_junction.map b/testmaps/q2_noclipfaces_junction.map new file mode 100644 index 00000000..60f0c34e --- /dev/null +++ b/testmaps/q2_noclipfaces_junction.map @@ -0,0 +1,40 @@ +// Game: Quake 2 +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"_tb_textures" "textures/e1u1" +} +// entity 1 +{ +"classname" "func_detail_wall" +"_noclipfaces" "1" +// brush 0 +{ +( 16 16 48 ) ( 16 17 48 ) ( 16 16 49 ) e1u1/wndow1_2 [ 0 -1 0 56 ] [ 0 0 -1 0 ] 0 2 2 +( 16 16 48 ) ( 16 16 49 ) ( 17 16 48 ) e1u1/wndow1_2 [ 1 0 0 -40 ] [ 0 0 -1 0 ] 0 2 2 +( 16 16 0 ) ( 17 16 0 ) ( 16 17 0 ) e1u1/wndow1_2 [ -1 0 0 40 ] [ 0 -1 0 56 ] 0 2 2 +( 96 96 64 ) ( 96 97 64 ) ( 97 96 64 ) e1u1/wndow1_2 [ 1 0 0 -40 ] [ 0 -1 0 56 ] 0 2 2 +( 96 96 64 ) ( 97 96 64 ) ( 96 96 65 ) e1u1/wndow1_2 [ -1 0 0 40 ] [ 0 0 -1 0 ] 0 2 2 +( 96 96 64 ) ( 96 96 65 ) ( 96 97 64 ) e1u1/wndow1_2 [ 0 1 0 -56 ] [ 0 0 -1 0 ] 0 2 2 +} +} +// entity 2 +{ +"classname" "func_detail_wall" +// brush 0 +{ +( 96 16 48 ) ( 96 17 48 ) ( 96 16 49 ) e1u1/window1 [ 0 -1 0 56 ] [ 0 0 -1 0 ] 0 2 2 +( 96 16 48 ) ( 96 16 49 ) ( 97 16 48 ) e1u1/window1 [ 1 0 0 -40 ] [ 0 0 -1 0 ] 0 2 2 +( 96 16 0 ) ( 97 16 0 ) ( 96 17 0 ) e1u1/window1 [ -1 0 0 40 ] [ 0 -1 0 56 ] 0 2 2 +( 176 96 64 ) ( 176 97 64 ) ( 177 96 64 ) e1u1/window1 [ 1 0 0 -40 ] [ 0 -1 0 56 ] 0 2 2 +( 176 96 64 ) ( 177 96 64 ) ( 176 96 65 ) e1u1/window1 [ -1 0 0 40 ] [ 0 0 -1 0 ] 0 2 2 +( 176 96 64 ) ( 176 96 65 ) ( 176 97 64 ) e1u1/window1 [ 0 1 0 -56 ] [ 0 0 -1 0 ] 0 2 2 +} +} +// entity 3 +{ +"classname" "info_player_start" +"origin" "64 48 88" +} diff --git a/testmaps/qbsp_detail_doesnt_seal.map b/testmaps/qbsp_detail_seals.map similarity index 100% rename from testmaps/qbsp_detail_doesnt_seal.map rename to testmaps/qbsp_detail_seals.map diff --git a/testmaps/qbsp_noclipfaces_junction.map b/testmaps/qbsp_noclipfaces_junction.map new file mode 100644 index 00000000..77faa39d --- /dev/null +++ b/testmaps/qbsp_noclipfaces_junction.map @@ -0,0 +1,42 @@ +// 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" +} +// entity 1 +{ +"classname" "func_detail_wall" +"_noclipfaces" "1" +// brush 0 +{ +( 16 16 48 ) ( 16 17 48 ) ( 16 16 49 ) {trigger [ 0 -1 0 56 ] [ 0 0 -1 0 ] 0 2 2 +( 16 16 48 ) ( 16 16 49 ) ( 17 16 48 ) {trigger [ 1 0 0 -40 ] [ 0 0 -1 0 ] 0 2 2 +( 16 16 0 ) ( 17 16 0 ) ( 16 17 0 ) {trigger [ -1 0 0 40 ] [ 0 -1 0 56 ] 0 2 2 +( 96 96 64 ) ( 96 97 64 ) ( 97 96 64 ) {trigger [ 1 0 0 -40 ] [ 0 -1 0 56 ] 0 2 2 +( 96 96 64 ) ( 97 96 64 ) ( 96 96 65 ) {trigger [ -1 0 0 40 ] [ 0 0 -1 0 ] 0 2 2 +( 96 96 64 ) ( 96 96 65 ) ( 96 97 64 ) {trigger [ 0 1 0 -56 ] [ 0 0 -1 0 ] 0 2 2 +} +} +// entity 2 +{ +"classname" "func_detail_wall" +// brush 0 +{ +( 96 16 48 ) ( 96 17 48 ) ( 96 16 49 ) blood1 [ 0 -1 0 56 ] [ 0 0 -1 0 ] 0 2 2 +( 96 16 48 ) ( 96 16 49 ) ( 97 16 48 ) blood1 [ 1 0 0 -40 ] [ 0 0 -1 0 ] 0 2 2 +( 96 16 0 ) ( 97 16 0 ) ( 96 17 0 ) blood1 [ -1 0 0 40 ] [ 0 -1 0 56 ] 0 2 2 +( 176 96 64 ) ( 176 97 64 ) ( 177 96 64 ) blood1 [ 1 0 0 -40 ] [ 0 -1 0 56 ] 0 2 2 +( 176 96 64 ) ( 177 96 64 ) ( 176 96 65 ) blood1 [ -1 0 0 40 ] [ 0 0 -1 0 ] 0 2 2 +( 176 96 64 ) ( 176 96 65 ) ( 176 97 64 ) blood1 [ 0 1 0 -56 ] [ 0 0 -1 0 ] 0 2 2 +} +} +// entity 3 +{ +"classname" "info_player_start" +"origin" "64 48 88" +} diff --git a/tests/test_common.cc b/tests/test_common.cc index a8777020..d08ea3e4 100644 --- a/tests/test_common.cc +++ b/tests/test_common.cc @@ -13,7 +13,7 @@ TEST_CASE("StripFilename", "[common]") REQUIRE("" == fs::path("bar.txt").parent_path()); } -TEST_CASE("q1 contents", "[common][!mayfail]") +TEST_CASE("q1 contents", "[common]") { auto* game_q1 = bspver_q1.game; @@ -55,6 +55,16 @@ TEST_CASE("q1 contents", "[common][!mayfail]") CHECK(combined.native == CONTENTS_WATER); CHECK(combined.is_detail_illusionary(game_q1)); } + + SECTION("detail properties") { + CHECK(detail_solid.is_any_detail(game_q1)); + CHECK(detail_fence.is_any_detail(game_q1)); + CHECK(detail_illusionary.is_any_detail(game_q1)); + + CHECK(detail_solid.is_any_solid(game_q1)); + CHECK(!detail_fence.is_any_solid(game_q1)); + CHECK(!detail_illusionary.is_any_solid(game_q1)); + } } TEST_CASE("q2 contents", "[common]") diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index 80bf8834..2fcc8dfb 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -718,47 +718,50 @@ TEST_CASE("simple_worldspawn_sky", "[testmaps_q1]") // FIXME: unsure what the expected number of visclusters is, does sky get one? } -TEST_CASE("water_detail_illusionary", "[testmaps_q1][!mayfail]") +TEST_CASE("water_detail_illusionary", "[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"; - auto mapname = GENERATE_REF(basic_mapname, mirrorinside_mapname); - const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname); + for (const auto& mapname : {basic_mapname, mirrorinside_mapname}) { + DYNAMIC_SECTION("testing " << mapname) { + const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname); - REQUIRE(prt.has_value()); + REQUIRE(prt.has_value()); - const qvec3d inside_water_and_fence{-20, -52, 124}; - const qvec3d inside_fence{-20, -52, 172}; + 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); + 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}; + 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}); + // 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}); + 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); + 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)); + 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); + 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); + 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); + } + } } } @@ -780,6 +783,46 @@ TEST_CASE("noclipfaces", "[testmaps_q1]") CHECK(prt->portalleafs == 1); } +/** + * _noclipfaces 1 detail_wall 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") +{ + const std::vector maps{ + "qbsp_noclipfaces_junction.map", + "q2_noclipfaces_junction.map" + }; + + for (const auto& map : maps) { + const bool q2 = (map.find("q2") == 0); + + DYNAMIC_SECTION(map) { + 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 */ @@ -850,11 +893,14 @@ TEST_CASE("detail_illusionary_noclipfaces_intersecting", "[testmaps_q1]") CHECK(prt->portalleafs == 1); } -TEST_CASE("detail_doesnt_seal", "[testmaps_q1]") +/** + * Since moving to a qbsp3 codebase, detail seals by default. + */ +TEST_CASE("detail_seals", "[testmaps_q1]") { - const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_doesnt_seal.map"); + const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_detail_seals.map"); - REQUIRE_FALSE(prt.has_value()); + CHECK(prt.has_value()); } TEST_CASE("detail_doesnt_remove_world_nodes", "[testmaps_q1]") @@ -867,22 +913,29 @@ TEST_CASE("detail_doesnt_remove_world_nodes", "[testmaps_q1]") // 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}); - REQUIRE(nullptr != floor_under_start_face); + 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}); - REQUIRE(nullptr == floor_inside_detail_face); + 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}); - REQUIRE(nullptr != covered_by_detail_node); + CHECK(nullptr != covered_by_detail_node); } +#endif } TEST_CASE("merge", "[testmaps_q1]") @@ -905,9 +958,9 @@ TEST_CASE("merge", "[testmaps_q1]") CHECK(top_winding.bounds().maxs() == exp_bounds.maxs()); } -TEST_CASE("tjunc_many_sided_face", "[testmaps_q1][!mayfail]") +TEST_CASE("tjunc_many_sided_face", "[testmaps_q1]") { - const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_tjunc_many_sided_face.map"); + const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_tjunc_many_sided_face.map", {"-tjunc", "rotate"}); REQUIRE(prt.has_value()); @@ -1029,10 +1082,12 @@ TEST_CASE("q1_cube", "[testmaps_q1]") // check the empty leafs for (int i = 1; i < 7; ++i) { - auto& leaf = bsp.dleafs[i]; - CHECK(CONTENTS_EMPTY == leaf.contents); + DYNAMIC_SECTION("leaf " << i) { + auto &leaf = bsp.dleafs[i]; + CHECK(CONTENTS_EMPTY == leaf.contents); - CHECK(1 == leaf.nummarksurfaces); + CHECK(1 == leaf.nummarksurfaces); + } } REQUIRE(6 == bsp.dfaces.size());