Vendor libra3assets (C++26 modules: BIG4, RefPack, BinaryAsset, CSF, CkMp map) under third_party/ and add the ra3_assets target. ra3.fs, ra3.map and ra3.terrain become thin adapters over it: - ra3.fs delegates RefPack and the BIG4 index/payload reads (index-only, payloads read on demand). - ra3.map decodes ObjectsList and CSF via map_document/csf_table; starts come from player_starts(), replacing the off-by-one whole-buffer scan. - ra3.terrain takes the CkMp container and HeightMapData from map_document, with BlendTileData (not modelled by the library) parsed from the chunk payload. Adds an opt-in real-asset check (OPENRA3_TEST_ASSETS) plus the library's own unit suite as ra3assets_unit.
192 lines
7.7 KiB
C++
192 lines
7.7 KiB
C++
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 `<install>\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<uint8, 4> 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<const uint8> data) -> std::span<const std::byte> { return std::as_bytes(data); }
|
|
|
|
/** Copy a `libra3assets` byte buffer into OpenRA3's `uint8` vector. */
|
|
[[nodiscard]] inline auto to_u8(std::vector<std::byte> bytes) -> std::vector<uint8> {
|
|
std::vector<uint8> 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<const uint8> 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<const uint8> data) -> std::vector<uint8> {
|
|
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<const uint8> data) -> std::vector<uint8> {
|
|
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<const uint8> 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<uint32>(p[0]) << 24U) | (static_cast<uint32>(p[1]) << 16U) | (static_cast<uint32>(p[2]) << 8U) | static_cast<uint32>(p[3]);
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto read_le32(const uint8 *p) -> uint32 {
|
|
return static_cast<uint32>(p[0]) | (static_cast<uint32>(p[1]) << 8U) | (static_cast<uint32>(p[2]) << 16U) | (static_cast<uint32>(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<big_entry> & { 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<const big_entry *> {
|
|
std::vector<const big_entry *> 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<uint8> {
|
|
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<uint8> {
|
|
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<big_entry> 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<std::filesystem::path> &explicit_dir = std::nullopt) -> std::optional<std::filesystem::path> {
|
|
return ra3::assets::find_game_dir(explicit_dir);
|
|
}
|
|
}
|