maputil starting point

This commit is contained in:
Jonathan 2023-09-04 00:16:23 -04:00
parent b893e67309
commit cec120dfc9
13 changed files with 1243 additions and 16 deletions

View File

@ -122,6 +122,7 @@ add_subdirectory(bsputil)
add_subdirectory(light)
add_subdirectory(qbsp)
add_subdirectory(vis)
add_subdirectory(maputil)
option(DISABLE_TESTS "Disables Tests" OFF)
option(DISABLE_DOCS "Disables Docs" OFF)

View File

@ -18,6 +18,7 @@ add_library(common STATIC
imglib.cc
settings.cc
prtfile.cc
mapfile.cc
debugger.natvis
../include/common/aabb.hh
../include/common/aligned_allocator.hh
@ -48,6 +49,7 @@ add_library(common STATIC
../include/common/prtfile.hh
../include/common/vectorutils.hh
../include/common/ostream.hh
../include/common/mapfile.hh
)
target_link_libraries(common ${CMAKE_THREAD_LIBS_INIT} TBB::tbb TBB::tbbmalloc fmt::fmt nlohmann_json::nlohmann_json pareto)

View File

@ -973,7 +973,7 @@ struct gamedef_hl_t : public gamedef_q1_like_t<GAME_HALF_LIFE>
struct gamedef_q2_t : public gamedef_t
{
gamedef_q2_t()
: gamedef_t("BASEQ2")
: gamedef_t("baseq2")
{
this->id = GAME_QUAKE_II;
has_rgb_lightmap = true;

729
common/mapfile.cc Normal file
View File

@ -0,0 +1,729 @@
#include <common/mapfile.hh>
#include <common/log.hh>
#include <common/ostream.hh>
#include <utility>
/*static*/ bool brush_side_t::is_valid_texture_projection(const qvec3f &faceNormal, const qvec3f &s_vec, const qvec3f &t_vec)
{
// TODO: This doesn't match how light does it (TexSpaceToWorld)
const qvec3f tex_normal = qv::normalize(qv::cross(s_vec, t_vec));
for (size_t i = 0; i < 3; i++) {
if (std::isnan(tex_normal[i])) {
return false;
}
}
const float cosangle = qv::dot(tex_normal, faceNormal);
if (std::isnan(cosangle)) {
return false;
} else if (fabs(cosangle) < ZERO_EPSILON) {
return false;
}
return true;
}
void brush_side_t::validate_texture_projection()
{
if (!is_valid_texture_projection()) {
/*
if (qbsp_options.verbose.value()) {
logging::print("WARNING: {}: repairing invalid texture projection (\"{}\" near {} {} {})\n", mapface.line,
mapface.texname, (int)mapface.planepts[0][0], (int)mapface.planepts[0][1], (int)mapface.planepts[0][2]);
} else {
issue_stats.num_repaired++;
}
*/
// Reset texturing to sensible defaults
set_texinfo(texdef_quake_ed_t {
{ 0.0, 0.0 },
0,
{ 1.0, 1.0 }
});
Q_assert(is_valid_texture_projection());
}
}
/*static*/ texdef_bp_t brush_side_t::parse_bp(parser_t &parser)
{
qmat<vec_t, 2, 3> texMat;
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "(") {
goto parse_error;
}
for (size_t i = 0; i < 2; i++) {
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "(") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
texMat.at(i, j) = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
return { texMat };
parse_error:
FError("{}: couldn't parse Brush Primitives texture info", parser.location);
}
/*static*/ texdef_valve_t brush_side_t::parse_valve_220(parser_t &parser)
{
qmat<vec_t, 2, 3> axis;
qvec2d shift, scale;
vec_t rotate;
for (size_t i = 0; i < 2; i++) {
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "[") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
axis.at(i, j) = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
shift[i] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "]") {
goto parse_error;
}
}
parser.parse_token(PARSE_SAMELINE);
rotate = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[1] = std::stod(parser.token);
return {
shift,
rotate,
scale,
axis
};
parse_error:
FError("{}: couldn't parse Valve220 texture info", parser.location);
}
/*static*/ texdef_quake_ed_t brush_side_t::parse_quake_ed(parser_t &parser)
{
qvec2d shift, scale;
vec_t rotate;
parser.parse_token(PARSE_SAMELINE);
shift[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
shift[1] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
rotate = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[1] = std::stod(parser.token);
return {
shift,
rotate,
scale
};
}
bool brush_side_t::parse_quark_comment(parser_t &parser)
{
if (!parser.parse_token(PARSE_COMMENT | PARSE_OPTIONAL)) {
return false;
}
if (parser.token.length() < 5 || strncmp(parser.token.c_str(), "//TX", 4)) {
return false;
}
// QuArK TX modes can only exist on Quaked-style maps
Q_assert(style == texcoord_style_t::quaked);
style = texcoord_style_t::etp;
if (parser.token[4] == '1') {
raw = texdef_etp_t { std::get<texdef_quake_ed_t>(raw), false };
} else if (parser.token[4] == '2') {
raw = texdef_etp_t { std::get<texdef_quake_ed_t>(raw), true };
} else {
return false;
}
return true;
}
void brush_side_t::parse_extended_texinfo(parser_t &parser)
{
if (!parse_quark_comment(parser)) {
// Parse extra Quake 2 surface info
if (parser.parse_token(PARSE_OPTIONAL)) {
texinfo_quake2_t q2_info;
q2_info.contents = {std::stoi(parser.token)};
if (parser.parse_token(PARSE_OPTIONAL)) {
q2_info.flags.native = std::stoi(parser.token);
}
if (parser.parse_token(PARSE_OPTIONAL)) {
q2_info.value = std::stoi(parser.token);
}
extended_info = q2_info;
parse_quark_comment(parser);
}
}
}
void brush_side_t::set_texinfo(const texdef_quake_ed_t &texdef)
{
texture_axis_t axis(plane);
qvec3d vectors[2] = {
axis.xv,
axis.yv
};
/* Rotate axis */
vec_t ang = texdef.rotate / 180.0 * Q_PI;
vec_t sinv = sin(ang);
vec_t cosv = cos(ang);
size_t sv, tv;
if (vectors[0][0]) {
sv = 0;
} else if (vectors[0][1]) {
sv = 1;
} else {
sv = 2; // unreachable, due to TextureAxisFromPlane lookup table
}
if (vectors[1][0]) {
tv = 0; // unreachable, due to TextureAxisFromPlane lookup table
} else if (vectors[1][1]) {
tv = 1;
} else {
tv = 2;
}
for (size_t i = 0; i < 2; i++) {
vec_t ns = cosv * vectors[i][sv] - sinv * vectors[i][tv];
vec_t nt = sinv * vectors[i][sv] + cosv * vectors[i][tv];
vectors[i][sv] = ns;
vectors[i][tv] = nt;
}
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 3; j++) {
/* Interpret zero scale as no scaling */
vecs.at(i, j) = vectors[i][j] / (texdef.scale[i] ? texdef.scale[i] : 1);
}
}
vecs.at(0, 3) = texdef.shift[0];
vecs.at(1, 3) = texdef.shift[1];
// TODO: move these self-tests somewhere else, do them for all types
#if 0
if (false) {
// Self-test of SetTexinfo_QuakeEd_New
texvecf check;
SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 4; j++) {
if (fabs(check.at(i, j) - out->vecs.at(i, j)) > 0.001) {
SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
FError("fail");
}
}
}
}
if (false) {
// Self-test of TexDef_BSPToQuakeEd
texdef_quake_ed_t reversed = TexDef_BSPToQuakeEd(plane, std::nullopt, out->vecs, planepts);
if (!EqualDegrees(reversed.rotate, rotate)) {
reversed.rotate += 180;
reversed.scale[0] *= -1;
reversed.scale[1] *= -1;
}
if (!EqualDegrees(reversed.rotate, rotate)) {
ewt::print("wrong rotate got {} expected {}\n", reversed.rotate, rotate);
}
if (fabs(reversed.scale[0] - scale[0]) > 0.001 || fabs(reversed.scale[1] - scale[1]) > 0.001) {
ewt::print("wrong scale, got {} {} exp {} {}\n", reversed.scale[0], reversed.scale[1], scale[0], scale[1]);
}
if (fabs(reversed.shift[0] - shift[0]) > 0.1 || fabs(reversed.shift[1] - shift[1]) > 0.1) {
ewt::print("wrong shift, got {} {} exp {} {}\n", reversed.shift[0], reversed.shift[1], shift[0], shift[1]);
}
}
#endif
}
void brush_side_t::set_texinfo(const texdef_valve_t &texdef)
{
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = texdef.axis.at(0, i) / texdef.scale[0];
vecs.at(1, i) = texdef.axis.at(1, i) / texdef.scale[1];
}
vecs.at(0, 3) = texdef.shift[0];
vecs.at(1, 3) = texdef.shift[1];
}
void brush_side_t::set_texinfo(const texdef_etp_t &texdef)
{
qvec3d vectors[2];
/*
* Type 1 uses vecs[0] = (pt[2] - pt[0]) and vecs[1] = (pt[1] - pt[0])
* Type 2 reverses the order of the vecs
* 128 is the scaling factor assumed by QuArK.
*/
if (!texdef.tx2) {
vectors[0] = planepts[2] - planepts[0];
vectors[1] = planepts[1] - planepts[0];
} else {
vectors[0] = planepts[1] - planepts[0];
vectors[1] = planepts[2] - planepts[0];
}
vectors[0] *= 1.0 / 128.0;
vectors[1] *= 1.0 / 128.0;
vec_t a = qv::dot(vectors[0], vectors[0]);
vec_t b = qv::dot(vectors[0], vectors[1]);
vec_t c = b; /* qv::dot(vectors[1], vectors[0]); */
vec_t d = qv::dot(vectors[1], vectors[1]);
/*
* Want to solve for out->vecs:
*
* | a b | | out->vecs[0] | = | vecs[0] |
* | c d | | out->vecs[1] | | vecs[1] |
*
* => | out->vecs[0] | = __ 1.0__ | d -b | | vecs[0] |
* | out->vecs[1] | a*d - b*c | -c a | | vecs[1] |
*/
vec_t determinant = a * d - b * c;
if (fabs(determinant) < ZERO_EPSILON) {
logging::print("WARNING: {}: Face with degenerate QuArK-style texture axes\n", location);
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = vecs.at(1, i) = 0;
}
} else {
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = (d * vectors[0][i] - b * vectors[1][i]) / determinant;
vecs.at(1, i) = -(a * vectors[1][i] - c * vectors[0][i]) / determinant;
}
}
/* Finally, the texture offset is indicated by planepts[0] */
for (size_t i = 0; i < 3; ++i) {
vectors[0][i] = vecs.at(0, i);
vectors[1][i] = vecs.at(1, i);
}
vecs.at(0, 3) = -qv::dot(vectors[0], planepts[0]);
vecs.at(1, 3) = -qv::dot(vectors[1], planepts[0]);
}
void brush_side_t::set_texinfo(const texdef_bp_t &texdef)
{
#if 0
const auto &texture = map.load_image_meta(mapface.texname.c_str());
const int32_t width = texture ? texture->width : 64;
const int32_t height = texture ? texture->height : 64;
SetTexinfo_BrushPrimitives(texMat, plane.normal, width, height, tx->vecs);
#endif
FError("todo BP");
}
void brush_side_t::parse_texture_def(parser_t &parser, texcoord_style_t base_format)
{
if (base_format == texcoord_style_t::brush_primitives) {
raw = parse_bp(parser);
style = texcoord_style_t::brush_primitives;
parser.parse_token(PARSE_SAMELINE);
texture = std::move(parser.token);
} else if (base_format == texcoord_style_t::quaked) {
parser.parse_token(PARSE_SAMELINE);
texture = std::move(parser.token);
parser.parse_token(PARSE_SAMELINE | PARSE_PEEK);
if (parser.token == "[") {
raw = parse_valve_220(parser);
style = texcoord_style_t::valve_220;
} else {
raw = parse_quake_ed(parser);
style = texcoord_style_t::quaked;
}
} else {
FError("{}: Bad brush format", parser.location);
}
// Read extra Q2 params and/or QuArK subtype
parse_extended_texinfo(parser);
std::visit([this](auto &&x) { set_texinfo(x); }, raw);
}
void brush_side_t::parse_plane_def(parser_t &parser)
{
for (size_t i = 0; i < 3; i++) {
if (i != 0) {
parser.parse_token();
}
if (parser.token != "(") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
planepts[i][j] = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
}
return;
parse_error:
FError("{}: Invalid brush plane format", parser.location);
}
void brush_side_t::write_extended_info(std::ostream &stream)
{
if (extended_info) {
ewt::print(stream, " {} {} {}", extended_info->contents.native, extended_info->flags.native, extended_info->value);
}
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_quake_ed_t &texdef)
{
ewt::print(stream, "{} {} {} {} {}", texdef.shift[0], texdef.shift[1], texdef.rotate, texdef.scale[0], texdef.scale[1]);
write_extended_info(stream);
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_valve_t &texdef)
{
ewt::print(stream, "[ {} {} {} {} ] [ {} {} {} {} ] {} {} {}",
texdef.axis.at(0, 0), texdef.axis.at(0, 1), texdef.axis.at(0, 2), texdef.shift[0],
texdef.axis.at(1, 0), texdef.axis.at(1, 1), texdef.axis.at(1, 2), texdef.shift[1],
texdef.rotate, texdef.scale[0], texdef.scale[1]);
write_extended_info(stream);
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_etp_t &texdef)
{
write_texinfo(stream, (const texdef_quake_ed_t &) texdef);
ewt::print(stream, "//TX{}", texdef.tx2 ? '2' : '1');
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_bp_t &texdef)
{
FError("todo bp");
}
void brush_side_t::write(std::ostream &stream)
{
ewt::print(stream, "( {} {} {} ) ( {} {} {} ) ( {} {} {} ) {} ", planepts[0][0], planepts[0][1], planepts[0][2],
planepts[1][0], planepts[1][1], planepts[1][2], planepts[2][0], planepts[2][1], planepts[2][2],
texture);
std::visit([this, &stream](auto &&x) { write_texinfo(stream, x); }, raw);
}
void brush_side_t::convert_to(texcoord_style_t style)
{
// we're already this style
if (this->style == style) {
return;
}
this->style = style;
}
void brush_t::parse_brush_face(parser_t &parser, texcoord_style_t base_format)
{
brush_side_t side;
side.location = parser.location;
side.parse_plane_def(parser);
/* calculate the normal/dist plane equation */
qvec3d ab = side.planepts[0] - side.planepts[1];
qvec3d cb = side.planepts[2] - side.planepts[1];
vec_t length;
qvec3d normal = qv::normalize(qv::cross(ab, cb), length);
vec_t dist = qv::dot(side.planepts[1], normal);
side.plane = { normal, dist };
side.parse_texture_def(parser, base_format);
if (length < NORMAL_EPSILON) {
logging::print("WARNING: {}: Brush plane with no normal\n", parser.location);
return;
}
/* Check for duplicate planes */
for (auto &check : faces) {
if (qv::epsilonEqual(check.plane, side.plane) ||
qv::epsilonEqual(-check.plane, side.plane)) {
logging::print("{}: Brush with duplicate plane\n", parser.location);
return;
}
}
// ericw -- round texture vector values that are within ZERO_EPSILON of integers,
// to attempt to attempt to work around corrupted lightmap sizes in DarkPlaces
// (it uses 32 bit precision in CalcSurfaceExtents)
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 4; j++) {
vec_t r = Q_rint(side.vecs.at(i, j));
if (fabs(side.vecs.at(i, j) - r) < ZERO_EPSILON) {
side.vecs.at(i, j) = r;
}
}
}
side.validate_texture_projection();
faces.emplace_back(std::move(side));
}
void brush_t::write(std::ostream &stream)
{
stream << "{\n";
if (base_format == texcoord_style_t::brush_primitives) {
stream << "brushDef\n{\n";
}
for (auto &face : faces) {
face.write(stream);
stream << "\n";
}
if (base_format == texcoord_style_t::brush_primitives) {
stream << "}\n";
}
stream << "}\n";
}
void brush_t::convert_to(texcoord_style_t style)
{
for (auto &face : faces) {
face.convert_to(style);
}
if (style == texcoord_style_t::brush_primitives) {
base_format = style;
} else {
base_format = texcoord_style_t::quaked;
}
}
// map file stuff
void map_entity_t::parse_entity_dict(parser_t &parser)
{
std::string key = std::move(parser.token);
// trim whitespace from start/end
while (std::isspace(key.front())) {
key.erase(key.begin());
}
while (std::isspace(key.back())) {
key.erase(key.end() - 1);
}
parser.parse_token(PARSE_SAMELINE);
epairs.set(key, parser.token);
}
void map_entity_t::parse_brush(parser_t &parser)
{
// ericw -- brush primitives
if (!parser.parse_token(PARSE_PEEK)) {
FError("{}: unexpected EOF after {{ beginning brush", parser.location);
}
brush_t brush;
if (parser.token == "(") {
brush.base_format = texcoord_style_t::quaked;
} else {
parser.parse_token();
brush.base_format = texcoord_style_t::brush_primitives;
// optional
if (parser.token == "brushDef") {
if (!parser.parse_token()) {
FError("Brush primitives: unexpected EOF (nothing after brushDef)");
}
}
// mandatory
if (parser.token != "{") {
FError("Brush primitives: expected second {{ at beginning of brush, got \"{}\"", parser.token);
}
}
// ericw -- end brush primitives
while (parser.parse_token()) {
// set linenum after first parsed token
if (!brush.location) {
brush.location = parser.location;
}
if (parser.token == "}") {
break;
}
brush.parse_brush_face(parser, brush.base_format);
}
// ericw -- brush primitives - there should be another closing }
if (brush.base_format == texcoord_style_t::brush_primitives) {
if (!parser.parse_token()) {
FError("Brush primitives: unexpected EOF (no closing brace)");
} else if (parser.token != "}") {
FError("Brush primitives: Expected }}, got: {}", parser.token);
}
}
// ericw -- end brush primitives
if (brush.faces.size()) {
brushes.push_back(std::move(brush));
}
}
bool map_entity_t::parse(parser_t &parser)
{
location = parser.location;
if (!parser.parse_token()) {
return false;
}
if (parser.token != "{") {
FError("{}: Invalid entity format, {{ not found", parser.location);
}
do {
if (!parser.parse_token()) {
FError("Unexpected EOF (no closing brace)");
}
if (parser.token == "}") {
break;
} else if (parser.token == "{") {
parse_brush(parser);
} else {
parse_entity_dict(parser);
}
} while (1);
return true;
}
void map_entity_t::write(std::ostream &stream)
{
stream << "{\n";
for (auto &kvp : epairs) {
ewt::print(stream, "\"{}\" \"{}\"\n", kvp.first, kvp.second);
}
size_t brush_id = 0;
for (auto &brush : brushes) {
ewt::print(stream, "// brush {}\n", brush_id++);
brush.write(stream);
}
stream << "}\n";
}
void map_file_t::parse(parser_t &parser)
{
while (true) {
map_entity_t &entity = entities.emplace_back();
if (!entity.parse(parser)) {
break;
}
}
// Remove dummy entity inserted above
assert(!entities.back().epairs.size());
entities.pop_back();
}
void map_file_t::write(std::ostream &stream)
{
size_t ent_id = 0;
for (auto &entity : entities) {
ewt::print(stream, "// entity {}\n", ent_id++);
entity.write(stream);
}
}
void map_file_t::convert_to(texcoord_style_t style)
{
for (auto &entity : entities) {
for (auto &brush : entity.brushes) {
brush.convert_to(style);
}
}
}

206
include/common/mapfile.hh Normal file
View File

@ -0,0 +1,206 @@
/*
Copyright (C) 1996-1997 Id Software, Inc.
Copyright (C) 1997 Greg Lewis
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.
*/
// qbsp.h
#pragma once
#include "mathlib.hh"
#include "bspfile.hh"
#include "entdata.h"
#include "parser.hh"
#include <string>
#include <variant>
#include <optional>
// main brush style; technically these can be mixed
enum class texcoord_style_t
{
quaked,
etp,
valve_220,
brush_primitives
};
// raw texdef values; the unchanged
// values in the .map
struct texdef_bp_t
{
qmat<vec_t, 2, 3> axis;
};
struct texdef_quake_ed_t
{
qvec2d shift;
vec_t rotate;
qvec2d scale;
};
struct texdef_valve_t : texdef_quake_ed_t
{
qmat<vec_t, 2, 3> axis;
};
struct texdef_etp_t : texdef_quake_ed_t
{
bool tx2 = false;
};
// extra Q2 info
struct texinfo_quake2_t
{
contentflags_t contents;
surfflags_t flags;
int value;
};
// convert a plane to a texture axis; used by quaked
struct texture_axis_t
{
qvec3d xv;
qvec3d yv;
qvec3d snapped_normal;
// use_new_axis = !qbsp_options.oldaxis.value()
constexpr texture_axis_t(const qplane3d &plane, bool use_new_axis = false)
{
constexpr qvec3d baseaxis[18] = {
{0, 0, 1}, {1, 0, 0}, {0, -1, 0}, // floor
{0, 0, -1}, {1, 0, 0}, {0, -1, 0}, // ceiling
{1, 0, 0}, {0, 1, 0}, {0, 0, -1}, // west wall
{-1, 0, 0}, {0, 1, 0}, {0, 0, -1}, // east wall
{0, 1, 0}, {1, 0, 0}, {0, 0, -1}, // south wall
{0, -1, 0}, {1, 0, 0}, {0, 0, -1} // north wall
};
vec_t best = 0;
size_t bestaxis = 0;
for (size_t i = 0; i < 6; i++) {
vec_t dot = qv::dot(plane.normal, baseaxis[i * 3]);
if (dot > best || (dot == best && use_new_axis)) {
best = dot;
bestaxis = i;
}
}
xv = baseaxis[bestaxis * 3 + 1];
yv = baseaxis[bestaxis * 3 + 2];
snapped_normal = baseaxis[bestaxis * 3];
}
};
// a single brush side from a .map
struct brush_side_t
{
// source location
parser_source_location location;
texcoord_style_t style;
// raw texture name
std::string texture;
// stores the original values that we loaded with, even if they were invalid.
std::variant<texdef_quake_ed_t, texdef_valve_t, texdef_etp_t, texdef_bp_t> raw;
// raw plane points
std::array<qvec3d, 3> planepts;
// Q2/Q3 data, if available
std::optional<texinfo_quake2_t> extended_info = std::nullopt;
// calculated texture vecs
texvecf vecs;
// calculated plane
qplane3d plane;
// TODO move to qv? keep local?
static bool is_valid_texture_projection(const qvec3f &faceNormal, const qvec3f &s_vec, const qvec3f &t_vec);
inline bool is_valid_texture_projection() const
{
return is_valid_texture_projection(plane.normal, vecs.row(0).xyz(), vecs.row(1).xyz());
}
void validate_texture_projection();
// parsing
// TODO: move to the individual texdefs?
static texdef_bp_t parse_bp(parser_t &parser);
static texdef_valve_t parse_valve_220(parser_t &parser);
static texdef_quake_ed_t parse_quake_ed(parser_t &parser);
bool parse_quark_comment(parser_t &parser);
void parse_extended_texinfo(parser_t &parser);
void set_texinfo(const texdef_quake_ed_t &texdef);
void set_texinfo(const texdef_valve_t &texdef);
void set_texinfo(const texdef_etp_t &texdef);
void set_texinfo(const texdef_bp_t &texdef);
void parse_texture_def(parser_t &parser, texcoord_style_t base_format);
void parse_plane_def(parser_t &parser);
void write_extended_info(std::ostream &stream);
void write_texinfo(std::ostream &stream, const texdef_quake_ed_t &texdef);
void write_texinfo(std::ostream &stream, const texdef_valve_t &texdef);
void write_texinfo(std::ostream &stream, const texdef_etp_t &texdef);
void write_texinfo(std::ostream &stream, const texdef_bp_t &texdef);
void write(std::ostream &stream);
void convert_to(texcoord_style_t style);
};
struct brush_t
{
parser_source_location location;
texcoord_style_t base_format;
std::vector<brush_side_t> faces;
void parse_brush_face(parser_t &parser, texcoord_style_t base_format);
void write(std::ostream &stream);
void convert_to(texcoord_style_t style);
};
struct map_entity_t
{
parser_source_location location;
entdict_t epairs;
std::vector<brush_t> brushes;
void parse_entity_dict(parser_t &parser);
void parse_brush(parser_t &parser);
bool parse(parser_t &parser);
void write(std::ostream &stream);
};
struct map_file_t
{
std::vector<map_entity_t> entities;
void parse(parser_t &parser);
void write(std::ostream &stream);
void convert_to(texcoord_style_t style);
};

View File

@ -46,6 +46,18 @@ constexpr vec_t DIST_EPSILON = 0.0001;
constexpr vec_t DEGREES_EPSILON = 0.001;
constexpr vec_t DEFAULT_ON_EPSILON = 0.1;
/*
* The quality of the bsp output is highly sensitive to these epsilon values.
* Notes:
* - some calculations are sensitive to errors and need the various
* epsilons to be such that QBSP_EQUAL_EPSILON < CONTINUOUS_EPSILON.
* ( TODO: re-check if CONTINUOUS_EPSILON is still directly related )
*/
constexpr vec_t ANGLEEPSILON = NORMAL_EPSILON;
constexpr vec_t ZERO_EPSILON = DIST_EPSILON;
constexpr vec_t QBSP_EQUAL_EPSILON = DIST_EPSILON;
constexpr vec_t CONTINUOUS_EPSILON = 0.0005;
enum planeside_t : int8_t
{
SIDE_FRONT,

View File

@ -24,6 +24,7 @@
#include <utility>
#include <vector>
#include <string_view>
#include "fs.hh"
enum : int32_t
{

View File

@ -0,0 +1,20 @@
/* Copyright (C) 1996-1997 Id Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See file, 'COPYING', for details.
*/
int maputil_main(int argc, char **argv);

View File

@ -255,18 +255,6 @@ private:
extern settings::qbsp_settings qbsp_options;
/*
* The quality of the bsp output is highly sensitive to these epsilon values.
* Notes:
* - some calculations are sensitive to errors and need the various
* epsilons to be such that QBSP_EQUAL_EPSILON < CONTINUOUS_EPSILON.
* ( TODO: re-check if CONTINUOUS_EPSILON is still directly related )
*/
constexpr vec_t ANGLEEPSILON = 0.000001;
constexpr vec_t ZERO_EPSILON = 0.0001;
constexpr vec_t QBSP_EQUAL_EPSILON = 0.0001;
constexpr vec_t CONTINUOUS_EPSILON = 0.0005;
// the exact bounding box of the brushes is expanded some for the headnode
// volume. this is done to avoid a zero-bounded node/leaf, the particular
// value doesn't matter but it shows up in the .bsp output.

31
maputil/CMakeLists.txt Normal file
View File

@ -0,0 +1,31 @@
set(MAPUTIL_SOURCES
maputil.cc
../include/maputil/maputil.hh
)
find_package(Lua)
add_library(libmaputil STATIC ${MAPUTIL_SOURCES})
target_link_libraries(libmaputil common TBB::tbb TBB::tbbmalloc fmt::fmt)
if (LUA_LIBRARIES)
target_link_libraries(libmaputil ${LUA_LIBRARIES})
target_include_directories(libmaputil PRIVATE ${LUA_INCLUDE_DIR})
endif()
add_executable(maputil main.cc)
target_link_libraries(maputil libmaputil)
if (LUA_LIBRARIES)
target_include_directories(maputil PRIVATE ${LUA_INCLUDE_DIR})
add_definitions(-DUSE_LUA)
endif()
# HACK: copy .dll dependencies
add_custom_command(TARGET maputil POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:TBB::tbb>" "$<TARGET_FILE_DIR:maputil>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:TBB::tbbmalloc>" "$<TARGET_FILE_DIR:maputil>"
)
install(TARGETS maputil RUNTIME DESTINATION bin)

30
maputil/main.cc Normal file
View File

@ -0,0 +1,30 @@
/* Copyright (C) 1996-1997 Id Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See file, 'COPYING', for details.
*/
#include <maputil/maputil.hh>
#include <common/log.hh>
int main(int argc, char **argv)
{
try {
return maputil_main(argc, argv);
} catch (const std::exception &e) {
exit_on_exception(e);
}
}

209
maputil/maputil.cc Normal file
View File

@ -0,0 +1,209 @@
/* Copyright (C) 1996-1997 Id Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See file, 'COPYING', for details.
*/
#include <cstdint>
#include <common\entdata.h>
#include <common\parser.hh>
#include <common\log.hh>
#include <common\mapfile.hh>
#ifdef USE_LUA
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#endif
map_file_t LoadMapOrEntFile(const fs::path &source)
{
logging::funcheader();
auto file = fs::load(source);
map_file_t map;
if (!file) {
FError("Couldn't load map/entity file \"{}\".", source);
return map;
}
parser_t parser(file, {source.string()});
map.parse(parser);
return map;
}
constexpr const char *usage = "\
usage: maputil [operations...]\
\
valid operations:\
--query \"<Lua expression>\"\
perform a query on entities and print out matching results.\
see docs for more details on globals.\
--script \"<path to Lua script file\"\
execute the given Lua script.\
--strip_extended_info\
removes extended Quake II/III information on faces.\
--convert <quake | valve | etp | bp>\
convert the current map to the given format.\
--save \"<output path>\"\
save the current map to the given output path.\
";
static void maputil_query(map_file_t &map_file, const char *query)
{
#ifdef USE_LUA
logging::print("query: {}\n", query);
lua_State *state = luaL_newstate();
luaL_openlibs(state);
int err = luaL_loadstring(state, query);
if (err != LUA_OK) {
logging::print("can't load query: {}\n", lua_tostring(state, -1));
lua_pop(state, 1);
} else {
lua_pushvalue(state, 1);
int ref = luaL_ref(state, LUA_REGISTRYINDEX);
lua_pop(state, 1);
for (auto &entity : map_file.entities) {
lua_createtable(state, 0, entity.epairs.size());
for (auto &kvp : entity.epairs) {
lua_pushstring(state, kvp.second.c_str());
lua_setfield(state, -2, kvp.first.c_str());
}
lua_setglobal(state, "entity");
lua_rawgeti(state, LUA_REGISTRYINDEX, ref);
err = lua_pcall(state, 0, 1, 0);
if (err != LUA_OK) {
logging::print("can't execute query: {}\n", lua_tostring(state, -1));
lua_pop(state, 1);
} else {
int b = lua_toboolean(state, -1);
lua_pop(state, 1);
if (b) {
logging::print("MATCHED: {} @ {}\n", entity.epairs.get("classname"), entity.location);
}
}
lua_gc(state, LUA_GCCOLLECT);
}
luaL_unref(state, LUA_REGISTRYINDEX, ref);
lua_close(state);
}
#else
logging::print("maputil not compiled with Lua support\n");
#endif
}
int maputil_main(int argc, char **argv)
{
logging::preinitialize();
fmt::print("---- maputil / ericw-tools {} ----\n", ERICWTOOLS_VERSION);
if (argc == 1) {
fmt::print("{}", usage);
exit(1);
}
fs::path source = argv[1];
if (!fs::exists(source)) {
source = DefaultExtension(argv[1], "map");
}
printf("---------------------\n");
fmt::print("{}\n", source);
map_file_t map_file;
map_file = LoadMapOrEntFile(source);
for (int32_t i = 2; i < argc - 1; i++) {
const char *cmd = argv[i];
if (!strcmp(cmd, "--query")) {
i++;
const char *query = argv[i];
maputil_query(map_file, query);
} else if (!strcmp(cmd, "--save")) {
i++;
const char *output = argv[i];
fs::path dest = DefaultExtension(output, "map");
fmt::print("saving to {}...\n", dest);
std::ofstream stream(dest);
map_file.write(stream);
} else if (!strcmp(cmd, "--strip_extended_info")) {
for (auto &entity : map_file.entities) {
for (auto &brush : entity.brushes) {
for (auto &face : brush.faces) {
face.extended_info = std::nullopt;
}
}
}
} else if (!strcmp(cmd, "--convert")) {
i++;
const char *type = argv[i];
texcoord_style_t dest_style;
if (!strcmp(type, "quake")) {
dest_style = texcoord_style_t::quaked;
} else if (!strcmp(type, "valve")) {
dest_style = texcoord_style_t::valve_220;
} else if (!strcmp(type, "etp")) {
dest_style = texcoord_style_t::etp;
} else if (!strcmp(type, "bp")) {
dest_style = texcoord_style_t::brush_primitives;
} else {
FError("unknown map style {}", type);
}
map_file.convert_to(dest_style);
}
}
printf("---------------------\n");
return 0;
}

View File

@ -2731,9 +2731,7 @@ bool ParseEntity(parser_t &parser, mapentity_t &entity, texture_def_issues_t &is
}
} while (parser.token != "}");
} else {
auto brush = ParseBrush(parser, entity, issue_stats);
if (brush.faces.size()) {
if (auto brush = ParseBrush(parser, entity, issue_stats); brush.faces.size()) {
entity.mapbrushes.push_back(std::move(brush));
}
}