Files
OpenRA3/src/core/ra3.core.cppm
T
EnderTheCoder b6d619254d v0.3.0: display abstraction, map-browser menu, SAGE terrain tiling/blends
- 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.
2026-09-20 02:20:39 +08:00

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);
}
}