OpenRA3 v0.0.1: C++26 modules, BIG4/RefPack reader, minimal skirmish
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
module;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
export module ra3.map;
|
||||
|
||||
export import ra3.core;
|
||||
export import ra3.fs;
|
||||
|
||||
/**
|
||||
* Red Alert 3 map discovery and loading.
|
||||
*
|
||||
* Multiplayer maps live in `Data\MapsMultiplayer.big` as one directory per map:
|
||||
* `<id>\<id>.map` plus compiled companions. The `.map` file is an `EAR\0`
|
||||
* wrapper around a RefPack stream whose payload is the `CkMp` chunk tree.
|
||||
*
|
||||
* This module reads the archive for real, unwraps the map, and extracts the
|
||||
* `Player_N_Start` waypoint coordinates that seed a skirmish. Anything beyond
|
||||
* start positions (terrain, objects) is the next milestone.
|
||||
*/
|
||||
export namespace ra3::map {
|
||||
using ra3::core::coord3d;
|
||||
using ra3::core::real;
|
||||
using ra3::core::uint32;
|
||||
using ra3::core::uint8;
|
||||
using ra3::core::usize;
|
||||
|
||||
/** A player start location extracted from a map's waypoints. */
|
||||
struct start_position {
|
||||
real x = 0.0F;
|
||||
real y = 0.0F;
|
||||
real z = 0.0F;
|
||||
|
||||
[[nodiscard]] auto as_coord() const -> coord3d { return {x, y, z}; }
|
||||
};
|
||||
|
||||
/** Identity and sizes of one map inside an archive. */
|
||||
struct map_info {
|
||||
std::string id;
|
||||
std::string entry;
|
||||
uint32 stored_size = 0;
|
||||
uint32 unpacked_size = 0;
|
||||
};
|
||||
|
||||
/** The result of loading a map: its metadata and recovered start positions. */
|
||||
struct loaded_map {
|
||||
map_info info;
|
||||
std::vector<start_position> starts;
|
||||
usize ckmp_size = 0;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
[[nodiscard]] inline auto split_path(std::string_view path) -> std::vector<std::string> {
|
||||
std::vector<std::string> parts;
|
||||
usize start = 0;
|
||||
while (start <= path.size()) {
|
||||
const auto at = path.find_first_of("\\/", start);
|
||||
const auto end = at == std::string_view::npos ? path.size() : at;
|
||||
parts.emplace_back(path.substr(start, end - start));
|
||||
if (at == std::string_view::npos) break;
|
||||
start = at + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** True for a main map entry (`.../<stem>/<stem>.map`). */
|
||||
[[nodiscard]] inline auto is_main_map(const fs::big_entry &entry) -> bool {
|
||||
const auto parts = split_path(entry.name);
|
||||
if (parts.size() < 2) return false;
|
||||
const auto &file = parts[parts.size() - 1];
|
||||
const auto &parent = parts[parts.size() - 2];
|
||||
if (file.size() < 5 || file.compare(file.size() - 4, 4, ".map") != 0) return false;
|
||||
const auto stem = file.substr(0, file.size() - 4);
|
||||
return stem == parent && !stem.ends_with("_edit");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto stem_of(std::string_view path) -> std::string {
|
||||
const auto parts = split_path(path);
|
||||
if (parts.empty()) return {};
|
||||
auto file = parts.back();
|
||||
if (file.size() > 4 && file.compare(file.size() - 4, 4, ".map") == 0) file.resize(file.size() - 4);
|
||||
return file;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto find_bytes(std::span<const uint8> haystack, std::string_view needle) -> usize {
|
||||
if (needle.empty()) return 0;
|
||||
const auto *needle_begin = reinterpret_cast<const uint8 *>(needle.data());
|
||||
const auto it = std::search(haystack.begin(), haystack.end(), needle_begin, needle_begin + needle.size());
|
||||
return it == haystack.end() ? static_cast<usize>(-1) : static_cast<usize>(it - haystack.begin());
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto read_le_f32(const uint8 *p) -> real {
|
||||
const auto bits = fs::read_le32(p);
|
||||
real value = 0.0F;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
/** First plausible `(x, y, 0)` float triple at or after `from`. */
|
||||
[[nodiscard]] inline auto scan_triple(std::span<const uint8> data, usize from, usize window) -> std::optional<start_position> {
|
||||
const auto end = std::min(data.size(), from + window);
|
||||
for (usize p = from; p + 12U <= end; ++p) {
|
||||
const auto x = read_le_f32(data.data() + p);
|
||||
const auto y = read_le_f32(data.data() + p + 4U);
|
||||
const auto z = read_le_f32(data.data() + p + 8U);
|
||||
if (z == 0.0F && std::isfinite(x) && std::isfinite(y) && x > 0.0F && x < 20000.0F && y > 0.0F && y < 20000.0F) {
|
||||
return start_position{x, y, z};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/** Extract `Player_1_Start` .. `Player_N_Start` waypoint coordinates. */
|
||||
[[nodiscard]] inline auto extract_start_positions(std::span<const uint8> ckmp, int max_players = 8) -> std::vector<start_position> {
|
||||
std::vector<start_position> result;
|
||||
for (int n = 1; n <= max_players; ++n) {
|
||||
const auto needle = std::string{"Player_"} + std::to_string(n) + "_Start";
|
||||
usize cursor = 0;
|
||||
while (cursor < ckmp.size()) {
|
||||
const auto at = find_bytes(ckmp.subspan(cursor), needle);
|
||||
if (at == static_cast<usize>(-1)) break;
|
||||
const auto abs = cursor + at;
|
||||
if (const auto triple = scan_triple(ckmp, abs + needle.size(), 128); triple) {
|
||||
result.push_back(*triple);
|
||||
break;
|
||||
}
|
||||
cursor = abs + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Number of distinct integer positions (used to reject degenerate sets). */
|
||||
[[nodiscard]] inline auto distinct_positions(const std::vector<start_position> &starts) -> usize {
|
||||
std::vector<std::pair<int, int>> seen;
|
||||
for (const auto &start: starts) {
|
||||
const auto key = std::pair{static_cast<int>(start.x), static_cast<int>(start.y)};
|
||||
if (std::find(seen.begin(), seen.end(), key) == seen.end()) seen.push_back(key);
|
||||
}
|
||||
return seen.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every main map discoverable in one archive.
|
||||
*/
|
||||
class map_catalog {
|
||||
public:
|
||||
/**
|
||||
* Scan an archive for main map entries.
|
||||
*
|
||||
* @param archive An opened `MapsMultiplayer.big` (or similar).
|
||||
*/
|
||||
[[nodiscard]] static auto from_archive(const fs::big_archive &archive) -> map_catalog {
|
||||
map_catalog catalog;
|
||||
for (const auto &entry: archive.entries()) {
|
||||
if (!detail::is_main_map(entry)) continue;
|
||||
map_info info;
|
||||
info.entry = entry.name;
|
||||
info.id = detail::stem_of(entry.name);
|
||||
info.stored_size = entry.size;
|
||||
info.unpacked_size = entry.size;
|
||||
// The BIG4 payload is RefPack; its output is the `EAR\0` wrapper.
|
||||
if (const auto prefix = archive.read_prefix(entry.name, 16); fs::is_refpack(prefix)) {
|
||||
info.unpacked_size = fs::refpack_output_size(prefix);
|
||||
}
|
||||
catalog.maps_.push_back(std::move(info));
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto maps() const -> const std::vector<map_info> & { return maps_; }
|
||||
[[nodiscard]] auto size() const -> usize { return maps_.size(); }
|
||||
|
||||
[[nodiscard]] auto find(std::string_view id) const -> const map_info * {
|
||||
for (const auto &info: maps_) {
|
||||
if (info.id == id) return &info;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<map_info> maps_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a map: unwrap the `EAR\0` + RefPack container and recover the
|
||||
* player start waypoints.
|
||||
*
|
||||
* @param archive Archive that holds the map.
|
||||
* @param info Map selected from a `map_catalog`.
|
||||
* @throws fs::archive_error / fs::refpack_error on malformed input.
|
||||
*/
|
||||
[[nodiscard]] inline auto load_map(const fs::big_archive &archive, const map_info &info) -> loaded_map {
|
||||
loaded_map result;
|
||||
result.info = info;
|
||||
|
||||
// Layer 1: BIG4 payload is RefPack, yielding the `EAR\0` wrapper.
|
||||
const auto wrapper = archive.read(info.entry, true);
|
||||
std::span<const uint8> payload{wrapper};
|
||||
if (wrapper.size() >= 8U && std::memcmp(wrapper.data(), "EAR\0", 4) == 0) payload = payload.subspan(8);
|
||||
|
||||
// Layer 2: the wrapper body is RefPack again, yielding the `CkMp` tree.
|
||||
auto ckmp = fs::maybe_decompress(payload);
|
||||
result.ckmp_size = ckmp.size();
|
||||
result.starts = detail::extract_start_positions(ckmp);
|
||||
// Maps that keep starts in `MPPositionList` instead of `Player_N_Start`
|
||||
// waypoints yield fewer than two distinct points; signal "unknown" so
|
||||
// the caller can fall back rather than spawn everyone at the origin.
|
||||
if (detail::distinct_positions(result.starts) < 2U) result.starts.clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user