/* 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 { path qdir, gamedir, basedir; 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) : archive_like(pathname), 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{'W', 'A', 'D', '2'}; uint32_t numlumps; uint32_t infotableofs; auto stream_data() { return std::tie(identification, numlumps, infotableofs); } }; struct wad_lump_header { uint32_t filepos; uint32_t disksize; uint32_t size; // uncompressed uint8_t type; uint8_t compression; uint8_t pad1, pad2; std::array name; // must be null terminated auto stream_data() { return std::tie(filepos, disksize, size, type, compression, pad1, pad2, name); } }; std::unordered_map, case_insensitive_hash, case_insensitive_equal> files; inline wad_archive(const path &pathname) : archive_like(pathname), wadstream(pathname, std::ios_base::in | std::ios_base::binary) { wadstream >> endianness; wad_header header; wadstream >= header; if (header.identification != std::array{'W', 'A', 'D', '2'}) { throw std::runtime_error("Bad magic"); } files.reserve(header.numlumps); wadstream.seekg(header.infotableofs); for (size_t i = 0; i < header.infotableofs; 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(""); std::list> archives, directories; /** It's possible to compile quake 1/hexen 2 maps without a qdir */ void clear() { qdir.clear(); gamedir.clear(); basedir.clear(); archives.clear(); directories.clear(); } std::shared_ptr addArchive(const path &p) { if (p.empty()) { logging::funcprint("WARNING: can't add empty archive path\n"); return nullptr; } if (!exists(p)) { logging::funcprint("WARNING: '{}' not found\n", p); return nullptr; } if (is_directory(p)) { for (auto &dir : directories) { if (equivalent(dir->pathname, p)) { return dir; } } auto &arch = directories.emplace_front(std::make_shared(p)); 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)); 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)); 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; } resolve_result where(const path &p) { // check absolute + relative first if (exists(p)) { return {absrel_dir, p}; } // check direct archive loading if (auto paths = splitArchivePath(p)) { auto arch = addArchive(paths.archive); if (arch) { return {arch, paths.filename}; } } // absolute doesn't make sense for other load types if (p.is_absolute()) { return {}; } // check directories for (auto &dir : directories) { if (dir->contains(p)) { return {dir, p}; } } // check archives for (auto &arch : archives) { if (arch->contains(p)) { return {arch, p}; } } return {}; } data load(const path &p) { auto [arch, filename] = where(p); if (!arch) { return std::nullopt; } logging::print(logging::flag::VERBOSE, "Loaded '{}' from archive '{}'\n", filename, arch->pathname); return arch->load(filename); } } // namespace fs