Skip to content

Instantly share code, notes, and snippets.

@RandyGaul
Created September 23, 2025 22:11
Show Gist options
  • Save RandyGaul/d19ae7d049d9756809af0b2d724a8c65 to your computer and use it in GitHub Desktop.
Save RandyGaul/d19ae7d049d9756809af0b2d724a8c65 to your computer and use it in GitHub Desktop.
Experimental highish level CF input binding layer
//--------------------------------------------------------------------------------------------------
// Basic input polling.
// Original implementation by Noel Berry, taken from his Blah code on GitHub.
#define joy_press cf_joypad_button_just_pressed
#define joy_release cf_joypad_button_just_released
#define joy_axis cf_joypad_axis
#define joy_axis_prev cf_joypad_axis_prev
#define joy_down cf_joypad_button_down
#define joy_up !cf_joypad_button_down
#define key_ctrl cf_key_ctrl
#define key_alt cf_key_alt
#define key_shift cf_key_shift
#define key_press cf_key_just_pressed
#define key_release cf_key_just_released
#define key_down cf_key_down
#define key_up cf_key_up
#define mouse_press cf_mouse_just_pressed
#define mouse_release cf_mouse_just_released
#define mouse_down cf_mouse_down
#define mouse_up(x) (!cf_mouse_down(x))
#define mouse_wheel cf_mouse_wheel_motion
v2 mouse() { return cf_screen_to_world(V2(cf_mouse_x(), cf_mouse_y())); }
#define rumble(lo, hi, duration_seconds) cf_joypad_rumble(0, (uint16_t)((lo) * 32767), (uint16_t)((hi) * 32767), (int)((duration_seconds) * 1000))
#define mouse_to_world() cf_screen_to_world(V2(cf_mouse_x(),cf_mouse_y()))
//--------------------------------------------------------------------------------------------------
// Input binding.
//
// Key features
// * Binding abstraction, able to bind multiple physical buttons to a
// single abstract binding object.
// * Input buffering, able to persist inputs over a small time buffer
// to implement QoL features for players, making certain inputs more
// forgiving.
//
// EXAPLE TODO
//
// TODO
#define add_dpad stick_binding_add_dpad
#define add_wasd stick_binding_add_wasd
#define add_arrow_keys stick_binding_add_arrow_keys
#define add_keys stick_binding_add_keys
#define add_left_stick stick_binding_add_left_stick
#define add_right_stick stick_binding_add_right_stick
#define add_key button_binding_add_key
#define add_mouse button_binding_add_mouse
#define add_joy button_binding_add_joy
#define add_trigger button_binding_add_trigger
#define binding_down button_binding_down
#define binding_up !button_binding_down
#define binding_sign(x) _Generic((x), \
ButtonBinding*: button_binding_sign, \
AxisBinding*: axis_binding_sign, \
StickBinding*: stick_binding_sign \
)(x)
#define binding_value(x) _Generic((x), \
ButtonBinding*: button_binding_value, \
AxisBinding*: axis_binding_value, \
StickBinding*: stick_binding_value \
)(x)
#define binding_press(x) _Generic((x), \
ButtonBinding*: button_binding_press, \
AxisBinding*: axis_binding_press, \
StickBinding*: stick_binding_press \
)(x)
#define binding_release(x) _Generic((x), \
ButtonBinding*: button_binding_release, \
AxisBinding*: axis_binding_release, \
StickBinding*: stick_binding_release \
)(x)
#define consume_press(x) _Generic((x), \
ButtonBinding*: button_binding_consume_press, \
AxisBinding*: axis_binding_consume_press, \
StickBinding*: stick_binding_consume_press \
)(x)
#define consume_release(x) _Generic((x), \
ButtonBinding*: button_binding_consume_release, \
AxisBinding*: axis_binding_consume_release, \
StickBinding*: stick_binding_consume_release \
)(x)
#define destroy_binding(x) _Generic((x), \
ButtonBinding*: destroy_button_binding, \
AxisBinding*: destroy_axis_binding, \
StickBinding*: destroy_stick_binding \
)(x)
//--------------------------------------------------------------------------------------------------
// Input binding implementation.
//
// Original implementation by Noel Berry (blah framework).
// https://github.com/NoelFB/blah
//
// The deadzone zeros out stick inputs from controllers when they are too close to
// zero. Inputs range from -1 to 1 from the device, but get stuck at near-zero, even
// for a fresh and healthy controller. Controller manufacturers tend to recommend to zero
// out any inputs from the device below roughly 0.15.
//
// Should be a number roughly from 0.1 to 0.2, whereas 0.1 is really on the conservative
// end where you'll start seeing stick drift.
//
// This deadzone implementation is dead simple (haha) using a square clear-to-zero. Adjust
// this value as-needed to get good results.
#define DEADZONE 0.15f
#define MAX_JOYPADS 8
typedef struct Joypad
{
int index;
bool connected;
CF_JoypadPowerLevel power_level;
const char* name;
CF_JoypadType type;
uint16_t vendor;
uint16_t product_id;
const char* serial_number;
uint16_t firmware_version;
uint16_t product_version;
} Joypad;
typedef struct JoypadOnConnect
{
void* udata;
void (*on_connect)(int, bool, void*);
} JoypadOnConnect;
dyna Joypad* g_joypads;
dyna JoypadOnConnect* g_on_joypad_connects;
Joypad get_joypad(int index)
{
Joypad joy = {
.index = index,
.connected = cf_joypad_is_connected(index),
.power_level = cf_joypad_power_level(index),
.name = cf_joypad_name(index),
.type = cf_joypad_type(index),
.vendor = cf_joypad_vendor(index),
.product_id = cf_joypad_product_id(index),
.serial_number = cf_joypad_serial_number(index),
.firmware_version = cf_joypad_firmware_version(index),
.product_version = cf_joypad_product_version(index),
};
return joy;
}
// Returns a value from -1.0f to 1.0f.
float int16_to_float(int16_t v)
{
// int16_t ranges from -32768 to 32767.
const int16_t max_int16 = 32767;
// Normalize the value to a float between -1 and 1.
float normalized_v = (float)v / max_int16;
// Simplest possible "square" clear-to-zero strategy.
float abs_v = abs(normalized_v);
if (abs_v < DEADZONE) {
return 0;
}
// Remap outputs back to 0...1 after applying the deadzone.
return sign(normalized_v) * remap_to_01(abs_v, DEADZONE, 1.0f);
}
typedef enum InputType
{
INPUT_TYPE_KEY,
INPUT_TYPE_MOUSE,
INPUT_TYPE_BUTTON,
INPUT_TYPE_TRIGGER,
} InputType;
typedef struct InputBinding
{
InputType type;
bool positive;
float threshold;
union {
int key;
int button;
int axis;
};
} InputBinding;
bool axis_is_down_raw(InputBinding input, float v)
{
if ((v > 0 && input.positive) || (v < 0 && !input.positive)) {
if (abs(v) >= input.threshold) {
return true;
}
}
return false;
}
bool axis_is_down(int index, InputBinding input)
{
float v = int16_to_float(joy_axis(index, input.axis));
return axis_is_down_raw(input, v);
}
bool axis_prev_is_down(int index, InputBinding input)
{
float v = int16_to_float(joy_axis_prev(index, input.axis));
return axis_is_down_raw(input, v);
}
bool input_is_down(int index, const dyna InputBinding* inputs)
{
for (int i = 0; i < asize(inputs); ++i) {
InputBinding input = inputs[i];
if (input.type == INPUT_TYPE_KEY) {
if (key_down(input.key)) return true;
} else if (input.type == INPUT_TYPE_MOUSE) {
if (mouse_down(input.button)) return true;
} else if (input.type == INPUT_TYPE_BUTTON) {
if (joy_down(index, input.button)) return true;
} else if (input.type == INPUT_TYPE_TRIGGER) {
if (axis_is_down(index, input)) return true;
} else {
assert(false);
}
}
return false;
}
bool input_get_pressed(int index, const dyna InputBinding* inputs)
{
for (int i = 0; i < asize(inputs); ++i) {
InputBinding input = inputs[i];
if (input.type == INPUT_TYPE_KEY) {
if (key_press(input.key)) return true;
} else if (input.type == INPUT_TYPE_MOUSE) {
if (mouse_press(input.button)) return true;
} else if (input.type == INPUT_TYPE_BUTTON) {
if (joy_press(index, input.button)) return true;
} else if (input.type == INPUT_TYPE_TRIGGER) {
if (axis_is_down(index, input) && !axis_prev_is_down(index, input)) return true;
} else {
assert(false);
}
}
return false;
}
bool input_get_released(int index, const dyna InputBinding* inputs)
{
for (int i = 0; i < asize(inputs); ++i) {
InputBinding input = inputs[i];
if (input.type == INPUT_TYPE_KEY) {
if (key_release(input.key)) return true;
} else if (input.type == INPUT_TYPE_MOUSE) {
if (mouse_release(input.button)) return true;
} else if (input.type == INPUT_TYPE_BUTTON) {
if (joy_release(index, input.button)) return true;
} else if (input.type == INPUT_TYPE_TRIGGER) {
if (!axis_is_down(index, input) && axis_prev_is_down(index, input)) return true;
} else {
assert(false);
}
}
return false;
}
float input_get_value(int index, const dyna InputBinding* inputs)
{
float highest_value = 0;
for (int i = 0; i < asize(inputs); ++i) {
InputBinding input = inputs[i];
if (input.type == INPUT_TYPE_KEY) {
if (key_down(input.key)) return 1.0f;
} else if (input.type == INPUT_TYPE_MOUSE) {
if (mouse_down(input.button)) return 1.0f;
} else if (input.type == INPUT_TYPE_BUTTON) {
if (joy_down(index, input.button)) return 1.0f;
} else if (input.type == INPUT_TYPE_TRIGGER) {
float v = int16_to_float(joy_axis(index, input.axis));
if (axis_is_down_raw(input, v)) {
highest_value = fmaxf(highest_value, fabsf(v));
}
} else {
assert(false);
}
}
return highest_value;
}
//--------------------------------------------------------------------------------------------------
typedef struct ButtonBinding
{
int index;
float press_buffer;
dyna InputBinding* inputs;
float last_timestep;
float last_press_time;
float last_release_time;
bool is_down;
float v;
bool pressed;
bool released;
bool press_consumed;
bool release_consumed;
} ButtonBinding;
dyna ButtonBinding* g_button_bindings;
ButtonBinding* make_button_binding(int player_index, float press_buffer)
{
ButtonBinding binding = {
.index = player_index,
.press_buffer = press_buffer,
.inputs = NULL,
.last_timestep = 0,
.last_press_time = -1,
.last_release_time = -1,
.is_down = false,
.v = 0,
.pressed = false,
.released = false,
.press_consumed = false,
.release_consumed = false,
};
apush(g_button_bindings, binding);
return &alast(g_button_bindings);
}
void destroy_button_binding(ButtonBinding* binding)
{
int i = index_of(g_button_bindings, binding);
if (i != -1) {
adel(g_button_bindings, i);
}
}
void button_binding_add_key(ButtonBinding* binding, int key)
{
InputBinding input = {
.type = INPUT_TYPE_KEY,
.key = key,
};
apush(binding->inputs, input);
}
void button_binding_add_mouse(ButtonBinding* binding, int mouse_button)
{
InputBinding input = {
.type = INPUT_TYPE_MOUSE,
.button = mouse_button,
};
apush(binding->inputs, input);
}
void button_binding_add_joy(ButtonBinding* binding, int button)
{
InputBinding input = {
.type = INPUT_TYPE_BUTTON,
.button = button,
};
apush(binding->inputs, input);
}
void button_binding_add_trigger(ButtonBinding* binding, int axis, float threshold, bool positive)
{
InputBinding input = {
.type = INPUT_TYPE_TRIGGER,
.axis = axis,
.threshold = threshold,
.positive = positive,
};
apush(binding->inputs, input);
}
bool button_binding_press(ButtonBinding* binding)
{
if (binding->press_consumed) return false;
if (binding->last_press_time >= 0 && (SECONDS-binding->last_press_time) <= binding->press_buffer) {
return true;
}
return binding->pressed;
}
bool button_binding_release(ButtonBinding* binding)
{
if (binding->release_consumed) return false;
if (binding->last_release_time >= 0 && (SECONDS-binding->last_release_time) <= binding->press_buffer) {
return true;
}
return binding->released;
}
void button_binding_consume_press(ButtonBinding* binding)
{
binding->press_consumed = true;
binding->last_press_time = -1;
}
void button_binding_consume_release(ButtonBinding* binding)
{
binding->release_consumed = true;
binding->last_release_time = -1;
}
bool button_binding_down(ButtonBinding* binding)
{
return binding->is_down;
}
float button_binding_value(ButtonBinding* binding)
{
return binding->v;
}
float button_binding_sign(ButtonBinding* binding)
{
if (binding->v > 0) return 1.0f;
else if (binding->v == 0) return 0;
else return -1.0f;
}
void button_binding_update(ButtonBinding* binding)
{
binding->press_consumed = false;
binding->release_consumed = false;
if (input_get_pressed(binding->index, binding->inputs)) {
binding->last_timestep = SECONDS;
binding->last_press_time = SECONDS;
binding->pressed = true;
} else {
binding->pressed = false;
}
if (input_get_released(binding->index, binding->inputs)) {
binding->last_release_time = SECONDS;
binding->released = true;
} else {
binding->released = false;
}
binding->is_down = input_is_down(binding->index, binding->inputs);
binding->v = input_get_value(binding->index, binding->inputs);
}
//--------------------------------------------------------------------------------------------------
typedef struct AxisBinding
{
int index;
int conflict;
ButtonBinding* negative;
ButtonBinding* positive;
} AxisBinding;
#define HANDLE_CONFLICT_NEWEST 0
#define HANDLE_CONFLICT_OLDEST 1
#define HANDLE_CONFLICT_CANCEL 2
AxisBinding* make_axis_binding(int player_index)
{
AxisBinding* axis_binding = CALLOC(AxisBinding);
*axis_binding = (AxisBinding){
.index = player_index,
.conflict = HANDLE_CONFLICT_NEWEST,
.negative = make_button_binding(player_index, 0),
.positive = make_button_binding(player_index, 0),
};
return axis_binding;
}
void destroy_axis_binding(AxisBinding* binding)
{
destroy_button_binding(binding->negative);
destroy_button_binding(binding->positive);
FREE(binding);
}
void axis_binding_add_key_pair(AxisBinding* binding, int negative, int positive)
{
button_binding_add_key(binding->negative, negative);
button_binding_add_key(binding->positive, positive);
}
void axis_binding_add_mouse_button_pair(AxisBinding* binding, int negative, int positive)
{
button_binding_add_mouse(binding->negative, negative);
button_binding_add_mouse(binding->positive, positive);
}
void axis_binding_add_joypad_button_pair(AxisBinding* binding, int negative, int positive)
{
button_binding_add_joy(binding->negative, negative);
button_binding_add_joy(binding->positive, positive);
}
void axis_binding_add_trigger_pair(AxisBinding* binding, int negative, int positive, float threshold)
{
button_binding_add_trigger(binding->negative, negative, threshold, false);
button_binding_add_trigger(binding->positive, positive, threshold, true);
}
void axis_binding_handle_conflict_by_newest(AxisBinding* binding)
{
binding->conflict = HANDLE_CONFLICT_NEWEST;
}
void axis_binding_handle_conflict_by_oldest(AxisBinding* binding)
{
binding->conflict = HANDLE_CONFLICT_OLDEST;
}
void axis_binding_handle_conflict_by_cancel(AxisBinding* binding)
{
binding->conflict = HANDLE_CONFLICT_CANCEL;
}
void axis_binding_add_left_stick_x(AxisBinding* binding, float threshold)
{
button_binding_add_trigger(binding->negative, CF_JOYPAD_AXIS_LEFTX, threshold, false);
button_binding_add_trigger(binding->positive, CF_JOYPAD_AXIS_LEFTX, threshold, true);
}
void axis_binding_add_left_stick_y(AxisBinding* binding, float threshold)
{
button_binding_add_trigger(binding->negative, CF_JOYPAD_AXIS_LEFTY, threshold, true);
button_binding_add_trigger(binding->positive, CF_JOYPAD_AXIS_LEFTY, threshold, false);
}
void axis_binding_add_right_stick_x(AxisBinding* binding, float threshold)
{
button_binding_add_trigger(binding->negative, CF_JOYPAD_AXIS_RIGHTX, threshold, false);
button_binding_add_trigger(binding->positive, CF_JOYPAD_AXIS_RIGHTX, threshold, true);
}
void axis_binding_add_right_stick_y(AxisBinding* binding, float threshold)
{
button_binding_add_trigger(binding->negative, CF_JOYPAD_AXIS_RIGHTY, threshold, true);
button_binding_add_trigger(binding->positive, CF_JOYPAD_AXIS_RIGHTY, threshold, false);
}
bool axis_binding_press(AxisBinding* binding)
{
return button_binding_press(binding->negative) || button_binding_press(binding->positive);
}
bool axis_binding_release(AxisBinding* binding)
{
return button_binding_release(binding->negative) || button_binding_release(binding->positive);
}
void axis_binding_consume_press(AxisBinding* binding)
{
button_binding_consume_press(binding->negative);
button_binding_consume_press(binding->positive);
}
void axis_binding_consume_release(AxisBinding* binding)
{
button_binding_consume_release(binding->negative);
button_binding_consume_release(binding->positive);
}
float axis_binding_value(AxisBinding* binding)
{
float negative = button_binding_value(binding->negative);
float positive = button_binding_value(binding->positive);
if (negative <= 0 && positive <= 0) return 0;
if (negative > 0 && positive <= 0) return -negative;
if (positive > 0 && negative <= 0) return positive;
if (binding->conflict == HANDLE_CONFLICT_CANCEL) return 0;
else if (binding->conflict == HANDLE_CONFLICT_OLDEST) {
if (binding->negative->last_timestep < binding->positive->last_timestep) {
return -negative;
} else {
return positive;
}
} else if (binding->conflict == HANDLE_CONFLICT_NEWEST) {
if (binding->negative->last_timestep < binding->positive->last_timestep) {
return positive;
} else {
return -negative;
}
}
return 0;
}
float axis_binding_sign(AxisBinding* binding)
{
float v = axis_binding_value(binding);
if (v > 0) return 1;
if (v < 0) return -1;
return 0;
}
//--------------------------------------------------------------------------------------------------
typedef struct StickBinding
{
AxisBinding* x_axis;
AxisBinding* y_axis;
} StickBinding;
StickBinding* make_stick_binding(int player_index)
{
StickBinding* stick_binding = CALLOC(StickBinding);
*stick_binding = (StickBinding){
.x_axis = make_axis_binding(player_index),
.y_axis = make_axis_binding(player_index),
};
return stick_binding;
}
void destroy_stick_binding(StickBinding* binding)
{
destroy_axis_binding(binding->x_axis);
destroy_axis_binding(binding->y_axis);
FREE(binding);
}
void stick_binding_add_keys(StickBinding* binding, int up, int down, int left, int right)
{
axis_binding_add_key_pair(binding->x_axis, left, right);
axis_binding_add_key_pair(binding->y_axis, up, down);
}
void stick_binding_add_wasd(StickBinding* binding)
{
stick_binding_add_keys(binding, CF_KEY_W, CF_KEY_S, CF_KEY_A, CF_KEY_D);
}
void stick_binding_add_arrow_keys(StickBinding* binding)
{
stick_binding_add_keys(binding, CF_KEY_UP, CF_KEY_DOWN, CF_KEY_LEFT, CF_KEY_RIGHT);
}
void stick_binding_add_dpad(StickBinding* binding)
{
axis_binding_add_joypad_button_pair(binding->x_axis, CF_JOYPAD_BUTTON_DPAD_LEFT, CF_JOYPAD_BUTTON_DPAD_RIGHT);
axis_binding_add_joypad_button_pair(binding->y_axis, CF_JOYPAD_BUTTON_DPAD_DOWN, CF_JOYPAD_BUTTON_DPAD_UP);
}
void stick_binding_add_left_stick(StickBinding* binding, float threshold)
{
axis_binding_add_left_stick_x(binding->x_axis, threshold);
axis_binding_add_left_stick_y(binding->y_axis, threshold);
}
void stick_binding_add_right_stick(StickBinding* binding, float threshold)
{
axis_binding_add_right_stick_x(binding->x_axis, threshold);
axis_binding_add_right_stick_y(binding->y_axis, threshold);
}
bool stick_binding_press(StickBinding* binding)
{
return axis_binding_press(binding->x_axis) || axis_binding_press(binding->y_axis);
}
bool stick_binding_release(StickBinding* binding)
{
return axis_binding_release(binding->x_axis) || axis_binding_release(binding->y_axis);
}
void stick_binding_consume_press(StickBinding* binding)
{
axis_binding_consume_press(binding->x_axis);
axis_binding_consume_press(binding->y_axis);
}
void stick_binding_consume_release(StickBinding* binding)
{
axis_binding_consume_release(binding->x_axis);
axis_binding_consume_release(binding->y_axis);
}
v2 stick_binding_value(StickBinding* binding)
{
v2 result;
result.x = axis_binding_value(binding->x_axis);
result.y = axis_binding_value(binding->y_axis);
return result;
}
// Returns -1, 0, or 1 on x/y axes. 0 means no input.
v2 stick_binding_sign(StickBinding* binding)
{
v2 value = stick_binding_value(binding);
v2 result;
result.x = (value.x == 0) ? 0 : sign(value.x);
result.y = (value.y == 0) ? 0 : sign(value.y);
return result;
}
//--------------------------------------------------------------------------------------------------
void register_joy_on_connect(void (*on_connect)(int, bool, void*), void* udata)
{
JoypadOnConnect callback = {
.on_connect = on_connect,
.udata = udata,
};
apush(g_on_joypad_connects, callback);
}
void input_init()
{
for (int i = 0; i < MAX_JOYPADS; ++i) {
apush(g_joypads, get_joypad(i));
}
}
void input_update()
{
// Handle joypad connect/disconnect.
for (int i = 0; i < asize(g_joypads); ++i) {
Joypad* joypad = g_joypads + i;
Joypad refresh = get_joypad(i);
if (refresh.connected) {
if (!joypad->connected) {
*joypad = refresh;
for (int j = 0; j < asize(g_on_joypad_connects); ++j) {
g_on_joypad_connects[j].on_connect(i, true, g_on_joypad_connects[j].udata);
}
}
} else {
if (joypad->connected) {
for (int j = 0; j < asize(g_on_joypad_connects); ++j) {
g_on_joypad_connects[j].on_connect(i, false, g_on_joypad_connects[j].udata);
}
joypad->connected = false;
}
}
}
// Update all binding objects.
for (int i = 0; i < asize(g_button_bindings); ++i) {
button_binding_update(g_button_bindings + i);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment