180 lines
8 KiB
Text
180 lines
8 KiB
Text
|
|
/*
|
||
|
|
Crafter®.Graphics
|
||
|
|
Copyright (C) 2026 Catcrafts®
|
||
|
|
Catcrafts.net
|
||
|
|
|
||
|
|
This library is free software; you can redistribute it and/or
|
||
|
|
modify it under the terms of the GNU Lesser General Public
|
||
|
|
License as published by the Free Software Foundation; either
|
||
|
|
version 3.0 of the License, or (at your option) any later version.
|
||
|
|
|
||
|
|
This library is distributed in the hope that it will be useful,
|
||
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
|
Lesser General Public License for more details.
|
||
|
|
|
||
|
|
You should have received a copy of the GNU Lesser General Public
|
||
|
|
License along with this library; if not, write to the Free Software
|
||
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
|
*/
|
||
|
|
export module Crafter.Graphics:Input;
|
||
|
|
import std;
|
||
|
|
import Crafter.Math;
|
||
|
|
import Crafter.Event;
|
||
|
|
import :Keys;
|
||
|
|
import :Gamepad;
|
||
|
|
import :Window;
|
||
|
|
|
||
|
|
// Named-action input mapping with rebindable bindings. Game code declares
|
||
|
|
// actions ("Jump", "Move", "Fire"), attaches binding sources (keys, mouse
|
||
|
|
// buttons, gamepad sticks, WASD-as-Vector2, etc.), and subscribes to
|
||
|
|
// `onPerformed` / `onCanceled` / `onValueChanged` events. Bindings can be
|
||
|
|
// rebound at runtime via `StartRebind` — the next qualifying input
|
||
|
|
// becomes the new binding.
|
||
|
|
//
|
||
|
|
// Polling-based evaluation: `Map::Tick()` (called once per frame from
|
||
|
|
// game code, AFTER `Gamepad::Tick()`) reads the polled state of the
|
||
|
|
// attached window + connected gamepads and diffs against the action's
|
||
|
|
// previous state. No per-device event listeners — works regardless of
|
||
|
|
// gamepad connect/disconnect timing.
|
||
|
|
//
|
||
|
|
// Bindings store raw platform key codes (the same `KeyCode` domain as
|
||
|
|
// `Window::onRawKeyDown`). Source code uses `Key(CrafterKeys::Space)`
|
||
|
|
// from :Keys to obtain cross-platform default codes at compile time.
|
||
|
|
// Binding files serialized with `BindingToString` are platform-specific
|
||
|
|
// (a Win32 save won't load on Wayland) — document this for callers.
|
||
|
|
|
||
|
|
export namespace Crafter::Input {
|
||
|
|
|
||
|
|
enum class ActionType : std::uint8_t {
|
||
|
|
Button, // digital — pressed/released
|
||
|
|
Axis1D, // -1..1 (or 0..1 for triggers / scroll)
|
||
|
|
Vector2, // 2D — sticks, WASD, mouse delta
|
||
|
|
};
|
||
|
|
|
||
|
|
// ─── Binding alternatives ───────────────────────────────────────
|
||
|
|
struct KeyBind { KeyCode code; };
|
||
|
|
struct MouseButtonBind { std::uint8_t button; }; // 0 = left, 1 = right
|
||
|
|
struct MouseScrollBind { }; // Axis1D, accumulator drained per tick
|
||
|
|
struct MouseDeltaBind { float scale = 1.0f; }; // Vector2
|
||
|
|
struct GamepadButtonBind { std::uint32_t gamepadId; Gamepad::Button button; };
|
||
|
|
struct GamepadAxisBind { std::uint32_t gamepadId; Gamepad::Axis axis; bool invert = false; };
|
||
|
|
struct GamepadStickBind { std::uint32_t gamepadId; Gamepad::Stick stick; }; // Vector2
|
||
|
|
struct WASDBind {
|
||
|
|
KeyCode up;
|
||
|
|
KeyCode down;
|
||
|
|
KeyCode left;
|
||
|
|
KeyCode right;
|
||
|
|
};
|
||
|
|
|
||
|
|
using Binding = std::variant<
|
||
|
|
KeyBind, MouseButtonBind, MouseScrollBind, MouseDeltaBind,
|
||
|
|
GamepadButtonBind, GamepadAxisBind, GamepadStickBind, WASDBind
|
||
|
|
>;
|
||
|
|
|
||
|
|
struct Action {
|
||
|
|
std::string name;
|
||
|
|
ActionType type;
|
||
|
|
std::vector<Binding> bindings;
|
||
|
|
// Analog deadzone — bindings below this magnitude read as zero.
|
||
|
|
// Applied per-axis to GamepadAxisBind, radially to
|
||
|
|
// GamepadStickBind. Doesn't apply to Button (digital).
|
||
|
|
float deadzone = 0.15f;
|
||
|
|
|
||
|
|
// Polled state. Read freely from the listener callback or
|
||
|
|
// anywhere else; updated by `Map::Tick()`.
|
||
|
|
bool pressed = false;
|
||
|
|
float value = 0.0f;
|
||
|
|
Vector<float, 2> vector2{};
|
||
|
|
|
||
|
|
// Events fired during `Map::Tick()` when the polled state crosses
|
||
|
|
// an edge or changes value.
|
||
|
|
Event<void> onPerformed; // pressed (Button) / non-zero edge (axis)
|
||
|
|
Event<void> onCanceled; // released (Button) / back to zero
|
||
|
|
Event<float> onValueChanged; // Axis1D — value
|
||
|
|
Event<Vector<float,2>> onVector2Changed; // Vector2 — vector2
|
||
|
|
};
|
||
|
|
|
||
|
|
enum class CaptureMask : std::uint8_t {
|
||
|
|
Keyboard = 1,
|
||
|
|
Mouse = 2,
|
||
|
|
Gamepad = 4,
|
||
|
|
Any = 0xFF,
|
||
|
|
};
|
||
|
|
constexpr CaptureMask operator|(CaptureMask a, CaptureMask b) {
|
||
|
|
return (CaptureMask)((std::uint8_t)a | (std::uint8_t)b);
|
||
|
|
}
|
||
|
|
constexpr bool HasFlag(CaptureMask m, CaptureMask f) {
|
||
|
|
return ((std::uint8_t)m & (std::uint8_t)f) != 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
struct Map {
|
||
|
|
std::vector<std::unique_ptr<Action>> actions;
|
||
|
|
|
||
|
|
Action& AddAction(std::string name, ActionType type);
|
||
|
|
Action* Find(std::string_view name);
|
||
|
|
|
||
|
|
// Subscribe internally to the window's mouse-scroll event so a
|
||
|
|
// single scroll between ticks isn't lost. All other state is
|
||
|
|
// polled from `Window` / `Gamepad`. Detaching releases that one
|
||
|
|
// listener; the polled-state reads automatically stop.
|
||
|
|
void Attach(Window& window);
|
||
|
|
void Detach();
|
||
|
|
|
||
|
|
// Apply current input state to all actions. Fire edge / value
|
||
|
|
// events. Call once per frame AFTER `Gamepad::Tick()`. Safe to
|
||
|
|
// call when not attached (no-op).
|
||
|
|
void Tick();
|
||
|
|
|
||
|
|
// Capture the next qualifying input as a binding. While capture
|
||
|
|
// is active, that input does NOT dispatch to actions — it only
|
||
|
|
// routes to `onCaptured`. Capturing replaces nothing in the
|
||
|
|
// action's bindings; the callback decides whether to assign,
|
||
|
|
// append, or discard. Cancel via `StopRebind`.
|
||
|
|
void StartRebind(Action& action, CaptureMask mask,
|
||
|
|
std::function<void(Binding)> onCaptured);
|
||
|
|
void StopRebind();
|
||
|
|
bool IsRebinding() const;
|
||
|
|
|
||
|
|
// ─── Internals (exposed because we're a POD-ish struct) ──────
|
||
|
|
Window* window = nullptr;
|
||
|
|
// Mouse scroll is the one event-driven source; everything else
|
||
|
|
// is polled. The listener feeds into `scrollAccumulator`,
|
||
|
|
// drained on the next Tick.
|
||
|
|
std::unique_ptr<EventListener<std::uint32_t>> scrollListener;
|
||
|
|
std::int32_t scrollAccumulator = 0;
|
||
|
|
|
||
|
|
// Mouse delta is computed from window.currentMousePos vs the
|
||
|
|
// position observed last tick. nullopt before first tick.
|
||
|
|
std::optional<Vector<float, 2>> lastMousePos;
|
||
|
|
|
||
|
|
// Rebind state.
|
||
|
|
struct RebindState {
|
||
|
|
Action* action;
|
||
|
|
CaptureMask mask;
|
||
|
|
std::function<void(Binding)> onCaptured;
|
||
|
|
// Snapshot of all input at rebind-start, so we only fire on
|
||
|
|
// a fresh down-edge (not on a button held since before).
|
||
|
|
std::unordered_set<KeyCode> keysHeldAtStart;
|
||
|
|
bool mouseLeftHeldAtStart = false;
|
||
|
|
bool mouseRightHeldAtStart = false;
|
||
|
|
std::unordered_map<std::uint32_t, std::array<bool, (std::size_t)Gamepad::Button::Max>> gamepadButtonsAtStart;
|
||
|
|
};
|
||
|
|
std::optional<RebindState> rebind;
|
||
|
|
};
|
||
|
|
|
||
|
|
// ─── Serialization ──────────────────────────────────────────────
|
||
|
|
// Single-binding text round-trip. Caller composes binding files
|
||
|
|
// however they like (JSON / INI / line-per-binding). Formats:
|
||
|
|
// "key:<hex>" — KeyBind
|
||
|
|
// "mb:<0|1>" — MouseButtonBind
|
||
|
|
// "mscroll" — MouseScrollBind
|
||
|
|
// "mdelta:<scale>" — MouseDeltaBind
|
||
|
|
// "gpb:<id>:<button>" — GamepadButtonBind
|
||
|
|
// "gpa:<id>:<axis>:<inv>" — GamepadAxisBind (inv = 0/1)
|
||
|
|
// "gps:<id>:<stick>" — GamepadStickBind
|
||
|
|
// "wasd:<u>:<d>:<l>:<r>" — WASDBind, four hex KeyCodes
|
||
|
|
// KeyCode hex values are platform-specific; see :Keys.
|
||
|
|
std::string BindingToString(const Binding&);
|
||
|
|
std::optional<Binding> BindingFromString(std::string_view);
|
||
|
|
}
|