Implement src/engine/debuglog module that records a comprehensive snapshot of game state every tick into a 4 MB in-memory ring buffer. Activated by --debug-log command-line flag. Press F12 during gameplay to dump the ring buffer to a human-readable debug_log.txt file. The buffer also auto-flushes every 10 seconds as a safety net. Each tick snapshot captures: input state (held/pressed/released bitmasks), full player state (position, velocity, health, dash, aim, timers), camera position, physics globals, level name, and a variable-length list of all active entity positions/velocities/health. New files: - src/engine/debuglog.h — API and snapshot data structures - src/engine/debuglog.c — ring buffer, record, and dump logic Modified files: - include/config.h — DEBUGLOG_BUFFER_SIZE constant - src/engine/input.h/c — input_get_snapshot() to pack input bitmasks - src/engine/core.c — debuglog_record_tick() call after update - src/main.c — CLI flag, init/shutdown, F12 hotkey, set_level calls Closes #19
216 lines
7.0 KiB
C
216 lines
7.0 KiB
C
#include "engine/input.h"
|
|
#include "engine/debuglog.h"
|
|
#include <string.h>
|
|
|
|
static bool s_current[ACTION_COUNT];
|
|
static bool s_previous[ACTION_COUNT];
|
|
|
|
/*
|
|
* Latched states: accumulate press/release events across frames
|
|
* so that a press is never lost even if no update tick runs
|
|
* during the frame it was detected.
|
|
*/
|
|
static bool s_latched_pressed[ACTION_COUNT];
|
|
static bool s_latched_released[ACTION_COUNT];
|
|
|
|
static bool s_quit_requested;
|
|
|
|
/* ── Mouse state ──────────────────────────────────── */
|
|
static int s_mouse_x, s_mouse_y;
|
|
static bool s_mouse_current[MOUSE_BUTTON_COUNT];
|
|
static bool s_mouse_previous[MOUSE_BUTTON_COUNT];
|
|
static bool s_mouse_latched_pressed[MOUSE_BUTTON_COUNT];
|
|
static bool s_mouse_latched_released[MOUSE_BUTTON_COUNT];
|
|
static int s_mouse_scroll;
|
|
|
|
/* ── Raw keyboard state ───────────────────────────── */
|
|
static const Uint8 *s_key_state = NULL;
|
|
static Uint8 s_latched_keys[SDL_NUM_SCANCODES];
|
|
|
|
/* Default key bindings (primary + alternate) */
|
|
static SDL_Scancode s_bindings[ACTION_COUNT] = {
|
|
[ACTION_LEFT] = SDL_SCANCODE_LEFT,
|
|
[ACTION_RIGHT] = SDL_SCANCODE_RIGHT,
|
|
[ACTION_UP] = SDL_SCANCODE_UP,
|
|
[ACTION_DOWN] = SDL_SCANCODE_DOWN,
|
|
[ACTION_JUMP] = SDL_SCANCODE_Z,
|
|
[ACTION_SHOOT] = SDL_SCANCODE_X,
|
|
[ACTION_DASH] = SDL_SCANCODE_C,
|
|
[ACTION_PAUSE] = SDL_SCANCODE_ESCAPE,
|
|
};
|
|
|
|
/* Alternate bindings (0 = no alternate) */
|
|
static SDL_Scancode s_alt_bindings[ACTION_COUNT] = {
|
|
[ACTION_JUMP] = SDL_SCANCODE_SPACE,
|
|
};
|
|
|
|
void input_init(void) {
|
|
memset(s_current, 0, sizeof(s_current));
|
|
memset(s_previous, 0, sizeof(s_previous));
|
|
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
|
|
memset(s_latched_released, 0, sizeof(s_latched_released));
|
|
memset(s_mouse_current, 0, sizeof(s_mouse_current));
|
|
memset(s_mouse_previous, 0, sizeof(s_mouse_previous));
|
|
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
|
|
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
|
|
|
|
memset(s_latched_keys, 0, sizeof(s_latched_keys));
|
|
s_mouse_x = s_mouse_y = 0;
|
|
s_mouse_scroll = 0;
|
|
s_quit_requested = false;
|
|
}
|
|
|
|
void input_poll(void) {
|
|
/* Save previous state */
|
|
memcpy(s_previous, s_current, sizeof(s_current));
|
|
memcpy(s_mouse_previous, s_mouse_current, sizeof(s_mouse_current));
|
|
s_quit_requested = false;
|
|
s_mouse_scroll = 0;
|
|
|
|
/* Process SDL events */
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
switch (event.type) {
|
|
case SDL_QUIT:
|
|
s_quit_requested = true;
|
|
break;
|
|
case SDL_MOUSEWHEEL:
|
|
s_mouse_scroll += event.wheel.y;
|
|
break;
|
|
case SDL_KEYDOWN:
|
|
/* Latch raw key press directly from the event.
|
|
* More reliable than state-snapshot comparison,
|
|
* especially on Emscripten where SDL_GetKeyboardState
|
|
* may not reflect keys pressed within the same frame. */
|
|
if (!event.key.repeat &&
|
|
event.key.keysym.scancode < SDL_NUM_SCANCODES) {
|
|
s_latched_keys[event.key.keysym.scancode] = 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Read keyboard state */
|
|
s_key_state = SDL_GetKeyboardState(NULL);
|
|
for (int i = 0; i < ACTION_COUNT; i++) {
|
|
s_current[i] = s_key_state[s_bindings[i]];
|
|
if (s_alt_bindings[i] && s_key_state[s_alt_bindings[i]]) {
|
|
s_current[i] = true;
|
|
}
|
|
|
|
/* Latch edges: once set, stays true until consumed */
|
|
if (s_current[i] && !s_previous[i]) {
|
|
s_latched_pressed[i] = true;
|
|
}
|
|
if (!s_current[i] && s_previous[i]) {
|
|
s_latched_released[i] = true;
|
|
}
|
|
}
|
|
|
|
/* Read mouse state */
|
|
Uint32 buttons = SDL_GetMouseState(&s_mouse_x, &s_mouse_y);
|
|
|
|
/* Convert window coords to logical coords (SDL handles this via
|
|
SDL_RenderSetLogicalSize, but GetMouseState returns window coords) */
|
|
SDL_Renderer *r = SDL_GetRenderer(SDL_GetMouseFocus());
|
|
if (r) {
|
|
float lx, ly;
|
|
SDL_RenderWindowToLogical(r, s_mouse_x, s_mouse_y, &lx, &ly);
|
|
s_mouse_x = (int)lx;
|
|
s_mouse_y = (int)ly;
|
|
}
|
|
|
|
s_mouse_current[MOUSE_LEFT] = (buttons & SDL_BUTTON_LMASK) != 0;
|
|
s_mouse_current[MOUSE_MIDDLE] = (buttons & SDL_BUTTON_MMASK) != 0;
|
|
s_mouse_current[MOUSE_RIGHT] = (buttons & SDL_BUTTON_RMASK) != 0;
|
|
|
|
for (int i = 0; i < MOUSE_BUTTON_COUNT; i++) {
|
|
if (s_mouse_current[i] && !s_mouse_previous[i]) {
|
|
s_mouse_latched_pressed[i] = true;
|
|
}
|
|
if (!s_mouse_current[i] && s_mouse_previous[i]) {
|
|
s_mouse_latched_released[i] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void input_consume(void) {
|
|
/* Clear latched states after an update tick has read them */
|
|
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
|
|
memset(s_latched_released, 0, sizeof(s_latched_released));
|
|
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
|
|
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
|
|
memset(s_latched_keys, 0, sizeof(s_latched_keys));
|
|
}
|
|
|
|
bool input_pressed(Action a) {
|
|
return s_latched_pressed[a];
|
|
}
|
|
|
|
bool input_held(Action a) {
|
|
return s_current[a];
|
|
}
|
|
|
|
bool input_released(Action a) {
|
|
return s_latched_released[a];
|
|
}
|
|
|
|
bool input_quit_requested(void) {
|
|
return s_quit_requested;
|
|
}
|
|
|
|
/* ── Mouse queries ────────────────────────────────── */
|
|
|
|
void input_mouse_pos(int *x, int *y) {
|
|
if (x) *x = s_mouse_x;
|
|
if (y) *y = s_mouse_y;
|
|
}
|
|
|
|
bool input_mouse_pressed(MouseButton btn) {
|
|
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
|
return s_mouse_latched_pressed[btn];
|
|
}
|
|
|
|
bool input_mouse_held(MouseButton btn) {
|
|
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
|
return s_mouse_current[btn];
|
|
}
|
|
|
|
bool input_mouse_released(MouseButton btn) {
|
|
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
|
return s_mouse_latched_released[btn];
|
|
}
|
|
|
|
int input_mouse_scroll(void) {
|
|
return s_mouse_scroll;
|
|
}
|
|
|
|
/* ── Raw keyboard queries ─────────────────────────── */
|
|
|
|
bool input_key_pressed(SDL_Scancode key) {
|
|
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
|
|
return s_latched_keys[key] != 0;
|
|
}
|
|
|
|
bool input_key_held(SDL_Scancode key) {
|
|
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
|
|
return s_key_state && s_key_state[key];
|
|
}
|
|
|
|
/* ── Debug log snapshot ───────────────────────────── */
|
|
|
|
void input_get_snapshot(InputSnapshot *out) {
|
|
out->held = 0;
|
|
out->pressed = 0;
|
|
out->released = 0;
|
|
for (int i = 0; i < ACTION_COUNT && i < 8; i++) {
|
|
if (s_current[i]) out->held |= (uint8_t)(1 << i);
|
|
if (s_latched_pressed[i]) out->pressed |= (uint8_t)(1 << i);
|
|
if (s_latched_released[i]) out->released |= (uint8_t)(1 << i);
|
|
}
|
|
}
|
|
|
|
void input_shutdown(void) {
|
|
/* Nothing to clean up */
|
|
}
|