ericw-tools/maputil/maputil.cc

1025 lines
26 KiB
C++

/* 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>
#include <common/settings.hh>
#include <common/imglib.hh>
#include <common/bsputils.hh>
#include <common/json.hh>
#ifdef USE_LUA
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#endif
// global map file state
map_file_t map_file;
const gamedef_t *current_game = nullptr;
settings::common_settings common_options;
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 = R"(
usage: maputil [operations...]
--script "<path to Lua script file>"
execute the given Lua script.
valid operations:
--query "<Lua expression>"
perform a query on entities and print out matching results.
see docs for more details on globals.
note that query has the same access as script
but is more suitable for small read-only operations.
--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.
--game <quake | quake2 | hexen2 | halflife>
set the current game; used for certain conversions
or operations.
)";
#ifdef USE_LUA
using array_iterate_callback = std::function<bool(lua_State *state, size_t index)>;
void lua_iterate_array(lua_State *state, array_iterate_callback cb)
{
size_t n = 0;
bool keep_iterating = false;
do {
lua_rawgeti(state, -1, n + 1);
if (lua_type(state, -1) != LUA_TNIL) {
keep_iterating = cb(state, n);
} else {
keep_iterating = false;
}
lua_pop(state, 1);
n++;
} while (keep_iterating);
}
size_t lua_count_array(lua_State *state)
{
size_t num = 0;
lua_iterate_array(state, [&num](auto state, auto index) {
num++;
return true;
});
return num;
}
// pushes value onto stack
static void json_to_lua(lua_State *state, const json &value)
{
switch (value.type()) {
case json::value_t::object: {
lua_newtable(state);
for (auto it = value.begin(); it != value.end(); ++it) {
lua_pushstring(state, it.key().c_str());
json_to_lua(state, it.value());
lua_settable(state, -3);
}
return;
}
case json::value_t::array: {
lua_newtable(state);
size_t i = 1;
for (auto &v : value) {
json_to_lua(state, v);
lua_rawseti(state, -2, i++);
}
return;
}
case json::value_t::string: {
lua_pushstring(state, value.get<std::string>().c_str());
return;
}
case json::value_t::number_unsigned: {
lua_pushnumber(state, value.get<uint64_t>());
return;
}
case json::value_t::number_integer: {
lua_pushnumber(state, value.get<int64_t>());
return;
}
case json::value_t::number_float: {
lua_pushnumber(state, value.get<double>());
return;
}
case json::value_t::boolean: {
lua_pushboolean(state, value.get<bool>());
return;
}
case json::value_t::null: {
lua_pushnil(state);
return;
}
default: {
luaL_error(state, "invalid JSON object type\n");
return;
}
}
}
static int l_load_json(lua_State *state)
{
const char *path = lua_tostring(state, -1);
lua_pop(state, 1);
auto result = fs::load(path);
if (!result) {
return luaL_error(state, "can't load JSON file: %s\n", path);
}
try
{
auto json = json::parse(result->begin(), result->end());
json_to_lua(state, json);
}
catch(std::exception &e)
{
return luaL_error(state, "JSON load exception for %s: %s\n", path, e.what());
}
return 1;
}
/*
* Lua layout:
* entities = table[]
* [E].dict = array
* [D] = [ key, value ]
* [E].brushes = table[]
* [S].texture = string
* [S].plane_points = [ [ x, y, z ] [ x, y, z ] [ x, y, z ] ]
* [S].raw = table (can only contain ONE member:)
* .quaked = table
* .shift = [ x, y ]
* .rotate = number
* .scale = [ x, y ]
* .valve = table
* .axis = [ [ x, y, z ] [ x, y, z ] ]
* .shift = [ x, y ]
* .rotate = number
* .scale = [ x, y ]
* .bp = table
* .axis = [ [ x, y, z ] [ x, y, z ] ]
* .etp = table
* .shift = [ x, y ]
* .rotate = number
* .scale = [ x, y ]
* .tx2 = boolean
* [S].info = table or nil
* .contents = number
* .value = number
* .flags = number
* [S].plane = [ x, y, z, d ] (read-only)
* [S].vecs = [ [ x, y, z, d ] [ x, y, z, d ] ] (read-only)
*/
static void maputil_make_brush_side(lua_State *state, const brush_side_t &side)
{
// make side
lua_createtable(state, 0, 4);
// make vecs
lua_createtable(state, 2, 0);
for (size_t i = 0; i < 2; i++) {
lua_createtable(state, 4, 0);
for (size_t v = 0; v < 4; v++) {
lua_pushnumber(state, side.vecs.at(i, v));
lua_rawseti(state, -2, v + 1);
}
lua_rawseti(state, -2, i + 1);
}
lua_setfield(state, -2, "vecs");
// set raw
lua_createtable(state, 0, 1);
lua_createtable(state, 0, 4);
if (std::holds_alternative<texdef_quake_ed_t>(side.raw) ||
std::holds_alternative<texdef_valve_t>(side.raw) ||
std::holds_alternative<texdef_etp_t>(side.raw)) {
const texdef_quake_ed_t &raw = std::holds_alternative<texdef_quake_ed_t>(side.raw) ? std::get<texdef_quake_ed_t>(side.raw) :
std::holds_alternative<texdef_valve_t>(side.raw) ? reinterpret_cast<const texdef_quake_ed_t &>(std::get<texdef_valve_t>(side.raw)) :
reinterpret_cast<const texdef_quake_ed_t &>(std::get<texdef_etp_t>(side.raw));
lua_createtable(state, 2, 0);
lua_pushnumber(state, raw.shift[0]);
lua_rawseti(state, -2, 1);
lua_pushnumber(state, raw.shift[1]);
lua_rawseti(state, -2, 2);
lua_setfield(state, -2, "shift");
lua_pushnumber(state, raw.rotate);
lua_setfield(state, -2, "rotate");
lua_createtable(state, 2, 0);
lua_pushnumber(state, raw.scale[0]);
lua_rawseti(state, -2, 1);
lua_pushnumber(state, raw.scale[1]);
lua_rawseti(state, -2, 2);
lua_setfield(state, -2, "scale");
if (std::holds_alternative<texdef_etp_t>(side.raw)) {
const auto &raw_etp = std::get<texdef_etp_t>(side.raw);
lua_pushboolean(state, raw_etp.tx2);
lua_setfield(state, -2, "tx2");
}
}
if (std::holds_alternative<texdef_valve_t>(side.raw) ||
std::holds_alternative<texdef_bp_t>(side.raw)) {
const texdef_bp_t &raw_bp =
std::holds_alternative<texdef_valve_t>(side.raw) ? std::get<texdef_valve_t>(side.raw) :
std::get<texdef_bp_t>(side.raw);
lua_createtable(state, 2, 0);
for (size_t i = 0; i < 2; i++) {
lua_createtable(state, 3, 0);
for (size_t v = 0; v < 3; v++) {
lua_pushnumber(state, raw_bp.axis.at(i, v));
lua_rawseti(state, -2, v + 1);
}
lua_rawseti(state, -2, i + 1);
}
lua_setfield(state, -2, "axis");
}
if (std::holds_alternative<texdef_quake_ed_t>(side.raw)) {
lua_setfield(state, -2, "quaked");
} if (std::holds_alternative<texdef_etp_t>(side.raw)) {
lua_setfield(state, -2, "etp");
} if (std::holds_alternative<texdef_valve_t>(side.raw)) {
lua_setfield(state, -2, "valve");
} else {
lua_setfield(state, -2, "bp");
}
lua_setfield(state, -2, "raw");
// make plane
lua_createtable(state, 4, 0);
lua_pushnumber(state, side.plane.normal[0]);
lua_rawseti(state, -2, 1);
lua_pushnumber(state, side.plane.normal[1]);
lua_rawseti(state, -2, 2);
lua_pushnumber(state, side.plane.normal[2]);
lua_rawseti(state, -2, 3);
lua_pushnumber(state, side.plane.dist);
lua_rawseti(state, -2, 4);
lua_setfield(state, -2, "plane");
// make plane points
lua_createtable(state, 3, 0);
for (size_t i = 0; i < 3; i++) {
lua_createtable(state, 3, 0);
for (size_t v = 0; v < 3; v++) {
lua_pushnumber(state, side.planepts[i][v]);
lua_rawseti(state, -2, v + 1);
}
lua_rawseti(state, -2, i + 1);
}
lua_setfield(state, -2, "plane_points");
// set texture
lua_pushstring(state, side.texture.c_str());
lua_setfield(state, -2, "texture");
if (side.extended_info) {
// set info
lua_createtable(state, 0, 3);
lua_pushnumber(state, side.extended_info->contents.native);
lua_setfield(state, -2, "contents");
lua_pushnumber(state, side.extended_info->value);
lua_setfield(state, -2, "value");
lua_pushnumber(state, side.extended_info->flags.native);
lua_setfield(state, -2, "flags");
lua_setfield(state, -2, "info");
}
}
static void maputil_make_brush(lua_State *state, const brush_t &brush)
{
// make sides
lua_createtable(state, brush.faces.size(), 0);
for (size_t s = 0; s < brush.faces.size(); s++) {
auto &side = brush.faces[s];
maputil_make_brush_side(state, side);
// put side into sides
lua_rawseti(state, -2, s + 1);
}
}
#define LUA_VERIFY_TOP_TYPE(type) \
Q_assert(lua_type(state, -1) == type)
static void maputil_copy_dict(lua_State *state, map_entity_t &entity)
{
LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
// check for dict
if (lua_getfield(state, -1, "dict") == LUA_TTABLE) {
// iterate kvps
size_t n = 0;
while (true) {
lua_rawgeti(state, -1, n + 1);
if (lua_type(state, -1) == LUA_TNIL) {
lua_pop(state, 1);
break;
}
LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
n++;
lua_rawgeti(state, -1, 1);
const char *key = lua_tostring(state, -1);
lua_pop(state, 1);
lua_rawgeti(state, -1, 2);
const char *value = lua_tostring(state, -1);
lua_pop(state, 1);
entity.epairs.set(key, value);
lua_pop(state, 1);
}
}
lua_pop(state, 1);
}
static texdef_quake_ed_t maputil_load_quaked(lua_State *state)
{
texdef_quake_ed_t quaked;
lua_getfield(state, -1, "shift");
for (size_t i = 0; i < 2; i++) {
lua_rawgeti(state, -1, i + 1);
quaked.shift[i] = lua_tonumber(state, -1);
lua_pop(state, 1);
}
lua_pop(state, 1);
lua_getfield(state, -1, "rotate");
quaked.rotate = lua_tonumber(state, -1);
lua_pop(state, 1);
lua_getfield(state, -1, "scale");
for (size_t i = 0; i < 2; i++) {
lua_rawgeti(state, -1, i + 1);
quaked.scale[i] = lua_tonumber(state, -1);
lua_pop(state, 1);
}
lua_pop(state, 1);
return quaked;
}
static texdef_bp_t maputil_load_bp(lua_State *state)
{
texdef_bp_t bp;
lua_getfield(state, -1, "axis");
for (size_t i = 0; i < 2; i++) {
lua_rawgeti(state, -1, i + 1);
for (size_t v = 0; v < 3; v++) {
lua_rawgeti(state, -1, v + 1);
bp.axis.at(i, v) = lua_tonumber(state, -1);
lua_pop(state, 1);
}
lua_pop(state, 1);
}
lua_pop(state, 1);
return bp;
}
static void maputil_copy_side(lua_State *state, brush_side_t &side)
{
// texture
lua_getfield(state, -1, "texture");
side.texture = lua_tostring(state, -1);
lua_pop(state, 1);
// plane points
lua_getfield(state, -1, "plane_points");
for (size_t i = 0; i < 3; i++) {
lua_rawgeti(state, -1, i + 1);
for (size_t z = 0; z < 3; z++) {
lua_rawgeti(state, -1, z + 1);
side.planepts[i][z] = lua_tonumber(state, -1);
lua_pop(state, 1);
}
lua_pop(state, 1);
}
lua_pop(state, 1);
// raw
lua_getfield(state, -1, "raw");
if (lua_getfield(state, -1, "quaked") != LUA_TNIL) {
side.raw = maputil_load_quaked(state);
}
lua_pop(state, 1);
if (lua_getfield(state, -1, "valve") != LUA_TNIL) {
texdef_bp_t bp = maputil_load_bp(state);
texdef_quake_ed_t qed = maputil_load_quaked(state);
side.raw = texdef_valve_t { qed, bp };
}
lua_pop(state, 1);
if (lua_getfield(state, -1, "bp") != LUA_TNIL) {
side.raw = maputil_load_bp(state);
}
lua_pop(state, 1);
if (lua_getfield(state, -1, "etp") != LUA_TNIL) {
texdef_quake_ed_t qed = maputil_load_quaked(state);
lua_getfield(state, -1, "tx2");
bool b = !!lua_toboolean(state, -1);
lua_pop(state, 1);
side.raw = texdef_etp_t { qed, b };
}
lua_pop(state, 1);
lua_pop(state, 1);
// extra info
lua_getfield(state, -1, "info");
if (lua_type(state, -1) == LUA_TTABLE) {
texinfo_quake2_t q2;
lua_getfield(state, -1, "contents");
q2.contents.native = lua_tonumber(state, -1);
lua_pop(state, 1);
lua_getfield(state, -1, "value");
q2.value = lua_tonumber(state, -1);
lua_pop(state, 1);
lua_getfield(state, -1, "flags");
q2.flags.native = lua_tonumber(state, -1);
lua_pop(state, 1);
side.extended_info = q2;
}
lua_pop(state, 1);
}
static void maputil_copy_brush(lua_State *state, brush_t &brush)
{
// count sides
size_t num_sides = lua_count_array(state);
brush.faces.resize(num_sides);
// iterate brushes
lua_iterate_array(state, [&brush](auto state, auto index) {
brush_side_t &side = brush.faces[index];
maputil_copy_side(state, side);
return true;
});
}
static void maputil_copy_brushes(lua_State *state, map_entity_t &entity)
{
LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
// check for dict
if (lua_getfield(state, -1, "brushes") == LUA_TTABLE) {
// count brushes
size_t num_brushes = lua_count_array(state);
entity.brushes.resize(num_brushes);
// iterate brushes
lua_iterate_array(state, [&entity](auto state, auto index) {
brush_t &brush = entity.brushes[index];
maputil_copy_brush(state, brush);
return true;
});
}
lua_pop(state, 1);
}
static int l_commit_map(lua_State *state)
{
map_file.entities.clear();
// verify entities global
lua_getglobal(state, "entities");
LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
// count entities
size_t num_entities = lua_count_array(state);
// create entities
map_file.entities.resize(num_entities);
for (size_t i = 0; i < num_entities; i++) {
auto &entity = map_file.entities[i];
lua_rawgeti(state, -1, i + 1);
maputil_copy_dict(state, entity);
maputil_copy_brushes(state, entity);
lua_pop(state, 1);
}
lua_pop(state, 1);
return 0;
}
static inline qplane3d pop_plane_from_side(lua_State *state)
{
qplane3d plane;
lua_getfield(state, -1, "plane");
for (size_t i = 0; i < 3; i++) {
lua_rawgeti(state, -1, i + 1);
plane.normal[i] = lua_tonumber(state, -1);
lua_pop(state, 1);
}
lua_rawgeti(state, -1, 4);
plane.dist = lua_tonumber(state, -1);
lua_pop(state, 1);
lua_pop(state, 1);
return plane;
}
static int l_create_winding(lua_State *state)
{
// -3 = face
// -2 = brush
// -1 = extents
bool found_face = false;
double extents = lua_tonumber(state, -1);
lua_pop(state, 1);
lua_pushvalue(state, -2);
qplane3d side_plane = pop_plane_from_side(state);
lua_pop(state, 1);
using winding_t = polylib::winding_base_t<polylib::winding_storage_hybrid_t<16>>;
std::optional<winding_t> winding = winding_t::from_plane(side_plane, extents);
// loop through sides on brush
{
if (lua_type(state, -1) == LUA_TTABLE) {
lua_iterate_array(state, [&found_face, &winding](auto state, auto index) {
LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
// check that the face is part of the brush
if (lua_rawequal(state, -1, -3)) {
found_face = true;
} else if (winding) {
qplane3d plane = pop_plane_from_side(state);
winding = winding->clip_front(-plane, 0.0f);
}
return true;
});
}
lua_pop(state, 1);
}
if (!winding) {
lua_pushnil(state);
} else {
lua_createtable(state, winding->size(), 0);
for (size_t i = 0; i < winding->size(); i++) {
lua_createtable(state, 3, 0);
auto &p = winding->at(i);
for (size_t v = 0; v < 3; v++) {
lua_pushnumber(state, p[v]);
lua_rawseti(state, -2, v + 1);
}
lua_rawseti(state, -2, i + 1);
}
}
return 1;
}
static int l_load_texture_meta(lua_State *state)
{
const char *path = lua_tostring(state, 1); /* get argument */
lua_pop(state, 1);
if (!current_game) {
luaL_error(state, "need a game loaded with -game for this function");
}
auto result = std::get<0>(img::load_texture_meta(path, current_game, common_options)).value_or(img::texture_meta {});
lua_createtable(state, 0, 5);
lua_pushnumber(state, result.contents.native);
lua_setfield(state, -2, "contents");
lua_pushnumber(state, result.flags.native);
lua_setfield(state, -2, "flags");
lua_pushnumber(state, result.value);
lua_setfield(state, -2, "value");
lua_pushnumber(state, result.width);
lua_setfield(state, -2, "width");
lua_pushnumber(state, result.height);
lua_setfield(state, -2, "height");
return 1;
}
static void maputil_setup_globals(lua_State *state)
{
lua_pushcfunction(state, l_load_json);
lua_setglobal(state, "load_json");
lua_pushcfunction(state, l_commit_map);
lua_setglobal(state, "commit_map");
lua_pushcfunction(state, l_create_winding);
lua_setglobal(state, "create_winding");
lua_pushcfunction(state, l_load_texture_meta);
lua_setglobal(state, "load_texture_meta");
// constants
lua_pushnumber(state, (int32_t) texcoord_style_t::quaked);
lua_setglobal(state, "TEXCOORD_QUAKED");
lua_pushnumber(state, (int32_t) texcoord_style_t::etp);
lua_setglobal(state, "TEXCOORD_ETP");
lua_pushnumber(state, (int32_t) texcoord_style_t::valve_220);
lua_setglobal(state, "TEXCOORD_VALVE");
lua_pushnumber(state, (int32_t) texcoord_style_t::brush_primitives);
lua_setglobal(state, "TEXCOORD_BP");
// convert map to a Lua representation.
lua_createtable(state, map_file.entities.size(), 0);
for (size_t i = 0; i < map_file.entities.size(); i++) {
auto &entity = map_file.entities[i];
// make entity table
lua_createtable(state, 0, 2);
// make dict
if (entity.epairs.size()) {
lua_createtable(state, entity.epairs.size(), 0);
size_t ent = 0;
for (auto &pair : entity.epairs) {
lua_createtable(state, 2, 0);
lua_pushstring(state, pair.first.c_str());
lua_rawseti(state, -2, 1);
lua_pushstring(state, pair.second.c_str());
lua_rawseti(state, -2, 2);
lua_rawseti(state, -2, ent + 1);
ent++;
}
// push dict to entity
lua_setfield(state, -2, "dict");
}
// make brushes
if (!entity.brushes.empty()) {
lua_createtable(state, entity.brushes.size(), 0);
for (size_t b = 0; b < entity.brushes.size(); b++) {
auto &brush = entity.brushes[b];
maputil_make_brush(state, brush);
// put brush into brushes
lua_rawseti(state, -2, b + 1);
}
// push dict to entity
lua_setfield(state, -2, "brushes");
}
// put entity into entities
lua_rawseti(state, -2, i + 1);
}
lua_setglobal(state, "entities");
}
static lua_State *maputil_setup_lua()
{
lua_State *state = luaL_newstate();
luaL_openlibs(state);
return state;
}
static void maputil_free_lua(lua_State *state)
{
lua_close(state);
}
static int maputil_lua_error(lua_State *state)
{
luaL_traceback(state, state, NULL, 1);
logging::print("can't execute script: {}\n{}\n", lua_tostring(state, -1), lua_tostring(state, -2));
return 0;
}
#endif
static void maputil_exec_script(const fs::path &file)
{
#ifdef USE_LUA
lua_State *state = maputil_setup_lua();
lua_pushcfunction(state, maputil_lua_error);
int err = luaL_loadfile(state, file.string().c_str());
if (err != LUA_OK) {
logging::print("can't load script: {}\n", lua_tostring(state, -1));
} else {
maputil_setup_globals(state);
err = lua_pcall(state, 0, 0, -2);
if (err != LUA_OK) {
lua_pop(state, 1);
}
}
maputil_free_lua(state);
#else
logging::print("maputil not compiled with Lua support\n");
#endif
}
static void maputil_exec_query(const char *query)
{
#ifdef USE_LUA
logging::print("query: {}\n", query);
lua_State *state = maputil_setup_lua();
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 {
maputil_setup_globals(state);
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);
}
maputil_free_lua(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 = 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_exec_query(query);
} else if (!strcmp(cmd, "--script")) {
i++;
const char *file = argv[i];
maputil_exec_script(file);
} else if (!strcmp(cmd, "--game")) {
i++;
const char *gamename = argv[i];
current_game = nullptr;
for (auto &game : gamedef_list()) {
if (!Q_strcasecmp(game->friendly_name, gamename)) {
current_game = game;
break;
}
}
if (!current_game) {
FError("not sure what game {} is\n", gamename);
}
current_game->init_filesystem(source, common_options);
} 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, current_game, common_options);
}
}
printf("---------------------\n");
return 0;
}