Merge branch 'type-cleanup' into brushbsp

# Conflicts:
#	common/bspfile.cc
#	include/common/bspfile.hh
#	light/trace.cc
This commit is contained in:
Jonathan 2022-06-30 05:23:05 -04:00
commit 9f734ff976
12 changed files with 325 additions and 81 deletions

2
3rdparty/Catch2 vendored

@ -1 +1 @@
Subproject commit 605a34765aa5d5ecbf476b4598a862ada971b0cc
Subproject commit 62fd660583d3ae7a7886930b413c3c570e89786c

View File

@ -98,7 +98,16 @@ public:
}
bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override
int32_t surfflags_from_string(const std::string_view &str) const
{
if (string_iequals(str, "special")) {
return TEX_SPECIAL;
}
return 0;
}
bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override {
// anything texname other than "hint" in a hint brush is treated as "hintskip", and discarded
return !string_iequals(name, "hint");
}
@ -310,8 +319,13 @@ public:
}
}
bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const override
int32_t contents_from_string(const std::string_view &str) const override
{
// Q1 doesn't get contents from files
return 0;
}
bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const override {
/* If water is transparent, liquids are like empty space */
if (transwater) {
if (contents_are_liquid(contents0) && contents_are_empty(contents1))
@ -709,8 +723,21 @@ struct gamedef_q2_t : public gamedef_t
return true;
}
bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override
static constexpr const char *surf_bitflag_names[] = {"LIGHT", "SLICK", "SKY", "WARP", "TRANS33", "TRANS66", "FLOWING", "NODRAW",
"HINT" };
int32_t surfflags_from_string(const std::string_view &str) const override
{
for (size_t i = 0; i < std::size(surf_bitflag_names); i++) {
if (string_iequals(str, surf_bitflag_names[i])) {
return nth_bit(i);
}
}
return 0;
}
bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override {
// any face in a hint brush that isn't HINT are treated as "hintskip", and discarded
return !(flags.native & Q2_SURF_HINT);
}
@ -913,6 +940,22 @@ struct gamedef_q2_t : public gamedef_t
return true;
}
static constexpr const char *bitflag_names[] = {"SOLID", "WINDOW", "AUX", "LAVA", "SLIME", "WATER", "MIST", "128",
"256", "512", "1024", "2048", "4096", "8192", "16384", "AREAPORTAL", "PLAYERCLIP", "MONSTERCLIP",
"CURRENT_0", "CURRENT_90", "CURRENT_180", "CURRENT_270", "CURRENT_UP", "CURRENT_DOWN", "ORIGIN", "MONSTER",
"DEADMONSTER", "DETAIL", "TRANSLUCENT", "LADDER", "1073741824", "2147483648"};
int32_t contents_from_string(const std::string_view &str) const
{
for (size_t i = 0; i < std::size(bitflag_names); i++) {
if (string_iequals(str, bitflag_names[i])) {
return nth_bit(i);
}
}
return 0;
}
/**
* Returns the single content bit of the strongest visible content present
*/
@ -1002,11 +1045,6 @@ struct gamedef_q2_t : public gamedef_t
return "EMPTY";
}
constexpr const char *bitflag_names[] = {"SOLID", "WINDOW", "AUX", "LAVA", "SLIME", "WATER", "MIST", "128",
"256", "512", "1024", "2048", "4096", "8192", "16384", "AREAPORTAL", "PLAYERCLIP", "MONSTERCLIP",
"CURRENT_0", "CURRENT_90", "CURRENT_180", "CURRENT_270", "CURRENT_UP", "CURRENT_DOWN", "ORIGIN", "MONSTER",
"DEADMONSTER", "DETAIL", "TRANSLUCENT", "LADDER", "1073741824", "2147483648"};
std::string s;
for (int32_t i = 0; i < std::size(bitflag_names); i++) {

View File

@ -78,7 +78,7 @@ string_replaceall(std::string &str, const std::string &from, const std::string &
}
bool // mxd
string_iequals(const std::string &a, const std::string &b)
string_iequals(const std::string_view &a, const std::string_view &b)
{
size_t sz = a.size();
if (b.size() != sz)

View File

@ -3,6 +3,7 @@
#include <common/fs.hh>
#include <common/imglib.hh>
#include <common/entdata.h>
#include <common/json.hh>
/*
============================================================================
@ -136,8 +137,8 @@ std::optional<texture> load_wal(const std::string_view &name, const fs::data &fi
// the .wal is ignored. it's extraneous and well-formed wals
// will all match up anyways.
tex.meta.name = name;
tex.meta.width = mt.width;
tex.meta.height = mt.height;
tex.meta.width = tex.width = mt.width;
tex.meta.height = tex.height = mt.height;
tex.meta.contents = {mt.contents};
tex.meta.flags = {mt.flags};
tex.meta.value = mt.value;
@ -182,17 +183,16 @@ std::optional<texture> load_mip(const std::string_view &name, const fs::data &fi
// the mip is ignored. it's extraneous and well-formed mips
// will all match up anyways.
tex.meta.name = name;
tex.meta.width = header.width;
tex.meta.height = header.height;
tex.meta.width = tex.width = header.width;
tex.meta.height = tex.height = header.height;
if (!meta_only) {
// convert the data into RGBA.
// miptex only has meta
if (header.offsets[0] <= 0) {
// this should never happen under normal circumstances
logging::funcprint("attempted to load external mip for {}\n", name);
return std::nullopt;
return tex;
}
// convert the data into RGBA.
// sanity check
if (header.offsets[0] + (header.width * header.height) > file->size()) {
logging::funcprint("mip offset0 overrun for {}\n", name);
@ -301,8 +301,8 @@ std::optional<texture> load_tga(const std::string_view &name, const fs::data &fi
tex.meta.extension = ext::TGA;
tex.meta.name = name;
tex.meta.width = columns;
tex.meta.height = rows;
tex.meta.width = tex.width = columns;
tex.meta.height = tex.height = rows;
if (!meta_only) {
tex.pixels.resize(numPixels);
@ -450,4 +450,133 @@ std::tuple<std::optional<img::texture>, fs::resolve_result, fs::data> load_textu
return {std::nullopt, {}, {}};
}
/*
JSON meta format, meant to supplant .wal's metadata for external texture use.
All of the values are optional.
{
// valid instances of "contents"; either:
// - a case-insensitive string containing the textual representation
// of the content type
// - a number
// - an array of the two above, which will be OR'd together
"contents": [ "SOLID", 8 ],
"contents": 24,
"contents": "SOLID",
// valid instances of "flags"; either:
// - a case-insensitive string containing the textual representation
// of the surface flags
// - a number
// - an array of the two above, which will be OR'd together
"flags": [ "SKY", 16 ],
"flags": 24,
"flags": "SKY",
// "value" must be an integer
"value": 1234,
// "animation" must be the name of the next texture in
// the chain.
"animation": "e1u1/comp2",
// width/height are allowed to be supplied in order to
// have the editor treat the surface as if its dimensions
// are these rather than the ones pulled in from the image
// itself. they must be integers.
"width": 64,
"height": 64
}
*/
std::optional<texture_meta> load_wal_json_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game)
{
try
{
auto json = json::parse(file->begin(), file->end());
texture_meta meta{};
if (json.contains("width") && json["width"].is_number_integer()) {
meta.width = json["width"].get<int32_t>();
}
if (json.contains("height") && json["height"].is_number_integer()) {
meta.height = json["height"].get<int32_t>();
}
if (json.contains("value") && json["value"].is_number_integer()) {
meta.value = json["value"].get<int32_t>();
}
if (json.contains("contents")) {
auto &contents = json["contents"];
if (contents.is_number_integer()) {
meta.contents.native = contents.get<int32_t>();
} else if (contents.is_string()) {
meta.contents.native = game->contents_from_string(contents.get<std::string>());
} else if (contents.is_array()) {
for (auto &content : contents) {
if (content.is_number_integer()) {
meta.contents.native |= content.get<int32_t>();
} else if (content.is_string()) {
meta.contents.native |= game->contents_from_string(content.get<std::string>());
}
}
}
}
if (json.contains("flags")) {
auto &flags = json["flags"];
if (flags.is_number_integer()) {
meta.flags.native = flags.get<int32_t>();
} else if (flags.is_string()) {
meta.flags.native = game->surfflags_from_string(flags.get<std::string>());
} else if (flags.is_array()) {
for (auto &flag : flags) {
if (flag.is_number_integer()) {
meta.flags.native |= flag.get<int32_t>();
} else if (flag.is_string()) {
meta.flags.native |= game->surfflags_from_string(flag.get<std::string>());
}
}
}
}
if (json.contains("animation") && json["animation"].is_string()) {
meta.animation = json["animation"].get<std::string>();
}
return meta;
}
catch (json::exception e)
{
logging::funcprint("{}, invalid JSON: {}\n", name, e.what());
return std::nullopt;
}
}
std::tuple<std::optional<img::texture_meta>, fs::resolve_result, fs::data> load_texture_meta(const std::string_view &name, const gamedef_t *game, const settings::common_settings &options)
{
fs::path prefix;
if (game->id == GAME_QUAKE_II) {
prefix = "textures";
}
for (auto &ext : img::meta_extension_list) {
fs::path p = (prefix / name) += ext.suffix;
if (auto pos = fs::where(p, options.filepriority.value() == settings::search_priority_t::LOOSE)) {
if (auto data = fs::load(pos)) {
if (auto texture = ext.loader(name.data(), data, game)) {
return {texture, pos, data};
}
}
}
}
return {std::nullopt, {}, {}};
}
} // namespace img

View File

@ -257,6 +257,7 @@ struct gamedef_t
virtual bool surf_is_lightmapped(const surfflags_t &flags) const = 0;
virtual bool surf_is_subdivided(const surfflags_t &flags) const = 0;
virtual bool surfflags_are_valid(const surfflags_t &flags) const = 0;
virtual int32_t surfflags_from_string(const std::string_view &str) const = 0;
// FIXME: fix so that we don't have to pass a name here
virtual bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const = 0;
virtual contentflags_t cluster_contents(const contentflags_t &contents0, const contentflags_t &contents1) const = 0;
@ -283,6 +284,7 @@ struct gamedef_t
virtual bool contents_are_sky(const contentflags_t &contents) const = 0;
virtual bool contents_are_liquid(const contentflags_t &contents) const = 0;
virtual bool contents_are_valid(const contentflags_t &contents, bool strict = true) const = 0;
virtual int32_t contents_from_string(const std::string_view &str) const = 0;
virtual bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const = 0;
virtual bool contents_seals_map(const contentflags_t &contents) const = 0;
virtual contentflags_t contents_remap_for_export(const contentflags_t &contents) const = 0;

View File

@ -63,7 +63,7 @@ inline int32_t Q_strcasecmp(const std::string_view &a, const std::string_view &b
(a.data(), b.data());
}
bool string_iequals(const std::string &a, const std::string &b); // mxd
bool string_iequals(const std::string_view &a, const std::string_view &b); // mxd
struct case_insensitive_hash
{

View File

@ -42,25 +42,35 @@ void init_palette(const gamedef_t *game);
struct texture_meta
{
std::string name;
uint32_t width, height;
uint32_t width = 0, height = 0;
ext extension;
// This member is only set before insertion into the table
// and not calculated by individual load functions.
qvec3b averageColor;
// extension that we pulled the pixels in from.
std::optional<ext> extension;
// Q2/WAL only
surfflags_t flags;
contentflags_t contents;
int32_t value;
surfflags_t flags{};
contentflags_t contents{};
int32_t value = 0;
std::string animation;
};
struct texture
{
texture_meta meta{};
// in the case of replacement textures, these may not
// the width/height of the metadata.
uint32_t width = 0, height = 0;
std::vector<qvec4b> pixels;
// the scale required to map a pixel from the
// meta data onto the real size (16x16 onto 32x32 -> 2)
float width_scale = 1, height_scale = 1;
// This member is only set before insertion into the table
// and not calculated by individual load functions.
qvec3b averageColor { 0 };
};
extern std::unordered_map<std::string, texture, case_insensitive_hash, case_insensitive_equal> textures;
@ -88,4 +98,31 @@ constexpr struct { const char *suffix; ext id; decltype(load_wal) *loader; } ext
// Attempt to load a texture from the specified name.
std::tuple<std::optional<texture>, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options);
enum class meta_ext
{
WAL,
WAL_JSON
};
// Load wal
inline std::optional<texture_meta> load_wal_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game)
{
if (auto tex = load_wal(name, file, true, game)) {
return tex->meta;
}
return std::nullopt;
}
std::optional<texture_meta> load_wal_json_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game);
// list of supported meta extensions and their loaders
constexpr struct { const char *suffix; meta_ext id; decltype(load_wal_meta) *loader; } meta_extension_list[] = {
{ ".wal", meta_ext::WAL, load_wal_meta },
{ ".wal_json", meta_ext::WAL_JSON, load_wal_json_meta }
};
// Attempt to load a texture meta from the specified name.
std::tuple<std::optional<texture_meta>, fs::resolve_result, fs::data> load_texture_meta(const std::string_view &name, const gamedef_t *game, const settings::common_settings &options);
}; // namespace img

View File

@ -39,7 +39,6 @@ void to_json(json &j, const qvec<T, N> &p)
template<typename T, size_t N>
void from_json(const json &j, qvec<T, N> &p)
{
for (size_t i = 0; i < N; i++) {
p[i] = j[i].get<T>();
}

View File

@ -79,7 +79,7 @@ qvec3b Face_LookupTextureColor(const mbsp_t *bsp, const mface_t *face)
auto it = img::find(Face_TextureName(bsp, face));
if (it) {
return it->meta.averageColor;
return it->averageColor;
}
return {127};

View File

@ -1013,43 +1013,63 @@ static inline void WriteNormals(const mbsp_t &bsp, bspdata_t &bspdata)
}
/*
==============================================================================
Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd)
==============================================================================
// Add empty to keep texture index in case of load problems...
auto &tex = img::textures.emplace(miptex.name, img::texture{}).first->second;
// try to load it externally first
auto [texture, _0, _1] = img::load_texture(miptex.name, false, bsp->loadversion->game, options);
if (texture) {
tex = std::move(texture.value());
} else {
if (miptex.data.size() <= sizeof(dmiptex_t)) {
logging::funcprint("WARNING: can't find texture {}\n", miptex.name);
continue;
}
auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game);
if (!loaded_tex) {
logging::funcprint("WARNING: Texture {} is invalid\n", miptex.name);
continue;
}
tex = std::move(loaded_tex.value());
}
tex.meta.averageColor = img::calculate_average(tex.pixels);
*/
static void AddTextureName(const char *textureName, const mbsp_t *bsp)
// Load the specified texture from the BSP
static void AddTextureName(const std::string_view &textureName, const mbsp_t *bsp)
{
if (img::find(textureName)) {
return;
}
// always add entry
auto &tex = img::textures.emplace(textureName, img::texture{}).first->second;
// find wal first, since we'll use it for metadata
auto wal = fs::load("textures" / fs::path(textureName) += ".wal");
// find texture & meta
auto [ texture, _0, _1 ] = img::load_texture(textureName, false, bsp->loadversion->game, options);
if (!wal) {
logging::funcprint("WARNING: can't find .wal for {}\n", textureName);
if (!texture) {
logging::funcprint("WARNING: can't find pixel data for {}\n", textureName);
} else {
auto walTex = img::load_wal(textureName, wal, false, bsp->loadversion->game);
if (walTex) {
tex = std::move(*walTex);
}
tex = std::move(texture.value());
}
// now check for replacements
auto [replacement_tex, _0, _1] = img::load_texture(textureName, false, bsp->loadversion->game, options);
// FIXME: I think this is fundamentally wrong; we need the
// original texture's size for texcoords
if (replacement_tex) {
tex.meta.width = replacement_tex->meta.width;
tex.meta.height = replacement_tex->meta.height;
tex.pixels = std::move(replacement_tex->pixels);
auto [ texture_meta, __0, __1 ] = img::load_texture_meta(textureName, bsp->loadversion->game, options);
if (!texture_meta) {
logging::funcprint("WARNING: can't find meta data for {}\n", textureName);
} else {
tex.meta = std::move(texture_meta.value());
}
tex.meta.averageColor = img::calculate_average(tex.pixels);
tex.averageColor = img::calculate_average(tex.pixels);
tex.width_scale = (float) tex.width / (float) tex.meta.width;
tex.height_scale = (float) tex.height / (float) tex.meta.height;
}
// Load all of the referenced textures from the BSP texinfos into
@ -1088,31 +1108,31 @@ static void ConvertTextures(const mbsp_t *bsp)
continue;
}
// Add empty to keep texture index in case of load problems...
// always add entry
auto &tex = img::textures.emplace(miptex.name, img::texture{}).first->second;
// try to load it externally first
auto [texture, _0, _1] = img::load_texture(miptex.name, false, bsp->loadversion->game, options);
if (texture) {
tex = std::move(texture.value());
} else {
if (miptex.data.size() <= sizeof(dmiptex_t)) {
logging::funcprint("WARNING: can't find texture {}\n", miptex.name);
continue;
// if the miptex entry isn't a dummy, use it as our base
if (miptex.data.size() >= sizeof(dmiptex_t)) {
if (auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game)) {
tex = std::move(loaded_tex.value());
}
auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game);
if (!loaded_tex) {
logging::funcprint("WARNING: Texture {} is invalid\n", miptex.name);
continue;
}
tex = std::move(loaded_tex.value());
}
tex.meta.averageColor = img::calculate_average(tex.pixels);
// find replacement texture
if (auto [ texture, _0, _1 ] = img::load_texture(miptex.name, false, bsp->loadversion->game, options); texture) {
tex.width = texture->width;
tex.height = texture->height;
tex.pixels = std::move(texture->pixels);
}
if (!tex.pixels.size() || !tex.width || !tex.meta.width) {
logging::funcprint("WARNING: invalid size data for {}\n", miptex.name);
continue;
}
tex.averageColor = img::calculate_average(tex.pixels);
tex.width_scale = (float) tex.width / (float) tex.meta.width;
tex.height_scale = (float) tex.height / (float) tex.meta.height;
}
}

View File

@ -78,14 +78,14 @@ uint32_t clamp_texcoord(vec_t in, uint32_t width)
qvec4b SampleTexture(const mface_t *face, const mtexinfo_t *tex, const img::texture *texture, const mbsp_t *bsp, const qvec3d &point)
{
if (texture == nullptr || !texture->meta.width) {
if (texture == nullptr || !texture->width) {
return {};
}
qvec2d texcoord = WorldToTexCoord(point, tex);
const uint32_t x = clamp_texcoord(texcoord[0], texture->meta.width);
const uint32_t y = clamp_texcoord(texcoord[1], texture->meta.height);
const uint32_t x = clamp_texcoord(texcoord[0], texture->width);
const uint32_t y = clamp_texcoord(texcoord[1], texture->width);
return texture->pixels[(texture->meta.width * y) + x];
return texture->pixels[(texture->width * (y * texture->width_scale)) + (x * texture->height_scale)];
}

View File

@ -52,9 +52,28 @@ const std::optional<img::texture_meta> &mapdata_t::load_image_meta(const std::st
return it->second;
}
auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options);
// try a meta-only texture first; this is all we really need anyways
if (auto [texture_meta, _0, _1] = img::load_texture_meta(name, options.target_game, 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, options.target_game, options);
if (texture) {
texture_meta->width = texture->meta.width;
texture_meta->height = texture->meta.height;
}
}
if (texture) {
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, options.target_game, options); texture) {
return meta_cache.emplace(name, texture->meta).first->second;
}