Files
OpenRA3/third_party/libra3assets/src/refpack.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

285 lines
14 KiB
C++

/**
* EA's RefPack compression codec.
*
* Red Alert 3 compresses individual `BIG4` payloads and every SAGE `.map` with
* RefPack (the `10 FB` stream shared across EA titles). This partition decodes
* and encodes that stream. The decoder is a port of the reference used across
* the project (`ra3tools/ra3_big.py`, `OpenRA3`'s `ra3.fs`); the encoder is a
* greedy LZ77 matcher that emits only canonical tokens, so anything it produces
* is readable by the same decoder (and by the game).
*/
export module ra3.assets:refpack;
import std;
import :bytes;
import :error;
export namespace ra3::assets {
/** Max back-reference distance (the 17-bit RefPack window). */
inline constexpr std::size_t refpack_window = 1U << 17U;
/** True when `data` starts with a RefPack header (`0b??010000`, `0xFB`). */
[[nodiscard]] inline auto is_refpack(std::span<const std::byte> data) -> bool {
return data.size() >= 2U && (std::to_integer<std::uint8_t>(data[0]) & 0x3EU) == 0x10U && std::to_integer<std::uint8_t>(data[1]) == 0xFBU;
}
/** Read the declared output size from a RefPack header without decompressing. */
[[nodiscard]] inline auto refpack_output_size(std::span<const std::byte> data) -> std::uint32_t {
if (!is_refpack(data)) throw refpack_error("not a RefPack stream");
std::size_t pos = 0;
const auto header = std::to_integer<std::uint8_t>(data[pos++]);
const bool large_files = (header & 0x80U) != 0U;
const bool compressed_size_present = (header & 0x01U) != 0U;
pos++; // 0xFB
const std::size_t size_bytes = large_files ? 4U : 3U;
const auto read_size = [&]() -> std::uint32_t {
std::uint32_t value = 0;
for (std::size_t i = 0; i < size_bytes; ++i) {
if (pos >= data.size()) throw refpack_error("truncated RefPack size field");
value = (value << 8U) | std::to_integer<std::uint8_t>(data[pos++]);
}
return value;
};
if (compressed_size_present) (void) read_size();
return read_size();
}
/**
* Decompress an EA RefPack stream.
*
* @throws refpack_error if the stream is malformed or the length disagrees.
*/
[[nodiscard]] inline auto refpack_decompress(std::span<const std::byte> data) -> std::vector<std::byte> {
if (!is_refpack(data)) throw refpack_error("not a RefPack stream");
std::size_t pos = 0;
const auto header = std::to_integer<std::uint8_t>(data[pos++]);
const bool large_files = (header & 0x80U) != 0U;
const bool compressed_size_present = (header & 0x01U) != 0U;
pos++; // 0xFB
const std::size_t size_bytes = large_files ? 4U : 3U;
const auto read_size = [&]() -> std::uint32_t {
std::uint32_t value = 0;
for (std::size_t i = 0; i < size_bytes; ++i) {
if (pos >= data.size()) throw refpack_error("truncated RefPack size field");
value = (value << 8U) | std::to_integer<std::uint8_t>(data[pos++]);
}
return value;
};
if (compressed_size_present) (void) read_size();
const auto out_len = read_size();
std::vector<std::byte> out;
out.reserve(out_len); // guarantees no reallocation, so overlapping reads stay valid
const auto copy_literals = [&](std::size_t count) {
if (pos + count > data.size()) throw refpack_error("truncated RefPack literals");
out.insert(out.end(), data.begin() + static_cast<std::ptrdiff_t>(pos), data.begin() + static_cast<std::ptrdiff_t>(pos + count));
pos += count;
};
// A back-reference may overlap its own output (an RLE run): out[start + i]
// is read one byte at a time, so the pattern repeats correctly.
const auto copy_reference = [&](std::size_t length, std::size_t distance) {
if (distance == 0U || distance > out.size()) throw refpack_error("RefPack back-reference out of range");
const auto start = out.size() - distance;
if (out.size() + length > out_len) throw refpack_error("RefPack output overrun");
for (std::size_t i = 0; i < length; ++i) out.push_back(out[start + i]);
};
while (pos < data.size()) {
const auto cmd = std::to_integer<std::uint8_t>(data[pos++]);
if ((cmd & 0x80U) == 0U) { // 2-byte command
if (pos >= data.size()) throw refpack_error("truncated 2-byte command");
const auto b2 = std::to_integer<std::uint8_t>(data[pos++]);
copy_literals(cmd & 0x03U);
copy_reference(((cmd & 0x1CU) >> 2U) + 3U, ((cmd & 0x60U) << 3U) + b2 + 1U);
} else if ((cmd & 0x40U) == 0U) { // 3-byte command
if (pos + 1U >= data.size()) throw refpack_error("truncated 3-byte command");
const auto b2 = std::to_integer<std::uint8_t>(data[pos]);
const auto b3 = std::to_integer<std::uint8_t>(data[pos + 1U]);
pos += 2U;
copy_literals((b2 & 0xC0U) >> 6U);
copy_reference((cmd & 0x3FU) + 4U, ((b2 & 0x3FU) << 8U) + b3 + 1U);
} else if ((cmd & 0x20U) == 0U) { // 4-byte command
if (pos + 2U >= data.size()) throw refpack_error("truncated 4-byte command");
const auto b2 = std::to_integer<std::uint8_t>(data[pos]);
const auto b3 = std::to_integer<std::uint8_t>(data[pos + 1U]);
const auto b4 = std::to_integer<std::uint8_t>(data[pos + 2U]);
pos += 3U;
copy_literals(cmd & 0x03U);
copy_reference(((cmd & 0x0CU) << 6U) + b4 + 5U, ((cmd & 0x10U) << 12U) + (static_cast<std::size_t>(b2) << 8U) + b3 + 1U);
} else if (cmd < 0xFCU) { // long literal run
copy_literals((static_cast<std::size_t>(cmd & 0x1FU) + 1U) << 2U);
} else { // stop
copy_literals(cmd & 0x03U);
break;
}
}
if (out.size() != out_len) throw refpack_error("RefPack length mismatch");
return out;
}
/** Decompress `data` when it is RefPack, otherwise copy it unchanged. */
[[nodiscard]] inline auto maybe_decompress(std::span<const std::byte> data) -> std::vector<std::byte> {
if (is_refpack(data)) return refpack_decompress(data);
return {data.begin(), data.end()};
}
namespace detail {
/** True when `(length, distance)` maps to one of the three canonical tokens. */
[[nodiscard]] inline constexpr auto refpack_match_encodable(std::size_t length, std::size_t distance) -> bool {
if (length >= 3U && length <= 10U && distance >= 1U && distance <= 1024U) return true;
if (length >= 4U && length <= 67U && distance >= 1U && distance <= 16384U) return true;
if (length >= 5U && length <= 1028U && distance >= 1U && distance <= 131072U) return true;
return false;
}
/** Greedy RefPack encoder state. */
class refpack_encoder {
public:
explicit refpack_encoder(std::span<const std::byte> input)
: input_(input), chain_(input.size(), -1) {
}
[[nodiscard]] auto run() -> std::vector<std::byte> {
const auto n = this->input_.size();
const bool large = n >= (1U << 24U);
this->out_.write_u8(static_cast<std::uint8_t>(0x10U | (large ? 0x80U : 0x00U)));
this->out_.write_u8(0xFBU);
const std::size_t size_bytes = large ? 4U : 3U;
for (std::size_t i = size_bytes; i-- > 0U;) this->out_.write_u8(static_cast<std::uint8_t>((n >> (8U * i)) & 0xFFU));
std::size_t pos = 0;
std::size_t literals_start = 0;
while (pos < n) {
std::size_t best_len = 0;
std::size_t best_dist = 0;
if (pos + 2U < n) this->find_match(pos, best_len, best_dist);
if (best_len >= 3U) {
const auto pending = pos - literals_start;
const auto carry = pending % 4U; // 0..3 literals ride with the token
const auto run = pending - carry; // always a multiple of 4
this->emit_literal_run(literals_start, run);
this->emit_match(literals_start + run, carry, best_len, best_dist);
for (std::size_t i = pos; i < pos + best_len; ++i) this->insert(i);
pos += best_len;
literals_start = pos;
} else {
this->insert(pos);
++pos;
}
}
const auto pending = n - literals_start;
const auto carry = pending % 4U;
this->emit_literal_run(literals_start, pending - carry);
this->out_.write_u8(static_cast<std::uint8_t>(0xFCU | carry));
this->write_literals(literals_start + (pending - carry), carry);
return this->out_.take();
}
private:
void insert(std::size_t pos) {
if (pos + 2U >= this->input_.size()) return;
const auto bucket = this->hash3(pos);
this->chain_[pos] = this->head_[bucket];
this->head_[bucket] = static_cast<std::int32_t>(pos);
}
void find_match(std::size_t pos, std::size_t &best_len, std::size_t &best_dist) const {
const auto n = this->input_.size();
const auto max_len = std::min<std::size_t>(1028U, n - pos);
auto candidate = this->head_[this->hash3(pos)];
int depth = 0;
while (candidate >= 0 && depth < 64) {
const auto c = static_cast<std::size_t>(candidate);
const auto distance = pos - c;
if (distance > refpack_window) break; // the chain only walks backwards
std::size_t length = 0;
while (length < max_len && this->input_[c + length] == this->input_[pos + length]) ++length;
if (length > best_len && refpack_match_encodable(length, distance)) {
best_len = length;
best_dist = distance;
if (length == max_len) break;
}
candidate = this->chain_[c];
++depth;
}
}
void emit_literal_run(std::size_t offset, std::size_t count) {
std::size_t remaining = count;
std::size_t at = offset;
while (remaining >= 4U) {
std::size_t step = std::min<std::size_t>(112U, remaining);
step -= step % 4U;
if (step < 4U) step = 4U;
this->out_.write_u8(static_cast<std::uint8_t>(0xE0U | ((step / 4U) - 1U)));
this->write_literals(at, step);
at += step;
remaining -= step;
}
}
void emit_match(std::size_t literal_offset, std::size_t literal_count, std::size_t length, std::size_t distance) {
const auto d = static_cast<std::uint32_t>(distance - 1U);
if (length >= 3U && length <= 10U && distance <= 1024U) { // 2-byte token
const auto cmd = static_cast<std::uint8_t>((((d >> 8U) & 0x03U) << 5U) | (static_cast<std::uint32_t>(length - 3U) << 2U) |
static_cast<std::uint32_t>(literal_count));
this->out_.write_u8(cmd);
this->out_.write_u8(static_cast<std::uint8_t>(d & 0xFFU));
} else if (length >= 4U && length <= 67U && distance <= 16384U) { // 3-byte token
this->out_.write_u8(static_cast<std::uint8_t>(0x80U | (length - 4U)));
this->out_.write_u8(static_cast<std::uint8_t>((literal_count << 6U) | ((d >> 8U) & 0x3FU)));
this->out_.write_u8(static_cast<std::uint8_t>(d & 0xFFU));
} else if (length >= 5U && length <= 1028U && distance <= 131072U) { // 4-byte token
const auto l = static_cast<std::uint32_t>(length - 5U);
const auto cmd = static_cast<std::uint8_t>(0xC0U | (((l >> 8U) & 0x03U) << 2U) | ((d >> 12U) & 0x10U) |
static_cast<std::uint32_t>(literal_count));
this->out_.write_u8(cmd);
this->out_.write_u8(static_cast<std::uint8_t>((d >> 8U) & 0xFFU));
this->out_.write_u8(static_cast<std::uint8_t>(d & 0xFFU));
this->out_.write_u8(static_cast<std::uint8_t>(l & 0xFFU));
} else {
throw refpack_error("internal error: unencodable RefPack match");
}
this->write_literals(literal_offset, literal_count);
}
void write_literals(std::size_t offset, std::size_t count) {
for (std::size_t i = 0; i < count; ++i) this->out_.write_u8(std::to_integer<std::uint8_t>(this->input_[offset + i]));
}
[[nodiscard]] auto hash3(std::size_t pos) const -> std::size_t {
const auto a = static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(this->input_[pos]));
const auto b = static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(this->input_[pos + 1U]));
const auto c = static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(this->input_[pos + 2U]));
return ((a | (b << 8U) | (c << 16U)) * 2654435761U) >> (32U - 16U);
}
std::span<const std::byte> input_;
std::vector<std::int32_t> chain_;
std::vector<std::int32_t> head_ = std::vector<std::int32_t>(1U << 16U, -1);
byte_writer out_;
};
}
/**
* Compress a byte range into a canonical RefPack stream.
*
* The encoder is a plain greedy LZ77: it never emits a token form the
* decoder above cannot read, so `refpack_decompress(refpack_compress(x))`
* is lossless for every input.
*/
[[nodiscard]] inline auto refpack_compress(std::span<const std::byte> input) -> std::vector<std::byte> {
return detail::refpack_encoder{input}.run();
}
} // namespace ra3::assets