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 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> modules_; }; /** * A participant in a match. * * The retail `Player` keeps money behind a `std::vector` 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 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> 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 { 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(static_cast(key.x)) << 32U) ^ static_cast(key.y); } }; [[nodiscard]] auto cell_of(const coord3d &position) const -> cell_key { return {static_cast(position.x / cell_size_), static_cast(position.y / cell_size_)}; } real cell_size_; std::unordered_map, 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 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> objects_; }; }