Files
major_tom/src/engine/core.c
LeSerjant 3b45572d38
All checks were successful
CI / build (pull_request) Successful in 32s
Deploy / deploy (push) Successful in 1m17s
Add game state debug log with binary ring buffer
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
2026-03-16 20:33:03 +00:00

180 lines
4.8 KiB
C

#include "engine/core.h"
#include "engine/input.h"
#include "engine/renderer.h"
#include "engine/audio.h"
#include "engine/assets.h"
#include "engine/debuglog.h"
#include <stdio.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
Engine g_engine = {0};
static GameCallbacks s_callbacks = {0};
void engine_set_callbacks(GameCallbacks cb) {
s_callbacks = cb;
}
bool engine_init(void) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return false;
}
int win_w = SCREEN_WIDTH * WINDOW_SCALE;
int win_h = SCREEN_HEIGHT * WINDOW_SCALE;
g_engine.window = SDL_CreateWindow(
WINDOW_TITLE,
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
win_w, win_h,
SDL_WINDOW_SHOWN
);
if (!g_engine.window) {
fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
return false;
}
g_engine.renderer = SDL_CreateRenderer(
g_engine.window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
if (!g_engine.renderer) {
fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
return false;
}
/* Set logical size for pixel-perfect scaling */
SDL_RenderSetLogicalSize(g_engine.renderer, SCREEN_WIDTH, SCREEN_HEIGHT);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); /* nearest neighbor */
/* Initialize subsystems */
input_init();
renderer_init(g_engine.renderer);
audio_init();
assets_init(g_engine.renderer);
g_engine.running = true;
g_engine.tick = 0;
g_engine.fps = 0.0f;
printf("Engine initialized: %dx%d (x%d scale)\n",
SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_SCALE);
return true;
}
/* ── Frame-loop state (file-scope so engine_frame can access it) ── */
static uint64_t s_freq;
static uint64_t s_prev_time;
static float s_accumulator;
static float s_fps_timer;
static int s_fps_frames;
/*
* engine_frame -- execute exactly one iteration of the game loop.
*
* Called repeatedly by the platform layer:
* Native : tight while-loop in engine_run()
* Browser : emscripten_set_main_loop() via requestAnimationFrame
*/
static void engine_frame(void) {
uint64_t current_time = SDL_GetPerformanceCounter();
float frame_time = (float)(current_time - s_prev_time) / (float)s_freq;
s_prev_time = current_time;
/* Clamp frame time to avoid spiral of death */
if (frame_time > MAX_FRAME_SKIP * DT) {
frame_time = MAX_FRAME_SKIP * DT;
}
/* FPS counter */
s_fps_timer += frame_time;
s_fps_frames++;
if (s_fps_timer >= 1.0f) {
g_engine.fps = (float)s_fps_frames / s_fps_timer;
s_fps_timer = 0.0f;
s_fps_frames = 0;
}
/* ── Input ────────────────────────────── */
input_poll();
if (input_quit_requested()) {
g_engine.running = false;
#ifdef __EMSCRIPTEN__
if (s_callbacks.shutdown) {
s_callbacks.shutdown();
}
emscripten_cancel_main_loop();
#endif
return;
}
/* ── Fixed timestep update ────────────── */
s_accumulator += frame_time;
while (s_accumulator >= DT) {
if (s_callbacks.update) {
s_callbacks.update(DT);
}
debuglog_record_tick();
input_consume();
g_engine.tick++;
s_accumulator -= DT;
}
/* ── Render ───────────────────────────── */
g_engine.interpolation = s_accumulator / DT;
renderer_clear();
if (s_callbacks.render) {
s_callbacks.render(g_engine.interpolation);
}
renderer_present();
}
void engine_run(void) {
if (s_callbacks.init) {
s_callbacks.init();
}
/* Initialise frame-loop timing state */
s_freq = SDL_GetPerformanceFrequency();
s_prev_time = SDL_GetPerformanceCounter();
s_accumulator = 0.0f;
s_fps_timer = 0.0f;
s_fps_frames = 0;
#ifdef __EMSCRIPTEN__
/*
* Hand control to the browser's requestAnimationFrame loop.
* fps=0 : match display refresh rate
* simulate_infinite_loop=1 : don't return from this function
* (so the rest of engine_run never executes until shutdown)
*/
emscripten_set_main_loop(engine_frame, 0, 1);
#else
while (g_engine.running) {
engine_frame();
}
#endif
if (s_callbacks.shutdown) {
s_callbacks.shutdown();
}
}
void engine_shutdown(void) {
assets_free_all();
audio_shutdown();
renderer_shutdown();
input_shutdown();
if (g_engine.renderer) SDL_DestroyRenderer(g_engine.renderer);
if (g_engine.window) SDL_DestroyWindow(g_engine.window);
SDL_Quit();
printf("Engine shut down cleanly.\n");
}