295 lines
10 KiB
C++
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_;
|
|
};
|
|
}
|