diff --git a/bsputil/bsputil.cc b/bsputil/bsputil.cc index bf11bc7c..30b29b11 100644 --- a/bsputil/bsputil.cc +++ b/bsputil/bsputil.cc @@ -69,7 +69,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp) /* Count up the valid lumps */ numvalid = 0; for (auto &texture : texdata.textures) { - if (!texture.data.empty()) { + if (texture.data.size() > sizeof(dmiptex_t)) { numvalid++; } } @@ -85,7 +85,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp) /* Miptex data will follow the lump headers */ filepos = sizeof(header) + numvalid * sizeof(lump); for (auto &miptex : texdata.textures) { - if (miptex.data.empty()) + if (miptex.data.size() <= sizeof(dmiptex_t)) continue; lump.filepos = filepos; @@ -99,7 +99,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp) wadfile <= lump; } for (auto &miptex : texdata.textures) { - if (!miptex.data.empty()) { + if (miptex.data.size() > sizeof(dmiptex_t)) { miptex.stream_write(wadfile); } } diff --git a/common/imglib.cc b/common/imglib.cc index d586bbc8..19674fd8 100644 --- a/common/imglib.cc +++ b/common/imglib.cc @@ -119,7 +119,7 @@ struct q2_miptex_t auto stream_data() { return std::tie(name, width, height, offsets, animname, flags, contents, value); } }; -std::optional load_wal(const std::string &name, const fs::data &file, bool meta_only) +std::optional load_wal(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game) { memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary); stream >> endianness; @@ -159,7 +159,7 @@ Quake/Half Life MIP ============================================================================ */ -std::optional load_mip(const std::string &name, const fs::data &file, bool meta_only, const gamedef_t *game) +std::optional load_mip(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game) { memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary); stream >> endianness; @@ -273,7 +273,7 @@ struct targa_t LoadTGA ============= */ -std::optional load_tga(const std::string &name, const fs::data &file, bool meta_only) +std::optional load_tga(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game) { memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary); stream >> endianness; @@ -401,9 +401,9 @@ breakOut:; // texture cache std::unordered_map textures; -const texture *find(const std::string &str) +const texture *find(const std::string_view &str) { - auto it = textures.find(str); + auto it = textures.find(str.data()); if (it == textures.end()) { return nullptr; @@ -428,128 +428,40 @@ qvec3b calculate_average(const std::vector &pixels) return avg /= n; } -/* -============================================================================== -Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd) -============================================================================== -*/ -static void AddTextureName(const char *textureName) +std::tuple, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options) { - if (textures.find(textureName) != textures.end()) { - return; + fs::path prefix; + + if (game->id == GAME_QUAKE_II) { + prefix = "textures"; } - auto &tex = textures.emplace(textureName, texture{}).first->second; + for (auto &ext : img::extension_list) { + fs::path p = (prefix / name) += ext.suffix; - static constexpr struct - { - const char *name; - decltype(load_wal) *loader; - } supportedExtensions[] = {{"tga", load_tga}}; + if (auto pos = fs::where(p, options.filepriority.value() == settings::search_priority_t::LOOSE)) { + if (auto data = fs::load(pos)) { + std::optional texture; - // find wal first, since we'll use it for metadata - auto wal = fs::load("textures" / fs::path(textureName) += ".wal"); + switch (ext.id) { + case img::ext::TGA: + texture = img::load_tga(name.data(), data, meta_only, game); + break; + case img::ext::WAL: + texture = img::load_wal(name.data(), data, meta_only, game); + break; + case img::ext::MIP: + texture = img::load_mip(name.data(), data, meta_only, game); + break; + } - if (!wal) { - logging::funcprint("WARNING: can't find .wal for {}\n", textureName); - } else { - auto walTex = load_wal(textureName, wal, false); - - if (walTex) { - tex = std::move(*walTex); - } - } - - // now check for replacements - for (auto &ext : supportedExtensions) { - auto replacement = fs::load(("textures" / fs::path(textureName) += ".") += ext.name); - - if (!replacement) { - continue; - } - - auto replacementTex = ext.loader(textureName, replacement, false); - - if (replacementTex) { - tex.meta.width = replacementTex->meta.width; - tex.meta.height = replacementTex->meta.height; - tex.pixels = std::move(replacementTex->pixels); - break; - } - } - - tex.meta.averageColor = calculate_average(tex.pixels); -} - -// Load all of the referenced textures from the BSP texinfos into -// the texture cache. -static void LoadTextures(const mbsp_t *bsp) -{ - // gather all loadable textures... - for (auto &texinfo : bsp->texinfo) { - AddTextureName(texinfo.texture.data()); - } - - // gather textures used by _project_texture. - // FIXME: I'm sure we can resolve this so we don't parse entdata twice. - auto entdicts = EntData_Parse(bsp->dentdata); - for (auto &entdict : entdicts) { - if (entdict.get("classname").find("light") == 0) { - const auto &tex = entdict.get("_project_texture"); - if (!tex.empty()) { - AddTextureName(tex.c_str()); + if (texture) { + return {texture, pos, data}; + } } } } -} -// Load all of the paletted textures from the BSP into -// the texture cache. -// TODO: doesn't handle external wads... -static void ConvertTextures(const mbsp_t *bsp) -{ - if (!bsp->dtex.textures.size()) { - return; - } - - for (auto &miptex : bsp->dtex.textures) { - if (textures.find(miptex.name) != textures.end()) { - logging::funcprint("WARNING: Texture {} duplicated\n", miptex.name); - continue; - } - - // Add empty to keep texture index in case of load problems... - auto &tex = textures.emplace(miptex.name, texture{}).first->second; - - // FIXME: fs::load - if (miptex.data.empty()) { - logging::funcprint("WARNING: Texture {} is external\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 = calculate_average(tex.pixels); - } -} - -void load_textures(const mbsp_t *bsp) -{ - logging::print("--- {} ---\n", __func__); - - if (bsp->loadversion->game->id == GAME_QUAKE_II) { - LoadTextures(bsp); - } else if (bsp->dtex.textures.size() > 0) { - ConvertTextures(bsp); - } else { - logging::print("WARNING: failed to load or convert textures.\n"); - } + return {std::nullopt, {}, {}}; } } // namespace img diff --git a/include/common/bspfile.hh b/include/common/bspfile.hh index a2f4c9cc..4da01694 100644 --- a/include/common/bspfile.hh +++ b/include/common/bspfile.hh @@ -329,7 +329,9 @@ struct dmiptexlump_t next_offset = offsets[i + 1]; } - tex.stream_read(stream, next_offset - offset); + if (next_offset > offset) { + tex.stream_read(stream, next_offset - offset); + } } } diff --git a/include/common/imglib.hh b/include/common/imglib.hh index 7ccc8c48..26812845 100644 --- a/include/common/imglib.hh +++ b/include/common/imglib.hh @@ -34,13 +34,6 @@ enum class ext MIP }; -constexpr struct { const char *suffix; ext id; } extension_list[] = { - { ".tga", ext::TGA }, - { ".wal", ext::WAL }, - { ".mip", ext::MIP }, - { "", ext::MIP } -}; - extern std::vector palette; // Palette @@ -74,17 +67,25 @@ extern std::unordered_map &pixels); -const texture *find(const std::string &str); +const texture *find(const std::string_view &str); // Load wal -std::optional load_wal(const std::string &name, const fs::data &file, bool meta_only); +std::optional load_wal(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game); // Load TGA -std::optional load_tga(const std::string &name, const fs::data &file, bool meta_only); +std::optional load_tga(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game); // Load Quake/Half Life mip (raw data) -std::optional load_mip(const std::string &name, const fs::data &file, bool meta_only, const gamedef_t *game); +std::optional load_mip(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game); -// Pull in texture data from the BSP into the textures map -void load_textures(const mbsp_t *bsp); +// list of supported extensions and their loaders +constexpr struct { const char *suffix; ext id; decltype(load_wal) *loader; } extension_list[] = { + { ".tga", ext::TGA, load_tga }, + { ".wal", ext::WAL, load_wal }, + { ".mip", ext::MIP, load_mip }, + { "", ext::MIP, load_mip } +}; + +// Attempt to load a texture from the specified name. +std::tuple, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options); }; // namespace img diff --git a/include/common/settings.hh b/include/common/settings.hh index bcec5a6a..8e8ec1d6 100644 --- a/include/common/settings.hh +++ b/include/common/settings.hh @@ -848,6 +848,7 @@ public: setting_redirect quiet{this, {"quiet", "noverbose"}, {&nopercent, &nostat, &noprogress}, &logging_group, "suppress non-important messages (equivalent to -nopercent -nostat -noprogress)"}; setting_string basedir{this, "basedir", "", "dir_name", &game_group, "override the default game base directory"}; setting_enum filepriority{this, "filepriority", search_priority_t::LOOSE, { { "loose", search_priority_t::LOOSE }, { "archive", search_priority_t::ARCHIVE } }, &game_group, "which types of archives (folders/loose files or packed archives) are higher priority and chosen first for path searching" }; + setting_set paths{this, "path", "\"/path/to/folder\" ", &game_group, "additional paths or archives to add to the search path, mostly for loose files"}; setting_bool q2rtx{this, "q2rtx", false, &game_group, "adjust settings to best support Q2RTX"}; virtual void setParameters(int argc, const char **argv); diff --git a/include/qbsp/map.hh b/include/qbsp/map.hh index b0c21721..cb70d316 100644 --- a/include/qbsp/map.hh +++ b/include/qbsp/map.hh @@ -181,8 +181,6 @@ struct mapdata_t std::unordered_map> meta_cache; // load or fetch image meta associated with the specified name const std::optional &load_image_meta(const std::string_view &name); - // load image data for the specified name - std::tuple, fs::resolve_result, fs::data> load_image_data(const std::string_view &name, bool meta_only); // whether we had attempted loading texture stuff bool textures_loaded = false; diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 87033489..0f1420e6 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -222,7 +222,6 @@ public: this, "expand", false, &common_format_group, "write hull 1 expanded brushes to expanded.map for debugging"}; setting_wadpathset wadpaths{this, {"wadpath", "xwadpath"}, &map_development_group, "add a path to the wad search paths; wads found in xwadpath's will not be embedded, otherwise they will be embedded (if not -notex)"}; - setting_set paths{this, "path", "\"/path/to/folder\" ", &map_development_group, "additional paths or archives to add to the search path, mostly for loose files"}; setting_bool notriggermodels{this, "notriggermodels", false, &common_format_group, "for supported game code only: triggers will not write a model\nout, and will instead just write out their mins/maxs."}; setting_set aliasdefs{this, "aliasdef", "\"path/to/file.def\" ", &map_development_group, "path to an alias definition file, which can transform entities in the .map into other entities."}; diff --git a/light/light.cc b/light/light.cc index e13258d3..6c7e99ca 100644 --- a/light/light.cc +++ b/light/light.cc @@ -863,6 +863,127 @@ static inline void WriteNormals(const mbsp_t &bsp, bspdata_t &bspdata) } } +/* +============================================================================== +Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd) +============================================================================== +*/ +static void AddTextureName(const char *textureName, const mbsp_t *bsp) +{ + if (img::find(textureName)) { + return; + } + + 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"); + + if (!wal) { + logging::funcprint("WARNING: can't find .wal for {}\n", textureName); + } else { + auto walTex = img::load_wal(textureName, wal, false, bsp->loadversion->game); + + if (walTex) { + tex = std::move(*walTex); + } + } + + // 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); + } + + tex.meta.averageColor = img::calculate_average(tex.pixels); +} + +// Load all of the referenced textures from the BSP texinfos into +// the texture cache. +static void LoadTextures(const mbsp_t *bsp) +{ + // gather all loadable textures... + for (auto &texinfo : bsp->texinfo) { + AddTextureName(texinfo.texture.data(), bsp); + } + + // gather textures used by _project_texture. + // FIXME: I'm sure we can resolve this so we don't parse entdata twice. + auto entdicts = EntData_Parse(bsp->dentdata); + for (auto &entdict : entdicts) { + if (entdict.get("classname").find("light") == 0) { + const auto &tex = entdict.get("_project_texture"); + if (!tex.empty()) { + AddTextureName(tex.c_str(), bsp); + } + } + } +} + +// Load all of the paletted textures from the BSP into +// the texture cache. +static void ConvertTextures(const mbsp_t *bsp) +{ + if (!bsp->dtex.textures.size()) { + return; + } + + for (auto &miptex : bsp->dtex.textures) { + if (img::find(miptex.name)) { + logging::funcprint("WARNING: Texture {} duplicated\n", miptex.name); + continue; + } + + // 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); + } +} + +void load_textures(const mbsp_t *bsp) +{ + logging::print("--- {} ---\n", __func__); + + for (auto &path : options.paths.values()) { + fs::addArchive(path, true); + } + + if (bsp->loadversion->game->id == GAME_QUAKE_II) { + LoadTextures(bsp); + } else if (bsp->dtex.textures.size() > 0) { + ConvertTextures(bsp); + } else { + logging::print("WARNING: failed to load or convert textures.\n"); + } +} + /* * ================== * main @@ -912,7 +1033,7 @@ int light_main(int argc, const char **argv) options.bouncescale.setValue(1.5f); } - img::load_textures(&bsp); + load_textures(&bsp); LoadExtendedTexinfoFlags(source, &bsp); LoadEntities(options, &bsp); diff --git a/qbsp/map.cc b/qbsp/map.cc index b1ba9f8b..2954de47 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -43,43 +43,6 @@ mapdata_t map; -std::tuple, fs::resolve_result, fs::data> mapdata_t::load_image_data(const std::string_view &name, bool meta_only) -{ - fs::path prefix; - - if (options.target_game->id == GAME_QUAKE_II) { - prefix = "textures"; - } - - for (auto &ext : img::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)) { - std::optional texture; - - switch (ext.id) { - case img::ext::TGA: - texture = img::load_tga(name.data(), data, meta_only); - break; - case img::ext::WAL: - texture = img::load_wal(name.data(), data, meta_only); - break; - case img::ext::MIP: - texture = img::load_mip(name.data(), data, meta_only, options.target_game); - break; - } - - if (texture) { - return {texture, pos, data}; - } - } - } - } - - return {std::nullopt, {}, {}}; -} - const std::optional &mapdata_t::load_image_meta(const std::string_view &name) { static std::optional nullmeta = std::nullopt; @@ -89,7 +52,7 @@ const std::optional &mapdata_t::load_image_meta(const std::st return it->second; } - auto [texture, _0, _1] = load_image_data(name, true); + auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options); if (texture) { return meta_cache.emplace(name, texture->meta).first->second; diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index e2267939..819c99c1 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -1131,44 +1131,49 @@ static void CreateHulls(void) static void LoadTextureData() { for (size_t i = 0; i < map.miptex.size(); i++) { - auto [tex, pos, file] = map.load_image_data(map.miptex[i].name, true); - - if (!tex) { - if (pos.archive) { - logging::print("WARNING: unable to load texture {} in archive {}\n", map.miptex[i].name, pos.archive->pathname); - } else { - logging::print("WARNING: unable to find texture {}\n", map.miptex[i].name); - } - continue; - } - + // always fill the name even if we can't find it auto &miptex = map.bsp.dtex.textures[i]; miptex.name = map.miptex[i].name; - miptex.width = tex->meta.width; - miptex.height = tex->meta.height; - // only mips can be embedded directly - if (!pos.archive->external && tex->meta.extension == img::ext::MIP) { - miptex.data = std::move(file.value()); - } else { - // construct fake data that solely contains the header. - miptex.data.resize(sizeof(dmiptex_t)); + { + auto [tex, pos, file] = img::load_texture(map.miptex[i].name, true, options.target_game, options); - dmiptex_t header {}; - if (miptex.name.size() >= 16) { - logging::print("WARNING: texture {} name too long for Quake miptex\n", miptex.name); - std::copy_n(miptex.name.begin(), 15, header.name.begin()); + if (!tex) { + if (pos.archive) { + logging::print("WARNING: unable to load texture {} in archive {}\n", map.miptex[i].name, pos.archive->pathname); + } else { + logging::print("WARNING: unable to find texture {}\n", map.miptex[i].name); + } } else { - std::copy(miptex.name.begin(), miptex.name.end(), header.name.begin()); - } - - header.width = miptex.width; - header.height = miptex.height; - header.offsets = { -1, -1, -1, -1 }; + miptex.width = tex->meta.width; + miptex.height = tex->meta.height; - memstream stream(miptex.data.data(), miptex.data.size(), std::ios_base::out | std::ios_base::binary); - stream <= header; + // only mips can be embedded directly + if (!pos.archive->external && tex->meta.extension == img::ext::MIP) { + miptex.data = std::move(file.value()); + continue; + } + } } + + // fall back to when we can't load the image. + // construct fake data that solely contains the header. + miptex.data.resize(sizeof(dmiptex_t)); + + dmiptex_t header {}; + if (miptex.name.size() >= 16) { + logging::print("WARNING: texture {} name too long for Quake miptex\n", miptex.name); + std::copy_n(miptex.name.begin(), 15, header.name.begin()); + } else { + std::copy(miptex.name.begin(), miptex.name.end(), header.name.begin()); + } + + header.width = miptex.width; + header.height = miptex.height; + header.offsets = { -1, -1, -1, -1 }; + + memstream stream(miptex.data.data(), miptex.data.size(), std::ios_base::out | std::ios_base::binary); + stream <= header; } }