diff --git a/CMakeLists.txt b/CMakeLists.txt index d7c1fbdc..efc6b53d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index e432711f..34648a50 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -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) diff --git a/common/bspfile.cc b/common/bspfile.cc index 8d98f509..8925cca9 100644 --- a/common/bspfile.cc +++ b/common/bspfile.cc @@ -973,7 +973,7 @@ struct gamedef_hl_t : public gamedef_q1_like_t 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; diff --git a/common/mapfile.cc b/common/mapfile.cc new file mode 100644 index 00000000..1bea761b --- /dev/null +++ b/common/mapfile.cc @@ -0,0 +1,729 @@ +#include +#include +#include +#include + +/*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 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 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(raw), false }; + } else if (parser.token[4] == '2') { + raw = texdef_etp_t { std::get(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); + } + } +} diff --git a/include/common/mapfile.hh b/include/common/mapfile.hh new file mode 100644 index 00000000..d450125b --- /dev/null +++ b/include/common/mapfile.hh @@ -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 +#include +#include + +// 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 axis; +}; + +struct texdef_quake_ed_t +{ + qvec2d shift; + vec_t rotate; + qvec2d scale; +}; + +struct texdef_valve_t : texdef_quake_ed_t +{ + qmat 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 raw; + // raw plane points + std::array planepts; + // Q2/Q3 data, if available + std::optional 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 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 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 entities; + + void parse(parser_t &parser); + + void write(std::ostream &stream); + + void convert_to(texcoord_style_t style); +}; diff --git a/include/common/mathlib.hh b/include/common/mathlib.hh index c8b59503..1507f112 100644 --- a/include/common/mathlib.hh +++ b/include/common/mathlib.hh @@ -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, diff --git a/include/common/parser.hh b/include/common/parser.hh index edf2a342..e882ae8d 100644 --- a/include/common/parser.hh +++ b/include/common/parser.hh @@ -24,6 +24,7 @@ #include #include #include +#include "fs.hh" enum : int32_t { diff --git a/include/maputil/maputil.hh b/include/maputil/maputil.hh new file mode 100644 index 00000000..4d437082 --- /dev/null +++ b/include/maputil/maputil.hh @@ -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); diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 6063f826..2384159c 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -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. diff --git a/maputil/CMakeLists.txt b/maputil/CMakeLists.txt new file mode 100644 index 00000000..55deabed --- /dev/null +++ b/maputil/CMakeLists.txt @@ -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 "$" "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" + ) + +install(TARGETS maputil RUNTIME DESTINATION bin) diff --git a/maputil/main.cc b/maputil/main.cc new file mode 100644 index 00000000..9c052c3a --- /dev/null +++ b/maputil/main.cc @@ -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 +#include + +int main(int argc, char **argv) +{ + try { + return maputil_main(argc, argv); + } catch (const std::exception &e) { + exit_on_exception(e); + } +} diff --git a/maputil/maputil.cc b/maputil/maputil.cc new file mode 100644 index 00000000..9fe4e194 --- /dev/null +++ b/maputil/maputil.cc @@ -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 + +#include +#include +#include +#include + +#ifdef USE_LUA +extern "C" +{ + #include + #include + #include +} +#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 \"\"\ + perform a query on entities and print out matching results.\ + see docs for more details on globals.\ +--script \"\ + convert the current map to the given format.\ +--save \"\"\ + 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; +} diff --git a/qbsp/map.cc b/qbsp/map.cc index 2820607a..1a93778a 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -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)); } }