#include #include #include #include #include #include #include #include #include import ra3; namespace { auto print_usage() -> void { std::puts("OpenRA3 - headless Red Alert 3 reconstruction"); std::puts("usage:"); std::puts(" openra3 maps [--game-dir DIR]"); std::puts(" openra3 skirmish [--game-dir DIR] [--map ID] [--frames N] [--seed N]"); std::puts(" openra3 render [--game-dir DIR] [--map ID] [--frames N] [--seed N] [--world-size S] [--out FILE.bmp]"); std::puts("env: RA3_GAME_DIR (default C:\\Red Alert 3); game data is never copied into the repo"); } auto option_value(const std::vector &args, std::string_view name) -> std::optional { for (std::size_t i = 0; i + 1U < args.size(); ++i) { if (args[i] == name) return args[i + 1U]; } return std::nullopt; } /** Build `/Data/` portably (avoids path/char[] ambiguity). */ auto data_path(const std::filesystem::path &dir, std::string_view name) -> std::filesystem::path { return dir / std::string{"Data"} / std::string{name}; } auto command_maps(const std::optional &game_dir) -> int { const auto dir = ra3::fs::find_game_dir(game_dir); if (!dir) { std::puts("no Red Alert 3 install found; pass --game-dir or set RA3_GAME_DIR"); return 1; } const auto archive_path = data_path(*dir, "MapsMultiplayer.big"); const auto archive = ra3::fs::big_archive::open(archive_path); const auto catalog = ra3::map::map_catalog::from_archive(archive); const auto name = archive_path.filename().string(); std::printf("maps in %s: %zu\n", name.c_str(), catalog.size()); for (const auto &entry: catalog.maps()) { std::printf(" %-28s %6u KB packed / %6u KB unpacked\n", entry.id.c_str(), entry.stored_size / 1024U, entry.unpacked_size / 1024U); } return 0; } auto command_skirmish(const std::vector &args, const std::optional &game_dir) -> int { ra3::skirmish::match_config config; std::vector starts; std::string source = "builtin"; const auto requested_map = option_value(args, "--map"); if (const auto dir = ra3::fs::find_game_dir(game_dir)) { try { const auto archive = ra3::fs::big_archive::open(data_path(*dir, "MapsMultiplayer.big")); const auto catalog = ra3::map::map_catalog::from_archive(archive); const ra3::map::map_info *info = nullptr; if (requested_map) { info = catalog.find(*requested_map); if (info == nullptr) std::printf("map '%s' not found; falling back\n", requested_map->c_str()); } else if (catalog.size() > 0U) { info = &catalog.maps().front(); } if (info != nullptr) { const auto loaded = ra3::map::load_map(archive, *info); starts = loaded.starts; config.map_id = info->id; source = "archive:" + info->id; } } catch (const std::exception &error) { std::printf("map load failed: %s\n", error.what()); } } if (starts.size() < 2U) { starts = ra3::skirmish::builtin_start_positions(); source = "builtin"; } if (const auto seed = option_value(args, "--seed")) config.seed = static_cast(std::stoul(*seed)); if (const auto frames = option_value(args, "--frames")) config.max_frames = static_cast(std::stoul(*frames)); auto match = ra3::skirmish::skirmish_match::create(config, starts); const auto summary = match.run(); std::printf("OpenRA3 skirmish map=%s source=%s seed=%u\n", config.map_id.c_str(), source.c_str(), config.seed); std::puts("start positions:"); for (std::size_t i = 0; i < starts.size(); ++i) { std::printf(" P%zu (%.0f, %.0f)\n", i, static_cast(starts[i].x), static_cast(starts[i].y)); } std::printf("result: %s winner=%d frames=%u (%.1f s)\n", summary.decided ? "decided" : "timeout", summary.winner, summary.frames, static_cast(summary.frames) / 30.0); for (const auto &player: match.players()) { const auto side = std::string{ra3::game::to_string(player.side)}; std::printf(" P%u %-10s %-7s money=%6d units=%2zu kills=%d losses=%d\n", player.index, player.name.c_str(), side.c_str(), player.money, match.unit_count(player.index), player.kills, player.losses); } return 0; } /** Entry name of a map's overview art (`_art.tga`). */ auto art_entry_for(const ra3::map::map_info &info) -> std::string { auto entry = info.entry; if (entry.size() > 4U) entry.resize(entry.size() - 4U); entry += "_art.tga"; return entry; } auto command_render(const std::vector &args, const std::optional &game_dir) -> int { const auto dir = ra3::fs::find_game_dir(game_dir); if (!dir) { std::puts("no Red Alert 3 install found; pass --game-dir or set RA3_GAME_DIR"); return 1; } const auto archive = ra3::fs::big_archive::open(data_path(*dir, "MapsMultiplayer.big")); const auto catalog = ra3::map::map_catalog::from_archive(archive); const auto requested = option_value(args, "--map"); const ra3::map::map_info *info = nullptr; if (requested) { info = catalog.find(*requested); } else if (catalog.size() > 0U) { info = &catalog.maps().front(); } if (info == nullptr) { std::printf("map '%s' not found\n", requested ? requested->c_str() : ""); return 1; } const auto tga = archive.read(art_entry_for(*info), true); const auto art = ra3::render::decode_tga(tga); ra3::render::scene_options scene; scene.title = "OpenRA3 - " + info->id; if (const auto size = option_value(args, "--world-size")) scene.world_width = scene.world_height = std::stod(*size); ra3::skirmish::match_config config; config.map_id = info->id; if (const auto seed = option_value(args, "--seed")) config.seed = static_cast(std::stoul(*seed)); if (const auto frames = option_value(args, "--frames")) config.max_frames = static_cast(std::stoul(*frames)); auto starts = ra3::map::load_map(archive, *info).starts; if (starts.size() < 2U) starts = ra3::skirmish::builtin_start_positions(); auto match = ra3::skirmish::skirmish_match::create(config, starts); const auto summary = match.run(); std::vector markers; for (const auto &player: match.players()) { markers.push_back({static_cast(player.start.x), static_cast(player.start.y), ra3::render::yellow, 6}); } for (const auto &entry: match.units()) { if (!entry.alive) continue; const auto color = entry.owner == 0U ? ra3::render::blue : ra3::render::red; int radius = 2; if (entry.cls == ra3::skirmish::unit_class::tank) radius = 4; if (entry.cls == ra3::skirmish::unit_class::base) radius = 6; markers.push_back({static_cast(entry.position.x), static_cast(entry.position.y), color, radius}); } const auto composed = ra3::render::compose(art, markers, scene); const auto out = option_value(args, "--out"); const bool no_window = std::find(args.begin(), args.end(), "--no-window") != args.end(); bool shown = false; if (!out && !no_window) { ra3::ui::viewer_options viewer; viewer.title = scene.title; shown = ra3::ui::run_viewer(composed, viewer); } if (!shown) { const auto path = out.value_or("openra3_view.bmp"); const auto bmp = ra3::render::encode_bmp(composed); std::ofstream file(path, std::ios::binary); if (!file) { std::printf("cannot write %s\n", path.c_str()); return 1; } file.write(reinterpret_cast(bmp.data()), static_cast(bmp.size())); std::printf("wrote %s (%ux%u)\n", path.c_str(), composed.width(), composed.height()); } std::printf("map=%s units P0=%d P1=%d winner=%d frames=%u\n", info->id.c_str(), summary.units_left[0], summary.units_left[1], summary.winner, summary.frames); return 0; } } auto main(int argc, char **argv) -> int { const std::vector args{argv + 1, argv + argc}; if (args.empty()) { print_usage(); return 0; } const auto command = args[0]; const std::vector rest{args.begin() + 1, args.end()}; std::optional game_dir; if (const auto dir = option_value(rest, "--game-dir")) game_dir = *dir; if (command == "maps") return command_maps(game_dir); if (command == "skirmish") return command_skirmish(rest, game_dir); if (command == "render") return command_render(rest, game_dir); if (command == "help" || command == "--help" || command == "-h") { print_usage(); return 0; } print_usage(); return 2; }