#ifndef TACTICAL_SHOOTER_SIMULATION_SERVER_H #define TACTICAL_SHOOTER_SIMULATION_SERVER_H #include "entity.h" #include "hit_detection.h" #include "movement_component.h" #include "state_serializer.h" #include #include #include #include #include #include #include #include #include namespace tactical_shooter { /** * Position history ring-buffer entry — stores entity positions at a given tick. * Used for lag compensation: we can rewind targets to where they were when a * shot was fired, even if they've since moved. */ struct TickPositionSnapshot { uint32_t tick = 0; struct Entry { uint16_t entity_id; godot::Vector3 position; uint16_t flags; }; std::vector entries; }; /** * A pending weapon fire with time-of-fire information for lag compensation. */ struct PendingFire { uint16_t shooter_id = 0xFFFF; uint32_t fire_tick = 0; // Server tick at which the fire was queued uint32_t input_sequence = 0; // Client's input sequence (for reconciliation) }; /** * SimulationServer — the heart of the tactical shooter simulation. * * This singleton manages the entire 128Hz game simulation loop from C++, * entirely avoiding GDScript VM overhead in the hot path. * * GDScript integration: * var server = SimulationServer.new() * server.set_tick_rate(128) * server.start() * # In _process(): * while server.can_tick(delta): * var serialized = server.tick() * # send serialized to network layer * * Architecture: * - All entity simulation (movement, hit detection) runs in C++ * - Serialized state is handed to GDScript for network transport * - Player input arrives from GDScript, gets applied per-entity * - Lag compensation stores a ring buffer of pre-move positions each tick * - Benchmark hooks for the 128Hz load test (t_f671f48a) */ class SimulationServer : public godot::Object { GDCLASS(SimulationServer, godot::Object) public: SimulationServer(); ~SimulationServer(); // ---- Lifecycle (GDScript API) ------------------------------------------ /** * Set the simulation tick rate (calls per second). * Default: 128 (for 128Hz tick). */ void set_tick_rate(uint32_t hz); uint32_t get_tick_rate() const { return tick_hz_; } /** * Start the simulation. Creates initial entities if count > 0. */ void start(); /** * Stop the simulation. Clears all entities and resets state. */ void stop(); /** * Check if the simulation is running. */ bool is_running() const { return running_; } /** * Accrue time and check if a tick is due. * Call this in _process(delta) and call tick() while this returns true. * * Example (GDScript): * while server.can_tick(delta): * input = server.tick() */ bool can_tick(float delta); /** * Advance one simulation tick. * Returns serialized snapshot as a PackedByteArray (or empty if no tick due). */ godot::PackedByteArray tick(); // ---- Entity Management (GDScript API) ----------------------------------- /** * Spawn a new entity at the given position. * Returns the entity ID (0..65535) or UINT16_MAX on failure. */ uint16_t spawn_entity(const godot::Vector3 &position); /** * Despawn an entity by ID. */ void despawn_entity(uint16_t entity_id); /** * Get an entity by ID. Returns null if not found. */ godot::Ref get_entity(uint16_t entity_id); /** * Get number of active entities. */ uint16_t get_entity_count() const; /** * Get entity IDs of all active entities as an array. */ godot::Array get_entity_ids() const; // ---- Input (GDScript API) ----------------------------------------------- /** * Apply input for a specific entity on the next tick. * Call this from _process() as input arrives. * * @param entity_id Target entity * @param input_dict Dictionary with keys: "move_direction" (Vector3), * "look_yaw" (float), "look_pitch" (float), "jump" (bool), * "crouch" (bool), "sprint" (bool), "shoot" (bool), "aim" (bool), * "input_sequence" (int) */ void apply_input(uint16_t entity_id, const godot::Dictionary &input_dict); /** * Queue a weapon fire from an entity. * Fires on the next tick. */ void fire_weapon(uint16_t entity_id); // ---- Movement Configuration (GDScript API) ------------------------------- void set_movement_walk_speed(float speed); void set_movement_sprint_speed(float speed); void set_movement_crouch_speed(float speed); void set_movement_acceleration(float accel); void set_movement_jump_velocity(float vel); void set_movement_gravity(float gravity); void set_movement_config(const godot::Dictionary &config); // ---- Weapon Configuration (GDScript API) ---------------------------------- /** * Set the weapon damage profile for hitscan weapons. * Called from GDScript to define weapon stats. * * Dictionary keys: * base_damage (float) — base damage per shot * head_multiplier (float) — damage multiplier for head hits * body_multiplier (float) — damage multiplier for body hits * arm_multiplier (float) — damage multiplier for arm hits * leg_multiplier (float) — damage multiplier for leg hits * max_range (float) — maximum shot distance in units * spread_degrees (float) — random spread per shot (degrees) * fire_rate_hz (float) — max shots per second */ void set_weapon_config(const godot::Dictionary &config); // ---- Lag Compensation Configuration (GDScript API) ------------------------- /** * Set the depth of the position history ring buffer (in ticks). * More ticks = can compensate for higher latency but uses more memory. * Default: 64 ticks (~500ms at 128Hz, enough for any reasonable ping). */ void set_history_depth(uint32_t ticks); // ---- Damaged / Death Signal (GDScript API via Dictionary return) ----------- /** * Get the most recent hit result from processing combat. * Returns Dictionary with: hit (bool), entity_id (int), damage (float), * distance (float), hitbox_id (int), killed (bool). * Empty if no shot was processed this tick. */ godot::Dictionary get_last_hit_result() const; // ---- Benchmark / Stats (GDScript API) ------------------------------------ /** * Get tick timing statistics for benchmarking. * Returns Dictionary with: "last_tick_usec" (int), "avg_tick_usec" (float), * "peak_tick_usec" (float), "tick_count" (int), "entity_count" (int) */ godot::Dictionary get_stats() const; /** * Reset benchmark statistics. */ void reset_stats(); /** * Populate simulation with N bots for load testing. * Places them in a grid pattern. */ void populate_bots(uint16_t count); protected: static void _bind_methods(); private: // ---- Internal tick logic ------------------------------------------------ /** * Advance the simulation by one tick (fixed timestep). */ void process_tick(); /** * Update all entity movement. */ void update_movement(); /** * Process all queued weapon fires. */ void update_combat(); /** * Collect and serialize the current simulation state. */ godot::PackedByteArray serialize_state(); // ---- Helpers ------------------------------------------------------------ /** * Find the lowest available entity ID. */ uint16_t allocate_entity_id(); // ---- Lag Compensation (Position History) ---------------------------------- /** * Record pre-move positions of all living entities for the given tick. * Called at the start of process_tick(), BEFORE update_movement(). */ void record_position_history(uint32_t tick); /** * Rewind target entities to positions stored at a given tick for hit * detection, then restore them back. */ struct RewindState { std::vector saved; }; RewindState begin_rewind(uint32_t target_tick); void end_rewind(const RewindState &state); /** * Process a single compensated fire: rewind, check hit, restore. */ void process_compensated_fire(const PendingFire &fire); // ---- State -------------------------------------------------------------- bool running_ = false; uint32_t tick_hz_ = 128; float tick_interval_ = 1.0f / 128.0f; float time_accumulator_ = 0.0f; // Tick counter uint32_t current_tick_ = 0; // Entity storage (by ID) std::vector> entities_by_id_; std::vector living_entity_ptrs_; // raw ptrs for hot loop // Systems MovementComponent movement_; HitDetection hit_detection_; StateSerializer serializer_; // Queued input (applied on next tick, cleared after) struct QueuedInput { EntityInput input; bool pending = false; }; std::vector queued_inputs_; // Queued weapon fires with lag-compensation info std::vector pending_fires_; WeaponDamage weapon_config_; float weapon_fire_rate_hz_ = 10.0f; // Last hit result (queried by GDScript for feedback) struct LastHitResult { bool hit = false; uint16_t entity_id = 0xFFFF; float damage = 0.0f; float distance = 0.0f; uint8_t hitbox_id = 0; bool killed = false; }; LastHitResult last_hit_result_; // Position history ring buffer (for lag compensation) std::vector position_history_; uint32_t history_depth_ = 64; uint32_t history_write_index_ = 0; // Last serialized snapshot per entity (for delta compression) std::unordered_map last_snapshots_; // Benchmark stats uint64_t tick_count_ = 0; uint64_t last_tick_usec_ = 0; uint64_t total_tick_usec_ = 0; uint64_t max_tick_usec_ = 0; }; } // namespace tactical_shooter #endif // TACTICAL_SHOOTER_SIMULATION_SERVER_H