9ea98aa7b8
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0] - Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting - Fix: Disabled GDExtension temporarily for clean export - Add: Windows PE32+ x86_64 client binary (109MB) at build/tactical-shooter-windows-x86_64/ - Add: tactical-shooter-windows.zip (portable zip package) - Build: Linux server binary (78MB) rebuilt
348 lines
10 KiB
C++
348 lines
10 KiB
C++
#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 <godot_cpp/classes/object.hpp>
|
|
#include <godot_cpp/classes/ref.hpp>
|
|
#include <godot_cpp/variant/dictionary.hpp>
|
|
#include <godot_cpp/variant/packed_byte_array.hpp>
|
|
#include <godot_cpp/variant/vector3.hpp>
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
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<Entry> 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<Entity> 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<TickPositionSnapshot::Entry> 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<godot::Ref<Entity>> entities_by_id_;
|
|
std::vector<Entity *> 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<QueuedInput> queued_inputs_;
|
|
|
|
// Queued weapon fires with lag-compensation info
|
|
std::vector<PendingFire> 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<TickPositionSnapshot> position_history_;
|
|
uint32_t history_depth_ = 64;
|
|
uint32_t history_write_index_ = 0;
|
|
|
|
// Last serialized snapshot per entity (for delta compression)
|
|
std::unordered_map<uint16_t, EntitySnapshot> 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
|