OpenRA3 v0.0.1: C++26 modules, BIG4/RefPack reader, minimal skirmish
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
module;
|
||||
|
||||
#include <cmath>
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
export module ra3.core;
|
||||
|
||||
/**
|
||||
* Fundamental engine types shared by every OpenRA3 module.
|
||||
*
|
||||
* The vocabulary mirrors SAGE 2.0 (the engine Red Alert 3 runs on) as it
|
||||
* appears in the CnC Generals source, with layouts cross-checked against the
|
||||
* retail `ra3_1.12.game` binary through Ghidra.
|
||||
*/
|
||||
export namespace ra3::core {
|
||||
inline constexpr int version_major = 0;
|
||||
inline constexpr int version_minor = 0;
|
||||
inline constexpr int version_patch = 1;
|
||||
|
||||
using real = float;
|
||||
using int32 = std::int32_t;
|
||||
using uint32 = std::uint32_t;
|
||||
using uint16 = std::uint16_t;
|
||||
using uint8 = std::uint8_t;
|
||||
using usize = std::size_t;
|
||||
|
||||
/** SAGE advances the simulation on a fixed 30 Hz logic tick. */
|
||||
inline constexpr int logic_frames_per_second = 30;
|
||||
inline constexpr real logic_frame_seconds = 1.0F / static_cast<real>(logic_frames_per_second);
|
||||
|
||||
/**
|
||||
* A world-space position or direction, matching SAGE's `Coord3D`.
|
||||
*/
|
||||
struct coord3d {
|
||||
real x = 0.0F;
|
||||
real y = 0.0F;
|
||||
real z = 0.0F;
|
||||
|
||||
auto operator+(const coord3d &rhs) const -> coord3d { return {x + rhs.x, y + rhs.y, z + rhs.z}; }
|
||||
auto operator-(const coord3d &rhs) const -> coord3d { return {x - rhs.x, y - rhs.y, z - rhs.z}; }
|
||||
auto operator*(real scalar) const -> coord3d { return {x * scalar, y * scalar, z * scalar}; }
|
||||
auto operator==(const coord3d &) const -> bool = default;
|
||||
|
||||
/** Squared length, cheaper than `length()` when only comparing. */
|
||||
[[nodiscard]] auto length_squared() const -> real { return x * x + y * y + z * z; }
|
||||
|
||||
[[nodiscard]] auto length() const -> real;
|
||||
|
||||
[[nodiscard]] auto distance_to(const coord3d &other) const -> real { return (*this - other).length(); }
|
||||
|
||||
/** Unit vector, or the zero vector when this one is degenerate. */
|
||||
[[nodiscard]] auto normalized() const -> coord3d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Integer grid coordinates, matching SAGE's `ICoord2D`.
|
||||
*/
|
||||
struct coord2d {
|
||||
int32 x = 0;
|
||||
int32 y = 0;
|
||||
|
||||
auto operator==(const coord2d &) const -> bool = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* An 8-bit-per-channel RGB colour.
|
||||
*/
|
||||
struct rgb_color {
|
||||
uint8 r = 0;
|
||||
uint8 g = 0;
|
||||
uint8 b = 0;
|
||||
|
||||
auto operator==(const rgb_color &) const -> bool = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive the 32-bit identifier SAGE associates with an ASCII name.
|
||||
*
|
||||
* The retail `NameKeyGenerator` hash is still being recovered; this
|
||||
* deterministic, case-insensitive FNV-1a stand-in keeps the interface
|
||||
* stable until exact parity is proven against the binary.
|
||||
*/
|
||||
constexpr auto make_name_key(std::string_view name) -> uint32 {
|
||||
uint32 key = 2166136261U;
|
||||
for (const auto ch: name) {
|
||||
auto c = static_cast<unsigned char>(ch);
|
||||
if (c >= 'a' && c <= 'z') c = static_cast<unsigned char>(c - 'a' + 'A');
|
||||
key ^= c;
|
||||
key *= 16777619U;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* An owned ASCII identifier carrying a cached `NameKey`.
|
||||
*/
|
||||
class ascii_string {
|
||||
public:
|
||||
ascii_string() = default;
|
||||
ascii_string(std::string_view text) : text_(text) {}
|
||||
explicit ascii_string(std::string text) : text_(std::move(text)) {}
|
||||
|
||||
[[nodiscard]] auto view() const -> std::string_view { return text_; }
|
||||
[[nodiscard]] auto empty() const -> bool { return text_.empty(); }
|
||||
[[nodiscard]] auto c_str() const -> const char * { return text_.c_str(); }
|
||||
[[nodiscard]] auto key() const -> uint32 { return make_name_key(text_); }
|
||||
|
||||
auto operator==(const ascii_string &) const -> bool = default;
|
||||
auto operator<=>(const ascii_string &) const = default;
|
||||
|
||||
friend auto operator<<(std::ostream &out, const ascii_string &value) -> std::ostream &;
|
||||
|
||||
private:
|
||||
std::string text_;
|
||||
};
|
||||
|
||||
inline auto operator<<(std::ostream &out, const ascii_string &value) -> std::ostream & {
|
||||
return out << value.text_;
|
||||
}
|
||||
|
||||
/**
|
||||
* A deterministic pseudo-random source.
|
||||
*
|
||||
* SAGE separates the logic and client random streams so replays stay in
|
||||
* sync; this is the logic-stream stand-in. The retail generator constants
|
||||
* are pending recovery, so this uses a standard 32-bit LCG.
|
||||
*/
|
||||
class random {
|
||||
public:
|
||||
explicit random(uint32 seed = 1U) : state_(seed == 0U ? 1U : seed) {}
|
||||
|
||||
auto seed(uint32 value) -> void { state_ = (value == 0U ? 1U : value); }
|
||||
[[nodiscard]] auto state() const -> uint32 { return state_; }
|
||||
|
||||
auto next_uint() -> uint32 {
|
||||
state_ = state_ * 1664525U + 1013904223U;
|
||||
return state_;
|
||||
}
|
||||
|
||||
/** Uniform integer in the inclusive range `[lo, hi]`. */
|
||||
auto next_int(int32 lo, int32 hi) -> int32 {
|
||||
if (hi <= lo) return lo;
|
||||
const auto span = static_cast<uint32>(hi - lo + 1);
|
||||
return lo + static_cast<int32>(this->next_uint() % span);
|
||||
}
|
||||
|
||||
/** Uniform real in `[0, 1)`. */
|
||||
auto next_real() -> real { return static_cast<real>(this->next_uint() >> 8) / static_cast<real>(1U << 24); }
|
||||
|
||||
private:
|
||||
uint32 state_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Message types carried by the engine's `MessageStream`.
|
||||
*
|
||||
* `new_game` is confirmed against the retail binary: the skirmish BEGIN
|
||||
* handler appends type `0x2` to start a match.
|
||||
*/
|
||||
enum class message_id : uint32 {
|
||||
invalid = 0x0,
|
||||
new_game = 0x2,
|
||||
clear_game_data = 0x3,
|
||||
begin_block = 0x5,
|
||||
end_block = 0x6,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr auto to_string(message_id id) -> std::string_view {
|
||||
switch (id) {
|
||||
case message_id::invalid:
|
||||
return "Invalid";
|
||||
case message_id::new_game:
|
||||
return "NewGame";
|
||||
case message_id::clear_game_data:
|
||||
return "ClearGameData";
|
||||
case message_id::begin_block:
|
||||
return "BeginBlock";
|
||||
case message_id::end_block:
|
||||
return "EndBlock";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* A single entry in the message stream.
|
||||
*
|
||||
* The retail `MessageStream::appendMessage` allocates a 0x74-byte node
|
||||
* (recovered at `0x0060c4a0`); this is its owning equivalent.
|
||||
*/
|
||||
struct game_message {
|
||||
message_id id = message_id::invalid;
|
||||
uint32 player_index = 0;
|
||||
std::vector<uint8> payload;
|
||||
};
|
||||
|
||||
/**
|
||||
* A FIFO of `game_message`s drained once per logic frame.
|
||||
*
|
||||
* In the retail engine the stream is an intrusive doubly linked list
|
||||
* (head at `+0x24`, tail at `+0x28` of the stream object); this is the
|
||||
* behaviourally equivalent owning version.
|
||||
*/
|
||||
class message_stream {
|
||||
public:
|
||||
auto append(message_id id, uint32 player_index = 0U) -> game_message & {
|
||||
auto &slot = messages_.emplace_back();
|
||||
slot.id = id;
|
||||
slot.player_index = player_index;
|
||||
return slot;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto empty() const -> bool { return messages_.empty(); }
|
||||
[[nodiscard]] auto size() const -> usize { return messages_.size(); }
|
||||
[[nodiscard]] auto pending() const -> std::span<const game_message> { return messages_; }
|
||||
|
||||
auto clear() -> void { messages_.clear(); }
|
||||
|
||||
/** Remove and return the oldest message, if any. */
|
||||
auto pop() -> std::optional<game_message> {
|
||||
if (messages_.empty()) return std::nullopt;
|
||||
auto front = std::move(messages_.front());
|
||||
messages_.erase(messages_.begin());
|
||||
return front;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<game_message> messages_;
|
||||
};
|
||||
|
||||
inline auto coord3d::length() const -> real { return std::sqrt(this->length_squared()); }
|
||||
|
||||
inline auto coord3d::normalized() const -> coord3d {
|
||||
const auto len = this->length();
|
||||
if (len <= std::numeric_limits<real>::epsilon()) return {0.0F, 0.0F, 0.0F};
|
||||
return *this * (1.0F / len);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user