Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config

Includes code from task workspaces that was never pushed:
- client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd
- gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files)
- scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd
- scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README
- scripts/config/ — server_config.gd (480 lines)
- scenes/ — client_main, server_main, player, test_range
- project.godot, export_presets.cfg
This commit is contained in:
2026-07-01 18:30:44 -04:00
parent 2cf57a989f
commit e9dc05983c
42 changed files with 6249 additions and 0 deletions
@@ -0,0 +1,243 @@
#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 <cstdint>
#include <unordered_map>
#include <vector>
namespace tactical_shooter {
/**
* 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
* - 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);
// ---- 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();
// ---- 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 (entity IDs)
std::vector<uint16_t> pending_fires_;
// 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