Files
OpenRA3/src/logic/ra3.logic.cppm
T

295 lines
10 KiB
C++

export module ra3.logic;
import std;
import ra3.core;
/**
* The deterministic simulation layer: objects, players, teams, spatial
* partitioning and the 30 Hz game loop.
*
* This is the OpenRA3 counterpart of SAGE's `GameLogic` subsystem. Addresses
* cited in comments come from the retail `ra3_1.12.game` binary (image base
* `0x400000`); they are the anchors used to keep this reconstruction honest.
*/
export namespace ra3::logic {
using ra3::core::coord3d;
using ra3::core::int32;
using ra3::core::message_id;
using ra3::core::message_stream;
using ra3::core::random;
using ra3::core::real;
using ra3::core::uint32;
using ra3::core::uint8;
using ra3::core::usize;
using object_id = uint32;
/**
* Broad classification of a game object.
*/
enum class object_kind : uint8 {
invalid = 0,
infantry,
vehicle,
aircraft,
structure,
projectile,
};
/**
* A thing the partition manager can index (SAGE `Thing`).
*/
class thing {
public:
explicit thing(object_id id) : id_(id) {}
virtual ~thing() = default;
[[nodiscard]] auto id() const -> object_id { return id_; }
[[nodiscard]] auto position() const -> const coord3d & { return position_; }
auto set_position(const coord3d &value) -> void { position_ = value; }
/** Bounding radius used by the spatial partition. */
[[nodiscard]] virtual auto radius() const -> real { return 0.0F; }
protected:
object_id id_;
coord3d position_;
};
class object;
/**
* A pluggable behaviour ticked every logic frame (SAGE `UpdateModule`).
*/
class update_module {
public:
virtual ~update_module() = default;
virtual auto update(object &owner) -> void = 0;
[[nodiscard]] virtual auto name() const -> std::string_view = 0;
};
/**
* A simulation entity (SAGE `Object`): identity, ownership, health and a
* list of behaviour modules.
*/
class object : public thing {
public:
object(object_id id, object_kind kind, uint32 owner_player) : thing(id), kind_(kind), owner_(owner_player) {}
[[nodiscard]] auto kind() const -> object_kind { return kind_; }
[[nodiscard]] auto owner() const -> uint32 { return owner_; }
[[nodiscard]] auto health() const -> real { return health_; }
auto set_health(real value) -> void { health_ = value; }
auto damage(real amount) -> void { health_ = health_ > amount ? health_ - amount : 0.0F; }
[[nodiscard]] auto destroyed() const -> bool { return health_ <= 0.0F; }
[[nodiscard]] auto radius() const -> real override { return radius_; }
auto set_radius(real value) -> void { radius_ = value; }
auto add_module(std::unique_ptr<update_module> module) -> void { modules_.push_back(std::move(module)); }
[[nodiscard]] auto module_count() const -> usize { return modules_.size(); }
/** Tick every attached behaviour module. */
auto update() -> void {
for (const auto &module: modules_) module->update(*this);
}
private:
object_kind kind_ = object_kind::invalid;
uint32 owner_ = 0U;
real health_ = 1.0F;
real radius_ = 0.0F;
std::vector<std::unique_ptr<update_module>> modules_;
};
/**
* A participant in a match.
*
* The retail `Player` keeps money behind a `std::vector<Money*>` at
* `+0xe4` and power at `+0x74`; the fields here collapse those into plain
* values until the pointer indirection matters.
*/
class player {
public:
player(uint32 index, std::string name) : index_(index), name_(std::move(name)) {}
[[nodiscard]] auto index() const -> uint32 { return index_; }
[[nodiscard]] auto name() const -> const std::string & { return name_; }
[[nodiscard]] auto money() const -> int32 { return money_; }
auto set_money(int32 value) -> void { money_ = value < 0 ? 0 : value; }
auto add_money(int32 delta) -> void { this->set_money(money_ + delta); }
[[nodiscard]] auto power_produced() const -> int32 { return power_produced_; }
[[nodiscard]] auto power_consumed() const -> int32 { return power_consumed_; }
auto set_power(int32 produced, int32 consumed) -> void {
power_produced_ = produced;
power_consumed_ = consumed;
}
/** Faction bit flag (see `ra3::game::faction`). */
[[nodiscard]] auto faction() const -> uint32 { return faction_; }
auto set_faction(uint32 value) -> void { faction_ = value; }
[[nodiscard]] auto human() const -> bool { return human_; }
auto set_human(bool value) -> void { human_ = value; }
private:
uint32 index_;
std::string name_;
int32 money_ = 0;
int32 power_produced_ = 0;
int32 power_consumed_ = 0;
uint32 faction_ = 0U;
bool human_ = false;
};
/**
* The ordered set of players in a match (SAGE `PlayerList`).
*
* The retail list materialises an inline array of 20 `Player*` starting at
* `+0x30`; this owns them instead.
*/
class player_list {
public:
auto add(std::unique_ptr<player> value) -> player & {
auto &ref = *value;
players_.push_back(std::move(value));
return ref;
}
[[nodiscard]] auto size() const -> usize { return players_.size(); }
[[nodiscard]] auto at(usize index) -> player & { return *players_.at(index); }
[[nodiscard]] auto at(usize index) const -> const player & { return *players_.at(index); }
/** First human-controlled player, or `nullptr` in an all-AI match. */
[[nodiscard]] auto human() -> player * {
for (const auto &entry: players_) {
if (entry->human()) return entry.get();
}
return nullptr;
}
private:
std::vector<std::unique_ptr<player>> players_;
};
/**
* A spatial index over `Thing`s, bucketed on a fixed-size grid.
*
* A stand-in for SAGE's `PartitionManager`, which indexes the world into
* cells and answers proximity queries.
*/
class partition_manager {
public:
explicit partition_manager(real cell_size = 40.0F) : cell_size_(cell_size) {}
auto insert(thing &value) -> void { cells_[this->cell_of(value.position())].push_back(&value); }
auto clear() -> void { cells_.clear(); }
[[nodiscard]] auto cell_count() const -> usize { return cells_.size(); }
/** Non-owning view of everything bucketed with `position`. */
[[nodiscard]] auto things_at(const coord3d &position) -> std::span<thing *> {
const auto it = cells_.find(this->cell_of(position));
if (it == cells_.end()) return {};
return it->second;
}
private:
struct cell_key {
int32 x = 0;
int32 y = 0;
auto operator==(const cell_key &) const -> bool = default;
};
struct cell_hash {
auto operator()(const cell_key &key) const -> usize {
return (static_cast<usize>(static_cast<uint32>(key.x)) << 32U) ^ static_cast<uint32>(key.y);
}
};
[[nodiscard]] auto cell_of(const coord3d &position) const -> cell_key {
return {static_cast<int32>(position.x / cell_size_), static_cast<int32>(position.y / cell_size_)};
}
real cell_size_;
std::unordered_map<cell_key, std::vector<thing *>, cell_hash> cells_;
};
/**
* The simulation root and frame driver (SAGE `GameLogic`, singleton at
* `[0x00cd8ce4]` in the retail binary).
*
* `frame()` corresponds to the tick counter at `TheGameLogic + 0x50`, and
* `starting()` to the guard flag at `+0xa7` that the retail start routine
* raises while subsystems initialise.
*/
class game_logic {
public:
game_logic() = default;
[[nodiscard]] auto frame() const -> uint32 { return frame_; }
[[nodiscard]] auto starting() const -> bool { return starting_; }
[[nodiscard]] auto players() -> player_list & { return players_; }
[[nodiscard]] auto partition() -> partition_manager & { return partition_; }
[[nodiscard]] auto messages() -> message_stream & { return messages_; }
[[nodiscard]] auto random() -> ra3::core::random & { return random_; }
/**
* First half of starting a match: reset subsystems and seed the random
* stream. Mirrors the retail reset that runs before the message pump
* drains (the retail `startNewGame` raises `TheGameLogic + 0xa7`).
*
* @param seed Deterministic seed for the logic random stream.
*/
auto prepare_new_game(uint32 seed) -> void {
frame_ = 0U;
starting_ = true;
partition_.clear();
objects_.clear();
messages_.clear();
random_.seed(seed);
}
/** Second half: emit `new_game` so the pump starts the match. */
auto start_new_game() -> void {
messages_.append(message_id::new_game, 0U);
starting_ = false;
}
auto add_object(std::unique_ptr<object> value) -> object & {
auto &ref = *value;
partition_.insert(ref);
objects_.push_back(std::move(value));
return ref;
}
[[nodiscard]] auto object_count() const -> usize { return objects_.size(); }
/**
* Advance the simulation by exactly one 30 Hz logic frame: drain the
* message stream, tick every object, then bump the frame counter.
*/
auto update() -> void {
while (const auto message = messages_.pop()) {
(void)message;
}
for (const auto &entry: objects_) entry->update();
++frame_;
}
private:
uint32 frame_ = 0U;
bool starting_ = false;
player_list players_;
partition_manager partition_;
message_stream messages_;
ra3::core::random random_{1U};
std::vector<std::unique_ptr<object>> objects_;
};
}