- ra3.client::display: shared interactive loops; SDL and Vulkan backends implement only the primitives (init/present/poll_event/window_size/ key_down/present_terrain). ra3.display picks the backend. - Menu: maps by localized name (gamestrings.csf), red/gold theme, hover highlight, mouse + keyboard, wheel scroll, fullscreen and FPS/vsync options, loading progress bar. - Terrain: continuous tile sampling via a texture array (REPEAT, uv = cell/(2*cellSize)) removes per-cell grid seams; SAGE blend ramp for material transitions; FPS label + top-right minimap overlays. - Skip the skirmish sim for map views; reuse the Vulkan texture; no idle terrain redraw.
235 lines
7.8 KiB
C++
235 lines
7.8 KiB
C++
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<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);
|
|
}
|
|
}
|