#pragma once #include #include #include #include #include #include "../core/math.h" namespace unify { class BinaryWriter; class BinaryReader; enum class InputDevice : uint8_t { Keyboard, Mouse, Gamepad, Touch }; enum class InputEventType : uint8_t { ButtonDown, ButtonUp, ButtonRepeat, Axis, PointerMove }; /// Engine-level key/button codes (platform backends translate into these). namespace key { enum : uint16_t { Unknown = 0, E = 'e', I = 'i', A = 'a', D = 'd', S = 's', W = 'w', R = 'r', P = 'p', Space = 32, Escape = 27, Enter = 13, Left = 256, Right, Up, Down, F5, F9, F1, F2, F3, LShift, MouseLeft = 512, MouseRight, MouseMiddle, PadA = 768, PadB, PadX, PadY, PadStart, PadBack, PadLeft, PadRight, PadUp, PadDown, PadAxisLeftX = 1024, PadAxisLeftY, Touch0 = 1280, }; } struct InputEvent { uint64_t timestamp_ns = 0; // input clock (engine-relative ns); decides which tick consumes it InputDevice device = InputDevice::Keyboard; InputEventType type = InputEventType::ButtonDown; uint16_t code = 0; float value = 0; // axis value, or pointer x float value2 = 0; // pointer y }; /// Per-tick digested input: what gameplay reads. Serializable, so it can be recorded, /// replayed, sent over a network, or stored in a save state. struct InputFrame { uint64_t tick = 0; std::vector pressed; // went down during the tick (even if released in the same tick) std::vector released; // went up during the tick std::vector repeated; std::vector held; // down at the end of the tick std::map axes; Vec2 pointer; bool operator==(const InputFrame& o) const { return tick == o.tick && pressed == o.pressed && released == o.released && repeated == o.repeated && held == o.held && axes == o.axes && pointer == o.pointer; } void serialize(BinaryWriter& w) const; static InputFrame deserialize(BinaryReader& r); }; /// OS thread → queue → timestamp → fixed-tick consumption → gameplay. /// /// Platform code calls push() (thread-safe) as events arrive. At each fixed tick the /// scheduler calls consume_tick(tick_end_ns), which drains every event stamped before the /// end of that tick and folds it into an InputFrame. Events stamped later stay queued for /// the next tick, so a burst of fast frames or a long frame never drops or double-counts. class InputSystem { public: void push(const InputEvent& e); /// Builds the frame for `tick` from all queued events with timestamp < tick_end_ns. const InputFrame& consume_tick(uint64_t tick, uint64_t tick_end_ns); /// Replay / network path: use a recorded frame instead of live events. const InputFrame& apply_frame(const InputFrame& f); // Action mapping ("jump" -> Space, W, PadA ...). void bind(const std::string& action, uint16_t code); void bind_axis(const std::string& action, uint16_t negative, uint16_t positive, uint16_t analog_axis = 0); bool is_pressed(const std::string& action) const; bool is_held(const std::string& action) const; bool is_released(const std::string& action) const; bool is_repeated(const std::string& action) const; float axis(const std::string& action) const; /// True if the action was pressed within the last `ticks` ticks and not yet consumed. /// This is the gameplay-side buffer (e.g. jump pressed just before landing). bool buffered(const std::string& action, uint32_t ticks) const; void consume_buffered(const std::string& action); Vec2 pointer() const { return frame_.pointer; } const InputFrame& frame() const { return frame_; } size_t queued() const { std::lock_guard l(mutex_); return queue_.size(); } uint64_t events_consumed() const { return consumed_; } // Recording for deterministic replay. void set_recording(bool on) { recording_ = on; if (on) recorded_.clear(); } const std::vector& recorded() const { return recorded_; } void serialize(BinaryWriter& w) const; void deserialize(BinaryReader& r); private: bool code_in(const std::vector& list, const std::string& action) const; mutable std::mutex mutex_; std::vector queue_; // guarded by mutex_ std::vector drained_; // tick-local scratch (no per-tick allocation after warmup) std::vector down_; // keys currently down (sorted) std::map axes_; Vec2 pointer_; InputFrame frame_; std::map> bindings_; struct AxisBinding { uint16_t neg, pos, analog; }; std::map axis_bindings_; std::map last_pressed_tick_; // action -> tick+1 of last unconsumed press uint64_t consumed_ = 0; bool recording_ = false; std::vector recorded_; }; } // namespace unify