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.
221 lines
9.9 KiB
C++
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
|