OpenRA3 v0.0.1: C++26 modules, BIG4/RefPack reader, minimal skirmish
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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("env: RA3_GAME_DIR (default C:\\Red Alert 3); game data is never copied into the repo");
|
||||
}
|
||||
|
||||
auto option_value(const std::vector<std::string> &args, std::string_view name) -> std::optional<std::string> {
|
||||
for (std::size_t i = 0; i + 1U < args.size(); ++i) {
|
||||
if (args[i] == name) return args[i + 1U];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/** Build `<dir>/Data/<name>` 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<std::filesystem::path> &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<std::string> &args, const std::optional<std::filesystem::path> &game_dir) -> int {
|
||||
ra3::skirmish::match_config config;
|
||||
std::vector<ra3::map::start_position> 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<ra3::core::uint32>(std::stoul(*seed));
|
||||
if (const auto frames = option_value(args, "--frames")) config.max_frames = static_cast<ra3::core::uint32>(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<double>(starts[i].x), static_cast<double>(starts[i].y));
|
||||
}
|
||||
std::printf("result: %s winner=%d frames=%u (%.1f s)\n", summary.decided ? "decided" : "timeout", summary.winner, summary.frames,
|
||||
static_cast<double>(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;
|
||||
}
|
||||
}
|
||||
|
||||
auto main(int argc, char **argv) -> int {
|
||||
const std::vector<std::string> args{argv + 1, argv + argc};
|
||||
if (args.empty()) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto command = args[0];
|
||||
const std::vector<std::string> rest{args.begin() + 1, args.end()};
|
||||
|
||||
std::optional<std::filesystem::path> 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 == "help" || command == "--help" || command == "-h") {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_usage();
|
||||
return 2;
|
||||
}
|
||||
Reference in New Issue
Block a user