2638 lines
84 KiB
C++
2638 lines
84 KiB
C++
/*
|
|
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 <cassert>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
|
|
#include <string>
|
|
#include <memory>
|
|
#include <list>
|
|
#include <utility>
|
|
#include <optional>
|
|
#include <fstream>
|
|
|
|
#include <qbsp/brush.hh>
|
|
#include <qbsp/map.hh>
|
|
#include <qbsp/qbsp.hh>
|
|
|
|
#include <common/log.hh>
|
|
#include <common/parser.hh>
|
|
#include <common/fs.hh>
|
|
#include <common/imglib.hh>
|
|
#include <common/qvec.hh>
|
|
#include <common/ostream.hh>
|
|
#include <common/mapfile.hh>
|
|
|
|
#include <pareto/spatial_map.h>
|
|
|
|
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<double, 4, size_t> hash;
|
|
};
|
|
|
|
struct vertexhash_t
|
|
{
|
|
// hashed vertices; generated by EmitVertices
|
|
pareto::spatial_map<double, 3, size_t> hash;
|
|
};
|
|
|
|
mapdata_t::mapdata_t()
|
|
: plane_hash(std::make_unique<planehash_t>()),
|
|
hashverts(std::make_unique<vertexhash_t>())
|
|
{
|
|
}
|
|
|
|
// 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<int32_t>(positive.get_type()) % 3] < 0.0) {
|
|
std::swap(positive, negative);
|
|
result = negative_index;
|
|
} else {
|
|
result = positive_index;
|
|
}
|
|
|
|
plane_hash->hash.emplace(pareto::point<double, 4>{positive.get_normal()[0], positive.get_normal()[1],
|
|
positive.get_normal()[2], positive.get_dist()},
|
|
positive_index);
|
|
plane_hash->hash.emplace(pareto::point<double, 4>{negative.get_normal()[0], negative.get_normal()[1],
|
|
negative.get_normal()[2], negative.get_dist()},
|
|
negative_index);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<size_t> 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<size_t> 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<double, 3>({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<img::texture_meta> &mapdata_t::load_image_meta(const std::string_view &name)
|
|
{
|
|
static std::optional<img::texture_meta> 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<fs::archive_like> 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<winding_t> w = BaseWindingForPlane<winding_t>(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_texinfo_t> &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<extended_texinfo_t> 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<int>(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_texinfo_t> 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<qvec3d, 3> &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<qvec3d, 3> &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<mapface_t> 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<qvec3d> points;
|
|
std::vector<qvec3d> corners;
|
|
std::vector<std::array<size_t, 2>> 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<size_t, 2> 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<mapface_t> 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<qvec3d, 3> 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<qvec3d, 3> 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<qvec3d, 3> 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<const char*>(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<mapbrush_t> &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<winding_t>(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<qvec3d> 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<double>::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 <atomic>
|
|
|
|
void CalculateWorldExtent(void)
|
|
{
|
|
std::atomic<double> extents = -std::numeric_limits<double>::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<winding_t>(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();
|
|
}
|