export module ra3.core; import std; /** * 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 = 3; inline constexpr int version_patch = 0; 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(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(ch); if (c >= 'a' && c <= 'z') c = static_cast(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(hi - lo + 1); return lo + static_cast(this->next_uint() % span); } /** Uniform real in `[0, 1)`. */ auto next_real() -> real { return static_cast(this->next_uint() >> 8) / static_cast(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 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 { return messages_; } auto clear() -> void { messages_.clear(); } /** Remove and return the oldest message, if any. */ auto pop() -> std::optional { if (messages_.empty()) return std::nullopt; auto front = std::move(messages_.front()); messages_.erase(messages_.begin()); return front; } private: std::vector 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::epsilon()) return {0.0F, 0.0F, 0.0F}; return *this * (1.0F / len); } }