Files
OpenRA3/third_party/libra3assets/src/bytes.cppm
T
EnderTheCoder 23f8be394c feat(assets): read retail assets through the vendored libra3assets library
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.
2026-09-29 23:58:13 +08:00

221 lines
9.9 KiB
C++

/**
* Bounds-checked little-/big-endian byte readers and writers.
*
* Red Alert 3's asset formats are little-endian, with one notable exception:
* the `BIG4` index stores its counts, offsets and sizes big-endian. Both byte
* orders are provided here. Every read is bounds-checked and throws
* `format_error` rather than reading past the end of the buffer.
*/
export module ra3.assets:bytes;
import std;
import :error;
export namespace ra3::assets {
/** A cursor over a read-only byte range with checked accessors. */
class byte_reader {
public:
explicit byte_reader(std::span<const std::byte> data)
: data_(data) {
}
[[nodiscard]] auto size() const -> std::size_t { return this->data_.size(); }
[[nodiscard]] auto position() const -> std::size_t { return this->pos_; }
[[nodiscard]] auto remaining() const -> std::size_t { return this->data_.size() - this->pos_; }
[[nodiscard]] auto empty() const -> bool { return this->pos_ >= this->data_.size(); }
auto seek(std::size_t at) -> void {
if (at > this->data_.size()) throw format_error("seek past end of buffer");
this->pos_ = at;
}
auto skip(std::size_t count) -> void {
this->require(count);
this->pos_ += count;
}
[[nodiscard]] auto read_u8() -> std::uint8_t {
this->require(1);
return std::to_integer<std::uint8_t>(this->data_[this->pos_++]);
}
[[nodiscard]] auto read_bool() -> bool { return this->read_u8() != 0U; }
[[nodiscard]] auto read_u16() -> std::uint16_t {
return static_cast<std::uint16_t>(this->read_le(2));
}
[[nodiscard]] auto read_u24() -> std::uint32_t { return this->read_le(3); }
[[nodiscard]] auto read_u32() -> std::uint32_t { return this->read_le(4); }
[[nodiscard]] auto read_u64() -> std::uint64_t { return this->read_le(8); }
[[nodiscard]] auto read_i16() -> std::int16_t { return static_cast<std::int16_t>(this->read_u16()); }
[[nodiscard]] auto read_i32() -> std::int32_t { return static_cast<std::int32_t>(this->read_u32()); }
[[nodiscard]] auto read_f32() -> float { return std::bit_cast<float>(this->read_u32()); }
[[nodiscard]] auto read_be_u16() -> std::uint16_t { return static_cast<std::uint16_t>(this->read_be(2)); }
[[nodiscard]] auto read_be_u32() -> std::uint32_t { return this->read_be(4); }
/** A view of the next `count` bytes; the cursor advances past them. */
[[nodiscard]] auto read_bytes(std::size_t count) -> std::span<const std::byte> {
this->require(count);
const auto view = this->data_.subspan(this->pos_, count);
this->pos_ += count;
return view;
}
/** `count` bytes decoded as Latin-1 (one byte per character). */
[[nodiscard]] auto read_ascii(std::size_t count) -> std::string {
const auto view = this->read_bytes(count);
return std::string{reinterpret_cast<const char *>(view.data()), view.size()};
}
/** A `u16`-length-prefixed ASCII string. */
[[nodiscard]] auto read_u16_prefixed_ascii() -> std::string { return this->read_ascii(this->read_u16()); }
/** A `u16`-length-prefixed string, decoded as UTF-8 (ASCII-compatible). */
[[nodiscard]] auto read_u16_prefixed_ascii_as_utf8() -> std::string { return this->read_u16_prefixed_ascii(); }
/** A `u16`-length-prefixed UTF-16LE string (length counted in code units). */
[[nodiscard]] auto read_u16_prefixed_utf16() -> std::u16string {
const auto count = this->read_u16();
this->require(static_cast<std::size_t>(count) * 2U);
std::u16string text(count, u'\0');
for (std::uint16_t i = 0; i < count; ++i) text[i] = static_cast<char16_t>(this->read_u16());
return text;
}
/** A NUL-terminated ASCII string; the cursor stops just past the NUL. */
[[nodiscard]] auto read_cstring() -> std::string {
std::string text;
while (this->pos_ < this->data_.size()) {
const auto ch = static_cast<char>(this->read_u8());
if (ch == '\0') break;
text.push_back(ch);
}
return text;
}
private:
auto require(std::size_t count) const -> void {
if (this->pos_ + count > this->data_.size()) throw format_error("unexpected end of buffer");
}
[[nodiscard]] auto read_le(std::size_t width) -> std::uint64_t {
this->require(width);
std::uint64_t value = 0;
for (std::size_t i = 0; i < width; ++i) value |= static_cast<std::uint64_t>(std::to_integer<std::uint8_t>(this->data_[this->pos_ + i])) << (8U * i);
this->pos_ += width;
return value;
}
[[nodiscard]] auto read_be(std::size_t width) -> std::uint64_t {
this->require(width);
std::uint64_t value = 0;
for (std::size_t i = 0; i < width; ++i) value = (value << 8U) | static_cast<std::uint64_t>(std::to_integer<std::uint8_t>(this->data_[this->pos_ + i]));
this->pos_ += width;
return value;
}
std::span<const std::byte> data_;
std::size_t pos_ = 0;
};
/** An append-only little-endian byte buffer. */
class byte_writer {
public:
[[nodiscard]] auto size() const -> std::size_t { return this->buffer_.size(); }
[[nodiscard]] auto data() const -> std::span<const std::byte> { return this->buffer_; }
[[nodiscard]] auto take() -> std::vector<std::byte> { return std::move(this->buffer_); }
auto write_u8(std::uint8_t value) -> void { this->buffer_.push_back(static_cast<std::byte>(value)); }
auto write_bool(bool value) -> void { this->write_u8(value ? 1U : 0U); }
auto write_u16(std::uint16_t value) -> void { this->write_le(value, 2); }
auto write_u24(std::uint32_t value) -> void { this->write_le(value, 3); }
auto write_u32(std::uint32_t value) -> void { this->write_le(value, 4); }
auto write_u64(std::uint64_t value) -> void { this->write_le(value, 8); }
auto write_i16(std::int16_t value) -> void { this->write_u16(static_cast<std::uint16_t>(value)); }
auto write_i32(std::int32_t value) -> void { this->write_u32(static_cast<std::uint32_t>(value)); }
auto write_f32(float value) -> void { this->write_u32(std::bit_cast<std::uint32_t>(value)); }
auto write_bytes(std::span<const std::byte> bytes) -> void { this->buffer_.insert(this->buffer_.end(), bytes.begin(), bytes.end()); }
auto write_ascii(std::string_view text) -> void {
for (const auto ch: text) this->buffer_.push_back(static_cast<std::byte>(static_cast<unsigned char>(ch)));
}
auto write_u16_prefixed_ascii(std::string_view text) -> void {
this->write_u16(static_cast<std::uint16_t>(text.size()));
this->write_ascii(text);
}
auto write_u16_prefixed_utf16(std::u16string_view text) -> void {
this->write_u16(static_cast<std::uint16_t>(text.size()));
for (const auto unit: text) {
this->write_u16(static_cast<std::uint16_t>(unit));
}
}
/** Overwrite a previously written `u32` (used to backpatch sizes). */
auto patch_u32(std::size_t offset, std::uint32_t value) -> void {
if (offset + 4U > this->buffer_.size()) throw format_error("patch offset past end of buffer");
for (std::size_t i = 0; i < 4U; ++i) this->buffer_[offset + i] = static_cast<std::byte>((value >> (8U * i)) & 0xFFU);
}
private:
auto write_le(std::uint64_t value, std::size_t width) -> void {
for (std::size_t i = 0; i < width; ++i) this->buffer_.push_back(static_cast<std::byte>((value >> (8U * i)) & 0xFFU));
}
std::vector<std::byte> buffer_;
};
/** Convenience: encode a UTF-8 string (ASCII subset) as UTF-16LE code units. */
[[nodiscard]] inline auto to_utf16(std::string_view text) -> std::u16string {
std::u16string out;
out.reserve(text.size());
for (const auto ch: text) out.push_back(static_cast<char16_t>(static_cast<unsigned char>(ch)));
return out;
}
/** Convenience: narrow a UTF-16 string that is known to be within Latin-1. */
[[nodiscard]] inline auto to_ascii(std::u16string_view text) -> std::string {
std::string out;
out.reserve(text.size());
for (const auto unit: text) out.push_back(static_cast<char>(unit & 0xFFU));
return out;
}
/** Read a whole file as bytes. */
[[nodiscard]] inline auto read_file(const std::filesystem::path &path) -> std::vector<std::byte> {
std::ifstream in(path, std::ios::binary);
if (!in) throw format_error("cannot open file: " + path.string());
in.seekg(0, std::ios::end);
const auto end = in.tellg();
if (end < 0) throw format_error("cannot size file: " + path.string());
std::vector<std::byte> bytes(static_cast<std::size_t>(end));
in.seekg(0, std::ios::beg);
if (!bytes.empty()) in.read(reinterpret_cast<char *>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
return bytes;
}
/** Write bytes to a file, creating parent directories as needed. */
inline auto write_file(const std::filesystem::path &path, std::span<const std::byte> bytes) -> void {
std::error_code ec;
if (!path.parent_path().empty()) std::filesystem::create_directories(path.parent_path(), ec);
std::ofstream out(path, std::ios::binary);
if (!out) throw format_error("cannot write file: " + path.string());
out.write(reinterpret_cast<const char *>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!out) throw format_error("cannot write file: " + path.string());
}
} // namespace ra3::assets