export module ra3.fs; import std; export import ra3.core; import ra3.assets; /** * Reading of the retail game's on-disk assets. * * Red Alert 3 ships its data in `BIG4` archives under `\Data`, with * individual payloads compressed by EA's RefPack codec. The container and codec * are implemented by the vendored `libra3assets` (`ra3.assets`, a sibling of * `libenderlog`); this module is the thin adapter OpenRA3's loader talks to, so * the format knowledge lives in one place. * * No game data is ever written into the repository; callers point the loader at * their local install (`--game-dir` / `RA3_GAME_DIR`, default `C:\Red Alert 3`). */ export namespace ra3::fs { using ra3::core::uint32; using ra3::core::uint8; using ra3::core::usize; /** Thrown when a `BIG4` archive is malformed or an entry is missing. */ class archive_error : public std::runtime_error { public: using std::runtime_error::runtime_error; }; /** Thrown when a RefPack stream is malformed. */ class refpack_error : public std::runtime_error { public: using std::runtime_error::runtime_error; }; inline constexpr std::array big_magic{'B', 'I', 'G', '4'}; inline constexpr uint8 refpack_mask = 0x3EU; inline constexpr uint8 refpack_magic2 = 0xFBU; /** One file inside a `BIG4` archive. */ struct big_entry { std::string name; uint32 offset = 0; uint32 size = 0; }; namespace detail { /** View a `uint8` range as bytes, the currency of `libra3assets`. */ [[nodiscard]] inline auto as_bytes(std::span data) -> std::span { return std::as_bytes(data); } /** Copy a `libra3assets` byte buffer into OpenRA3's `uint8` vector. */ [[nodiscard]] inline auto to_u8(std::vector bytes) -> std::vector { std::vector out(bytes.size()); if (!bytes.empty()) std::memcpy(out.data(), bytes.data(), bytes.size()); return out; } } /** True when `data` starts with a RefPack header (`0b??010000`, `0xFB`). */ [[nodiscard]] inline auto is_refpack(std::span data) -> bool { return ra3::assets::is_refpack(detail::as_bytes(data)); } /** * Decompress an EA RefPack stream. * * @param data Compressed stream, starting at the header byte. * @return The decompressed bytes. * @throws refpack_error if the stream is malformed or the length disagrees. */ [[nodiscard]] inline auto refpack_decompress(std::span data) -> std::vector { try { return detail::to_u8(ra3::assets::refpack_decompress(detail::as_bytes(data))); } catch (const ra3::assets::refpack_error &error) { throw refpack_error(error.what()); } } /** Decompress `data` when it is RefPack, otherwise copy it unchanged. */ [[nodiscard]] inline auto maybe_decompress(std::span data) -> std::vector { if (!is_refpack(data)) return {data.begin(), data.end()}; return refpack_decompress(data); } /** * Read the declared output size from a RefPack header without decompressing. * * @throws refpack_error if the stream is malformed. */ [[nodiscard]] inline auto refpack_output_size(std::span data) -> uint32 { try { return ra3::assets::refpack_output_size(detail::as_bytes(data)); } catch (const ra3::assets::refpack_error &error) { throw refpack_error(error.what()); } } [[nodiscard]] constexpr auto read_be32(const uint8 *p) -> uint32 { return (static_cast(p[0]) << 24U) | (static_cast(p[1]) << 16U) | (static_cast(p[2]) << 8U) | static_cast(p[3]); } [[nodiscard]] constexpr auto read_le32(const uint8 *p) -> uint32 { return static_cast(p[0]) | (static_cast(p[1]) << 8U) | (static_cast(p[2]) << 16U) | (static_cast(p[3]) << 24U); } /** * A parsed `BIG4` archive. * * The index and payloads are held by a `ra3::assets::big_archive`; this * adapter exposes the OpenRA3-facing surface (`big_entry`, `uint8` buffers) * over it. Payloads are RefPack-decompressed on request. */ class big_archive { public: /** * Parse a `BIG4` archive. * * @param path Archive path. * @throws archive_error if the file is missing, not `BIG4`, or truncated. */ [[nodiscard]] static auto open(const std::filesystem::path &path) -> big_archive { try { return big_archive{ra3::assets::big_archive::open(path), path}; } catch (const ra3::assets::asset_error &error) { throw archive_error(error.what()); } } [[nodiscard]] auto path() const -> const std::filesystem::path & { return path_; } [[nodiscard]] auto entries() const -> const std::vector & { return entries_; } [[nodiscard]] auto size() const -> usize { return entries_.size(); } [[nodiscard]] auto contains(std::string_view name) const -> bool { return archive_.contains(name); } /** Entry names whose path contains `needle`, in index order. */ [[nodiscard]] auto find(std::string_view needle) const -> std::vector { std::vector matches; for (const auto &entry: entries_) { if (entry.name.find(needle) != std::string::npos) matches.push_back(&entry); } return matches; } /** * Read an entry's payload. * * @param name Entry name (backslash-separated, case-sensitive). * @param decompress RefPack-decompress the payload when true. * @throws archive_error if the entry is missing or unreadable. */ [[nodiscard]] auto read(std::string_view name, bool decompress = true) const -> std::vector { try { return detail::to_u8(archive_.read(name, decompress)); } catch (const ra3::assets::refpack_error &error) { throw refpack_error(error.what()); } catch (const ra3::assets::asset_error &error) { throw archive_error(error.what()); } } /** Read the first `count` stored bytes of an entry (no decompression). */ [[nodiscard]] auto read_prefix(std::string_view name, usize count) const -> std::vector { try { return detail::to_u8(archive_.read_prefix(name, count)); } catch (const ra3::assets::asset_error &error) { throw archive_error(error.what()); } } private: big_archive(ra3::assets::big_archive archive, std::filesystem::path path) : archive_(std::move(archive)), path_(std::move(path)) { entries_.reserve(archive_.size()); for (const auto &entry: archive_.entries()) entries_.push_back({entry.name, entry.offset, entry.size}); } ra3::assets::big_archive archive_; std::filesystem::path path_; std::vector entries_; }; /** * Locate a Red Alert 3 installation. * * Resolution order: the explicit argument, then `$RA3_GAME_DIR`, then the * default `C:\Red Alert 3`. A directory qualifies only if it has a `Data` * subdirectory. * * @return The install root, or `std::nullopt` when none is found. */ [[nodiscard]] inline auto find_game_dir(const std::optional &explicit_dir = std::nullopt) -> std::optional { return ra3::assets::find_game_dir(explicit_dir); } }