/* Copyright (C) 1996-1997 Id Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA See file, 'COPYING', for details. */ #include #include #include #include #include #include #include static std::vector make_palette(std::initializer_list bytes) { Q_assert((bytes.size() % 3) == 0); std::vector result; result.reserve(bytes.size() / 3); for (const uint8_t *it = bytes.begin(); it < bytes.end(); it += 3) { result.emplace_back(it[0], it[1], it[2]); } return result; } struct gamedef_generic_t : public gamedef_t { gamedef_generic_t() : gamedef_t("") { id = GAME_UNKNOWN; } bool surf_is_lightmapped(const surfflags_t &) const override { throw std::bad_cast(); } bool surf_is_subdivided(const surfflags_t &) const override { throw std::bad_cast(); } bool surfflags_are_valid(const surfflags_t &) const override { throw std::bad_cast(); } bool texinfo_is_hintskip(const surfflags_t &, const std::string &) const override { throw std::bad_cast(); } contentflags_t cluster_contents(const contentflags_t &, const contentflags_t &) const override { throw std::bad_cast(); } int32_t contents_priority(const contentflags_t &) const override { throw std::bad_cast(); } bool chops(const contentflags_t &) const override { throw std::bad_cast(); } contentflags_t create_empty_contents() const override { throw std::bad_cast(); } contentflags_t create_solid_contents() const override { throw std::bad_cast(); } contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const override { throw std::bad_cast(); } contentflags_t create_detail_fence_contents(const contentflags_t &original) const override { throw std::bad_cast(); } contentflags_t create_detail_solid_contents(const contentflags_t &original) const override { throw std::bad_cast(); } bool contents_are_type_equal(const contentflags_t &self, const contentflags_t &other) const override { throw std::bad_cast(); } bool contents_are_equal(const contentflags_t &self, const contentflags_t &other) const override { throw std::bad_cast(); } bool contents_are_any_detail(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_detail_solid(const contentflags_t &contents) const override { throw std::bad_cast(); } bool contents_are_detail_fence(const contentflags_t &contents) const override { throw std::bad_cast(); } bool contents_are_detail_illusionary(const contentflags_t &contents) const override { throw std::bad_cast(); } bool contents_are_empty(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_mirrored(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_origin(const contentflags_t &contents) const override { throw std::bad_cast(); } bool contents_are_clip(const contentflags_t &contents) const override { throw std::bad_cast(); } bool contents_clip_same_type(const contentflags_t &, const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_solid(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_sky(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_liquid(const contentflags_t &) const override { throw std::bad_cast(); } bool contents_are_valid(const contentflags_t &, bool) const override { throw std::bad_cast(); } bool portal_can_see_through(const contentflags_t &, const contentflags_t &, bool, bool) const override { throw std::bad_cast(); } bool contents_seals_map(const contentflags_t &contents) const override { throw std::bad_cast(); } contentflags_t contents_remap_for_export(const contentflags_t &contents) const override { throw std::bad_cast(); } contentflags_t combine_contents(const contentflags_t &a, const contentflags_t &b) const override { throw std::bad_cast(); } std::string get_contents_display(const contentflags_t &) const override { throw std::bad_cast(); } const std::initializer_list &get_hull_sizes() const override { throw std::bad_cast(); } contentflags_t face_get_contents(const std::string &, const surfflags_t &, const contentflags_t &) const override { throw std::bad_cast(); } void init_filesystem(const fs::path &, const settings::common_settings &) const override { throw std::bad_cast(); } const std::vector &get_default_palette() const override { throw std::bad_cast(); } std::any create_content_stats() const override { throw std::bad_cast(); } void count_contents_in_stats(const contentflags_t &contents, std::any &stats) const override { throw std::bad_cast(); } void print_content_stats(const std::any &stats, const char *what) const override { throw std::bad_alloc(); } }; 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 struct q1_contentflags_data { // detail type detail_type_t detail = detail_type_t::STRUCTURAL; bool origin = false; // is an origin brush 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; } }; // 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; if (!contents.game_data.has_value()) { return blank_data; } return std::any_cast(contents.game_data); } public: 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 { return !(flags.native & TEX_SPECIAL); } bool surf_is_subdivided(const surfflags_t &flags) const { return !(flags.native & TEX_SPECIAL); } bool surfflags_are_valid(const surfflags_t &flags) const { // Q1 only supports TEX_SPECIAL return (flags.native & ~TEX_SPECIAL) == 0; } bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const { // anything texname other than "hint" in a hint brush is treated as "hintskip", and discarded 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 { if (contents0.equals(this, contents1)) return contents0; /* * 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(); 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 { 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; } 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; } } bool chops(const contentflags_t &contents) const { 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 { return {CONTENTS_EMPTY}; } contentflags_t create_solid_contents() const { return {CONTENTS_SOLID}; } contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const { // ignore the original contents in Q1 return create_extended_contents({detail_type_t::ILLUSIONARY}); } contentflags_t create_detail_fence_contents(const contentflags_t &original) const { return create_extended_contents({detail_type_t::FENCE}); } contentflags_t create_detail_solid_contents(const contentflags_t &original) const { return create_extended_contents({detail_type_t::DETAIL}); } bool contents_are_type_equal(const contentflags_t &self, const contentflags_t &other) const { if (get_data(self) != get_data(other)) { return false; } return self.illusionary_visblocker == other.illusionary_visblocker && self.native == other.native; } bool contents_are_equal(const contentflags_t &self, const contentflags_t &other) const { return contents_are_type_equal(self, other); } bool contents_are_any_detail(const contentflags_t &contents) const { // in Q1, there are only CFLAGS_DETAIL, CFLAGS_DETAIL_ILLUSIONARY, or CFLAGS_DETAIL_FENCE return get_data(contents).detail != detail_type_t::STRUCTURAL; } bool contents_are_detail_solid(const contentflags_t &contents) const { return get_data(contents).detail == detail_type_t::DETAIL; } bool contents_are_detail_fence(const contentflags_t &contents) const { return get_data(contents).detail == detail_type_t::FENCE; } bool contents_are_detail_illusionary(const contentflags_t &contents) const { return get_data(contents).detail == detail_type_t::ILLUSIONARY; } bool contents_are_mirrored(const contentflags_t &contents) const { // if we have mirrorinside set, go ahead if (contents.mirror_inside.has_value()) { return contents.mirror_inside.value(); } // 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); } bool contents_are_origin(const contentflags_t &contents) const { return get_data(contents).origin; } bool contents_are_clip(const contentflags_t &contents) const { return get_data(contents).clip; } bool contents_clip_same_type(const contentflags_t &self, const contentflags_t &other) const { 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 { return !contents_has_extended(contents) && contents.native == CONTENTS_EMPTY; } bool contents_are_solid(const contentflags_t &contents) const { return !contents_has_extended(contents) && contents.native == CONTENTS_SOLID; } bool contents_are_sky(const contentflags_t &contents) const { return !contents_has_extended(contents) && contents.native == CONTENTS_SKY; } bool contents_are_liquid(const contentflags_t &contents) const { return !contents_has_extended(contents) && contents.native <= CONTENTS_WATER && contents.native >= CONTENTS_LAVA; } bool contents_are_valid(const contentflags_t &contents, bool strict) const { if (!contents.native && !strict) { return true; } switch (contents.native) { case CONTENTS_EMPTY: case CONTENTS_SOLID: case CONTENTS_WATER: case CONTENTS_SLIME: case CONTENTS_LAVA: case CONTENTS_SKY: return true; default: return false; } } bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const { /* 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; } /* 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; } /* 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); } bool contents_seals_map(const contentflags_t &contents) const override { return contents_are_solid(contents) || contents_are_sky(contents); } contentflags_t contents_remap_for_export(const contentflags_t &contents) const override { /* * This is for func_detail_wall.. we want to write a solid leaf that has faces, * because it may be possible to see inside (fence textures). * * Normally solid leafs are not written and just referenced as leaf 0. */ if (contents_are_detail_fence(contents)) { return create_solid_contents(); } return contents; } 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); if (a_pri > b_pri) { return a; } else { return b; } } std::string get_contents_display(const contentflags_t &contents) const { 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 std::initializer_list &get_hull_sizes() const { static std::initializer_list hulls = { {{0, 0, 0}, {0, 0, 0}}, {{-16, -16, -32}, {16, 16, 24}}, {{-32, -32, -64}, {32, 32, 24}}}; return hulls; } contentflags_t face_get_contents(const std::string &texname, const surfflags_t &flags, const contentflags_t &) const { // check for strong content indicators if (!Q_strcasecmp(texname.data(), "origin")) { return create_extended_contents({ detail_type_t::STRUCTURAL, true, false }); } 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 }); } else if (texname[0] == '*') { if (!Q_strncasecmp(texname.data() + 1, "lava", 4)) { return create_liquid_contents(CONTENTS_LAVA); } else if (!Q_strncasecmp(texname.data() + 1, "slime", 5)) { return create_liquid_contents(CONTENTS_SLIME); } else { return create_liquid_contents(CONTENTS_WATER); } } else if (!Q_strncasecmp(texname.data(), "sky", 3)) { return create_sky_contents(); } // and anything else is assumed to be a regular solid. return create_solid_contents(); } void init_filesystem(const fs::path &, const settings::common_settings &) const { // Q1-like games don't care about the local // filesystem. } const std::vector &get_default_palette() const { static constexpr std::initializer_list palette_bytes{0, 0, 0, 15, 15, 15, 31, 31, 31, 47, 47, 47, 63, 63, 63, 75, 75, 75, 91, 91, 91, 107, 107, 107, 123, 123, 123, 139, 139, 139, 155, 155, 155, 171, 171, 171, 187, 187, 187, 203, 203, 203, 219, 219, 219, 235, 235, 235, 15, 11, 7, 23, 15, 11, 31, 23, 11, 39, 27, 15, 47, 35, 19, 55, 43, 23, 63, 47, 23, 75, 55, 27, 83, 59, 27, 91, 67, 31, 99, 75, 31, 107, 83, 31, 115, 87, 31, 123, 95, 35, 131, 103, 35, 143, 111, 35, 11, 11, 15, 19, 19, 27, 27, 27, 39, 39, 39, 51, 47, 47, 63, 55, 55, 75, 63, 63, 87, 71, 71, 103, 79, 79, 115, 91, 91, 127, 99, 99, 139, 107, 107, 151, 115, 115, 163, 123, 123, 175, 131, 131, 187, 139, 139, 203, 0, 0, 0, 7, 7, 0, 11, 11, 0, 19, 19, 0, 27, 27, 0, 35, 35, 0, 43, 43, 7, 47, 47, 7, 55, 55, 7, 63, 63, 7, 71, 71, 7, 75, 75, 11, 83, 83, 11, 91, 91, 11, 99, 99, 11, 107, 107, 15, 7, 0, 0, 15, 0, 0, 23, 0, 0, 31, 0, 0, 39, 0, 0, 47, 0, 0, 55, 0, 0, 63, 0, 0, 71, 0, 0, 79, 0, 0, 87, 0, 0, 95, 0, 0, 103, 0, 0, 111, 0, 0, 119, 0, 0, 127, 0, 0, 19, 19, 0, 27, 27, 0, 35, 35, 0, 47, 43, 0, 55, 47, 0, 67, 55, 0, 75, 59, 7, 87, 67, 7, 95, 71, 7, 107, 75, 11, 119, 83, 15, 131, 87, 19, 139, 91, 19, 151, 95, 27, 163, 99, 31, 175, 103, 35, 35, 19, 7, 47, 23, 11, 59, 31, 15, 75, 35, 19, 87, 43, 23, 99, 47, 31, 115, 55, 35, 127, 59, 43, 143, 67, 51, 159, 79, 51, 175, 99, 47, 191, 119, 47, 207, 143, 43, 223, 171, 39, 239, 203, 31, 255, 243, 27, 11, 7, 0, 27, 19, 0, 43, 35, 15, 55, 43, 19, 71, 51, 27, 83, 55, 35, 99, 63, 43, 111, 71, 51, 127, 83, 63, 139, 95, 71, 155, 107, 83, 167, 123, 95, 183, 135, 107, 195, 147, 123, 211, 163, 139, 227, 179, 151, 171, 139, 163, 159, 127, 151, 147, 115, 135, 139, 103, 123, 127, 91, 111, 119, 83, 99, 107, 75, 87, 95, 63, 75, 87, 55, 67, 75, 47, 55, 67, 39, 47, 55, 31, 35, 43, 23, 27, 35, 19, 19, 23, 11, 11, 15, 7, 7, 187, 115, 159, 175, 107, 143, 163, 95, 131, 151, 87, 119, 139, 79, 107, 127, 75, 95, 115, 67, 83, 107, 59, 75, 95, 51, 63, 83, 43, 55, 71, 35, 43, 59, 31, 35, 47, 23, 27, 35, 19, 19, 23, 11, 11, 15, 7, 7, 219, 195, 187, 203, 179, 167, 191, 163, 155, 175, 151, 139, 163, 135, 123, 151, 123, 111, 135, 111, 95, 123, 99, 83, 107, 87, 71, 95, 75, 59, 83, 63, 51, 67, 51, 39, 55, 43, 31, 39, 31, 23, 27, 19, 15, 15, 11, 7, 111, 131, 123, 103, 123, 111, 95, 115, 103, 87, 107, 95, 79, 99, 87, 71, 91, 79, 63, 83, 71, 55, 75, 63, 47, 67, 55, 43, 59, 47, 35, 51, 39, 31, 43, 31, 23, 35, 23, 15, 27, 19, 11, 19, 11, 7, 11, 7, 255, 243, 27, 239, 223, 23, 219, 203, 19, 203, 183, 15, 187, 167, 15, 171, 151, 11, 155, 131, 7, 139, 115, 7, 123, 99, 7, 107, 83, 0, 91, 71, 0, 75, 55, 0, 59, 43, 0, 43, 31, 0, 27, 15, 0, 11, 7, 0, 0, 0, 255, 11, 11, 239, 19, 19, 223, 27, 27, 207, 35, 35, 191, 43, 43, 175, 47, 47, 159, 47, 47, 143, 47, 47, 127, 47, 47, 111, 47, 47, 95, 43, 43, 79, 35, 35, 63, 27, 27, 47, 19, 19, 31, 11, 11, 15, 43, 0, 0, 59, 0, 0, 75, 7, 0, 95, 7, 0, 111, 15, 0, 127, 23, 7, 147, 31, 7, 163, 39, 11, 183, 51, 15, 195, 75, 27, 207, 99, 43, 219, 127, 59, 227, 151, 79, 231, 171, 95, 239, 191, 119, 247, 211, 139, 167, 123, 59, 183, 155, 55, 199, 195, 55, 231, 227, 87, 127, 191, 255, 171, 231, 255, 215, 255, 255, 103, 0, 0, 139, 0, 0, 179, 0, 0, 215, 0, 0, 255, 0, 0, 255, 243, 147, 255, 247, 199, 255, 255, 255, 159, 91, 83}; static const auto palette = make_palette(palette_bytes); return palette; } private: struct content_stats_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; content_stats_t() = default; inline content_stats_t(const content_stats_t ©) { } }; public: std::any create_content_stats() const override { return content_stats_t{}; } void count_contents_in_stats(const contentflags_t &contents, std::any &stats_any) const override { content_stats_t &stats = std::any_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++; } } void print_content_stats(const std::any &stats_any, const char *what) const override { const content_stats_t &stats = std::any_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); } } }; struct gamedef_h2_t : public gamedef_q1_like_t { gamedef_h2_t() : gamedef_q1_like_t("DATA1") { } const std::initializer_list &get_hull_sizes() const { static std::initializer_list hulls = {{{0, 0, 0}, {0, 0, 0}}, {{-16, -16, -32}, {16, 16, 24}}, {{-24, -24, -20}, {24, 24, 20}}, {{-16, -16, -16}, {16, 16, 12}}, {{-8, -8, -8}, {8, 8, 8}}, // {{-40, -40, -42}, {40, 40, 42}} = original game {{-28, -28, -40}, {28, 28, 40}}}; return hulls; } const std::vector &get_default_palette() const { static constexpr std::initializer_list palette_bytes{0, 0, 0, 0, 0, 0, 8, 8, 8, 16, 16, 16, 24, 24, 24, 32, 32, 32, 40, 40, 40, 48, 48, 48, 56, 56, 56, 64, 64, 64, 72, 72, 72, 80, 80, 80, 84, 84, 84, 88, 88, 88, 96, 96, 96, 104, 104, 104, 112, 112, 112, 120, 120, 120, 128, 128, 128, 136, 136, 136, 148, 148, 148, 156, 156, 156, 168, 168, 168, 180, 180, 180, 184, 184, 184, 196, 196, 196, 204, 204, 204, 212, 212, 212, 224, 224, 224, 232, 232, 232, 240, 240, 240, 252, 252, 252, 8, 8, 12, 16, 16, 20, 24, 24, 28, 28, 32, 36, 36, 36, 44, 44, 44, 52, 48, 52, 60, 56, 56, 68, 64, 64, 72, 76, 76, 88, 92, 92, 104, 108, 112, 128, 128, 132, 152, 152, 156, 176, 168, 172, 196, 188, 196, 220, 32, 24, 20, 40, 32, 28, 48, 36, 32, 52, 44, 40, 60, 52, 44, 68, 56, 52, 76, 64, 56, 84, 72, 64, 92, 76, 72, 100, 84, 76, 108, 92, 84, 112, 96, 88, 120, 104, 96, 128, 112, 100, 136, 116, 108, 144, 124, 112, 20, 24, 20, 28, 32, 28, 32, 36, 32, 40, 44, 40, 44, 48, 44, 48, 56, 48, 56, 64, 56, 64, 68, 64, 68, 76, 68, 84, 92, 84, 104, 112, 104, 120, 128, 120, 140, 148, 136, 156, 164, 152, 172, 180, 168, 188, 196, 184, 48, 32, 8, 60, 40, 8, 72, 48, 16, 84, 56, 20, 92, 64, 28, 100, 72, 36, 108, 80, 44, 120, 92, 52, 136, 104, 60, 148, 116, 72, 160, 128, 84, 168, 136, 92, 180, 144, 100, 188, 152, 108, 196, 160, 116, 204, 168, 124, 16, 20, 16, 20, 28, 20, 24, 32, 24, 28, 36, 28, 32, 44, 32, 36, 48, 36, 40, 56, 40, 44, 60, 44, 48, 68, 48, 52, 76, 52, 60, 84, 60, 68, 92, 64, 76, 100, 72, 84, 108, 76, 92, 116, 84, 100, 128, 92, 24, 12, 8, 32, 16, 8, 40, 20, 8, 52, 24, 12, 60, 28, 12, 68, 32, 12, 76, 36, 16, 84, 44, 20, 92, 48, 24, 100, 56, 28, 112, 64, 32, 120, 72, 36, 128, 80, 44, 144, 92, 56, 168, 112, 72, 192, 132, 88, 24, 4, 4, 36, 4, 4, 48, 0, 0, 60, 0, 0, 68, 0, 0, 80, 0, 0, 88, 0, 0, 100, 0, 0, 112, 0, 0, 132, 0, 0, 152, 0, 0, 172, 0, 0, 192, 0, 0, 212, 0, 0, 232, 0, 0, 252, 0, 0, 16, 12, 32, 28, 20, 48, 32, 28, 56, 40, 36, 68, 52, 44, 80, 60, 56, 92, 68, 64, 104, 80, 72, 116, 88, 84, 128, 100, 96, 140, 108, 108, 152, 120, 116, 164, 132, 132, 176, 144, 144, 188, 156, 156, 200, 172, 172, 212, 36, 20, 4, 52, 24, 4, 68, 32, 4, 80, 40, 0, 100, 48, 4, 124, 60, 4, 140, 72, 4, 156, 88, 8, 172, 100, 8, 188, 116, 12, 204, 128, 12, 220, 144, 16, 236, 160, 20, 252, 184, 56, 248, 200, 80, 248, 220, 120, 20, 16, 4, 28, 24, 8, 36, 32, 8, 44, 40, 12, 52, 48, 16, 56, 56, 16, 64, 64, 20, 68, 72, 24, 72, 80, 28, 80, 92, 32, 84, 104, 40, 88, 116, 44, 92, 128, 52, 92, 140, 52, 92, 148, 56, 96, 160, 64, 60, 16, 16, 72, 24, 24, 84, 28, 28, 100, 36, 36, 112, 44, 44, 124, 52, 48, 140, 64, 56, 152, 76, 64, 44, 20, 8, 56, 28, 12, 72, 32, 16, 84, 40, 20, 96, 44, 28, 112, 52, 32, 124, 56, 40, 140, 64, 48, 24, 20, 16, 36, 28, 20, 44, 36, 28, 56, 44, 32, 64, 52, 36, 72, 60, 44, 80, 68, 48, 92, 76, 52, 100, 84, 60, 112, 92, 68, 120, 100, 72, 132, 112, 80, 144, 120, 88, 152, 128, 96, 160, 136, 104, 168, 148, 112, 36, 24, 12, 44, 32, 16, 52, 40, 20, 60, 44, 20, 72, 52, 24, 80, 60, 28, 88, 68, 28, 104, 76, 32, 148, 96, 56, 160, 108, 64, 172, 116, 72, 180, 124, 80, 192, 132, 88, 204, 140, 92, 216, 156, 108, 60, 20, 92, 100, 36, 116, 168, 72, 164, 204, 108, 192, 4, 84, 4, 4, 132, 4, 0, 180, 0, 0, 216, 0, 4, 4, 144, 16, 68, 204, 36, 132, 224, 88, 168, 232, 216, 4, 4, 244, 72, 0, 252, 128, 0, 252, 172, 24, 252, 252, 252}; static const auto palette = make_palette(palette_bytes); return palette; } }; struct gamedef_hl_t : public gamedef_q1_like_t { gamedef_hl_t() : gamedef_q1_like_t("VALVE") { has_rgb_lightmap = true; } const std::initializer_list &get_hull_sizes() const { static std::initializer_list hulls = {{{0, 0, 0}, {0, 0, 0}}, {{-16, -16, -36}, {16, 16, 36}}, {{-32, -32, -32}, {32, 32, 32}}, {{-16, -16, -18}, {16, 16, 18}}}; return hulls; } const std::vector &get_default_palette() const { static const std::vector palette; return palette; } }; #include struct gamedef_q2_t : public gamedef_t { gamedef_q2_t() : gamedef_t("BASEQ2") { this->id = GAME_QUAKE_II; has_rgb_lightmap = true; allow_contented_bmodels = true; max_entity_key = 256; } bool surf_is_lightmapped(const surfflags_t &flags) const { return !(flags.native & Q2_SURF_NODRAW); } bool surf_is_subdivided(const surfflags_t &flags) const { return true; } bool surfflags_are_valid(const surfflags_t &flags) const { // no rules in Quake II baby return true; } bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const { // any face in a hint brush that isn't HINT are treated as "hintskip", and discarded return !(flags.native & Q2_SURF_HINT); } contentflags_t cluster_contents(const contentflags_t &contents0, const contentflags_t &contents1) const { contentflags_t c = {contents0.native | contents1.native}; c.illusionary_visblocker = contents0.illusionary_visblocker || contents1.illusionary_visblocker; // a cluster may include some solid detail areas, but // still be seen into if (!(contents0.native & Q2_CONTENTS_SOLID) || !(contents1.native & Q2_CONTENTS_SOLID)) { c.native &= ~Q2_CONTENTS_SOLID; } return c; } inline int32_t get_content_type(const contentflags_t &contents) const { // fixme-brushbsp: should TRANSLUCENT be here? does it need to be? return contents.native & (Q2_ALL_VISIBLE_CONTENTS | (Q2_CONTENTS_PLAYERCLIP | Q2_CONTENTS_MONSTERCLIP | Q2_CONTENTS_ORIGIN | Q2_CONTENTS_TRANSLUCENT | Q2_CONTENTS_AREAPORTAL)); } int32_t contents_priority(const contentflags_t &contents) const { 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 { return !!(contents.native & Q2_CONTENTS_SOLID); } contentflags_t create_empty_contents() const { return {Q2_CONTENTS_EMPTY}; } contentflags_t create_solid_contents() const { return {Q2_CONTENTS_SOLID}; } contentflags_t create_detail_illusionary_contents(const contentflags_t &original) const { contentflags_t result = original; result.native &= ~Q2_CONTENTS_SOLID; result.native |= Q2_CONTENTS_MIST | Q2_CONTENTS_DETAIL; return result; } contentflags_t create_detail_fence_contents(const contentflags_t &original) const { contentflags_t result = original; result.native &= ~Q2_CONTENTS_SOLID; result.native |= (Q2_CONTENTS_WINDOW | Q2_CONTENTS_TRANSLUCENT | Q2_CONTENTS_DETAIL); return result; } contentflags_t create_detail_solid_contents(const contentflags_t &original) const { contentflags_t result = original; result.native |= (Q2_CONTENTS_SOLID | Q2_CONTENTS_DETAIL); return result; } bool contents_are_type_equal(const contentflags_t &self, const contentflags_t &other) const { return self.illusionary_visblocker == other.illusionary_visblocker && get_content_type(self) == get_content_type(other); } bool contents_are_equal(const contentflags_t &self, const contentflags_t &other) const { return self.illusionary_visblocker == other.illusionary_visblocker && self.native == other.native; } bool contents_are_any_detail(const contentflags_t &contents) const { return ((contents.native & Q2_CONTENTS_DETAIL) != 0); } bool contents_are_detail_solid(const contentflags_t &contents) const { int32_t test = (Q2_CONTENTS_DETAIL|Q2_CONTENTS_SOLID); return ((contents.native & test) == test); } bool contents_are_detail_fence(const contentflags_t &contents) const { if (contents.native & Q2_CONTENTS_SOLID) { return false; } int32_t test = (Q2_CONTENTS_DETAIL|Q2_CONTENTS_WINDOW); return ((contents.native & test) == test); } bool contents_are_detail_illusionary(const contentflags_t &contents) const { if (contents.native & Q2_CONTENTS_SOLID) { return false; } int32_t mist1_type = (Q2_CONTENTS_DETAIL|Q2_CONTENTS_MIST); int32_t mist2_type = (Q2_CONTENTS_DETAIL|Q2_CONTENTS_AUX); return ((contents.native & mist1_type) == mist1_type) || ((contents.native & mist2_type) == mist2_type); } bool contents_are_mirrored(const contentflags_t &contents) const { // if we have mirrorinside set, go ahead if (contents.mirror_inside.has_value()) return contents.mirror_inside.value(); // Q2 is a bit different here. in vanilla tools, // every content except SOLID is implicitly mirrorinside. // the only exception is that 4bsp has the unused AUX // contents to default to not mirroring the insides. return !(contents.native & (Q2_CONTENTS_SOLID | Q2_CONTENTS_AUX)); } bool contents_are_origin(const contentflags_t &contents) const { return contents.native & Q2_CONTENTS_ORIGIN; } bool contents_are_clip(const contentflags_t &contents) const { return contents.native & (Q2_CONTENTS_PLAYERCLIP | Q2_CONTENTS_MONSTERCLIP); } bool contents_clip_same_type(const contentflags_t &self, const contentflags_t &other) const { return (self.native & Q2_ALL_VISIBLE_CONTENTS) == (other.native & Q2_ALL_VISIBLE_CONTENTS) && self.clips_same_type.value_or(true); } inline bool contents_has_extended(const contentflags_t &contents) const { return contents.illusionary_visblocker; } bool contents_are_empty(const contentflags_t &contents) const { return !contents_has_extended(contents) && !get_content_type(contents); } bool contents_are_solid(const contentflags_t &contents) const { return !contents_has_extended(contents) && (contents.native & Q2_CONTENTS_SOLID) && !(contents.native & Q2_CONTENTS_DETAIL); } bool contents_are_sky(const contentflags_t &contents) const { return false; } bool contents_are_liquid(const contentflags_t &contents) const { if (contents_has_extended(contents)) return false; if (contents.native & Q2_CONTENTS_AREAPORTAL) return true; // HACK: treat areaportal as a liquid for the purposes of the CSG code return contents.native & Q2_CONTENTS_LIQUID; } bool contents_are_valid(const contentflags_t &contents, bool strict) const { // check that we don't have more than one visible contents type const int32_t x = contents.native & Q2_ALL_VISIBLE_CONTENTS; // TODO: check other invalid mixes if (!x && strict) { return false; } return true; } constexpr int32_t visible_contents(const int32_t &contents) const { for (int32_t i = 1; i <= Q2_LAST_VISIBLE_CONTENTS; i <<= 1) { if (contents & i) { return i; } } return 0; } bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool, bool) const { int32_t c0 = contents0.native, c1 = contents1.native; // can't see through solid if ((c0 | c1) & Q2_CONTENTS_SOLID) { return false; } if (!visible_contents(c0 ^ c1)) { return true; } if ((c0 & Q2_CONTENTS_TRANSLUCENT) || contents0.is_any_detail(this)) { c0 = 0; } if ((c1 & Q2_CONTENTS_TRANSLUCENT) || contents1.is_any_detail(this)) { c1 = 0; } // identical on both sides if (!(c0 ^ c1)) return true; return !visible_contents(c0 ^ c1); } bool contents_seals_map(const contentflags_t& contents) const override { return contents_are_solid(contents) || contents_are_sky(contents); } contentflags_t contents_remap_for_export(const contentflags_t &contents) const override { return contents; } contentflags_t combine_contents(const contentflags_t &a, const contentflags_t &b) const override { if ((a.native & Q2_CONTENTS_SOLID) || (b.native & Q2_CONTENTS_SOLID)) { return {Q2_CONTENTS_SOLID}; } return {a.native | b.native}; } std::string get_contents_display(const contentflags_t &contents) const { if (!contents.native) { return "EMPTY"; } constexpr const char *bitflag_names[] = {"SOLID", "WINDOW", "AUX", "LAVA", "SLIME", "WATER", "MIST", "128", "256", "512", "1024", "2048", "4096", "8192", "16384", "AREAPORTAL", "PLAYERCLIP", "MONSTERCLIP", "CURRENT_0", "CURRENT_90", "CURRENT_180", "CURRENT_270", "CURRENT_UP", "CURRENT_DOWN", "ORIGIN", "MONSTER", "DEADMONSTER", "DETAIL", "TRANSLUCENT", "LADDER", "1073741824", "2147483648"}; std::string s; for (int32_t i = 0; i < std::size(bitflag_names); i++) { if (contents.native & nth_bit(i)) { if (s.size()) { s += " | "; } s += bitflag_names[i]; } } return s; } const std::initializer_list &get_hull_sizes() const { static constexpr std::initializer_list hulls = {}; return hulls; } contentflags_t face_get_contents( const std::string &texname, const surfflags_t &flags, const contentflags_t &contents) const { // hints and skips are never detail, and have no content if (flags.native & (Q2_SURF_HINT | Q2_SURF_SKIP)) { return {Q2_CONTENTS_EMPTY}; } contentflags_t surf_contents = contents; // if we don't have a declared content type, assume solid. if (!get_content_type(surf_contents)) { surf_contents.native |= Q2_CONTENTS_SOLID; } // if we have TRANS33 or TRANS66, we have to be marked as WINDOW, // so unset SOLID, give us WINDOW, and give us TRANSLUCENT if (flags.native & (Q2_SURF_TRANS33 | Q2_SURF_TRANS66)) { surf_contents.native |= Q2_CONTENTS_TRANSLUCENT; if (surf_contents.native & Q2_CONTENTS_SOLID) { surf_contents.native = (surf_contents.native & ~Q2_CONTENTS_SOLID) | Q2_CONTENTS_WINDOW; } } // translucent objects are automatically classified as detail if (surf_contents.native & Q2_CONTENTS_TRANSLUCENT) { surf_contents.native |= Q2_CONTENTS_DETAIL; } else if (surf_contents.native & (Q2_CONTENTS_MIST | Q2_CONTENTS_AUX)) { surf_contents.native |= Q2_CONTENTS_DETAIL; } if (surf_contents.native & (Q2_CONTENTS_MONSTERCLIP | Q2_CONTENTS_PLAYERCLIP)) { surf_contents.native |= Q2_CONTENTS_DETAIL; } return surf_contents; } private: void discoverArchives(const fs::path &base) const { fs::directory_iterator it(base); std::set packs; for (auto &entry : it) { if (string_iequals(entry.path().extension().generic_string(), ".pak")) { packs.insert(entry.path().generic_string()); } } for (auto &pak : packs) { fs::addArchive(pak); } } public: void init_filesystem(const fs::path &source, const settings::common_settings &settings) const { constexpr const char *MAPS_FOLDER = "maps"; // detect gamedir (mod directory path) fs::path gamedir; // expand canonicals, and fetch parent of source file if (auto paths = fs::splitArchivePath(source)) { // if the source is an archive, use the parent // of that folder as the mod directory // pak0.pak/maps/source.map -> C:/Quake/ID1 gamedir = fs::canonical(paths.archive).parent_path(); } else { // maps/source.map -> C:/Quake/ID1/maps // this is weak because the source may not exist yet gamedir = fs::weakly_canonical(source).parent_path(); if (!string_iequals(gamedir.filename().generic_string(), "maps")) { logging::print("WARNING: '{}' is not directly inside '{}'. This may confuse automated path detection.\n", source, MAPS_FOLDER); return; } // C:/Quake/ID1/maps -> C:/Quake/ID1 gamedir = gamedir.parent_path(); } // C:/Quake/ID1 -> C:/Quake fs::path qdir = gamedir.parent_path(); // Set base dir and make sure it exists fs::path basedir = qdir / (settings.basedir.isChanged() ? settings.basedir.value() : default_base_dir); if (!exists(basedir)) { logging::print("WARNING: failed to find '{}' in '{}'\n", default_base_dir, qdir); } else if (!equivalent(gamedir, basedir)) { fs::addArchive(basedir); discoverArchives(basedir); } fs::addArchive(gamedir); discoverArchives(gamedir); } const std::vector &get_default_palette() const { static constexpr std::initializer_list palette_bytes{0, 0, 0, 15, 15, 15, 31, 31, 31, 47, 47, 47, 63, 63, 63, 75, 75, 75, 91, 91, 91, 107, 107, 107, 123, 123, 123, 139, 139, 139, 155, 155, 155, 171, 171, 171, 187, 187, 187, 203, 203, 203, 219, 219, 219, 235, 235, 235, 99, 75, 35, 91, 67, 31, 83, 63, 31, 79, 59, 27, 71, 55, 27, 63, 47, 23, 59, 43, 23, 51, 39, 19, 47, 35, 19, 43, 31, 19, 39, 27, 15, 35, 23, 15, 27, 19, 11, 23, 15, 11, 19, 15, 7, 15, 11, 7, 95, 95, 111, 91, 91, 103, 91, 83, 95, 87, 79, 91, 83, 75, 83, 79, 71, 75, 71, 63, 67, 63, 59, 59, 59, 55, 55, 51, 47, 47, 47, 43, 43, 39, 39, 39, 35, 35, 35, 27, 27, 27, 23, 23, 23, 19, 19, 19, 143, 119, 83, 123, 99, 67, 115, 91, 59, 103, 79, 47, 207, 151, 75, 167, 123, 59, 139, 103, 47, 111, 83, 39, 235, 159, 39, 203, 139, 35, 175, 119, 31, 147, 99, 27, 119, 79, 23, 91, 59, 15, 63, 39, 11, 35, 23, 7, 167, 59, 43, 159, 47, 35, 151, 43, 27, 139, 39, 19, 127, 31, 15, 115, 23, 11, 103, 23, 7, 87, 19, 0, 75, 15, 0, 67, 15, 0, 59, 15, 0, 51, 11, 0, 43, 11, 0, 35, 11, 0, 27, 7, 0, 19, 7, 0, 123, 95, 75, 115, 87, 67, 107, 83, 63, 103, 79, 59, 95, 71, 55, 87, 67, 51, 83, 63, 47, 75, 55, 43, 67, 51, 39, 63, 47, 35, 55, 39, 27, 47, 35, 23, 39, 27, 19, 31, 23, 15, 23, 15, 11, 15, 11, 7, 111, 59, 23, 95, 55, 23, 83, 47, 23, 67, 43, 23, 55, 35, 19, 39, 27, 15, 27, 19, 11, 15, 11, 7, 179, 91, 79, 191, 123, 111, 203, 155, 147, 215, 187, 183, 203, 215, 223, 179, 199, 211, 159, 183, 195, 135, 167, 183, 115, 151, 167, 91, 135, 155, 71, 119, 139, 47, 103, 127, 23, 83, 111, 19, 75, 103, 15, 67, 91, 11, 63, 83, 7, 55, 75, 7, 47, 63, 7, 39, 51, 0, 31, 43, 0, 23, 31, 0, 15, 19, 0, 7, 11, 0, 0, 0, 139, 87, 87, 131, 79, 79, 123, 71, 71, 115, 67, 67, 107, 59, 59, 99, 51, 51, 91, 47, 47, 87, 43, 43, 75, 35, 35, 63, 31, 31, 51, 27, 27, 43, 19, 19, 31, 15, 15, 19, 11, 11, 11, 7, 7, 0, 0, 0, 151, 159, 123, 143, 151, 115, 135, 139, 107, 127, 131, 99, 119, 123, 95, 115, 115, 87, 107, 107, 79, 99, 99, 71, 91, 91, 67, 79, 79, 59, 67, 67, 51, 55, 55, 43, 47, 47, 35, 35, 35, 27, 23, 23, 19, 15, 15, 11, 159, 75, 63, 147, 67, 55, 139, 59, 47, 127, 55, 39, 119, 47, 35, 107, 43, 27, 99, 35, 23, 87, 31, 19, 79, 27, 15, 67, 23, 11, 55, 19, 11, 43, 15, 7, 31, 11, 7, 23, 7, 0, 11, 0, 0, 0, 0, 0, 119, 123, 207, 111, 115, 195, 103, 107, 183, 99, 99, 167, 91, 91, 155, 83, 87, 143, 75, 79, 127, 71, 71, 115, 63, 63, 103, 55, 55, 87, 47, 47, 75, 39, 39, 63, 35, 31, 47, 27, 23, 35, 19, 15, 23, 11, 7, 7, 155, 171, 123, 143, 159, 111, 135, 151, 99, 123, 139, 87, 115, 131, 75, 103, 119, 67, 95, 111, 59, 87, 103, 51, 75, 91, 39, 63, 79, 27, 55, 67, 19, 47, 59, 11, 35, 47, 7, 27, 35, 0, 19, 23, 0, 11, 15, 0, 0, 255, 0, 35, 231, 15, 63, 211, 27, 83, 187, 39, 95, 167, 47, 95, 143, 51, 95, 123, 51, 255, 255, 255, 255, 255, 211, 255, 255, 167, 255, 255, 127, 255, 255, 83, 255, 255, 39, 255, 235, 31, 255, 215, 23, 255, 191, 15, 255, 171, 7, 255, 147, 0, 239, 127, 0, 227, 107, 0, 211, 87, 0, 199, 71, 0, 183, 59, 0, 171, 43, 0, 155, 31, 0, 143, 23, 0, 127, 15, 0, 115, 7, 0, 95, 0, 0, 71, 0, 0, 47, 0, 0, 27, 0, 0, 239, 0, 0, 55, 55, 255, 255, 0, 0, 0, 0, 255, 43, 43, 35, 27, 27, 23, 19, 19, 15, 235, 151, 127, 195, 115, 83, 159, 87, 51, 123, 63, 27, 235, 211, 199, 199, 171, 155, 167, 139, 119, 135, 107, 87, 159, 91, 83}; static const auto palette = make_palette(palette_bytes); return palette; } private: struct content_stats_t { //std::array, 32> native_types; std::unordered_map native_types; std::atomic total_brushes; std::atomic visblocker_brushes; content_stats_t() = default; inline content_stats_t(const content_stats_t ©) { } }; public: std::any create_content_stats() const override { return content_stats_t{}; } void count_contents_in_stats(const contentflags_t &contents, std::any &stats_any) const override { content_stats_t &stats = std::any_cast(stats_any); /*for (int32_t i = 0; i < 32; i++) { if (contents.native & nth_bit(i)) { stats.native_types[i]++; } }*/ static std::mutex stat_mutex; { std::unique_lock lock(stat_mutex); stats.native_types[contents.native]++; } if (contents.illusionary_visblocker) { stats.visblocker_brushes++; } stats.total_brushes++; } void print_content_stats(const std::any &stats_any, const char *what) const override { const content_stats_t &stats = std::any_cast(stats_any); /*for (int32_t i = 0; i < 32; i++) { if (stats.native_types[i]) { logging::print(logging::flag::STAT, " {:8} {} {}\n", stats.native_types[i], get_contents_display({nth_bit(i)}), what); } }*/ for (auto &it : stats.native_types) { logging::print(logging::flag::STAT, " {:8} {} {}\n", it.second, get_contents_display({it.first}), what); } if (stats.visblocker_brushes) { logging::print(logging::flag::STAT, " {:8} VISBLOCKER {}\n", stats.visblocker_brushes, what); } logging::print(logging::flag::STAT, " {:8} {} total\n", stats.total_brushes, what); } }; // Game definitions, used for the bsp versions below static const gamedef_generic_t gamedef_generic; static const gamedef_q1_like_t gamedef_q1; static const gamedef_h2_t gamedef_h2; static const gamedef_hl_t gamedef_hl; static const gamedef_q2_t gamedef_q2; const bspversion_t bspver_generic{NO_VERSION, NO_VERSION, "mbsp", "generic BSP", {}, &gamedef_generic}; const bspversion_t bspver_q1{BSPVERSION, NO_VERSION, "bsp29", "Quake BSP", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp29_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp29_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp29_dclipnode_t)}, {"leafs", sizeof(bsp29_dleaf_t)}, {"marksurfaces", sizeof(uint16_t)}, {"edges", sizeof(bsp29_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelq1_t)}, }, &gamedef_q1, &bspver_bsp2}; const bspversion_t bspver_bsp2{BSP2VERSION, NO_VERSION, "bsp2", "Quake BSP2", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp2_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp2_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp2_dclipnode_t)}, {"leafs", sizeof(bsp2_dleaf_t)}, {"marksurfaces", sizeof(uint32_t)}, {"edges", sizeof(bsp2_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelq1_t)}, }, &gamedef_q1}; const bspversion_t bspver_bsp2rmq{BSP2RMQVERSION, NO_VERSION, "bsp2rmq", "Quake BSP2-RMQ", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp2rmq_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp2_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp2_dclipnode_t)}, {"leafs", sizeof(bsp2rmq_dleaf_t)}, {"marksurfaces", sizeof(uint32_t)}, {"edges", sizeof(bsp2_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelq1_t)}, }, &gamedef_q1}; /* Hexen II doesn't use a separate version, but we can still use a separate tag/name for it */ const bspversion_t bspver_h2{BSPVERSION, NO_VERSION, "hexen2", "Hexen II BSP", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp29_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp29_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp29_dclipnode_t)}, {"leafs", sizeof(bsp29_dleaf_t)}, {"marksurfaces", sizeof(uint16_t)}, {"edges", sizeof(bsp29_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelh2_t)}, }, &gamedef_h2, &bspver_h2bsp2}; const bspversion_t bspver_h2bsp2{BSP2VERSION, NO_VERSION, "hexen2bsp2", "Hexen II BSP2", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp2_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp2_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp2_dclipnode_t)}, {"leafs", sizeof(bsp2_dleaf_t)}, {"marksurfaces", sizeof(uint32_t)}, {"edges", sizeof(bsp2_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelh2_t)}, }, &gamedef_h2}; const bspversion_t bspver_h2bsp2rmq{BSP2RMQVERSION, NO_VERSION, "hexen2bsp2rmq", "Hexen II BSP2-RMQ", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"texture", sizeof(uint8_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(bsp2rmq_dnode_t)}, {"texinfos", sizeof(texinfo_t)}, {"faces", sizeof(bsp2_dface_t)}, {"lighting", sizeof(uint8_t)}, {"clipnodes", sizeof(bsp2_dclipnode_t)}, {"leafs", sizeof(bsp2rmq_dleaf_t)}, {"marksurfaces", sizeof(uint32_t)}, {"edges", sizeof(bsp2_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(dmodelh2_t)}, }, &gamedef_h2}; const bspversion_t bspver_hl{BSPHLVERSION, NO_VERSION, "hl", "Half-Life BSP", bspver_q1.lumps, &gamedef_hl}; const bspversion_t bspver_q2{Q2_BSPIDENT, Q2_BSPVERSION, "q2bsp", "Quake II BSP", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(q2_dnode_t)}, {"texinfos", sizeof(q2_texinfo_t)}, {"faces", sizeof(q2_dface_t)}, {"lighting", sizeof(uint8_t)}, {"leafs", sizeof(q2_dleaf_t)}, {"leaffaces", sizeof(uint16_t)}, {"leafbrushes", sizeof(uint16_t)}, {"edges", sizeof(bsp29_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(q2_dmodel_t)}, {"brushes", sizeof(dbrush_t)}, {"brushsides", sizeof(q2_dbrushside_t)}, {"pop", sizeof(uint8_t)}, {"areas", sizeof(darea_t)}, {"areaportals", sizeof(dareaportal_t)}, }, &gamedef_q2, &bspver_qbism}; const bspversion_t bspver_qbism{Q2_QBISMIDENT, Q2_BSPVERSION, "qbism", "Quake II Qbism BSP", { {"entities", sizeof(char)}, {"planes", sizeof(dplane_t)}, {"vertexes", sizeof(qvec3f)}, {"visibility", sizeof(uint8_t)}, {"nodes", sizeof(q2_dnode_qbism_t)}, {"texinfos", sizeof(q2_texinfo_t)}, {"faces", sizeof(q2_dface_qbism_t)}, {"lighting", sizeof(uint8_t)}, {"leafs", sizeof(q2_dleaf_qbism_t)}, {"leaffaces", sizeof(uint32_t)}, {"leafbrushes", sizeof(uint32_t)}, {"edges", sizeof(bsp2_dedge_t)}, {"surfedges", sizeof(int32_t)}, {"models", sizeof(q2_dmodel_t)}, {"brushes", sizeof(dbrush_t)}, {"brushsides", sizeof(q2_dbrushside_qbism_t)}, {"pop", sizeof(uint8_t)}, {"areas", sizeof(darea_t)}, {"areaportals", sizeof(dareaportal_t)}, }, &gamedef_q2}; bool surfflags_t::is_valid(const gamedef_t *game) const { return game->surfflags_are_valid(*this); } bool contentflags_t::equals(const gamedef_t *game, const contentflags_t &other) const { return game->contents_are_equal(*this, other) && mirror_inside == other.mirror_inside && clips_same_type == other.clips_same_type; } bool contentflags_t::types_equal(const contentflags_t &other, const gamedef_t *game) const { 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); } bool contentflags_t::is_detail_solid(const gamedef_t *game) const { return game->contents_are_detail_solid(*this); } bool contentflags_t::is_detail_fence(const gamedef_t *game) const { return game->contents_are_detail_fence(*this); } bool contentflags_t::is_detail_illusionary(const gamedef_t *game) const { return game->contents_are_detail_illusionary(*this); } bool contentflags_t::is_mirrored(const gamedef_t *game) const { return game->contents_are_mirrored(*this); } bool contentflags_t::will_clip_same_type(const gamedef_t *game, const contentflags_t &other) const { return game->contents_clip_same_type(*this, other); } bool contentflags_t::is_empty(const gamedef_t *game) const { return game->contents_are_empty(*this); } bool contentflags_t::is_solid(const gamedef_t *game) const { return game->contents_are_solid(*this); } bool contentflags_t::is_sky(const gamedef_t *game) const { return game->contents_are_sky(*this); } bool contentflags_t::is_liquid(const gamedef_t *game) const { return game->contents_are_liquid(*this); } bool contentflags_t::is_valid(const gamedef_t *game, bool strict) const { return game->contents_are_valid(*this, strict); } bool contentflags_t::is_clip(const gamedef_t *game) const { return game->contents_are_clip(*this); } bool contentflags_t::is_origin(const gamedef_t *game) const { return game->contents_are_origin(*this); } std::string contentflags_t::to_string(const gamedef_t *game) const { std::string s = game->get_contents_display(*this); // FIXME: how do we conditionally display this only when it matters (when it's not default basically)? s += fmt::format("| MIRROR_INSIDE[{}]", mirror_inside.has_value() ? (clips_same_type.value() ? "true" : "false") : "nullopt"); s += fmt::format("| CLIPS_SAME_TYPE[{}]", clips_same_type.has_value() ? (clips_same_type.value() ? "true" : "false") : "nullopt"); if (illusionary_visblocker) { s += "| ILLUSIONARY_VISBLOCKER"; } return s; } static bool BSPVersionSupported(int32_t ident, int32_t version, const bspversion_t **out_version) { for (const bspversion_t *bspver : bspversions) { if (bspver->ident == ident && bspver->version == version) { if (bspver->game->id == GAME_HEXEN_II) { // HACK: don't detect as Hexen II here, it's done later (isHexen2). // Since the Hexen II bspversion_t's have the same ident/version as Quake // we need to assume Quake. continue; } *out_version = bspver; return true; } } return false; } /* * ========================================================================= * BSP Format Conversion * ========================================================================= */ // move structured data if the input and output // are of the same type template inline void CopyArray(T &in, T &out) { out = in; } // convert structured data if we're different types template>> inline void CopyArray(std::vector &from, std::vector &to) { to.reserve(from.size()); for (auto &v : from) { if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) to.emplace_back(numeric_cast(v)); else to.emplace_back(v); } } // move structured data if the input and output // are of the same type template inline void CopyArray(F &in, T &out) { out = in; } // convert structured data if we're different types // with numeric casting for arrays template>> inline void CopyArray(std::vector> &from, std::vector> &to) { to.reserve(from.size()); for (auto &v : from) { if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) to.emplace_back(array_cast>(v)); else to.emplace_back(v); } } // Convert from a Q1-esque format to Generic template inline void ConvertQ1BSPToGeneric(T &bsp, mbsp_t &mbsp) { CopyArray(bsp.dentdata, mbsp.dentdata); CopyArray(bsp.dplanes, mbsp.dplanes); if (std::holds_alternative(bsp.dtex)) { CopyArray(std::get(bsp.dtex), mbsp.dtex); } else { CopyArray(std::get(bsp.dtex), mbsp.dtex); } CopyArray(bsp.dvertexes, mbsp.dvertexes); CopyArray(bsp.dvisdata, mbsp.dvis.bits); CopyArray(bsp.dnodes, mbsp.dnodes); CopyArray(bsp.texinfo, mbsp.texinfo); CopyArray(bsp.dfaces, mbsp.dfaces); CopyArray(bsp.dlightdata, mbsp.dlightdata); CopyArray(bsp.dclipnodes, mbsp.dclipnodes); CopyArray(bsp.dleafs, mbsp.dleafs); CopyArray(bsp.dmarksurfaces, mbsp.dleaffaces); CopyArray(bsp.dedges, mbsp.dedges); CopyArray(bsp.dsurfedges, mbsp.dsurfedges); if (std::holds_alternative(bsp.dmodels)) { CopyArray(std::get(bsp.dmodels), mbsp.dmodels); } else { CopyArray(std::get(bsp.dmodels), mbsp.dmodels); } } // Convert from a Q2-esque format to Generic template inline void ConvertQ2BSPToGeneric(T &bsp, mbsp_t &mbsp) { CopyArray(bsp.dentdata, mbsp.dentdata); CopyArray(bsp.dplanes, mbsp.dplanes); CopyArray(bsp.dvertexes, mbsp.dvertexes); CopyArray(bsp.dvis, mbsp.dvis); CopyArray(bsp.dnodes, mbsp.dnodes); CopyArray(bsp.texinfo, mbsp.texinfo); CopyArray(bsp.dfaces, mbsp.dfaces); CopyArray(bsp.dlightdata, mbsp.dlightdata); CopyArray(bsp.dleafs, mbsp.dleafs); CopyArray(bsp.dleaffaces, mbsp.dleaffaces); CopyArray(bsp.dleafbrushes, mbsp.dleafbrushes); CopyArray(bsp.dedges, mbsp.dedges); CopyArray(bsp.dsurfedges, mbsp.dsurfedges); CopyArray(bsp.dmodels, mbsp.dmodels); CopyArray(bsp.dbrushes, mbsp.dbrushes); CopyArray(bsp.dbrushsides, mbsp.dbrushsides); CopyArray(bsp.dareas, mbsp.dareas); CopyArray(bsp.dareaportals, mbsp.dareaportals); } // Convert from a Q1-esque format to Generic template inline T ConvertGenericToQ1BSP(mbsp_t &mbsp, const bspversion_t *to_version) { T bsp{}; // copy or convert data CopyArray(mbsp.dentdata, bsp.dentdata); CopyArray(mbsp.dplanes, bsp.dplanes); if (to_version->game->id == GAME_HALF_LIFE) { CopyArray(mbsp.dtex, bsp.dtex.template emplace()); } else { CopyArray(mbsp.dtex, bsp.dtex.template emplace()); } CopyArray(mbsp.dvertexes, bsp.dvertexes); CopyArray(mbsp.dvis.bits, bsp.dvisdata); CopyArray(mbsp.dnodes, bsp.dnodes); CopyArray(mbsp.texinfo, bsp.texinfo); CopyArray(mbsp.dfaces, bsp.dfaces); CopyArray(mbsp.dlightdata, bsp.dlightdata); CopyArray(mbsp.dclipnodes, bsp.dclipnodes); CopyArray(mbsp.dleafs, bsp.dleafs); CopyArray(mbsp.dleaffaces, bsp.dmarksurfaces); CopyArray(mbsp.dedges, bsp.dedges); CopyArray(mbsp.dsurfedges, bsp.dsurfedges); if (to_version->game->id == GAME_HEXEN_II) { CopyArray(mbsp.dmodels, bsp.dmodels.template emplace()); } else { CopyArray(mbsp.dmodels, bsp.dmodels.template emplace()); } return bsp; } // Convert from a Q2-esque format to Generic template inline T ConvertGenericToQ2BSP(mbsp_t &mbsp, const bspversion_t *to_version) { T bsp{}; // copy or convert data CopyArray(mbsp.dentdata, bsp.dentdata); CopyArray(mbsp.dplanes, bsp.dplanes); CopyArray(mbsp.dvertexes, bsp.dvertexes); CopyArray(mbsp.dvis, bsp.dvis); CopyArray(mbsp.dnodes, bsp.dnodes); CopyArray(mbsp.texinfo, bsp.texinfo); CopyArray(mbsp.dfaces, bsp.dfaces); CopyArray(mbsp.dlightdata, bsp.dlightdata); CopyArray(mbsp.dleafs, bsp.dleafs); CopyArray(mbsp.dleaffaces, bsp.dleaffaces); CopyArray(mbsp.dleafbrushes, bsp.dleafbrushes); CopyArray(mbsp.dedges, bsp.dedges); CopyArray(mbsp.dsurfedges, bsp.dsurfedges); CopyArray(mbsp.dmodels, bsp.dmodels); CopyArray(mbsp.dbrushes, bsp.dbrushes); CopyArray(mbsp.dbrushsides, bsp.dbrushsides); CopyArray(mbsp.dareas, bsp.dareas); CopyArray(mbsp.dareaportals, bsp.dareaportals); return bsp; } /* * ========================================================================= * ConvertBSPFormat * - BSP is assumed to be in CPU byte order already * - No checks are done here (yet) for overflow of values when down-converting * ========================================================================= */ bool ConvertBSPFormat(bspdata_t *bspdata, const bspversion_t *to_version) { if (bspdata->version == to_version) return true; if (to_version == &bspver_generic) { // Conversions to bspver_generic mbsp_t mbsp{}; if (std::holds_alternative(bspdata->bsp)) { ConvertQ1BSPToGeneric(std::get(bspdata->bsp), mbsp); } else if (std::holds_alternative(bspdata->bsp)) { ConvertQ2BSPToGeneric(std::get(bspdata->bsp), mbsp); } else if (std::holds_alternative(bspdata->bsp)) { ConvertQ2BSPToGeneric(std::get(bspdata->bsp), mbsp); } else if (std::holds_alternative(bspdata->bsp)) { ConvertQ1BSPToGeneric(std::get(bspdata->bsp), mbsp); } else if (std::holds_alternative(bspdata->bsp)) { ConvertQ1BSPToGeneric(std::get(bspdata->bsp), mbsp); } else { return false; } bspdata->loadversion = mbsp.loadversion = bspdata->version; bspdata->version = to_version; bspdata->bsp = std::move(mbsp); return true; } else if (bspdata->version == &bspver_generic) { // Conversions from bspver_generic mbsp_t &mbsp = std::get(bspdata->bsp); try { if (to_version == &bspver_q1 || to_version == &bspver_h2 || to_version == &bspver_hl) { bspdata->bsp = ConvertGenericToQ1BSP(mbsp, to_version); } else if (to_version == &bspver_q2) { bspdata->bsp = ConvertGenericToQ2BSP(mbsp, to_version); } else if (to_version == &bspver_qbism) { bspdata->bsp = ConvertGenericToQ2BSP(mbsp, to_version); } else if (to_version == &bspver_bsp2rmq || to_version == &bspver_h2bsp2rmq) { bspdata->bsp = ConvertGenericToQ1BSP(mbsp, to_version); } else if (to_version == &bspver_bsp2 || to_version == &bspver_h2bsp2) { bspdata->bsp = ConvertGenericToQ1BSP(mbsp, to_version); } else { return false; } } catch (std::overflow_error e) { logging::print("LIMITS EXCEEDED ON {}\n", e.what()); return false; } bspdata->version = to_version; return true; } return false; } static bool isHexen2(const dheader_t *header) { /* the world should always have some face. however, if the sizes are wrong then we're actually reading headnode[6]. hexen2 only used 5 hulls, so this should be 0 in hexen2, and not in quake. */ const dmodelq1_t *modelsq1 = (const dmodelq1_t *)((const uint8_t *)header + header->lumps[LUMP_MODELS].fileofs); return !modelsq1->numfaces; } struct lump_reader { std::istream &s; const bspversion_t *version; const std::vector &lumps; // read structured lump data from stream into vector template void read(size_t lump_num, std::vector &buffer) { Q_assert(version->lumps.size() > lump_num); const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; const lump_t &lump = lumps[lump_num]; size_t length; Q_assert(!buffer.size()); if (lumpspec.size > 1) { if (sizeof(T) != lumpspec.size) FError("odd {} value size ({} != {})", lumpspec.name, sizeof(T), lumpspec.size); else if (lump.filelen % lumpspec.size) FError("odd {} lump size ({} not multiple of {})", lumpspec.name, lump.filelen, lumpspec.size); buffer.reserve(length = (lump.filelen / lumpspec.size)); } else { buffer.resize(length = lump.filelen); } if (!lump.filelen) return; s.seekg(lump.fileofs); if (lumpspec.size > 1) { for (size_t i = 0; i < length; i++) { T &val = buffer.emplace_back(); s >= val; } } else { s.read(reinterpret_cast(buffer.data()), length); } } // read string from stream void read(size_t lump_num, std::string &buffer) { Q_assert(version->lumps.size() > lump_num); const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; const lump_t &lump = lumps[lump_num]; Q_assert(lumpspec.size == 1); Q_assert(!buffer.size()); buffer.resize(lump.filelen); if (!lump.filelen) return; s.seekg(lump.fileofs); s.read(reinterpret_cast(buffer.data()), lump.filelen); // the last byte is required to be '\0' which was added when the .bsp was // written. chop it off now, since we want the std::string to // be the logical string (we'll add the null terminator again when saving the .bsp) if (buffer[lump.filelen - 1] == 0) { buffer.resize(lump.filelen - 1); } // TODO: warn about bad .bsp if missing \0? } // read structured lump data from stream into struct template>> void read(size_t lump_num, T &buffer) { Q_assert(version->lumps.size() > lump_num); const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; const lump_t &lump = lumps[lump_num]; if (!lump.filelen) return; Q_assert(lumpspec.size == 1); s.seekg(lump.fileofs); buffer.stream_read(s, lump); } }; template inline void ReadQ1BSP(lump_reader &reader, T &bsp) { reader.read(LUMP_ENTITIES, bsp.dentdata); reader.read(LUMP_PLANES, bsp.dplanes); if (reader.version->game->id == GAME_HALF_LIFE) { reader.read(LUMP_TEXTURES, bsp.dtex.template emplace()); } else { reader.read(LUMP_TEXTURES, bsp.dtex.template emplace()); } reader.read(LUMP_VERTEXES, bsp.dvertexes); reader.read(LUMP_VISIBILITY, bsp.dvisdata); reader.read(LUMP_NODES, bsp.dnodes); reader.read(LUMP_TEXINFO, bsp.texinfo); reader.read(LUMP_FACES, bsp.dfaces); reader.read(LUMP_LIGHTING, bsp.dlightdata); reader.read(LUMP_CLIPNODES, bsp.dclipnodes); reader.read(LUMP_LEAFS, bsp.dleafs); reader.read(LUMP_MARKSURFACES, bsp.dmarksurfaces); reader.read(LUMP_EDGES, bsp.dedges); reader.read(LUMP_SURFEDGES, bsp.dsurfedges); if (reader.version->game->id == GAME_HEXEN_II) { reader.read(LUMP_MODELS, bsp.dmodels.template emplace()); } else { reader.read(LUMP_MODELS, bsp.dmodels.template emplace()); } } template inline void ReadQ2BSP(lump_reader &reader, T &bsp) { reader.read(Q2_LUMP_ENTITIES, bsp.dentdata); reader.read(Q2_LUMP_PLANES, bsp.dplanes); reader.read(Q2_LUMP_VERTEXES, bsp.dvertexes); reader.read(Q2_LUMP_VISIBILITY, bsp.dvis); reader.read(Q2_LUMP_NODES, bsp.dnodes); reader.read(Q2_LUMP_TEXINFO, bsp.texinfo); reader.read(Q2_LUMP_FACES, bsp.dfaces); reader.read(Q2_LUMP_LIGHTING, bsp.dlightdata); reader.read(Q2_LUMP_LEAFS, bsp.dleafs); reader.read(Q2_LUMP_LEAFFACES, bsp.dleaffaces); reader.read(Q2_LUMP_LEAFBRUSHES, bsp.dleafbrushes); reader.read(Q2_LUMP_EDGES, bsp.dedges); reader.read(Q2_LUMP_SURFEDGES, bsp.dsurfedges); reader.read(Q2_LUMP_MODELS, bsp.dmodels); reader.read(Q2_LUMP_BRUSHES, bsp.dbrushes); reader.read(Q2_LUMP_BRUSHSIDES, bsp.dbrushsides); reader.read(Q2_LUMP_AREAS, bsp.dareas); reader.read(Q2_LUMP_AREAPORTALS, bsp.dareaportals); } /* * ============= * LoadBSPFile * ============= */ void LoadBSPFile(fs::path &filename, bspdata_t *bspdata) { int i; uint32_t bspxofs; const bspx_header_t *bspx; logging::funcprint("'{}'\n", filename); /* load the file header */ fs::data file_data = fs::load(filename); if (!file_data) { FError("Unable to load \"{}\"\n", filename); } filename = fs::resolveArchivePath(filename); memstream stream(file_data->data(), file_data->size()); stream >> endianness; /* transfer the header data to this */ std::vector lumps; /* check for IBSP */ bspversion_t temp_version{}; stream >= temp_version.ident; stream.seekg(0); if (temp_version.ident == Q2_BSPIDENT || temp_version.ident == Q2_QBISMIDENT) { q2_dheader_t q2header; stream >= q2header; temp_version.version = q2header.version; std::copy(q2header.lumps.begin(), q2header.lumps.end(), std::back_inserter(lumps)); } else { dheader_t q1header; stream >= q1header; temp_version.version = NO_VERSION; std::copy(q1header.lumps.begin(), q1header.lumps.end(), std::back_inserter(lumps)); } /* check the file version */ if (!BSPVersionSupported(temp_version.ident, temp_version.version, &bspdata->version)) { logging::print("BSP is version {}\n", temp_version); Error("Sorry, this bsp version is not supported."); } else { // special case handling for Hexen II if (bspdata->version->game->id == GAME_QUAKE && isHexen2((dheader_t *)file_data->data())) { if (bspdata->version == &bspver_q1) { bspdata->version = &bspver_h2; } else if (bspdata->version == &bspver_bsp2) { bspdata->version = &bspver_h2bsp2; } else if (bspdata->version == &bspver_bsp2rmq) { bspdata->version = &bspver_h2bsp2rmq; } } logging::print("BSP is version {}\n", *bspdata->version); } lump_reader reader{stream, bspdata->version, lumps}; /* copy the data */ if (bspdata->version == &bspver_q2) { ReadQ2BSP(reader, bspdata->bsp.emplace()); } else if (bspdata->version == &bspver_qbism) { ReadQ2BSP(reader, bspdata->bsp.emplace()); } else if (bspdata->version == &bspver_q1 || bspdata->version == &bspver_h2 || bspdata->version == &bspver_hl) { ReadQ1BSP(reader, bspdata->bsp.emplace()); } else if (bspdata->version == &bspver_bsp2rmq || bspdata->version == &bspver_h2bsp2rmq) { ReadQ1BSP(reader, bspdata->bsp.emplace()); } else if (bspdata->version == &bspver_bsp2 || bspdata->version == &bspver_h2bsp2) { ReadQ1BSP(reader, bspdata->bsp.emplace()); } else { FError("Unknown format"); } // detect BSPX /*bspx header is positioned exactly+4align at the end of the last lump position (regardless of order)*/ for (i = 0, bspxofs = 0; i < lumps.size(); i++) { if (bspxofs < lumps[i].fileofs + lumps[i].filelen) bspxofs = lumps[i].fileofs + lumps[i].filelen; } bspxofs = (bspxofs + 3) & ~3; /*okay, so that's where it *should* be if it exists */ if (bspxofs + sizeof(*bspx) <= file_data->size()) { int xlumps; const bspx_lump_t *xlump; bspx = (const bspx_header_t *)((const uint8_t *)file_data->data() + bspxofs); xlump = (const bspx_lump_t *)(bspx + 1); xlumps = LittleLong(bspx->numlumps); if (!memcmp(&bspx->id, "BSPX", 4) && xlumps >= 0 && bspxofs + sizeof(*bspx) + sizeof(*xlump) * xlumps <= file_data->size()) { /*header seems valid so far. just add the lumps as we normally would if we were generating them, ensuring * that they get written out anew*/ while (xlumps-- > 0) { uint32_t ofs = LittleLong(xlump[xlumps].fileofs); uint32_t len = LittleLong(xlump[xlumps].filelen); uint8_t *lumpdata = new uint8_t[len]; memcpy(lumpdata, (const uint8_t *)file_data->data() + ofs, len); bspdata->bspx.transfer(xlump[xlumps].lumpname.data(), lumpdata, len); } } else { if (memcmp(&bspx->id, "BSPX", 4)) printf("invalid bspx header\n"); } } } /* ========================================================================= */ #include struct bspfile_t { const bspversion_t *version; // which one is used depends on version union { dheader_t q1header; q2_dheader_t q2header; }; std::ofstream stream; private: // write structured lump data from vector template inline void write_lump(size_t lump_num, const std::vector &data) { static constexpr char pad[4]{}; Q_assert(version->lumps.size() > lump_num); const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; lump_t *lumps; if (version->version != NO_VERSION) { lumps = q2header.lumps.data(); } else { lumps = q1header.lumps.data(); } lump_t &lump = lumps[lump_num]; lump.fileofs = stream.tellp(); for (auto &v : data) stream <= v; auto written = static_cast(stream.tellp()) - lump.fileofs; if (sizeof(T) == 1 || lumpspec.size > 1) Q_assert(written == (lumpspec.size * data.size())); lump.filelen = written; if (written % 4) stream.write(pad, 4 - (written % 4)); } // this is only here to satisfy std::visit constexpr void write_lump(size_t, const std::monostate &) { } // write structured string data inline void write_lump(size_t lump_num, const std::string &data) { Q_assert(version->lumps.size() > lump_num); static constexpr char pad[4]{}; const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; lump_t *lumps; Q_assert(lumpspec.size == 1); if (version->version != NO_VERSION) { lumps = q2header.lumps.data(); } else { lumps = q1header.lumps.data(); } lump_t &lump = lumps[lump_num]; lump.fileofs = stream.tellp(); stream.write(data.c_str(), data.size() + 1); // null terminator auto written = static_cast(stream.tellp()) - lump.fileofs; Q_assert(written == data.size() + 1); lump.filelen = written; if (written % 4) stream.write(pad, 4 - (written % 4)); } // write structured lump data template>> inline void write_lump(size_t lump_num, const T &data) { static constexpr char pad[4]{}; const lumpspec_t &lumpspec = version->lumps.begin()[lump_num]; lump_t *lumps; Q_assert(lumpspec.size == 1); if (version->version != NO_VERSION) { lumps = q2header.lumps.data(); } else { lumps = q1header.lumps.data(); } lump_t &lump = lumps[lump_num]; lump.fileofs = stream.tellp(); data.stream_write(stream); auto written = static_cast(stream.tellp()) - lump.fileofs; lump.filelen = written; if (written % 4) stream.write(pad, 4 - (written % 4)); } public: inline void write_bsp(const mbsp_t &) { FError("Can't write generic BSP"); } inline void write_bsp(const std::monostate &) { FError("No BSP to write"); } template, int> = 0> inline void write_bsp(const T &bsp) { write_lump(LUMP_PLANES, bsp.dplanes); write_lump(LUMP_LEAFS, bsp.dleafs); write_lump(LUMP_VERTEXES, bsp.dvertexes); write_lump(LUMP_NODES, bsp.dnodes); write_lump(LUMP_TEXINFO, bsp.texinfo); write_lump(LUMP_FACES, bsp.dfaces); write_lump(LUMP_CLIPNODES, bsp.dclipnodes); write_lump(LUMP_MARKSURFACES, bsp.dmarksurfaces); write_lump(LUMP_SURFEDGES, bsp.dsurfedges); write_lump(LUMP_EDGES, bsp.dedges); std::visit([this](auto &&arg) { this->write_lump(LUMP_MODELS, arg); }, bsp.dmodels); write_lump(LUMP_LIGHTING, bsp.dlightdata); write_lump(LUMP_VISIBILITY, bsp.dvisdata); write_lump(LUMP_ENTITIES, bsp.dentdata); std::visit([this](auto &&arg) { this->write_lump(LUMP_TEXTURES, arg); }, bsp.dtex); } template, int> = 0> inline void write_bsp(const T &bsp) { write_lump(Q2_LUMP_PLANES, bsp.dplanes); write_lump(Q2_LUMP_LEAFS, bsp.dleafs); write_lump(Q2_LUMP_VERTEXES, bsp.dvertexes); write_lump(Q2_LUMP_NODES, bsp.dnodes); write_lump(Q2_LUMP_TEXINFO, bsp.texinfo); write_lump(Q2_LUMP_FACES, bsp.dfaces); write_lump(Q2_LUMP_LEAFFACES, bsp.dleaffaces); write_lump(Q2_LUMP_SURFEDGES, bsp.dsurfedges); write_lump(Q2_LUMP_EDGES, bsp.dedges); write_lump(Q2_LUMP_MODELS, bsp.dmodels); write_lump(Q2_LUMP_LEAFBRUSHES, bsp.dleafbrushes); write_lump(Q2_LUMP_BRUSHES, bsp.dbrushes); write_lump(Q2_LUMP_BRUSHSIDES, bsp.dbrushsides); write_lump(Q2_LUMP_AREAS, bsp.dareas); write_lump(Q2_LUMP_AREAPORTALS, bsp.dareaportals); write_lump(Q2_LUMP_LIGHTING, bsp.dlightdata); write_lump(Q2_LUMP_VISIBILITY, bsp.dvis); write_lump(Q2_LUMP_ENTITIES, bsp.dentdata); } inline void write_bspx(const bspdata_t &bspdata) { if (!bspdata.bspx.entries.size()) return; if (stream.tellp() & 3) FError("BSPX header is misaligned"); stream <= bspx_header_t(bspdata.bspx.entries.size()); auto bspxheader = stream.tellp(); // write dummy lump headers for ([[maybe_unused]] auto &_ : bspdata.bspx.entries) { stream <= bspx_lump_t{}; } std::vector xlumps; xlumps.reserve(bspdata.bspx.entries.size()); for (auto &x : bspdata.bspx.entries) { static constexpr char pad[4]{}; bspx_lump_t &lump = xlumps.emplace_back(); lump.filelen = x.second.lumpsize; lump.fileofs = stream.tellp(); memcpy(lump.lumpname.data(), x.first.c_str(), std::min(x.first.size(), lump.lumpname.size() - 1)); stream.write(reinterpret_cast(x.second.lumpdata.get()), x.second.lumpsize); if (x.second.lumpsize % 4) stream.write(pad, 4 - (x.second.lumpsize % 4)); } stream.seekp(bspxheader); for (auto &lump : xlumps) stream <= lump; } }; /* * ============= * WriteBSPFile * Swaps the bsp file in place, so it should not be referenced again * ============= */ void WriteBSPFile(const fs::path &filename, bspdata_t *bspdata) { bspfile_t bspfile{}; bspfile.version = bspdata->version; // headers are union'd, so this sets both bspfile.q2header.ident = bspfile.version->ident; if (bspfile.version->version != NO_VERSION) { bspfile.q2header.version = bspfile.version->version; } logging::print("Writing {} as BSP version {}\n", filename, *bspdata->version); bspfile.stream.open(filename, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary); if (!bspfile.stream) FError("unable to open {} for writing", filename); bspfile.stream << endianness; /* Save header space, updated after adding the lumps */ if (bspfile.version->version != NO_VERSION) { bspfile.stream <= bspfile.q2header; } else { bspfile.stream <= bspfile.q1header; } std::visit([&bspfile](auto &&arg) { bspfile.write_bsp(arg); }, bspdata->bsp); /*BSPX lumps are at a 4-byte alignment after the last of any official lump*/ bspfile.write_bspx(*bspdata); bspfile.stream.seekp(0); // write the real header if (bspfile.version->version != NO_VERSION) { bspfile.stream <= bspfile.q2header; } else { bspfile.stream <= bspfile.q1header; } } /* ========================================================================= */ inline void PrintLumpSize(const lumpspec_t &lump, size_t count) { logging::print("{:7} {:<12} {:10}\n", count, lump.name, count * lump.size); } template inline void PrintQ1BSPLumps(const std::initializer_list &lumpspec, const T &bsp) { if (std::holds_alternative(bsp.dmodels)) logging::print("{:7} {:<12}\n", std::get(bsp.dmodels).size(), "models"); else logging::print("{:7} {:<12}\n", std::get(bsp.dmodels).size(), "models"); PrintLumpSize(lumpspec.begin()[LUMP_PLANES], bsp.dplanes.size()); PrintLumpSize(lumpspec.begin()[LUMP_VERTEXES], bsp.dvertexes.size()); PrintLumpSize(lumpspec.begin()[LUMP_NODES], bsp.dnodes.size()); PrintLumpSize(lumpspec.begin()[LUMP_TEXINFO], bsp.texinfo.size()); PrintLumpSize(lumpspec.begin()[LUMP_FACES], bsp.dfaces.size()); PrintLumpSize(lumpspec.begin()[LUMP_CLIPNODES], bsp.dclipnodes.size()); PrintLumpSize(lumpspec.begin()[LUMP_LEAFS], bsp.dleafs.size()); PrintLumpSize(lumpspec.begin()[LUMP_MARKSURFACES], bsp.dmarksurfaces.size()); PrintLumpSize(lumpspec.begin()[LUMP_EDGES], bsp.dedges.size()); PrintLumpSize(lumpspec.begin()[LUMP_SURFEDGES], bsp.dsurfedges.size()); if (std::holds_alternative(bsp.dtex)) logging::print("{:7} {:<12} {:10}\n", "", "textures", std::get(bsp.dtex).textures.size()); else logging::print("{:7} {:<12} {:10}\n", "", "textures", std::get(bsp.dtex).textures.size()); logging::print("{:7} {:<12} {:10}\n", "", "lightdata", bsp.dlightdata.size()); logging::print("{:7} {:<12} {:10}\n", "", "visdata", bsp.dvisdata.size()); logging::print("{:7} {:<12} {:10}\n", "", "entdata", bsp.dentdata.size() + 1); // include the null terminator } template inline void PrintQ2BSPLumps(const std::initializer_list &lumpspec, const T &bsp) { logging::print("{:7} {:<12}\n", bsp.dmodels.size(), "models"); PrintLumpSize(lumpspec.begin()[Q2_LUMP_PLANES], bsp.dplanes.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_VERTEXES], bsp.dvertexes.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_NODES], bsp.dnodes.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_TEXINFO], bsp.texinfo.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_FACES], bsp.dfaces.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_LEAFS], bsp.dleafs.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_LEAFFACES], bsp.dleaffaces.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_LEAFBRUSHES], bsp.dleafbrushes.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_EDGES], bsp.dedges.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_SURFEDGES], bsp.dsurfedges.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_BRUSHES], bsp.dbrushes.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_BRUSHSIDES], bsp.dbrushsides.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_AREAS], bsp.dareas.size()); PrintLumpSize(lumpspec.begin()[Q2_LUMP_AREAPORTALS], bsp.dareaportals.size()); logging::print("{:7} {:<12} {:10}\n", "", "lightdata", bsp.dlightdata.size()); logging::print("{:7} {:<12} {:10}\n", "", "visdata", bsp.dvis.bits.size()); logging::print("{:7} {:<12} {:10}\n", "", "entdata", bsp.dentdata.size() + 1); // include the null terminator } /* * ============= * PrintBSPFileSizes * Dumps info about the bsp data * ============= */ void PrintBSPFileSizes(const bspdata_t *bspdata) { const auto &lumpspec = bspdata->version->lumps; if (std::holds_alternative(bspdata->bsp)) { PrintQ2BSPLumps(lumpspec, std::get(bspdata->bsp)); } else if (std::holds_alternative(bspdata->bsp)) { PrintQ2BSPLumps(lumpspec, std::get(bspdata->bsp)); } else if (std::holds_alternative(bspdata->bsp)) { PrintQ1BSPLumps(lumpspec, std::get(bspdata->bsp)); } else if (std::holds_alternative(bspdata->bsp)) { PrintQ1BSPLumps(lumpspec, std::get(bspdata->bsp)); } else if (std::holds_alternative(bspdata->bsp)) { PrintQ1BSPLumps(lumpspec, std::get(bspdata->bsp)); } else { Error("Unsupported BSP version: {}", *bspdata->version); } for (auto &x : bspdata->bspx.entries) { logging::print("{:7} {:<12} {:10}\n", "BSPX", x.first, x.second.lumpsize); } }