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
180 lines
4.8 KiB
C
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");
|
|
}
|