/* Copyright (C) 1996-1997 Id Software, Inc. Copyright (C) 1997 Greg Lewis Copyright (C) 1999-2005 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 #include #include #include #include #include #include #include #include #include #include #include #include #include mapdata_t map; mapplane_t::mapplane_t(const qbsp_plane_t ©) : qbsp_plane_t(copy) { } struct planehash_t { // planes indices (into the `planes` vector) pareto::spatial_map hash; }; struct vertexhash_t { // hashed vertices; generated by EmitVertices pareto::spatial_map hash; }; mapdata_t::mapdata_t() : plane_hash(std::make_unique()), hashverts(std::make_unique()) { } // add the specified plane to the list size_t mapdata_t::add_plane(const qplane3d &plane) { planes.emplace_back(plane); planes.emplace_back(-plane); size_t positive_index = planes.size() - 2; size_t negative_index = planes.size() - 1; auto &positive = planes[positive_index]; auto &negative = planes[negative_index]; size_t result; if (positive.get_normal()[static_cast(positive.get_type()) % 3] < 0.0) { std::swap(positive, negative); result = negative_index; } else { result = positive_index; } plane_hash->hash.emplace(pareto::point{positive.get_normal()[0], positive.get_normal()[1], positive.get_normal()[2], positive.get_dist()}, positive_index); plane_hash->hash.emplace(pareto::point{negative.get_normal()[0], negative.get_normal()[1], negative.get_normal()[2], negative.get_dist()}, negative_index); return result; } std::optional mapdata_t::find_plane_nonfatal(const qplane3d &plane) { constexpr double HALF_NORMAL_EPSILON = NORMAL_EPSILON * 0.5; constexpr double HALF_DIST_EPSILON = DIST_EPSILON * 0.5; if (auto it = plane_hash->hash.find_intersection( {plane.normal[0] - HALF_NORMAL_EPSILON, plane.normal[1] - HALF_NORMAL_EPSILON, plane.normal[2] - HALF_NORMAL_EPSILON, plane.dist - HALF_DIST_EPSILON}, {plane.normal[0] + HALF_NORMAL_EPSILON, plane.normal[1] + HALF_NORMAL_EPSILON, plane.normal[2] + HALF_NORMAL_EPSILON, plane.dist + HALF_DIST_EPSILON}); it != plane_hash->hash.end()) { return it->second; } return std::nullopt; } // find the specified plane in the list if it exists. throws // if not. size_t mapdata_t::find_plane(const qplane3d &plane) { if (auto index = find_plane_nonfatal(plane)) { return *index; } throw std::bad_function_call(); } // find the specified plane in the list if it exists, or // return a new one size_t mapdata_t::add_or_find_plane(const qplane3d &plane) { if (auto index = find_plane_nonfatal(plane)) { return *index; } return add_plane(plane); } const qbsp_plane_t &mapdata_t::get_plane(size_t pnum) { return planes[pnum]; } // find output index for specified already-output vector. std::optional mapdata_t::find_emitted_hash_vector(const qvec3d &vert) { constexpr double HALF_EPSILON = POINT_EQUAL_EPSILON * 0.5; if (auto it = hashverts->hash.find_intersection({vert[0] - HALF_EPSILON, vert[1] - HALF_EPSILON, vert[2] - HALF_EPSILON}, {vert[0] + HALF_EPSILON, vert[1] + HALF_EPSILON, vert[2] + HALF_EPSILON}); it != hashverts->hash.end()) { return it->second; } return std::nullopt; } // add vector to hash void mapdata_t::add_hash_vector(const qvec3d &point, const size_t &num) { hashverts->hash.emplace(pareto::point({point[0], point[1], point[2]}), num); } void mapdata_t::add_hash_edge(size_t v1, size_t v2, int64_t edge_index, const face_t *face) { hashedges.emplace(std::make_pair(v1, v2), hashedge_t{.v1 = v1, .v2 = v2, .edge_index = edge_index, .face = face}); } const std::optional &mapdata_t::load_image_meta(const std::string_view &name) { static std::optional nullmeta = std::nullopt; auto it = meta_cache.find(name.data()); if (it != meta_cache.end()) { return it->second; } // try a meta-only texture first; this is all we really need anyways if (auto [texture_meta, _0, _1] = img::load_texture_meta(name, qbsp_options.target_game, qbsp_options); texture_meta) { // slight special case: if the meta has no width/height defined, // pull it from the real texture. if (!texture_meta->width || !texture_meta->height) { auto [texture, _0, _1] = img::load_texture(name, true, qbsp_options.target_game, qbsp_options); if (texture) { texture_meta->width = texture->meta.width; texture_meta->height = texture->meta.height; } } if (!texture_meta->width || !texture_meta->height) { logging::print("WARNING: texture {} has empty width/height \n", name); } return meta_cache.emplace(name, texture_meta).first->second; } // couldn't find a meta texture, so pull it from the pixel image if (auto [texture, _0, _1] = img::load_texture(name, true, qbsp_options.target_game, qbsp_options); texture) { return meta_cache.emplace(name, texture->meta).first->second; } logging::print("WARNING: Couldn't locate texture for {}\n", name); meta_cache.emplace(name, std::nullopt); return nullmeta; } static std::shared_ptr LoadTexturePath(const fs::path &path) { // if absolute, don't try anything else if (path.is_absolute()) { return fs::addArchive(path, false); } // try wadpath (this includes relative to the .map file) for (auto &wadpath : qbsp_options.wadpaths.pathsValue()) { if (auto archive = fs::addArchive(wadpath.path / path, wadpath.external)) { return archive; } } // try relative to cwd if (auto archive = fs::addArchive(path, false)) { return archive; } return nullptr; } static void EnsureTexturesLoaded() { // Q2 doesn't need this if (qbsp_options.target_game->id == GAME_QUAKE_II) { return; } if (map.textures_loaded) return; map.textures_loaded = true; const mapentity_t &entity = map.world_entity(); std::string wadstring = entity.epairs.get("_wad"); if (wadstring.empty()) { wadstring = entity.epairs.get("wad"); } bool loaded_any_archive = false; if (wadstring.empty()) { logging::print("WARNING: No wad or _wad key exists in the worldmodel\n"); } else { imemstream stream(wadstring.data(), wadstring.size()); std::string wad; while (std::getline(stream, wad, ';')) { if (LoadTexturePath(wad)) { loaded_any_archive = true; } } } if (!loaded_any_archive) { if (!wadstring.empty()) { logging::print("WARNING: No valid WAD filenames in worldmodel\n"); } /* Try the default wad name */ fs::path defaultwad = qbsp_options.map_path; defaultwad.replace_extension("wad"); if (fs::exists(defaultwad)) { logging::print("INFO: Using default WAD: {}\n", defaultwad); LoadTexturePath(defaultwad); } } } // Useful shortcuts const std::string &mapdata_t::miptexTextureName(int mt) const { return miptex.at(mt).name; } const std::string &mapdata_t::texinfoTextureName(int texinfo) const { return miptexTextureName(mtexinfos.at(texinfo).miptex); } mapentity_t &mapdata_t::world_entity() { if (entities.empty()) { FError("no world entity"); } return entities.at(0); } bool mapdata_t::is_world_entity(const mapentity_t &entity) { return &entity == &world_entity(); } void mapdata_t::reset() { *this = mapdata_t{}; } /* ================ CalculateBrushBounds ================ */ inline void CalculateBrushBounds(mapbrush_t &ob) { ob.bounds = {}; for (size_t i = 0; i < ob.faces.size(); i++) { const auto &plane = ob.faces[i].get_plane(); std::optional w = BaseWindingForPlane(plane); for (size_t j = 0; j < ob.faces.size() && w; j++) { if (i == j) { continue; } if (ob.faces[j].bevel) { continue; } const auto &plane = map.get_plane(ob.faces[j].planenum ^ 1); w = w->clip_front(plane.get_plane(), 0); // CLIP_EPSILON); } if (w) { // calc bounds before moving from w for (auto &p : w.value()) { ob.bounds += p; } ob.faces[i].winding = std::move(w.value()); } } for (size_t i = 0; i < 3; i++) { if (ob.bounds.mins()[i] <= -qbsp_options.worldextent.value() || ob.bounds.maxs()[i] >= qbsp_options.worldextent.value()) { logging::print("WARNING: {}: brush bounds out of range\n", ob.line); } if (ob.bounds.mins()[i] >= qbsp_options.worldextent.value() || ob.bounds.maxs()[i] <= -qbsp_options.worldextent.value()) { logging::print("WARNING: {}: no visible sides on brush\n", ob.line); } } } static void AddAnimTex(const char *name) { int i, j, frame; char framename[16], basechar = '0'; frame = name[1]; if (frame >= 'a' && frame <= 'j') frame -= 'a' - 'A'; if (frame >= '0' && frame <= '9') { frame -= '0'; basechar = '0'; } else if (frame >= 'A' && frame <= 'J') { frame -= 'A'; basechar = 'A'; } if (frame < 0 || frame > 9) FError("Bad animating texture {}", name); /* * Always add the lower numbered animation frames first, otherwise * many Quake engines will exit with an error loading the bsp. */ snprintf(framename, sizeof(framename), "%s", name); for (i = 0; i < frame; i++) { framename[1] = basechar + i; for (j = 0; j < map.miptex.size(); j++) { if (!Q_strcasecmp(framename, map.miptex.at(j).name.c_str())) break; } if (j < map.miptex.size()) continue; map.miptex.push_back({framename}); } } int FindMiptex(const char *name, std::optional &extended_info, bool internal, bool recursive) { const char *pathsep; int i; // FIXME: figure out a way that we can move this to gamedef if (qbsp_options.target_game->id != GAME_QUAKE_II) { /* Ignore leading path in texture names (Q2 map compatibility) */ pathsep = strrchr(name, '/'); if (pathsep) name = pathsep + 1; if (!extended_info.has_value()) { extended_info = extended_texinfo_t{}; } for (i = 0; i < map.miptex.size(); i++) { const maptexdata_t &tex = map.miptex.at(i); if (!Q_strcasecmp(name, tex.name.c_str())) { return i; } } i = map.miptex.size(); map.miptex.push_back({name}); /* Handle animating textures carefully */ if (name[0] == '+') { AddAnimTex(name); } } else { // load .wal first auto wal = map.load_image_meta(name); if (wal && !internal && !extended_info.has_value()) { extended_info = extended_texinfo_t{wal->contents_native, wal->flags, wal->value, wal->animation}; } if (!extended_info.has_value()) { extended_info = extended_texinfo_t{}; } for (i = 0; i < map.miptex.size(); i++) { const maptexdata_t &tex = map.miptex.at(i); if (!Q_strcasecmp(name, tex.name.c_str()) && tex.flags.native == extended_info->flags.native && tex.value == extended_info->value && tex.animation == extended_info->animation) { return i; } } i = map.miptex.size(); map.miptex.push_back({name, extended_info->flags, extended_info->value, extended_info->animation}); /* Handle animating textures carefully */ if (!extended_info->animation.empty() && recursive && Q_strcasecmp(name, wal->animation.c_str())) { int last_i = i; // recursively load animated textures until we loop back to us while (true) { if (wal->animation.empty()) break; // wal for next chain wal = map.load_image_meta(wal->animation.c_str()); // can't find... if (wal == std::nullopt) break; // texinfo base for animated wal std::optional animation_info = extended_info; animation_info->animation = wal->animation; // fetch animation chain int next_i = FindMiptex(wal->name.data(), animation_info, internal, false); map.miptex[last_i].animation_miptex = next_i; // looped back if (!Q_strcasecmp(wal->name.c_str(), name) || last_i == next_i) break; last_i = next_i; } // link back to the start map.miptex[last_i].animation_miptex = i; } } return i; } static bool IsSkipName(const char *name) { if (qbsp_options.noskip.value()) return false; if (!Q_strcasecmp(name, "skip")) return true; if (!Q_strcasecmp(name, "*waterskip")) return true; if (!Q_strcasecmp(name, "*slimeskip")) return true; if (!Q_strcasecmp(name, "*lavaskip")) return true; if (!Q_strcasecmp(name, "bevel")) // zhlt compat return true; if (!Q_strcasecmp(name, "null")) // zhlt compat return true; if (!Q_strcasecmp(name, "__TB_empty")) return true; return false; } static bool IsNoExpandName(const char *name) { if (!Q_strcasecmp(name, "bevel")) // zhlt compat return true; return false; } /** * "Special" refers to TEX_SPECIAL, which means "non-lightmapped" and * therefore non-subdivided. */ static bool IsSpecialName(const char *name, bool allow_litwater) { if (name[0] == '*' && !allow_litwater) return true; if (!Q_strncasecmp(name, "sky", 3) && !qbsp_options.splitsky.value()) return true; return false; } static bool IsHintName(const char *name) { if (!Q_strcasecmp(name, "hint")) return true; if (!Q_strcasecmp(name, "hintskip")) return true; return false; } /* =============== FindTexinfo Returns a global texinfo number =============== */ int FindTexinfo(const maptexinfo_t &texinfo, const qplane3d &plane, bool add) { // NaN's will break mtexinfo_lookup, since they're being used as a std::map key and don't compare properly with <. // They should have been stripped out already in ValidateTextureProjection. for (int i = 0; i < 2; i++) { for (int j = 0; j < 4; j++) { Q_assert(!std::isnan(texinfo.vecs.at(i, j))); } } // check for an exact match in the reverse lookup const auto it = map.mtexinfo_lookup.find(texinfo); if (it != map.mtexinfo_lookup.end()) { return it->second; } if (!add) { return -1; } /* Allocate a new texinfo at the end of the array */ const int num_texinfo = static_cast(map.mtexinfos.size()); map.mtexinfos.emplace_back(texinfo); map.mtexinfo_lookup[texinfo] = num_texinfo; // catch broken < implementations in maptexinfo_t assert(map.mtexinfo_lookup.find(texinfo) != map.mtexinfo_lookup.end()); // create a copy of the miptex for animation chains if (map.miptex[texinfo.miptex].animation_miptex.has_value()) { maptexinfo_t anim_next = texinfo; #if 0 brush_side_t temp; temp.plane = plane; temp.set_texinfo(texdef_quake_ed_t{ { 0, 0 }, 0, { 1, 1 }}); anim_next.vecs = temp.vecs; #endif anim_next.miptex = map.miptex[texinfo.miptex].animation_miptex.value(); map.mtexinfos[num_texinfo].next = FindTexinfo(anim_next, plane); } return num_texinfo; } int FindMiptex(const char *name, bool internal, bool recursive) { std::optional extended_info; return FindMiptex(name, extended_info, internal, recursive); } static surfflags_t SurfFlagsForEntity( const maptexinfo_t &texinfo, const mapentity_t &entity, const contentflags_t &face_contents) { surfflags_t flags{}; const char *texname = map.miptex.at(texinfo.miptex).name.c_str(); const int shadow = entity.epairs.get_int("_shadow"); bool is_translucent = false; // lit water: use worldspawn key by default, but allow overriding with bmodel keys // TODO: use a setting_container for these things, rather than custom parsing // TODO: support lit water opt-out in Q2 mode bool allow_litwater = false; if (entity.epairs.has("_litwater")) { allow_litwater = (entity.epairs.get_int("_litwater") > 0); } else if (entity.epairs.has("_splitturb")) { allow_litwater = (entity.epairs.get_int("_splitturb") > 0); } else { allow_litwater = qbsp_options.splitturb.value(); } // These flags are pulled from surf flags in Q2. // TODO: the Q1 version of this block can now be moved into texinfo // loading by shoving them inside of texinfo.flags like // Q2 does. Similarly, we can move the Q2 block out // into a special function, like.. I dunno, // game->surface_flags_from_name(surfflags_t &inout, const char *name) // which we can just call instead of this block. // the only annoyance is we can't access the various options (noskip, // splitturb, etc) from there. if (qbsp_options.target_game->id != GAME_QUAKE_II) { if (IsSkipName(texname)) flags.is_nodraw = true; if (IsHintName(texname)) flags.is_hint = true; if (IsSpecialName(texname, allow_litwater)) flags.native |= TEX_SPECIAL; } else { flags.native = texinfo.flags.native; if ((flags.native & Q2_SURF_NODRAW) || IsSkipName(texname)) flags.is_nodraw = true; if ((flags.native & Q2_SURF_HINT) || IsHintName(texname)) flags.is_hint = true; if ((flags.native & Q2_SURF_TRANS33) || (flags.native & Q2_SURF_TRANS66)) is_translucent = true; } if (IsNoExpandName(texname)) flags.no_expand = true; if (entity.epairs.get_int("_dirt") == -1) flags.no_dirt = true; if (entity.epairs.get_int("_bounce") == -1) flags.no_bounce = true; if (entity.epairs.get_int("_minlight") == -1) flags.no_minlight = true; if (entity.epairs.get_int("_lightignore") == 1) flags.light_ignore = true; if (entity.epairs.has("_surflight_rescale")) { flags.surflight_rescale = entity.epairs.get_int("_surflight_rescale") == 1; } { qvec3f color; // FIXME: get_color, to match settings if (entity.epairs.has("_surflight_color") && entity.epairs.get_vector("_surflight_color", color) == 3) { if (color[0] <= 1 && color[1] <= 1 && color[2] <= 1) { flags.surflight_color = qvec3b{(uint8_t)(color[0] * 255), (uint8_t)(color[1] * 255), (uint8_t)(color[2] * 255)}; } else { flags.surflight_color = qvec3b{(uint8_t)(color[0]), (uint8_t)(color[1]), (uint8_t)(color[2])}; } } } if (entity.epairs.has("_surflight_style") && entity.epairs.get_int("_surflight_style") != 0) flags.surflight_style = entity.epairs.get_int("_surflight_style"); if (entity.epairs.has("_surflight_targetname")) flags.surflight_targetname = entity.epairs.get("_surflight_targetname"); if (entity.epairs.has("_surflight_minlight_scale")) flags.surflight_minlight_scale = entity.epairs.get_float("_surflight_minlight_scale"); // Paril: inherit _surflight_minlight_scale from worldspawn if unset else if (!entity.epairs.has("_surflight_minlight_scale") && map.world_entity().epairs.has("_surflight_minlight_scale")) flags.surflight_minlight_scale = map.world_entity().epairs.get_float("_surflight_minlight_scale"); // "_minlight_exclude", "_minlight_exclude2", "_minlight_exclude3"... for (int i = 0; i <= 9; i++) { std::string key = "_minlight_exclude"; if (i > 0) { key += std::to_string(i); } const std::string &excludeTex = entity.epairs.get(key.c_str()); if (!excludeTex.empty() && !Q_strcasecmp(texname, excludeTex)) { flags.no_minlight = true; } } if (shadow == -1) flags.no_shadow = true; if (!Q_strcasecmp("func_detail_illusionary", entity.epairs.get("classname"))) { /* Mark these entities as TEX_NOSHADOW unless the mapper set "_shadow" "1" */ if (shadow != 1) { flags.no_shadow = true; } } if (face_contents.is_liquid(qbsp_options.target_game) && !is_translucent) { // opaque liquids don't cast shadow unless opted in if (shadow != 1) { flags.no_shadow = true; } } // handle "_phong" and "_phong_angle" and "_phong_angle_concave" double phongangle = entity.epairs.get_float("_phong_angle"); int phong = entity.epairs.get_int("_phong"); // Paril: inherit phong from worldspawn if unset if (!entity.epairs.has("_phong") && map.world_entity().epairs.has("_phong")) { phong = map.world_entity().epairs.get_int("_phong"); } // Paril: inherit phong from worldspawn if unset if (!entity.epairs.has("_phong_angle") && map.world_entity().epairs.has("_phong_angle")) { phongangle = map.world_entity().epairs.get_float("_phong_angle"); } if (phong && (phongangle == 0.0)) { phongangle = 89.0; // default _phong_angle } if (phongangle) { flags.phong_angle = std::clamp(phongangle, 0.0, 360.0); } const double phong_angle_concave = entity.epairs.get_float("_phong_angle_concave"); flags.phong_angle_concave = std::clamp(phong_angle_concave, 0.0, 360.0); flags.phong_group = entity.epairs.get_int("_phong_group"); // handle "_minlight" if (entity.epairs.has("_minlight")) { const double minlight = entity.epairs.get_float("_minlight"); // handle -1 as an alias for 0 (same with other negative values). flags.minlight = std::max(0., minlight); } // handle "_maxlight" const double maxlight = entity.epairs.get_float("_maxlight"); if (maxlight > 0) { // CHECK: allow > 510 now that we're float? or is it not worth it since it will // be beyond max? flags.maxlight = std::clamp(maxlight, 0.0, 510.0); } // handle "_lightcolorscale" if (entity.epairs.has("_lightcolorscale")) { const double lightcolorscale = entity.epairs.get_float("_lightcolorscale"); if (lightcolorscale != 1.0) { flags.lightcolorscale = std::clamp(lightcolorscale, 0.0, 1.0); } } if (entity.epairs.has("_surflight_group")) { const int32_t surflight_group = entity.epairs.get_int("_surflight_group"); if (surflight_group) { flags.surflight_group = surflight_group; } } if (entity.epairs.has("_world_units_per_luxel")) { flags.world_units_per_luxel = entity.epairs.get_float("_world_units_per_luxel"); } if (entity.epairs.has("_object_channel_mask")) { flags.object_channel_mask = entity.epairs.get_int("_object_channel_mask"); } // handle "_mincolor" { qvec3f mincolor{}; entity.epairs.get_vector("_mincolor", mincolor); if (qv::epsilonEmpty(mincolor, (float) QBSP_EQUAL_EPSILON)) { entity.epairs.get_vector("_minlight_color", mincolor); } mincolor = qv::normalize_color_format(mincolor); if (!qv::epsilonEmpty(mincolor, (float) QBSP_EQUAL_EPSILON)) { for (int32_t i = 0; i < 3; i++) { flags.minlight_color[i] = std::clamp(mincolor[i], 0.0f, 255.0f); } } } // handle "_light_alpha" if (entity.epairs.has("_light_alpha")) { const double lightalpha = entity.epairs.get_float("_light_alpha"); flags.light_alpha = std::clamp(lightalpha, 0.0, 1.0); } // handle "_light_twosided" if (entity.epairs.has("_light_twosided")) { flags.light_twosided = entity.epairs.get_int("_light_twosided"); } return flags; } static void ParseTextureDef(const mapentity_t &entity, const mapfile::brush_side_t &input_side, mapface_t &mapface, const mapbrush_t &brush, maptexinfo_t *tx, std::array &planepts, const qplane3d &plane, texture_def_issues_t &issue_stats) { quark_tx_info_t extinfo; mapface.texname = input_side.texture; // copy in Q2 attributes if present if (input_side.extended_info) { extinfo.info = {extended_texinfo_t{}}; extinfo.info->contents_native = input_side.extended_info->contents; extinfo.info->flags = input_side.extended_info->flags; extinfo.info->value = input_side.extended_info->value; mapface.raw_info = extinfo.info; } // if we have texture defs, see if we should remap this one if (auto it = qbsp_options.loaded_texture_defs.find(mapface.texname); it != qbsp_options.loaded_texture_defs.end()) { mapface.texname = std::get<0>(it->second); if (std::get<1>(it->second).has_value()) { mapface.raw_info = extinfo.info = std::get<1>(it->second).value(); } } // If we're not Q2 but we're loading a Q2 map, just remove the extra // info so it can at least compile. if (qbsp_options.target_game->id != GAME_QUAKE_II) { extinfo.info = std::nullopt; } else { // assign animation to extinfo, so that we load the animated // first one first if (auto &wal = map.load_image_meta(mapface.texname.c_str())) { if (!extinfo.info) { extinfo.info = extended_texinfo_t{wal->contents_native, wal->flags, wal->value}; } extinfo.info->animation = wal->animation; } else if (!extinfo.info) { extinfo.info = extended_texinfo_t{}; } if (extinfo.info->contents_native & Q2_CONTENTS_TRANSLUCENT) { // remove TRANSLUCENT; it's only meant to be set by the compiler extinfo.info->contents_native &= ~Q2_CONTENTS_TRANSLUCENT; // but give us detail if we lack trans. this is likely what they intended if (!(extinfo.info->flags.native & (Q2_SURF_TRANS33 | Q2_SURF_TRANS66))) { extinfo.info->contents_native |= Q2_CONTENTS_DETAIL; if (qbsp_options.verbose.value()) { logging::print("WARNING: {}: swapped TRANSLUCENT for DETAIL\n", mapface.line); } else { issue_stats.num_translucent++; } } } // This fixes a bug in some old maps. if ((extinfo.info->flags.native & (Q2_SURF_SKY | Q2_SURF_NODRAW)) == (Q2_SURF_SKY | Q2_SURF_NODRAW)) { extinfo.info->flags.native &= ~Q2_SURF_NODRAW; if (qbsp_options.verbose.value()) { logging::print("WARNING: {}: SKY | NODRAW mixed. Removing NODRAW.\n", mapface.line); } else { issue_stats.num_sky_nodraw++; } } // Mixing visible contents on the input brush is illegal { const int32_t visible_contents = extinfo.info->contents_native & Q2_ALL_VISIBLE_CONTENTS; // TODO: Move to bspfile.hh API for (int32_t i = Q2_CONTENTS_SOLID; i <= Q2_LAST_VISIBLE_CONTENTS; i <<= 1) { if (visible_contents & i) { if (visible_contents != i) { FError("{}: Mixed visible contents: {}", mapface.line, qbsp_options.target_game->create_contents_from_native(extinfo.info->contents_native) .to_string(qbsp_options.target_game)); } } } } // Other Q2 hard errors if (extinfo.info->contents_native & (Q2_CONTENTS_MONSTER | Q2_CONTENTS_DEADMONSTER)) { FError( "{}: Illegal contents: {}", mapface.line, qbsp_options.target_game->create_contents_from_native( extinfo.info->contents_native).to_string(qbsp_options.target_game)); } // If Q2 style phong is enabled on a mirrored face, `light` will erroneously try to blend normals between // the front and back faces leading to light artifacts. const bool wants_phong = !(extinfo.info->flags.native & Q2_SURF_LIGHT) && (extinfo.info->value != 0); // Technically this is not the 100% correct check for mirrored, but we don't have the full brush // contents set up at this point. Correct would be to call `portal_generates_face()`. bool mirrored = (extinfo.info->contents_native != 0) && !(extinfo.info->contents_native & (Q2_CONTENTS_DETAIL | Q2_CONTENTS_SOLID | Q2_CONTENTS_WINDOW | Q2_CONTENTS_AUX)); if (entity.epairs.has("_mirrorinside") && !entity.epairs.get_int("_mirrorinside")) { mirrored = false; } if (wants_phong && mirrored) { logging::print("WARNING: {}: Q2 phong (value set, LIGHT unset) used on a mirrored face.\n", mapface.line); } } tx->miptex = FindMiptex(mapface.texname.c_str(), extinfo.info); if (extinfo.info->contents_native != 0) mapface.contents = qbsp_options.target_game->create_contents_from_native(extinfo.info->contents_native); else mapface.contents = contentflags_t::make(EWT_VISCONTENTS_EMPTY); tx->flags = {extinfo.info->flags}; tx->value = extinfo.info->value; if (!mapface.contents.is_valid(qbsp_options.target_game, false)) { auto old_contents = mapface.contents; qbsp_options.target_game->contents_make_valid(mapface.contents); logging::print("WARNING: {}: face has invalid contents {}, remapped to {}\n", mapface.line, old_contents.to_string(qbsp_options.target_game), mapface.contents.to_string(qbsp_options.target_game)); } tx->vecs = input_side.vecs; } bool mapface_t::set_planepts(const std::array &pts) { planepts = pts; /* calculate the normal/dist plane equation */ qvec3d ab = planepts[0] - planepts[1]; qvec3d cb = planepts[2] - planepts[1]; double length; qvec3d normal = qv::normalize(qv::cross(ab, cb), length); double dist = qv::dot(planepts[1], normal); planenum = map.add_or_find_plane({normal, dist}); return length >= NORMAL_EPSILON; } const maptexinfo_t &mapface_t::get_texinfo() const { return map.mtexinfos.at(this->texinfo); } const texvecf &mapface_t::get_texvecs() const { return get_texinfo().vecs; } void mapface_t::set_texvecs(const texvecf &vecs) { // start with a copy of the current texinfo structure maptexinfo_t texInfoNew = get_texinfo(); texInfoNew.outputnum = std::nullopt; texInfoNew.vecs = vecs; this->texinfo = FindTexinfo(texInfoNew, this->get_plane()); } const qbsp_plane_t &mapface_t::get_plane() const { return map.get_plane(planenum); } const qbsp_plane_t &mapface_t::get_positive_plane() const { return map.get_plane(planenum & ~1); } static std::optional ParseBrushFace( const mapfile::brush_side_t &input_side, const mapbrush_t &brush, const mapentity_t &entity, texture_def_issues_t &issue_stats) { maptexinfo_t tx; mapface_t face; face.line = input_side.location; const bool normal_ok = face.set_planepts(input_side.planepts); ParseTextureDef(entity, input_side, face, brush, &tx, face.planepts, face.get_plane(), issue_stats); if (!normal_ok) { logging::print("WARNING: {}: Brush plane with no normal\n", input_side.location); return std::nullopt; } tx.flags = SurfFlagsForEntity(tx, entity, face.contents); // to save on texinfo, reset all invisible sides to default texvecs if (tx.flags.is_nodraw || tx.flags.is_hintskip || tx.flags.is_hint) { mapfile::brush_side_t temp; temp.plane = face.get_plane(); temp.set_texinfo(mapfile::texdef_quake_ed_t{ { 0, 0 }, 0, { 1, 1 }}); tx.vecs = temp.vecs; } face.texinfo = FindTexinfo(tx, face.get_plane()); return face; } #define QBSP3 #ifdef QBSP3 /* ================= AddBrushBevels Adds any additional planes necessary to allow the brush to be expanded against axial bounding boxes ================= */ inline void AddBrushBevels(mapentity_t &e, mapbrush_t &b) { // // add the axial planes // int32_t order = 0; for (int32_t axis = 0; axis < 3; axis++) { for (int32_t dir = -1; dir <= 1; dir += 2, order++) { // see if the plane is already present int32_t i; for (i = 0; i < b.faces.size(); i++) { auto &s = b.faces[i]; if (map.get_plane(s.planenum).get_normal()[axis] == dir) { break; } } if (i == b.faces.size()) { // add a new side mapface_t &s = b.faces.emplace_back(); qplane3d plane{}; plane.normal[axis] = dir; if (dir == 1) { plane.dist = b.bounds.maxs()[axis]; } else { plane.dist = -b.bounds.mins()[axis]; } s.planenum = map.add_or_find_plane(plane); // FIXME: use the face closest to the new bevel for picking // its surface info to copy from. s.texinfo = b.faces[0].texinfo; s.contents = b.faces[0].contents; s.texname = b.faces[0].texname; s.bevel = true; e.numboxbevels++; } // if the plane is not in it canonical order, swap it if (i != order) { std::swap(b.faces[order], b.faces[i]); } } } // // add the edge bevels // if (b.faces.size() == 6) { return; // pure axial } // test the non-axial plane edges // note: no references to b.faces[...] stored since this modifies // the vector. for (size_t i = 6; i < b.faces.size(); i++) { if (!b.faces[i].winding) { continue; } for (size_t j = 0; j < b.faces[i].winding.size(); j++) { size_t k = (j + 1) % b.faces[i].winding.size(); qvec3d vec = b.faces[i].winding[j] - b.faces[i].winding[k]; if (qv::normalizeInPlace(vec) < 0.5) { continue; } vec = qv::Snap(vec); for (k = 0; k < 3; k++) { if (vec[k] == -1 || vec[k] == 1) { break; // axial } } if (k != 3) { continue; // only test non-axial edges } // try the six possible slanted axials from this edge for (size_t axis = 0; axis < 3; axis++) { for (int32_t dir = -1; dir <= 1; dir += 2) { // construct a plane qplane3d plane{}; plane.normal[axis] = dir; plane.normal = qv::cross(vec, plane.normal); // If this edge is almost parallel to the hull edge, skip it double sin_of_angle = qv::normalizeInPlace(plane.normal); if (sin_of_angle < ANGLEEPSILON) { continue; } plane.dist = qv::dot(b.faces[i].winding[j], plane.normal); // if all the points on all the sides are // behind this plane, it is a proper edge bevel for (k = 0; k < b.faces.size(); k++) { // if this plane has allready been used, skip it if (qv::epsilonEqual(b.faces[k].get_plane(), plane)) { break; } auto &w2 = b.faces[k].winding; if (!w2) { continue; } size_t l = 0; for (; l < w2.size(); l++) { double d = qv::dot(w2[l], plane.normal) - plane.dist; if (d > qbsp_options.epsilon.value()) { break; // point in front } } if (l != w2.size()) { break; } } if (k != b.faces.size()) { continue; // wasn't part of the outer hull } // add this plane mapface_t &s = b.faces.emplace_back(); s.planenum = map.add_or_find_plane(plane); s.texinfo = b.faces[i].texinfo; s.contents = b.faces[i].contents; s.texname = b.faces[i].texname; s.bevel = true; e.numedgebevels++; } } } } } #else /* ============================================================================== BEVELED CLIPPING HULL GENERATION This is done by brute force, and could easily get a lot faster if anyone cares. ============================================================================== */ struct map_hullbrush_t { mapentity_t &entity; mapbrush_t &brush; std::vector points; std::vector corners; std::vector> edges; }; /* ============ AddBrushPlane ============= */ static bool AddBrushPlane(map_hullbrush_t &hullbrush, const qbsp_plane_t &plane, size_t &index) { for (auto &s : hullbrush.brush.faces) { if (qv::epsilonEqual(s.get_plane(), plane)) { index = &s - hullbrush.brush.faces.data(); return false; } } index = hullbrush.brush.faces.size(); auto &s = hullbrush.brush.faces.emplace_back(); s.planenum = map.add_or_find_plane(plane); // add this plane s.texinfo = hullbrush.brush.faces[0].texinfo; s.contents = hullbrush.brush.faces[0].contents; // fixme: why did we need to store all this stuff again, isn't // it in texinfo? s.raw_info = hullbrush.brush.faces[0].raw_info; s.texname = hullbrush.brush.faces[0].texname; s.bevel = true; return true; } /* ============ TestAddPlane Adds the given plane to the brush description if all of the original brush vertexes can be put on the front side ============= */ static bool TestAddPlane(map_hullbrush_t &hullbrush, const qbsp_plane_t &plane) { /* see if the plane has already been added */ for (auto &s : hullbrush.brush.faces) { if (qv::epsilonEqual(plane, s.get_plane()) || qv::epsilonEqual(plane, s.get_positive_plane())) { return false; } } /* check all the corner points */ bool points_front = false; bool points_back = false; for (size_t i = 0; i < hullbrush.corners.size(); i++) { double d = qv::dot(hullbrush.corners[i], plane.get_normal()) - plane.get_dist(); if (d < -qbsp_options.epsilon.value()) { if (points_front) { return false; } points_back = true; } else if (d > qbsp_options.epsilon.value()) { if (points_back) { return false; } points_front = true; } } bool added; size_t index; // the plane is a seperator if (points_front) { added = AddBrushPlane(hullbrush, -plane, index); } else { added = AddBrushPlane(hullbrush, plane, index); } if (added) { hullbrush.entity.numedgebevels++; } return added; } /* ============ AddHullPoint Doesn't add if duplicated ============= */ static size_t AddHullPoint(map_hullbrush_t &hullbrush, const qvec3d &p, const aabb3d &hull_size) { for (auto &pt : hullbrush.points) { if (qv::epsilonEqual(p, pt, QBSP_EQUAL_EPSILON)) { return &pt - hullbrush.points.data(); } } hullbrush.points.emplace_back(p); for (size_t x = 0; x < 2; x++) { for (size_t y = 0; y < 2; y++) { for (size_t z = 0; z < 2; z++) { hullbrush.corners.emplace_back(p + qvec3d{hull_size[x][0], hull_size[y][1], hull_size[z][2]}); } } } return hullbrush.points.size() - 1; } /* ============ AddHullEdge Creates all of the hull planes around the given edge, if not done already ============= */ static bool AddHullEdge(map_hullbrush_t &hullbrush, const qvec3d &p1, const qvec3d &p2, const aabb3d &hull_size) { std::array edge = {AddHullPoint(hullbrush, p1, hull_size), AddHullPoint(hullbrush, p2, hull_size)}; for (auto &e : hullbrush.edges) { if (e == edge || e == decltype(edge){edge[1], edge[0]}) { return false; } } hullbrush.edges.emplace_back(edge); qvec3d edgevec = qv::normalize(p1 - p2); bool added = false; for (size_t a = 0; a < 3; a++) { qvec3d planevec{}; planevec[a] = 1; qplane3d plane; plane.normal = qv::cross(planevec, edgevec); double length = qv::normalizeInPlace(plane.normal); /* If this edge is almost parallel to the hull edge, skip it. */ if (length < ANGLEEPSILON) { continue; } size_t b = (a + 1) % 3; size_t c = (a + 2) % 3; for (size_t d = 0; d < 2; d++) { for (size_t e = 0; e < 2; e++) { qvec3d planeorg = p1; planeorg[b] += hull_size[d][b]; planeorg[c] += hull_size[e][c]; plane.dist = qv::dot(planeorg, plane.normal); added = TestAddPlane(hullbrush, plane) || added; } } } return added; } /* ============ ExpandBrush ============= */ static void ExpandBrush(map_hullbrush_t &hullbrush, const aabb3d &hull_size) { // create all the hull points for (auto &f : hullbrush.brush.faces) { for (auto &pt : f.winding) { AddHullPoint(hullbrush, pt, hull_size); } } // expand all of the planes for (auto &f : hullbrush.brush.faces) { /*if (f.get_texinfo().flags.no_expand) { continue; }*/ qvec3d corner = {}; qplane3d plane = f.get_plane(); for (size_t x = 0; x < 3; x++) { if (plane.normal[x] > 0) { corner[x] = hull_size[1][x]; } else if (plane.normal[x] < 0) { corner[x] = hull_size[0][x]; } } plane.dist += qv::dot(corner, plane.normal); f.planenum = map.add_or_find_plane(plane); } // add any axis planes not contained in the brush to bevel off corners for (size_t x = 0, o = 0; x < 3; x++) { for (int32_t s = -1; s <= 1; s += 2, o++) { // add the plane qplane3d plane; plane.normal = {}; plane.normal[x] = (double)s; if (s == -1) { plane.dist = -hullbrush.brush.bounds.mins()[x] + -hull_size[0][x]; } else { plane.dist = hullbrush.brush.bounds.maxs()[x] + hull_size[1][x]; } size_t index; AddBrushPlane(hullbrush, plane, index); // if the plane is not in it canonical order, swap it if (index != o) { std::swap(hullbrush.brush.faces[o], hullbrush.brush.faces[index]); } } } // add all of the edge bevels for (size_t f = 0; f < hullbrush.brush.faces.size(); f++) { auto *side = &hullbrush.brush.faces[f]; auto *w = &side->winding; for (size_t i = 0; i < w->size(); i++) { if (AddHullEdge(hullbrush, (*w)[i], (*w)[(i + 1) % w->size()], hull_size)) { // re-fetch ptrs side = &hullbrush.brush.faces[f]; w = &side->winding; } } } } #endif /* ================= Brush_GetContents Fetch the final contents flag of the given mapbrush. ================= */ static contentflags_t Brush_GetContents(const mapentity_t &entity, const mapbrush_t &mapbrush) { bool base_contents_set = false; contentflags_t base_contents = qbsp_options.target_game->create_empty_contents(); // validate that all of the sides have valid contents for (auto &mapface : mapbrush.faces) { const maptexinfo_t &texinfo = mapface.get_texinfo(); contentflags_t contents = qbsp_options.target_game->face_get_contents(mapface.texname.data(), texinfo.flags, mapface.contents); if (contents.is_empty(qbsp_options.target_game)) { continue; } // use the first non-empty as the base contents value if (!base_contents_set) { base_contents_set = true; base_contents = contents; } if (!contents.types_equal(base_contents, qbsp_options.target_game)) { logging::print("WARNING: {}: brush has multiple face contents ({} vs {}), the former will be used.\n", mapface.line, base_contents.to_string(qbsp_options.target_game), contents.to_string(qbsp_options.target_game)); break; } } // make sure we found a valid type Q_assert(base_contents.is_valid(qbsp_options.target_game, false)); // extended flags if (entity.epairs.has("_mirrorinside")) { base_contents.set_mirrored(entity.epairs.get_int("_mirrorinside") ? true : false); } else { // fixme-brushbsp: this shouldn't be necessary, but Q1's game contents // store these as booleans and not trinaries base_contents.set_mirrored(std::nullopt); } if (entity.epairs.has("_noclipfaces")) { base_contents.set_clips_same_type(entity.epairs.get_int("_noclipfaces") ? false : true); } else { // fixme-brushbsp: this shouldn't be necessary, but Q1's game contents // store these as booleans and not trinaries base_contents.set_clips_same_type(std::nullopt); } if (string_iequals(entity.epairs.get("classname"), "func_illusionary_visblocker")) { base_contents = contentflags_t::make(base_contents.flags | EWT_INVISCONTENTS_ILLUSIONARY_VISBLOCKER); } // non-Q2: -transwater implies liquids are detail if (qbsp_options.target_game->id != GAME_QUAKE_II && qbsp_options.transwater.value()) { if (base_contents.is_liquid(qbsp_options.target_game)) { base_contents = qbsp_options.target_game->set_detail(base_contents); } } return base_contents; } static mapbrush_t CloneBrush(const mapbrush_t &input, bool faces = false) { mapbrush_t brush; brush.contents = input.contents; brush.line = input.line; if (faces) { for (auto &face : input.faces) { auto &new_face = brush.faces.emplace_back(); new_face.contents = face.contents; new_face.line = face.line; new_face.planenum = face.planenum; new_face.planepts = face.planepts; new_face.raw_info = face.raw_info; new_face.texinfo = face.texinfo; new_face.texname = face.texname; } } return brush; } static mapbrush_t ParseBrush(const mapfile::brush_t &in, mapentity_t &entity, texture_def_issues_t &issue_stats) { mapbrush_t brush; brush.line = in.location; bool is_hint = false; for (const auto &in_face : in.faces) { std::optional face = ParseBrushFace(in_face, brush, entity, issue_stats); if (!face) { continue; } /* Check for duplicate planes */ bool discardFace = false; for (auto &check : brush.faces) { if (qv::epsilonEqual(check.get_plane(), face->get_plane())) { logging::print("{}: Brush with duplicate plane\n", in_face.location); discardFace = true; continue; } if (qv::epsilonEqual(-check.get_plane(), face->get_plane())) { /* FIXME - this is actually an invalid brush */ logging::print("{}: Brush with duplicate plane\n", in_face.location); continue; } } if (discardFace) { continue; } if (face->get_texinfo().flags.is_hint) { is_hint = true; } /* Save the face, update progress */ brush.faces.emplace_back(std::move(face.value())); } bool is_antiregion = !brush.faces.empty() && brush.faces[0].texname.ends_with("antiregion"), is_region = !is_antiregion && !brush.faces.empty() && brush.faces[0].texname.ends_with("region"); // check regionness if (is_antiregion) { for (auto &face : brush.faces) { if (!face.texname.ends_with("antiregion")) { is_antiregion = false; break; } } } if (is_region) { for (auto &face : brush.faces) { if (!face.texname.ends_with("region")) { is_region = false; break; } } } // check for region/antiregion brushes if (is_antiregion) { if (!map.is_world_entity(entity)) { FError("Region brush at {} isn't part of the world entity", in.location); } map.antiregions.push_back(CloneBrush(brush, true)); } else if (is_region) { if (!map.is_world_entity(entity)) { FError("Region brush at {} isn't part of the world entity", in.location); } // construct region brushes for (auto &new_brush_side : brush.faces) { // copy the brush mapbrush_t new_brush; new_brush.contents = brush.contents; new_brush.line = brush.line; for (auto &side : brush.faces) { // if it's the side we're extruding, increase its dist if (side.planenum == new_brush_side.planenum) { mapface_t new_side; new_side.texinfo = side.texinfo; new_side.contents = side.contents; new_side.raw_info = side.raw_info; new_side.texname = side.texname; new_side.planenum = side.planenum; new_side.planenum = map.add_or_find_plane( {new_side.get_plane().get_normal(), new_side.get_plane().get_dist() + 16.f}); new_brush.faces.emplace_back(std::move(new_side)); // the inverted side is special } else if (side.get_plane().get_normal() == -new_brush_side.get_plane().get_normal()) { // add the other side mapface_t flipped_side; flipped_side.texinfo = side.texinfo; flipped_side.contents = side.contents; flipped_side.raw_info = side.raw_info; flipped_side.texname = side.texname; flipped_side.planenum = map.add_or_find_plane( {-new_brush_side.get_plane().get_normal(), -new_brush_side.get_plane().get_dist()}); new_brush.faces.emplace_back(std::move(flipped_side)); } else { mapface_t new_side; new_side.texinfo = side.texinfo; new_side.contents = side.contents; new_side.raw_info = side.raw_info; new_side.texname = side.texname; new_side.planenum = side.planenum; new_brush.faces.emplace_back(std::move(new_side)); } } // add new_brush.contents = Brush_GetContents(entity, new_brush); map.world_entity().mapbrushes.push_back(std::move(new_brush)); } if (!map.region) { map.region = std::move(brush); } else { FError("Multiple region brushes detected; newest at {}", in.location); } return brush; } // mark hintskip faces if (is_hint) { for (auto &face : brush.faces) { if (qbsp_options.target_game->texinfo_is_hintskip( face.get_texinfo().flags, map.miptexTextureName(face.get_texinfo().miptex))) { auto copy = face.get_texinfo(); copy.flags.is_hintskip = true; face.texinfo = FindTexinfo(copy, face.get_plane()); } } } brush.contents = Brush_GetContents(entity, brush); return brush; } void ParseEntity(const mapfile::map_entity_t &in_entity, mapentity_t &entity, texture_def_issues_t &issue_stats) { entity.location = in_entity.location; entity.epairs = in_entity.epairs; // cache origin key if (in_entity.epairs.has("origin")) { in_entity.epairs.get_vector("origin", entity.origin); } // _omitbrushes 1 just discards all brushes in the entity. // could be useful for geometry guides, selective compilation, etc. bool omit = in_entity.epairs.get_int("_omitbrushes"); if (!omit) { for (const mapfile::brush_t &in_brush : in_entity.brushes) { // once we run into the first brush, set up textures state. EnsureTexturesLoaded(); if (auto brush = ParseBrush(in_brush, entity, issue_stats); brush.faces.size()) { entity.mapbrushes.push_back(std::move(brush)); } } } // replace aliases auto alias_it = qbsp_options.loaded_entity_defs.find(entity.epairs.get("classname")); if (alias_it != qbsp_options.loaded_entity_defs.end()) { for (auto &pair : alias_it->second) { if (pair.first == "classname" || !entity.epairs.has(pair.first)) { entity.epairs.set(pair.first, pair.second); } } } } static void ScaleMapFace(mapface_t &face, const qvec3d &scale) { const qmat3x3d scaleM{// column-major... scale[0], 0.0, 0.0, 0.0, scale[1], 0.0, 0.0, 0.0, scale[2]}; std::array new_planepts; for (int i = 0; i < 3; i++) { new_planepts[i] = scaleM * face.planepts[i]; } face.set_planepts(new_planepts); // update texinfo const qmat3x3d inversescaleM{// column-major... 1 / scale[0], 0.0, 0.0, 0.0, 1 / scale[1], 0.0, 0.0, 0.0, 1 / scale[2]}; const auto &texvecs = face.get_texvecs(); texvecf newtexvecs; for (int i = 0; i < 2; i++) { const qvec4f in = texvecs.row(i); const qvec3f in_first3(in); const qvec3f out_first3 = inversescaleM * in_first3; newtexvecs.set_row(i, {out_first3[0], out_first3[1], out_first3[2], in[3]}); } face.set_texvecs(newtexvecs); // update winding for (qvec3d &p : face.winding) { p = scaleM * p; } } static void RotateMapFace(mapface_t &face, const qvec3d &angles) { const double pitch = DEG2RAD(angles[0]); const double yaw = DEG2RAD(angles[1]); const double roll = DEG2RAD(angles[2]); qmat3x3d rotation = RotateAboutZ(yaw) * RotateAboutY(pitch) * RotateAboutX(roll); std::array new_planepts; for (int i = 0; i < 3; i++) { new_planepts[i] = rotation * face.planepts[i]; } face.set_planepts(new_planepts); // update texinfo const auto &texvecs = face.get_texvecs(); texvecf newtexvecs; for (int i = 0; i < 2; i++) { const qvec4f in = texvecs.row(i); const qvec3f in_first3(in); const qvec3f out_first3 = rotation * in_first3; newtexvecs.set_row(i, {out_first3[0], out_first3[1], out_first3[2], in[3]}); } face.set_texvecs(newtexvecs); } static void TranslateMapFace(mapface_t &face, const qvec3d &offset) { std::array new_planepts; for (int i = 0; i < 3; i++) { new_planepts[i] = face.planepts[i] + offset; } face.set_planepts(new_planepts); // update texinfo const auto &texvecs = face.get_texvecs(); texvecf newtexvecs; for (int i = 0; i < 2; i++) { qvec4f out = texvecs.row(i); // CHECK: precision loss here? out[3] += qv::dot(qvec3f(out), qvec3f(offset) * -1.0f); newtexvecs.set_row(i, {out[0], out[1], out[2], out[3]}); } face.set_texvecs(newtexvecs); } /** * Loads an external .map file. * * The loaded brushes/planes/etc. will be stored in the global mapdata_t. */ static mapentity_t LoadExternalMap(const std::string &filename) { mapentity_t dest{}; auto file = fs::load(filename); if (!file) { FError("Couldn't load external map file \"{}\".\n", filename); } auto in_map = mapfile::parse(std::string_view(reinterpret_cast(file->data()), file->size()), parser_source_location{filename}); texture_def_issues_t issue_stats; // parse the worldspawn ParseEntity(in_map.entities.at(0), dest, issue_stats); const std::string &classname = dest.epairs.get("classname"); if (Q_strcasecmp("worldspawn", classname)) { FError("'{}': Expected first entity to be worldspawn, got: '{}'\n", filename, classname); } // parse any subsequent entities, move any brushes to worldspawn for (size_t i = 1; i < in_map.entities.size(); ++i) { mapentity_t dummy{}; ParseEntity(in_map.entities[i], dummy, issue_stats); // move the brushes to the worldspawn dest.mapbrushes.insert(dest.mapbrushes.end(), std::make_move_iterator(dummy.mapbrushes.begin()), std::make_move_iterator(dummy.mapbrushes.end())); } if (!dest.mapbrushes.size()) { FError("Expected at least one brush for external map {}\n", filename); } logging::print( logging::flag::STAT, " {}: '{}': Loaded {} mapbrushes.\n", __func__, filename, dest.mapbrushes.size()); return dest; } void ProcessExternalMapEntity(mapentity_t &entity) { Q_assert(!qbsp_options.onlyents.value()); const std::string &classname = entity.epairs.get("classname"); if (Q_strcasecmp(classname, "misc_external_map")) { return; } const std::string &file = entity.epairs.get("_external_map"); const std::string &new_classname = entity.epairs.get("_external_map_classname"); // FIXME: throw specific error message instead? this might be confusing for mappers Q_assert(!file.empty()); Q_assert(!new_classname.empty()); Q_assert(entity.mapbrushes.empty()); // misc_external_map must be a point entity mapentity_t external_worldspawn = LoadExternalMap(file); // copy the brushes into the target entity.mapbrushes = std::move(external_worldspawn.mapbrushes); qvec3f origin; entity.epairs.get_vector("origin", origin); qvec3f angles; entity.epairs.get_vector("_external_map_angles", angles); if (qv::epsilonEmpty(angles, (float) QBSP_EQUAL_EPSILON)) { angles[1] = entity.epairs.get_float("_external_map_angle"); } qvec3f scale; int ncomps = entity.epairs.get_vector("_external_map_scale", scale); if (ncomps < 3) { if (scale[0] == 0.0) { scale = 1; } else { scale = scale[0]; } } for (auto &brush : entity.mapbrushes) { for (auto &face : brush.faces) { ScaleMapFace(face, scale); RotateMapFace(face, angles); TranslateMapFace(face, origin); } } entity.epairs.set("classname", new_classname); // FIXME: Should really just delete the origin key? entity.epairs.set("origin", "0 0 0"); } void ProcessAreaPortal(mapentity_t &entity) { Q_assert(!qbsp_options.onlyents.value()); const std::string &classname = entity.epairs.get("classname"); if (Q_strcasecmp(classname, "func_areaportal")) { return; } // areaportal entities move their brushes, but don't eliminate // the entity if (entity.mapbrushes.size() != 1) { FError("func_areaportal ({}) can only be a single brush", entity.location); } for (auto &brush : entity.mapbrushes) { brush.contents = contentflags_t::make(EWT_INVISCONTENTS_AREAPORTAL); for (auto &face : brush.faces) { face.contents = brush.contents; face.texinfo = map.skip_texinfo; } } if (map.antiregions.size() || map.region) { return; } entity.areaportalnum = ++map.numareaportals; // set the portal number as "style" entity.epairs.set("style", std::to_string(map.numareaportals)); } /* * Special world entities are entities which have their brushes added to the * world before being removed from the map. */ bool IsWorldBrushEntity(const mapentity_t &entity) { const std::string &classname = entity.epairs.get("classname"); /* These entities should have their classname remapped to the value of _external_map_classname before ever calling IsWorldBrushEntity */ Q_assert(Q_strcasecmp(classname, "misc_external_map")); if (!Q_strcasecmp(classname, "func_detail")) return true; if (!Q_strcasecmp(classname, "func_group")) return true; if (!Q_strcasecmp(classname, "func_detail_illusionary")) return true; if (!Q_strcasecmp(classname, "func_detail_wall")) return true; if (!Q_strcasecmp(classname, "func_detail_fence")) return true; if (!Q_strcasecmp(classname, "func_illusionary_visblocker")) return true; return false; } /** * Some games need special entities that are merged into the world, but not * removed from the map entirely. */ bool IsNonRemoveWorldBrushEntity(const mapentity_t &entity) { const std::string &classname = entity.epairs.get("classname"); if (!Q_strcasecmp(classname, "func_areaportal")) return true; return false; } inline bool MapBrush_IsHint(const mapbrush_t &brush) { for (auto &f : brush.faces) { if (f.get_texinfo().flags.is_hint) return true; } return false; } /* ================== WriteBspBrushMap from q3map ================== */ inline void WriteMapBrushMap(const fs::path &name, const std::vector &list, const aabb3d &hull) { logging::print("writing {}\n", name); std::ofstream f(name); if (!f) FError("Can't write {}", name); ewt::print(f, "{{\n\"classname\" \"worldspawn\"\n"); for (auto &brush : list) { ewt::print(f, "{{\n"); for (auto &face : brush.faces) { qvec3d corner = {}; qplane3d plane = face.get_plane(); for (size_t x = 0; x < 3; x++) { if (plane.normal[x] > 0) { corner[x] = hull[1][x]; } else if (plane.normal[x] < 0) { corner[x] = hull[0][x]; } } plane.dist += qv::dot(corner, plane.normal); winding_t w = BaseWindingForPlane(plane); ewt::print(f, "( {} ) ", w[0]); ewt::print(f, "( {} ) ", w[1]); ewt::print(f, "( {} ) ", w[2]); #if 0 if (face.visible) { ewt::print(f, "skip 0 0 0 1 1\n"); } else { ewt::print(f, "nonvisible 0 0 0 1 1\n"); } #endif ewt::print(f, "{} 0 0 0 1 1\n", face.texname); } ewt::print(f, "}}\n"); } ewt::print(f, "}}\n"); f.close(); } void ProcessMapBrushes() { logging::funcheader(); // load external maps (needs to be before world extents are calculated) for (auto &source : map.entities) { ProcessExternalMapEntity(source); } // calculate extents, if required if (!qbsp_options.worldextent.value()) { CalculateWorldExtent(); } map.total_brushes = 0; if (map.region) { CalculateBrushBounds(map.region.value()); logging::print("NOTE: map region detected! only compiling map within {}\n", map.region.value().bounds); } if (map.antiregions.size()) { logging::print("NOTE: map anti-regions detected! {} brush regions will be omitted\n", map.antiregions.size()); for (auto ®ion : map.antiregions) { CalculateBrushBounds(region); } } { logging::percent_clock clock(map.entities.size()); struct map_brushes_stats_t : logging::stat_tracker_t { stat &brushes = register_stat("brushes"); stat &utility_brushes = register_stat("utility brushes removed"); stat &offset_brushes = register_stat("brushes offset by origins"); stat &sides = register_stat("sides"); stat &bevels = register_stat("side bevels"); } stats; // calculate brush extents and brush bevels for (auto &entity : map.entities) { clock(); /* Origin brush support */ entity.rotation = rotation_t::none; /* entities with custom lmscales are important for the qbsp to know about */ int i = 16 * entity.epairs.get_float("_lmscale"); if (!i) { i = 16; // if 0, pick a suitable default } int lmshift = 0; while (i > 1) { lmshift++; // only allow power-of-two scales i /= 2; } mapentity_t *areaportal = nullptr; if (entity.epairs.get("classname") == "func_areaportal") { areaportal = &entity; } for (auto it = entity.mapbrushes.begin(); it != entity.mapbrushes.end();) { auto &brush = *it; // set properties calculated above brush.lmshift = lmshift; brush.func_areaportal = areaportal; brush.is_hint = MapBrush_IsHint(brush); // _chop signals that a brush does not partake in the BSP chopping phase. // this allows brushes embedded in others to be retained. if (entity.epairs.has("_chop") && !entity.epairs.get_int("_chop")) { brush.no_chop = true; } // brushes are sorted by their _chop_order; higher numbered brushes // will "eat" lower numbered brushes. This effectively overrides the // brush order of the map. if (entity.epairs.has("_chop_order")) { brush.chop_index = entity.epairs.get_int("_chop_order"); } // calculate brush bounds CalculateBrushBounds(brush); // origin brushes are removed, and the origin of the entity is overwritten // with its centroid. if (brush.contents.is_origin(qbsp_options.target_game)) { if (map.is_world_entity(entity)) { logging::print("WARNING: Ignoring origin brush in worldspawn\n"); } else if (entity.epairs.has("origin")) { // fixme-brushbsp: entity.line logging::print( "WARNING: Entity at {} has multiple origin brushes\n", entity.mapbrushes.front().line); } else { entity.origin = brush.bounds.centroid(); entity.epairs.set("origin", qv::to_string(entity.origin)); } stats.utility_brushes++; // this is kinda slow but since most origin brushes are in // small brush models this won't matter much in practice it = entity.mapbrushes.erase(it); entity.rotation = rotation_t::origin_brush; continue; } size_t old_num_faces = brush.faces.size(); stats.sides += old_num_faces; // add the brush bevels #ifdef QBSP3 AddBrushBevels(entity, brush); #else { map_hullbrush_t hullbrush{entity, brush}; ExpandBrush(hullbrush, {{0, 0, 0}, {0, 0, 0}}); } #endif for (auto &f : brush.faces) { f.lmshift = brush.lmshift; } stats.bevels += brush.faces.size() - old_num_faces; it++; } map.total_brushes += entity.mapbrushes.size(); stats.brushes += entity.mapbrushes.size(); /* Hipnotic rotation */ if (entity.rotation == rotation_t::none) { if (!Q_strncasecmp(entity.epairs.get("classname"), "rotate_", 7)) { entity.origin = FixRotateOrigin(entity); entity.rotation = rotation_t::hipnotic; } } // offset brush bounds if (entity.rotation != rotation_t::none) { for (auto &brush : entity.mapbrushes) { for (auto &f : brush.faces) { // account for texture offset, from txqbsp-xt if (!qbsp_options.oldrottex.value()) { maptexinfo_t texInfoNew = f.get_texinfo(); texInfoNew.outputnum = std::nullopt; texInfoNew.vecs.at(0, 3) += qv::dot(entity.origin, texInfoNew.vecs.row(0).xyz()); texInfoNew.vecs.at(1, 3) += qv::dot(entity.origin, texInfoNew.vecs.row(1).xyz()); f.texinfo = FindTexinfo(texInfoNew, f.get_plane()); } qplane3d plane = f.get_plane(); plane.dist -= qv::dot(plane.normal, entity.origin); f.planenum = map.add_or_find_plane(plane); } // re-calculate brush bounds/windings CalculateBrushBounds(brush); stats.offset_brushes++; } } // apply global scale if (qbsp_options.scale.value() != 1.0) { // scale brushes for (auto &brush : entity.mapbrushes) { for (auto &f : brush.faces) { ScaleMapFace(f, qvec3d(qbsp_options.scale.value())); } CalculateBrushBounds(brush); } // scale point entity origin if (entity.epairs.find("origin") != entity.epairs.end()) { qvec3f origin; if (entity.epairs.get_vector("origin", origin) == 3) { origin *= qbsp_options.scale.value(); entity.epairs.set("origin", qv::to_string(origin)); } } } // remove windings, we no longer need them if (!entity.epairs.get_int("_super_detail")) { for (auto &brush : entity.mapbrushes) { for (auto &f : brush.faces) { f.winding = {}; } } } } clock.print(); } logging::print(logging::flag::STAT, "\n"); // remove ents in region if (map.region || map.antiregions.size()) { for (auto it = map.entities.begin(); it != map.entities.end();) { auto &entity = *it; bool removed = false; if (!entity.mapbrushes.size()) { if (map.region && !map.region->bounds.containsPoint(entity.origin)) { it = map.entities.erase(it); removed = true; } for (auto ®ion : map.antiregions) { if (region.bounds.containsPoint(entity.origin)) { logging::print("removed {}\n", entity.epairs.get("classname")); it = map.entities.erase(it); removed = true; break; } } } if (!removed) { ++it; } } } if (qbsp_options.debugexpand.is_changed()) { aabb3d hull; if (qbsp_options.debugexpand.is_hull()) { const auto &hulls = qbsp_options.target_game->get_hull_sizes(); if (hulls.size() <= qbsp_options.debugexpand.hull_index_value()) { FError("invalid hull index passed to debugexpand\n"); } hull = *(hulls.begin() + qbsp_options.debugexpand.hull_index_value()); } else { hull = qbsp_options.debugexpand.hull_bounds_value(); } fs::path name = qbsp_options.bsp_path; name.replace_extension("expanded.map"); WriteMapBrushMap(name, map.world_entity().mapbrushes, hull); } } void LoadMapFile() { logging::funcheader(); { texture_def_issues_t issue_stats; { auto file = fs::load(qbsp_options.map_path); if (!file) { FError("Couldn't load map file \"{}\".\n", qbsp_options.map_path); return; } parser_t parser(file, {qbsp_options.map_path.string()}); mapfile::map_file_t parsed_map; parsed_map.parse(parser); for (const mapfile::map_entity_t &in_entity : parsed_map.entities) { mapentity_t &entity = map.entities.emplace_back(); ParseEntity(in_entity, entity, issue_stats); } } // -add function if (!qbsp_options.add.value().empty()) { auto file = fs::load(qbsp_options.add.value()); if (!file) { FError("Couldn't load map file \"{}\".\n", qbsp_options.add.value()); return; } parser_t parser(file, {qbsp_options.add.value()}); auto input_map = mapfile::map_file_t{}; input_map.parse(parser); for (const auto &in_entity : input_map.entities) { mapentity_t &entity = map.entities.emplace_back(); ParseEntity(in_entity, entity,issue_stats); if (entity.epairs.get("classname") == "worldspawn") { // The easiest way to get the additional map's worldspawn brushes // into the base map's is to rename the additional map's worldspawn classname to func_group entity.epairs.set("classname", "func_group"); } } } } { struct map_file_stats_t : logging::stat_tracker_t { stat &num_entity = register_stat("entities"); stat &num_miptex = register_stat("unique textures"); stat &num_texinfo = register_stat("texinfos"); stat &num_plane = register_stat("unique planes"); } stats; stats.num_entity += map.entities.size(); stats.num_miptex += map.miptex.size(); stats.num_texinfo += map.mtexinfos.size(); stats.num_plane += map.planes.size(); } logging::print(logging::flag::STAT, "\n"); } void ConvertMapFile() { logging::funcheader(); auto file = fs::load(qbsp_options.map_path); if (!file) { FError("Couldn't load map file \"{}\".\n", qbsp_options.map_path); return; } // parse the map parser_t parser(file, {qbsp_options.map_path.string()}); mapfile::map_file_t parsed_map; parsed_map.parse(parser); // choose output filename std::string append; switch (qbsp_options.convertmapformat.value()) { case conversion_t::quake: append = "-quake"; break; case conversion_t::quake2: append = "-quake2"; break; case conversion_t::valve: append = "-valve"; break; case conversion_t::bp: append = "-bp"; break; default: FError("Internal error: unknown conversion_t\n"); } fs::path filename = qbsp_options.bsp_path; filename.replace_filename(qbsp_options.bsp_path.stem().string() + append).replace_extension(".map"); // do conversion conversion_t target = qbsp_options.convertmapformat.value(); switch (target) { case conversion_t::quake: parsed_map.convert_to(mapfile::texcoord_style_t::quaked, qbsp_options.target_game, qbsp_options); break; case conversion_t::quake2: parsed_map.convert_to(mapfile::texcoord_style_t::quaked, qbsp_options.target_game, qbsp_options); break; case conversion_t::valve: parsed_map.convert_to(mapfile::texcoord_style_t::valve_220, qbsp_options.target_game, qbsp_options); break; case conversion_t::bp: parsed_map.convert_to(mapfile::texcoord_style_t::brush_primitives, qbsp_options.target_game, qbsp_options); break; default: FError("Internal error: unknown conversion_t\n"); } // clear q2 attributes // FIXME: should have a way to convert to Q2 Valve if (target != conversion_t::quake2) for (mapfile::map_entity_t &ent : parsed_map.entities) for (mapfile::brush_t &brush : ent.brushes) for (mapfile::brush_side_t &side : brush.faces) side.extended_info = std::nullopt; // write out std::ofstream f(filename); if (!f) FError("Couldn't open file\n"); parsed_map.write(f); logging::print("Conversion saved to {}\n", filename); } void PrintEntity(const mapentity_t &entity) { for (auto &epair : entity.epairs) { logging::print(logging::flag::STAT, " {:20} : {}\n", epair.first, epair.second); } } void WriteEntitiesToString() { for (auto &entity : map.entities) { /* Check if entity needs to be removed */ if (!entity.epairs.size() || IsWorldBrushEntity(entity)) { continue; } map.bsp.dentdata += "{\n"; for (auto &ep : entity.epairs) { if (ep.first.starts_with("_tb_")) { // Remove TrenchBroom keys. _tb_textures tends to be long and can crash vanilla clients. // generally, these are mapper metadata and unwanted in the .bsp. continue; } if (ep.first.size() >= qbsp_options.target_game->max_entity_key - 1) { logging::print("WARNING: {} at {} has long key {} (length {} >= {})\n", entity.epairs.get("classname"), entity.origin, ep.first, ep.first.size(), qbsp_options.target_game->max_entity_key - 1); } if (ep.second.size() >= qbsp_options.target_game->max_entity_value - 1) { logging::print("WARNING: {} at {} has long value for key {} (length {} >= {})\n", entity.epairs.get("classname"), entity.origin, ep.first, ep.second.size(), qbsp_options.target_game->max_entity_value - 1); } fmt::format_to(std::back_inserter(map.bsp.dentdata), "\"{}\" \"{}\"\n", ep.first, ep.second); } map.bsp.dentdata += "}\n"; } } //==================================================================== inline std::optional GetIntersection(const qplane3d &p1, const qplane3d &p2, const qplane3d &p3) { const double denom = qv::dot(p1.normal, qv::cross(p2.normal, p3.normal)); if (denom == 0.f) { return std::nullopt; } return (qv::cross(p2.normal, p3.normal) * p1.dist - qv::cross(p3.normal, p1.normal) * -p2.dist - qv::cross(p1.normal, p2.normal) * -p3.dist) / denom; } /* ================= GetBrushExtents ================= */ inline double GetBrushExtents(const mapbrush_t &hullbrush) { double extents = -std::numeric_limits::infinity(); for (int32_t i = 0; i < hullbrush.faces.size() - 2; i++) { for (int32_t j = i; j < hullbrush.faces.size() - 1; j++) { for (int32_t k = j; k < hullbrush.faces.size(); k++) { if (i == j || j == k || k == i) { continue; } auto &fi = hullbrush.faces[i]; auto &fj = hullbrush.faces[j]; auto &fk = hullbrush.faces[k]; bool legal = true; auto vertex = GetIntersection(fi.get_plane(), fj.get_plane(), fk.get_plane()); if (!vertex) { continue; } for (int32_t m = 0; m < hullbrush.faces.size(); m++) { if (hullbrush.faces[m].get_plane().distance_to(*vertex) > NORMAL_EPSILON) { legal = false; break; } } if (legal) { for (auto &p : *vertex) { extents = std::max(extents, fabs(p)); } } } } } if (qbsp_options.scale.value() != 1) { extents *= qbsp_options.scale.value(); } return extents; } #include "tbb/parallel_for_each.h" #include void CalculateWorldExtent(void) { std::atomic extents = -std::numeric_limits::infinity(); tbb::parallel_for_each(map.entities, [&](const mapentity_t &entity) { tbb::parallel_for_each(entity.mapbrushes, [&](const mapbrush_t &mapbrush) { const double brushExtents = std::max(extents.load(), GetBrushExtents(mapbrush)); double currentExtents = extents; while (currentExtents < brushExtents && !extents.compare_exchange_weak(currentExtents, brushExtents)) ; }); }); double hull_extents = 0; for (auto &hull : qbsp_options.target_game->get_hull_sizes()) { for (auto &v : hull.size()) { hull_extents = std::max(hull_extents, fabs(v)); } } qbsp_options.worldextent.set_value(ceil((extents + hull_extents) * 2) + SIDESPACE, settings::source::GAME_TARGET); logging::print("INFO: world extents calculated to {} units\n", qbsp_options.worldextent.value()); } /* ================== WriteBspBrushMap from q3map ================== */ void WriteBspBrushMap(std::string_view filename_suffix, const bspbrush_t::container &list) { fs::path name = qbsp_options.bsp_path; name.replace_extension(std::string(filename_suffix) + ".map"); logging::print("writing {}\n", name); std::ofstream f(name); if (!f) FError("Can't write {}", name); ewt::print(f, "{{\n\"classname\" \"worldspawn\"\n"); for (auto &brush : list) { if (!brush) { continue; } ewt::print(f, "{{\n"); for (auto &face : brush->sides) { winding_t w = BaseWindingForPlane(face.get_plane()); ewt::print(f, "( {} ) ", w[0]); ewt::print(f, "( {} ) ", w[1]); ewt::print(f, "( {} ) ", w[2]); #if 0 if (face.visible) { ewt::print(f, "skip 0 0 0 1 1\n"); } else { ewt::print(f, "nonvisible 0 0 0 1 1\n"); } #endif ewt::print(f, "{} 0 0 0 1 1\n", map.miptex[face.get_texinfo().miptex].name); } ewt::print(f, "}}\n"); } ewt::print(f, "}}\n"); f.close(); }