OpenRA3 v0.0.1: C++26 modules, BIG4/RefPack reader, minimal skirmish
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
import ra3;
|
||||
|
||||
namespace {
|
||||
int failures = 0;
|
||||
|
||||
auto check(bool condition, std::string_view what) -> void {
|
||||
if (!condition) {
|
||||
std::printf("FAIL: %.*s\n", static_cast<int>(what.size()), what.data());
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
auto append_be32(std::vector<std::uint8_t> &out, std::uint32_t value) -> void {
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 24U));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 16U));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 8U));
|
||||
out.push_back(static_cast<std::uint8_t>(value));
|
||||
}
|
||||
|
||||
auto append_le32(std::vector<std::uint8_t> &out, std::uint32_t value) -> void {
|
||||
out.push_back(static_cast<std::uint8_t>(value));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 8U));
|
||||
out.push_back(static_cast<std::uint8_t>(value >> 16U));
|
||||
out.push_back(static_cast<std::uint8_t>(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<std::uint8_t> index;
|
||||
append_be32(index, 0); // offset, patched below
|
||||
append_be32(index, static_cast<std::uint32_t>(payload.size()));
|
||||
index.insert(index.end(), name.begin(), name.end());
|
||||
index.push_back(0);
|
||||
|
||||
const std::uint32_t payload_offset = 16U + static_cast<std::uint32_t>(index.size());
|
||||
std::vector<std::uint8_t> index_patched;
|
||||
append_be32(index_patched, payload_offset);
|
||||
index_patched.insert(index_patched.end(), index.begin() + 4, index.end());
|
||||
|
||||
std::vector<std::uint8_t> file;
|
||||
file.insert(file.end(), {'B', 'I', 'G', '4'});
|
||||
append_le32(file, static_cast<std::uint32_t>(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<const char *>(file.data()), static_cast<std::streamsize>(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<logic::player>(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");
|
||||
|
||||
// RefPack: a literal-only stream decodes to its payload.
|
||||
const std::vector<core::uint8> 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<core::uint8>{'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);
|
||||
|
||||
// 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);
|
||||
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");
|
||||
|
||||
if (failures == 0) {
|
||||
std::puts("ra3_tests: OK");
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user