/* 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. */ #include "common/cmdlib.hh" #include "common/fs.hh" #include #include #include #include #include #include #include namespace fs { struct directory_archive : archive_like { using archive_like::archive_like; bool contains(const path &filename) override { return exists(!pathname.empty() ? (pathname / filename) : filename); } data load(const path &filename) override { path p = !pathname.empty() ? (pathname / filename) : filename; if (!exists(p)) { return std::nullopt; } uintmax_t size = file_size(p); std::ifstream stream(p, std::ios_base::in | std::ios_base::binary); std::vector data(size); stream.read(reinterpret_cast(data.data()), size); return data; } }; struct pak_archive : archive_like { std::ifstream pakstream; struct pak_header { std::array magic; uint32_t offset; uint32_t size; auto stream_data() { return std::tie(magic, offset, size); } }; struct pak_file { std::array name; uint32_t offset; uint32_t size; auto stream_data() { return std::tie(name, offset, size); } }; std::unordered_map, case_insensitive_hash, case_insensitive_equal> files; inline pak_archive(const path &pathname, bool external) : archive_like(pathname, external), pakstream(pathname, std::ios_base::in | std::ios_base::binary) { pakstream >> endianness; pak_header header; pakstream >= header; if (header.magic != std::array{'P', 'A', 'C', 'K'}) { throw std::runtime_error("Bad magic"); } size_t totalFiles = header.size / sizeof(pak_file); files.reserve(totalFiles); pakstream.seekg(header.offset); for (size_t i = 0; i < totalFiles; i++) { pak_file file; pakstream >= file; files[file.name.data()] = std::make_tuple(file.offset, file.size); } } bool contains(const path &filename) override { return files.find(filename.generic_string()) != files.end(); } data load(const path &filename) override { auto it = files.find(filename.generic_string()); if (it == files.end()) { return std::nullopt; } pakstream.seekg(std::get<0>(it->second)); uintmax_t size = std::get<1>(it->second); std::vector data(size); pakstream.read(reinterpret_cast(data.data()), size); return data; } }; struct wad_archive : archive_like { std::ifstream wadstream; // WAD Format struct wad_header { std::array identification; uint32_t numlumps; uint32_t infotableofs; auto stream_data() { return std::tie(identification, numlumps, infotableofs); } }; static constexpr std::array wad2_ident = {'W', 'A', 'D', '2'}; static constexpr std::array wad3_ident = {'W', 'A', 'D', '3'}; struct wad_lump_header { uint32_t filepos; uint32_t disksize; uint32_t size; // uncompressed uint8_t type; uint8_t compression; padding<2> pad; std::array name; // must be null terminated auto stream_data() { return std::tie(filepos, disksize, size, type, compression, pad, name); } }; std::unordered_map, case_insensitive_hash, case_insensitive_equal> files; inline wad_archive(const path &pathname, bool external) : archive_like(pathname, external), wadstream(pathname, std::ios_base::in | std::ios_base::binary) { wadstream >> endianness; wad_header header; wadstream >= header; if (header.identification != wad2_ident && header.identification != wad3_ident) { throw std::runtime_error("Bad magic"); } files.reserve(header.numlumps); wadstream.seekg(header.infotableofs); for (size_t i = 0; i < header.numlumps; i++) { wad_lump_header file; wadstream >= file; files[file.name.data()] = std::make_tuple(file.filepos, file.disksize); } } bool contains(const path &filename) override { return files.find(filename.generic_string()) != files.end(); } data load(const path &filename) override { auto it = files.find(filename.generic_string()); if (it == files.end()) { return std::nullopt; } wadstream.seekg(std::get<0>(it->second)); uintmax_t size = std::get<1>(it->second); std::vector data(size); wadstream.read(reinterpret_cast(data.data()), size); return data; } }; static std::shared_ptr absrel_dir = std::make_shared("", false); std::list> archives, directories; /** It's possible to compile quake 1/hexen 2 maps without a qdir */ void clear() { archives.clear(); directories.clear(); } inline std::shared_ptr addArchiveInternal(const path &p, bool external) { if (is_directory(p)) { for (auto &dir : directories) { if (equivalent(dir->pathname, p)) { return dir; } } auto &arch = directories.emplace_front(std::make_shared(p, external)); logging::print(logging::flag::VERBOSE, "Added directory '{}'\n", p); return arch; } else { for (auto &arch : archives) { if (equivalent(arch->pathname, p)) { return arch; } } auto ext = p.extension(); try { if (string_iequals(ext.generic_string(), ".pak")) { auto &arch = archives.emplace_front(std::make_shared(p, external)); auto &pak = reinterpret_cast &>(arch); logging::print(logging::flag::VERBOSE, "Added pak '{}' with {} files\n", p, pak->files.size()); return arch; } else if (string_iequals(ext.generic_string(), ".wad")) { auto &arch = archives.emplace_front(std::make_shared(p, external)); auto &wad = reinterpret_cast &>(arch); logging::print(logging::flag::VERBOSE, "Added wad '{}' with {} lumps\n", p, wad->files.size()); return arch; } else { logging::funcprint("WARNING: no idea what to do with archive '{}'\n", p); } } catch (std::exception e) { logging::funcprint("WARNING: unable to load archive '{}': {}\n", p, e.what()); } } return nullptr; } std::shared_ptr addArchive(const path &p, bool external) { if (p.empty()) { logging::funcprint("WARNING: can't add empty archive path\n"); return nullptr; } if (!exists(p)) { // check relative path filename = p.filename(); if (!exists(filename)) { logging::funcprint("WARNING: archive '{}' not found\n", p); return nullptr; } return addArchiveInternal(filename, external); } return addArchiveInternal(p, external); } resolve_result where(const path &p, bool prefer_loose) { // check direct archive loading first; it can't ever // be loose, so there's no sense for it to be in the // loop below if (auto paths = splitArchivePath(p)) { if (auto arch = addArchive(paths.archive)) { return {arch, paths.filename}; } } for (int32_t pass = 0; pass < 2; pass++) { if (prefer_loose != !!pass) { // check absolute + relative if (exists(p)) { return {absrel_dir, p}; } } else if (!p.is_absolute()) { // absolute doesn't make sense for other load types for (int32_t archive_pass = 0; archive_pass < 2; archive_pass++) { // check directories & archives, depending on whether // we want loose first or not for (auto &dir : (prefer_loose != !!archive_pass) ? directories : archives) { if (dir->contains(p)) { return {dir, p}; } } } } } return {}; } data load(const resolve_result &pos) { if (!pos) { return std::nullopt; } logging::print(logging::flag::VERBOSE, "Loaded '{}' from archive '{}'\n", pos.filename, pos.archive->pathname); return pos.archive->load(pos.filename); } } // namespace fs