import std; import ra3; import ra3.assets; namespace { int failures = 0; auto check(bool condition, std::string_view what) -> void { if (!condition) { std::printf("FAIL: %.*s\n", static_cast(what.size()), what.data()); ++failures; } } auto append_be32(std::vector &out, std::uint32_t value) -> void { out.push_back(static_cast(value >> 24U)); out.push_back(static_cast(value >> 16U)); out.push_back(static_cast(value >> 8U)); out.push_back(static_cast(value)); } auto append_le32(std::vector &out, std::uint32_t value) -> void { out.push_back(static_cast(value)); out.push_back(static_cast(value >> 8U)); out.push_back(static_cast(value >> 16U)); out.push_back(static_cast(value >> 24U)); } /** Build a one-entry BIG4 archive and return its path. */ auto make_test_archive(const std::filesystem::path &path) -> void { const std::string name = "data\\hello.txt"; const std::string payload = "hello big4"; std::vector index; append_be32(index, 0); // offset, patched below append_be32(index, static_cast(payload.size())); index.insert(index.end(), name.begin(), name.end()); index.push_back(0); const std::uint32_t payload_offset = 16U + static_cast(index.size()); std::vector index_patched; append_be32(index_patched, payload_offset); index_patched.insert(index_patched.end(), index.begin() + 4, index.end()); std::vector file; file.insert(file.end(), {'B', 'I', 'G', '4'}); append_le32(file, static_cast(16U + index_patched.size() + payload.size())); append_be32(file, 1U); append_le32(file, 0U); file.insert(file.end(), index_patched.begin(), index_patched.end()); file.insert(file.end(), payload.begin(), payload.end()); std::ofstream out(path, std::ios::binary); out.write(reinterpret_cast(file.data()), static_cast(file.size())); } } auto main() -> int { using namespace ra3; check(core::make_name_key("Tank") == core::make_name_key("tank"), "name keys are case-insensitive"); core::message_stream stream; stream.append(core::message_id::begin_block); stream.append(core::message_id::new_game); check(stream.size() == 2U, "message stream counts entries"); check(stream.pop()->id == core::message_id::begin_block, "message stream is FIFO"); logic::game_logic simulation; simulation.prepare_new_game(7U); check(simulation.starting(), "prepare raises the starting flag"); simulation.start_new_game(); check(!simulation.messages().empty(), "start appends new_game"); simulation.update(); check(simulation.frame() == 1U, "update advances the frame counter"); check(simulation.messages().empty(), "update drains the message stream"); auto &side = simulation.players().add(std::make_unique(0U, "Test")); side.set_money(250); side.add_money(-100); check(side.money() == 150, "money clamps and accumulates"); check(game::to_string(game::faction::soviet) == "Soviet", "faction names round-trip"); // RA3 data: damage types, armour resolution and weapon target masks. check(data::to_string(data::damage_type::auto_cannon) == "AUTO_CANNON", "damage type names round-trip"); check(data::damage_type_from_name("PRISM") == data::damage_type::prism, "damage type parses from asset name"); check(data::allied_scout_infantry_armor.adjust(data::damage_type::gun, 100.0F) == 1.0F, "scout armour takes 1% from GUN"); check(data::allied_scout_infantry_armor.adjust(data::damage_type::tesla, 100.0F) == 50.0F, "scout armour takes 50% from TESLA"); check(data::allied_anti_infantry_infantry_armor.adjust(data::damage_type::tesla, 100.0F) == 1000.0F, "peacekeeper armour takes 10x from TESLA"); check(data::allied_anti_infantry_infantry_armor.adjust(data::damage_type::unresistable, 100.0F) == 100.0F, "UNRESISTABLE bypasses armour"); check(data::maul_weapon.can_target(data::target_class::infantry), "the maul can target infantry"); check(!data::maul_weapon.can_target(data::target_class::vehicle), "the maul cannot target vehicles"); check(!data::maul_weapon.can_target(data::target_class::structure), "the maul cannot target structures"); check(!data::maul_weapon.can_target(data::target_class::aircraft), "the maul cannot target aircraft"); check(data::shotgun_weapon.can_target(data::target_class::infantry) && data::shotgun_weapon.can_target(data::target_class::structure), "the shotgun hits infantry and structures"); check(!data::shotgun_weapon.can_target(data::target_class::aircraft), "the shotgun cannot hit aircraft"); check(data::cost_of(data::entity_kind::guardian_tank) == 950, "guardian tank costs 950"); check(data::max_health_of(data::entity_kind::power_plant) == 1000.0F, "power plant has 1000 health"); check(data::structure_of(data::entity_kind::power_plant).energy == 100, "power plant supplies 100 energy"); check(data::seconds_to_frames(1.0F) == 30U, "one second is 30 logic frames"); // RefPack: a literal-only stream decodes to its payload. const std::vector refpack{0x10, 0xFB, 0x00, 0x00, 0x03, 0xFF, 'A', 'B', 'C'}; check(fs::is_refpack(refpack), "literal RefPack stream is recognised"); const auto decoded = fs::refpack_decompress(refpack); check(decoded.size() == 3U && decoded[0] == 'A' && decoded[2] == 'C', "RefPack literals decode"); check(fs::maybe_decompress(std::vector{'x', 'y'}).size() == 2U, "non-RefPack passes through"); // BIG4: build and read a synthetic archive. const auto archive_path = std::filesystem::temp_directory_path() / "openra3_test.big"; make_test_archive(archive_path); const auto archive = fs::big_archive::open(archive_path); check(archive.size() == 1U, "BIG4 index has one entry"); check(archive.contains("data\\hello.txt"), "BIG4 entry is indexed"); const auto payload = archive.read("data\\hello.txt"); check(std::string(payload.begin(), payload.end()) == "hello big4", "BIG4 payload reads back"); std::error_code ignored; std::filesystem::remove(archive_path, ignored); // TGA: a 2x2 top-origin truecolor image decodes with correct pixels. std::vector tga{0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 24, 0x20}; const core::uint8 tga_pixels[] = {0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255}; tga.insert(tga.end(), tga_pixels, tga_pixels + 12); const auto decoded_tga = render::decode_tga(tga); check(decoded_tga.width() == 2U && decoded_tga.height() == 2U, "TGA dimensions"); check(decoded_tga.data()[0] == render::argb(255, 0, 0), "TGA top-left is red"); check(decoded_tga.data()[3] == render::argb(255, 255, 255), "TGA bottom-right is white"); const auto bmp = render::encode_bmp(decoded_tga); check(bmp.size() > 54U && bmp[0] == 'B' && bmp[1] == 'M', "BMP header"); check(bmp[18] == 2U && bmp[19] == 0U && bmp[22] == 2U, "BMP dimensions"); // Compositing keeps the source size and accepts markers. render::scene_options scene; scene.world_width = 100.0; scene.world_height = 100.0; const std::vector markers{{10.0, 20.0, render::red, 3}}; const auto composed = render::compose(decoded_tga, markers, scene); check(composed.width() == 2U && composed.height() == 2U, "compose preserves size"); // Camera: wheel zoom, clamped pan and RA3-style edge scrolling. render::view_camera camera; camera.zoom = 2.0F; camera.center_x = 0.5F; camera.center_y = 0.5F; camera.scroll(10.0F, 10.0F); check(camera.center_x == 0.75F && camera.center_y == 0.75F, "camera clamps the centre to the map"); camera.zoom_by(100.0F); check(camera.zoom == camera.max_zoom, "camera clamps the maximum zoom"); camera.zoom_by(0.001F); check(camera.zoom == camera.min_zoom, "camera clamps the minimum zoom"); const auto fit_rect = camera.rect(512.0F, 512.0F, 1024.0F, 768.0F); check(fit_rect.w == 768.0F && fit_rect.h == 768.0F && fit_rect.x == 128.0F && fit_rect.y == 0.0F, "camera letterboxes a square map into a wide window"); render::view_camera edge; edge.zoom = 2.0F; edge.center_x = 0.5F; edge.center_y = 0.5F; edge.edge_scroll(0.0F, 0.0F, 1024.0F, 768.0F, 1.0F); check(edge.center_x < 0.5F && edge.center_y < 0.5F, "the cursor at the top-left edge scrolls up-left"); render::view_camera whole; whole.edge_scroll(0.0F, 0.0F, 1024.0F, 768.0F, 1.0F); check(whole.center_x == 0.5F && whole.center_y == 0.5F, "no edge scroll while the whole map fits"); // Skirmish: deterministic, resolves within the frame cap. skirmish::match_config config; config.seed = 42U; const auto starts = skirmish::builtin_start_positions(); auto first = skirmish::skirmish_match::create(config, starts); check(first.unit_count(0U) == 1U && first.unit_count(1U) == 1U, "each side starts with a construction yard"); const auto first_result = first.run(); auto second = skirmish::skirmish_match::create(config, starts); const auto second_result = second.run(); check(first_result.decided, "skirmish reaches a decision"); check(first_result.winner == 0 || first_result.winner == 1, "skirmish has a winner"); check(first_result.frames == second_result.frames && first_result.winner == second_result.winner, "skirmish is deterministic"); // Text/font: glyphs paint, spaces are blank and widths scale. check(render::text_width("ABC", 2U) == 48U, "text width scales with glyph size"); render::image text_image(64U, 16U, render::black); render::draw_text(text_image, 0, 0, "A", render::white, 2U); int lit = 0; for (std::size_t i = 0; i < 64U * 16U; ++i) { if (text_image.data()[i] != render::black) ++lit; } check(lit > 0, "draw_text paints glyph pixels"); render::image space_image(64U, 16U, render::black); render::draw_text(space_image, 0, 0, " ", render::white, 2U); bool space_lit = false; for (std::size_t i = 0; i < 64U * 16U; ++i) { if (space_image.data()[i] != render::black) space_lit = true; } check(!space_lit, "a space paints nothing"); const auto box = render::fit_rect(1280.0F, 720.0F, 1920.0F, 1080.0F); check(box.x == 0.0F && box.y == 0.0F && box.w == 1920.0F && box.h == 1080.0F, "fit_rect fills a same-aspect window"); // Map display names: decode a synthetic `gamestrings.csf` `MAP:` label. std::vector csf; const auto put_le32 = [&csf](std::uint32_t v) { for (int i = 0; i < 4; ++i) csf.push_back(static_cast(v >> (8 * i))); }; csf.insert(csf.end(), {' ', 'F', 'S', 'C'}); put_le32(3U); put_le32(1U); put_le32(1U); for (int i = 0; i < 8; ++i) csf.push_back(0U); csf.insert(csf.end(), {' ', 'L', 'B', 'L'}); put_le32(1U); const std::string csf_label = "MAP:MAP_MP_2_FEASEL4"; put_le32(static_cast(csf_label.size())); csf.insert(csf.end(), csf_label.begin(), csf_label.end()); csf.insert(csf.end(), {' ', 'R', 'T', 'S'}); const std::string csf_value = "Battlebase Beta"; put_le32(static_cast(csf_value.size())); for (const auto ch: csf_value) { csf.push_back(static_cast(ch ^ 0xFF)); csf.push_back(0xFFU); } const auto names = map::parse_map_names(csf); check(names.lookup("map_mp_2_feasel4") == "Battlebase Beta", "CSF map names decode (byte-XOR 0xFF)"); check(names.lookup("unknown_map") == "unknown_map", "an unknown id falls back to itself"); // Map objects (`ObjectsList`): decode a synthetic one-object chunk. const auto put16 = [](std::vector &out, std::uint16_t v) { out.push_back(static_cast(v)); out.push_back(static_cast(v >> 8U)); }; const auto put32 = [](std::vector &out, std::uint32_t v) { for (int i = 0; i < 4; ++i) out.push_back(static_cast(v >> (8 * i))); }; const auto putf = [&put32](std::vector &out, float f) { std::uint32_t bits = 0; std::memcpy(&bits, &f, sizeof(bits)); put32(out, bits); }; std::vector object_data; putf(object_data, 100.0F); putf(object_data, 200.0F); putf(object_data, 0.0F); putf(object_data, 0.5F); put32(object_data, 0U); // road type const std::string object_type = "BB_TEST"; put16(object_data, static_cast(object_type.size())); object_data.insert(object_data.end(), object_type.begin(), object_type.end()); put16(object_data, 0U); // no properties std::vector object_asset; put32(object_asset, 1U); // asset index put16(object_asset, 1U); // version put32(object_asset, static_cast(object_data.size())); object_asset.insert(object_asset.end(), object_data.begin(), object_data.end()); std::vector ckmp; ckmp.insert(ckmp.end(), {'C', 'k', 'M', 'p'}); put32(ckmp, 1U); // one asset name const std::string chunk_name = "ObjectsList"; ckmp.push_back(static_cast(chunk_name.size())); ckmp.insert(ckmp.end(), chunk_name.begin(), chunk_name.end()); put32(ckmp, 1U); // name index put32(ckmp, 1U); // chunk index put16(ckmp, 3U); // chunk version put32(ckmp, static_cast(object_asset.size())); ckmp.insert(ckmp.end(), object_asset.begin(), object_asset.end()); const auto objects = map::parse_objects(ckmp); check(objects.size() == 1U, "ObjectsList parses one object"); check(!objects.empty() && objects[0].type == "BB_TEST", "object type-name decodes"); check(!objects.empty() && objects[0].x == 100.0F && objects[0].y == 200.0F && objects[0].angle == 0.5F, "object position/angle decode"); // DDS: a 1x1 uncompressed RGB32 image decodes with correct channels. std::vector dds(128U, 0U); dds[0] = 'D'; dds[1] = 'D'; dds[2] = 'S'; dds[3] = ' '; const auto put_dds = [&dds](std::size_t off, std::uint32_t v) { for (int i = 0; i < 4; ++i) dds[off + static_cast(i)] = static_cast(v >> (8 * i)); }; put_dds(4U, 124U); put_dds(8U, 0x1U); // flags: DDSD_CAPS|... (unused) put_dds(12U, 1U); // height put_dds(16U, 1U); // width put_dds(76U, 32U); // pixel format size put_dds(80U, 0x40U); // DDPF_RGB put_dds(84U, 0U); // no fourcc put_dds(88U, 32U); // bits per pixel put_dds(92U, 0x00FF0000U); put_dds(96U, 0x0000FF00U); put_dds(100U, 0x000000FFU); put_dds(104U, 0xFF000000U); dds.push_back(0x00U); // B dds.push_back(0x00U); // G dds.push_back(0xFFU); // R dds.push_back(0xFFU); // A const auto decoded_dds = models::decode_dds(dds); check(decoded_dds.width() == 1U && decoded_dds.height() == 1U, "DDS dimensions decode"); check(!decoded_dds.empty() && decoded_dds.data()[0] == render::argb(255, 0, 0), "DDS RGB32 channels map to ARGB"); check(models::decode_dds(std::vector{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{std::istreambuf_iterator(in), std::istreambuf_iterator()}; }; const auto golden = assets / "maps" / "map_names.tsv"; // The golden table came from the newest `Lang-English.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 data) { return std::span{reinterpret_cast(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(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"); } return failures == 0 ? 0 : 1; }