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.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import std;
|
||||
import ra3;
|
||||
import ra3.assets;
|
||||
|
||||
namespace {
|
||||
int failures = 0;
|
||||
@@ -300,6 +301,100 @@ auto main() -> int {
|
||||
|
||||
check(models::decode_dds(std::vector<core::uint8>{1U, 2U, 3U}).empty(), "a non-DDS payload yields no image");
|
||||
|
||||
// Real-asset checks (opt-in): set OPENRA3_TEST_ASSETS to an extracted assets
|
||||
// directory. They run the libra3assets-backed readers over the retail data and
|
||||
// compare the map names against the golden `maps/map_names.tsv` written by the
|
||||
// previous hand-rolled parser.
|
||||
if (const char *assets_env = std::getenv("OPENRA3_TEST_ASSETS"); assets_env != nullptr && *assets_env != '\0') {
|
||||
const std::filesystem::path assets{assets_env};
|
||||
const auto read_all = [](const std::filesystem::path &path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
return std::vector<core::uint8>{std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
|
||||
};
|
||||
|
||||
const auto golden = assets / "maps" / "map_names.tsv";
|
||||
// The golden table came from the newest `Lang-English<N>.big`; mirror that.
|
||||
const auto pick_english_csf = [](const std::filesystem::path &raw) -> std::filesystem::path {
|
||||
std::filesystem::path best;
|
||||
int best_version = -1;
|
||||
std::error_code ec;
|
||||
for (const auto &entry: std::filesystem::directory_iterator(raw, ec)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
const auto name = entry.path().filename().string();
|
||||
if (name.rfind("Lang-English", 0) != 0) continue;
|
||||
int version = 0;
|
||||
try {
|
||||
version = std::stoi(name.substr(12));
|
||||
} catch (const std::exception &) {
|
||||
continue;
|
||||
}
|
||||
if (version > best_version) {
|
||||
best_version = version;
|
||||
best = entry.path() / "data" / "gamestrings.csf";
|
||||
}
|
||||
}
|
||||
if (!best.empty() && std::filesystem::exists(best)) return best;
|
||||
return raw / "English" / "data" / "gamestrings.csf";
|
||||
};
|
||||
const auto csf_path = pick_english_csf(assets / "raw");
|
||||
if (std::filesystem::exists(golden) && std::filesystem::exists(csf_path)) {
|
||||
const auto table = map::parse_map_names(read_all(csf_path));
|
||||
std::ifstream in(golden, std::ios::binary);
|
||||
std::string line;
|
||||
std::size_t matched = 0;
|
||||
std::size_t mismatched = 0;
|
||||
while (std::getline(in, line)) {
|
||||
const auto tab = line.find('\t');
|
||||
if (tab == std::string::npos) continue;
|
||||
const auto id = line.substr(0, tab);
|
||||
const auto expected = line.substr(tab + 1U);
|
||||
if (table.lookup(id) == expected) {
|
||||
++matched;
|
||||
} else {
|
||||
++mismatched;
|
||||
if (mismatched <= 5U) std::printf(" name mismatch %s: golden '%s' new '%s'\n", id.c_str(), expected.c_str(), table.lookup(id).c_str());
|
||||
}
|
||||
}
|
||||
std::printf(" real CSF: %zu names, golden %zu matched, %zu mismatched\n", table.names.size(), matched, mismatched);
|
||||
check(mismatched == 0U, "map names reproduce the golden map_names.tsv");
|
||||
}
|
||||
|
||||
std::size_t maps_ok = 0;
|
||||
std::size_t maps_bad = 0;
|
||||
std::size_t library_starts = 0;
|
||||
std::size_t no_starts = 0;
|
||||
std::size_t roundtrip_bad = 0;
|
||||
const auto to_bytes = [](std::span<const core::uint8> data) {
|
||||
return std::span<const std::byte>{reinterpret_cast<const std::byte *>(data.data()), data.size()};
|
||||
};
|
||||
for (const auto &entry: std::filesystem::directory_iterator(assets / "maps")) {
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".map") continue;
|
||||
try {
|
||||
const auto ckmp = map::to_ckmp(read_all(entry.path()));
|
||||
const auto parsed = terrain::parse_map(ckmp);
|
||||
const auto starts = map::starts_from_ckmp(ckmp);
|
||||
const auto document = ra3::assets::map_document::parse(to_bytes(ckmp));
|
||||
if (document.player_starts().size() >= 2U) ++library_starts; else ++no_starts;
|
||||
if (!std::ranges::equal(document.to_ckmp(), to_bytes(ckmp))) ++roundtrip_bad;
|
||||
if (parsed.width > 0U && parsed.height > 0U && parsed.elevations.size() == static_cast<std::size_t>(parsed.width) * parsed.height && starts.size() >= 2U) {
|
||||
++maps_ok;
|
||||
} else {
|
||||
++maps_bad;
|
||||
if (maps_bad <= 5U)
|
||||
std::printf(" map oddity %s: %ux%u starts %zu\n", entry.path().filename().string().c_str(), parsed.width, parsed.height, starts.size());
|
||||
}
|
||||
} catch (const std::exception &error) {
|
||||
++maps_bad;
|
||||
if (maps_bad <= 5U) std::printf(" map FAIL %s: %s\n", entry.path().filename().string().c_str(), error.what());
|
||||
}
|
||||
}
|
||||
std::printf(" real maps: %zu ok, %zu bad; player_starts resolved %zu, unresolved %zu; round-trip mismatched %zu\n", maps_ok, maps_bad, library_starts,
|
||||
no_starts, roundtrip_bad);
|
||||
check(maps_bad == 0U, "every extracted map parses terrain + starts through libra3assets");
|
||||
check(no_starts == 0U, "libra3assets player_starts() resolves every retail map's start waypoints");
|
||||
check(roundtrip_bad == 0U, "libra3assets re-serialises every retail map byte-for-byte");
|
||||
}
|
||||
|
||||
if (failures == 0) {
|
||||
std::puts("ra3_tests: OK");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user