Add game state debug log with binary ring buffer
All checks were successful
CI / build (pull_request) Successful in 32s
Deploy / deploy (push) Successful in 1m17s

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
This commit was merged in pull request #27.
This commit is contained in:
2026-03-16 20:29:18 +00:00
committed by tas
parent 66a7b9e7e6
commit 3b45572d38
7 changed files with 618 additions and 0 deletions

View File

@@ -49,4 +49,7 @@ typedef enum TransitionStyle {
#define MAX_ASSETS 128
#define ASSET_PATH_MAX 256
/* ── Debug log ──────────────────────────────────────── */
#define DEBUGLOG_BUFFER_SIZE (4 * 1024 * 1024) /* 4 MB ring buffer */
#endif /* JNR_CONFIG_H */

View File

@@ -3,6 +3,7 @@
#include "engine/renderer.h"
#include "engine/audio.h"
#include "engine/assets.h"
#include "engine/debuglog.h"
#include <stdio.h>
#ifdef __EMSCRIPTEN__
@@ -118,6 +119,7 @@ static void engine_frame(void) {
if (s_callbacks.update) {
s_callbacks.update(DT);
}
debuglog_record_tick();
input_consume();
g_engine.tick++;
s_accumulator -= DT;

474
src/engine/debuglog.c Normal file
View File

@@ -0,0 +1,474 @@
#include "engine/debuglog.h"
#include "engine/core.h"
#include "engine/input.h"
#include "engine/physics.h"
#include "engine/entity.h"
#include "game/level.h"
#include "game/player.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ═══════════════════════════════════════════════════
* File-scope state
* ═══════════════════════════════════════════════════ */
/* Ring buffer header stored at the start of the memory block. */
typedef struct RingHeader {
uint32_t magic; /* 0x44424C47 = "DBLG" */
uint32_t version;
uint32_t write_cursor; /* byte offset into data region */
uint32_t total_written; /* total snapshots written (wraps) */
uint32_t data_size; /* usable data bytes after header */
} RingHeader;
#define RING_MAGIC 0x44424C47
#define RING_VERSION 1
/* Flush the buffer to disk every this many ticks (10 s at 60 Hz). */
#define FLUSH_INTERVAL 600
static bool s_enabled;
static uint8_t *s_buffer; /* full allocation: header + data */
static RingHeader *s_header;
static uint8_t *s_data; /* start of ring data region */
static Level *s_level;
static char s_level_name[32]; /* human-readable level label */
static int s_flush_counter;
/* Path used for periodic safety-net flushes. */
static const char *s_flush_path = "debug_log_autosave.bin";
/* ═══════════════════════════════════════════════════
* Internal helpers
* ═══════════════════════════════════════════════════ */
/* Write raw bytes into the ring, wrapping at the boundary. */
static void ring_write(const void *src, uint32_t len) {
uint32_t cursor = s_header->write_cursor;
uint32_t cap = s_header->data_size;
const uint8_t *p = (const uint8_t *)src;
uint32_t first = cap - cursor;
if (first >= len) {
memcpy(s_data + cursor, p, len);
} else {
memcpy(s_data + cursor, p, first);
memcpy(s_data, p + first, len - first);
}
s_header->write_cursor = (cursor + len) % cap;
}
/* Read raw bytes from an arbitrary ring offset, wrapping. */
static void ring_read(uint32_t offset, void *dst, uint32_t len) {
uint32_t cap = s_header->data_size;
offset = offset % cap;
uint8_t *out = (uint8_t *)dst;
uint32_t first = cap - offset;
if (first >= len) {
memcpy(out, s_data + offset, len);
} else {
memcpy(out, s_data + offset, first);
memcpy(out + first, s_data, len - first);
}
}
/* Flush the entire buffer (header + data) to a binary file. */
static void flush_to_file(const char *path) {
FILE *f = fopen(path, "wb");
if (!f) {
fprintf(stderr, "Warning: debuglog flush failed — cannot open %s\n", path);
return;
}
uint32_t total = (uint32_t)sizeof(RingHeader) + s_header->data_size;
size_t written = fwrite(s_buffer, 1, total, f);
fclose(f);
if (written != total) {
fprintf(stderr, "Warning: debuglog flush incomplete (%zu / %u bytes)\n",
written, total);
}
}
/* Find the player entity in the current level, or NULL. */
static Entity *find_player(void) {
if (!s_level) return NULL;
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
/* Pack player-specific flags into a single byte.
* Bit layout: 0=on_ground, 1=jumping, 2=dashing, 3=invincible, 4=has_gun */
static uint8_t pack_player_flags(const Entity *player) {
uint8_t f = 0;
if (player->body.on_ground) f |= (1 << 0);
PlayerData *pd = (PlayerData *)player->data;
if (pd) {
if (pd->jumping) f |= (1 << 1);
if (pd->dash_timer > 0) f |= (1 << 2);
if (pd->inv_timer > 0) f |= (1 << 3);
if (pd->has_gun) f |= (1 << 4);
}
return f;
}
/* Pack entity-level flags into a single byte.
* Bit layout: 0=active, 1=facing_left, 2=dead, 3=invincible */
static uint8_t pack_entity_flags(const Entity *e) {
uint8_t f = 0;
if (e->active) f |= (1 << 0);
if (e->flags & ENTITY_FACING_LEFT) f |= (1 << 1);
if (e->flags & ENTITY_DEAD) f |= (1 << 2);
if (e->flags & ENTITY_INVINCIBLE) f |= (1 << 3);
return f;
}
/* Names for entity types — kept in sync with EntityType enum. */
static const char *entity_type_name(uint8_t type) {
switch ((EntityType)type) {
case ENT_NONE: return "NONE";
case ENT_PLAYER: return "PLAYER";
case ENT_ENEMY_GRUNT: return "GRUNT";
case ENT_ENEMY_FLYER: return "FLYER";
case ENT_PROJECTILE: return "PROJ";
case ENT_PICKUP: return "PICKUP";
case ENT_PARTICLE: return "PARTICLE";
case ENT_TURRET: return "TURRET";
case ENT_MOVING_PLATFORM: return "PLATFORM";
case ENT_FLAME_VENT: return "FLAME";
case ENT_FORCE_FIELD: return "FORCEFIELD";
case ENT_POWERUP: return "POWERUP";
case ENT_DRONE: return "DRONE";
case ENT_ASTEROID: return "ASTEROID";
case ENT_SPACECRAFT: return "SPACECRAFT";
case ENT_LASER_TURRET: return "LASER_TURRET";
case ENT_ENEMY_CHARGER: return "CHARGER";
case ENT_SPAWNER: return "SPAWNER";
default: return "???";
}
}
/* Names for aim directions. */
static const char *aim_dir_name(uint8_t aim) {
switch ((AimDir)aim) {
case AIM_FORWARD: return "FORWARD";
case AIM_UP: return "UP";
case AIM_DIAG_UP: return "DIAG_UP";
default: return "???";
}
}
/* Names for input actions. */
static const char *action_name(int a) {
switch ((Action)a) {
case ACTION_LEFT: return "LEFT";
case ACTION_RIGHT: return "RIGHT";
case ACTION_UP: return "UP";
case ACTION_DOWN: return "DOWN";
case ACTION_JUMP: return "JUMP";
case ACTION_SHOOT: return "SHOOT";
case ACTION_DASH: return "DASH";
case ACTION_PAUSE: return "PAUSE";
default: return "???";
}
}
/* ═══════════════════════════════════════════════════
* Public API
* ═══════════════════════════════════════════════════ */
void debuglog_init(void) {
uint32_t total = (uint32_t)sizeof(RingHeader) + DEBUGLOG_BUFFER_SIZE;
s_buffer = calloc(1, total);
if (!s_buffer) {
fprintf(stderr, "Warning: debuglog_init failed to allocate %u bytes\n", total);
return;
}
s_header = (RingHeader *)s_buffer;
s_data = s_buffer + sizeof(RingHeader);
s_header->magic = RING_MAGIC;
s_header->version = RING_VERSION;
s_header->write_cursor = 0;
s_header->total_written = 0;
s_header->data_size = DEBUGLOG_BUFFER_SIZE;
s_enabled = false;
s_level = NULL;
s_flush_counter = 0;
printf("Debug log initialized (%u KB ring buffer)\n",
DEBUGLOG_BUFFER_SIZE / 1024);
}
void debuglog_shutdown(void) {
if (!s_buffer) return;
if (s_enabled && s_header->total_written > 0) {
flush_to_file(s_flush_path);
printf("Debug log flushed to %s on shutdown (%u snapshots)\n",
s_flush_path, s_header->total_written);
}
free(s_buffer);
s_buffer = NULL;
s_header = NULL;
s_data = NULL;
s_enabled = false;
s_level = NULL;
}
void debuglog_enable(void) {
s_enabled = true;
printf("Debug log recording enabled\n");
}
bool debuglog_is_enabled(void) {
return s_enabled;
}
void debuglog_set_level(Level *lvl, const char *name) {
s_level = lvl;
if (name && name[0]) {
snprintf(s_level_name, sizeof(s_level_name), "%s", name);
} else {
s_level_name[0] = '\0';
}
}
void debuglog_record_tick(void) {
if (!s_enabled || !s_buffer) return;
/* Count active entities (skip player — recorded separately). */
uint16_t ent_count = 0;
if (s_level) {
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES && ent_count < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type != ENT_PLAYER) ent_count++;
}
}
uint32_t frame_size = (uint32_t)sizeof(TickSnapshot)
+ (uint32_t)(ent_count * sizeof(EntitySnapshot));
/* Don't write if the frame won't fit in the ring at all. */
if (frame_size > s_header->data_size) return;
/* Build the header portion on the stack. */
TickSnapshot snap;
memset(&snap, 0, sizeof(snap));
snap.tick = g_engine.tick;
snap.frame_size = frame_size;
/* Input. */
input_get_snapshot(&snap.input);
/* Player state. */
Entity *player = find_player();
if (player) {
snap.player_x = player->body.pos.x;
snap.player_y = player->body.pos.y;
snap.player_vx = player->body.vel.x;
snap.player_vy = player->body.vel.y;
snap.player_health = (int8_t)player->health;
snap.player_flags = pack_player_flags(player);
PlayerData *pd = (PlayerData *)player->data;
if (pd) {
snap.player_dash_timer = pd->dash_timer;
snap.player_dash_charges = (int8_t)pd->dash_charges;
snap.player_inv_timer = pd->inv_timer;
snap.player_coyote = pd->coyote_timer;
snap.player_aim_dir = (uint8_t)pd->aim_dir;
}
}
/* Camera. */
if (s_level) {
snap.cam_x = s_level->camera.pos.x;
snap.cam_y = s_level->camera.pos.y;
}
/* Physics globals. */
snap.gravity = physics_get_gravity();
snap.wind = physics_get_wind();
/* Level name (truncated to 31 chars + NUL). */
if (s_level_name[0]) {
snprintf(snap.level_name, sizeof(snap.level_name), "%s", s_level_name);
}
snap.entity_count = ent_count;
/* Write header into ring. */
ring_write(&snap, sizeof(TickSnapshot));
/* Write entity snapshots. */
if (s_level && ent_count > 0) {
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (!e->active || e->type == ENT_PLAYER) continue;
EntitySnapshot es;
es.type = (uint8_t)e->type;
es.flags = pack_entity_flags(e);
es.health = (int16_t)e->health;
es.pos_x = e->body.pos.x;
es.pos_y = e->body.pos.y;
es.vel_x = e->body.vel.x;
es.vel_y = e->body.vel.y;
ring_write(&es, sizeof(EntitySnapshot));
}
}
s_header->total_written++;
/* Periodic safety-net flush. */
s_flush_counter++;
if (s_flush_counter >= FLUSH_INTERVAL) {
s_flush_counter = 0;
flush_to_file(s_flush_path);
}
}
void debuglog_dump(const char *path) {
if (!s_buffer || s_header->total_written == 0) {
printf("Debug log: nothing to dump (0 snapshots recorded)\n");
return;
}
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "Error: cannot open %s for writing\n", path);
return;
}
fprintf(f, "=== Debug Log Dump ===\n");
fprintf(f, "Total snapshots written: %u\n\n", s_header->total_written);
/* Walk backwards from write_cursor to find snapshots.
* We read forward through the ring, starting from the oldest data. */
/* To find the oldest snapshot: if the buffer has wrapped, oldest
* starts at write_cursor. Otherwise it starts at 0. */
uint32_t cap = s_header->data_size;
uint32_t total_data_bytes;
uint32_t read_pos;
/* Estimate how much data we've written total. If total_written
* snapshots have been recorded and we assume average frame size
* is frame_size, the buffer may have wrapped. We use a simpler
* approach: scan forward from the oldest point. */
if (s_header->total_written * sizeof(TickSnapshot) >= cap) {
/* Buffer has likely wrapped. Start reading at write_cursor
* (which is where the oldest data begins). */
read_pos = s_header->write_cursor;
total_data_bytes = cap;
} else {
read_pos = 0;
total_data_bytes = s_header->write_cursor;
}
uint32_t bytes_read = 0;
uint32_t snapshots_dumped = 0;
uint32_t first_tick = 0;
uint32_t last_tick = 0;
while (bytes_read + sizeof(TickSnapshot) <= total_data_bytes) {
TickSnapshot snap;
ring_read(read_pos, &snap, sizeof(TickSnapshot));
/* Sanity check. */
if (snap.frame_size < sizeof(TickSnapshot) ||
snap.frame_size > cap) {
break;
}
if (bytes_read + snap.frame_size > total_data_bytes) {
break;
}
if (snapshots_dumped == 0) first_tick = snap.tick;
last_tick = snap.tick;
fprintf(f, "--- Tick %u ---\n", snap.tick);
/* Input. */
fprintf(f, "Input:");
for (int a = 0; a < ACTION_COUNT && a < 8; a++) {
if (snap.input.held & (1 << a)) {
fprintf(f, " [%s]", action_name(a));
}
if (snap.input.pressed & (1 << a)) {
fprintf(f, " [%s_pressed]", action_name(a));
}
if (snap.input.released & (1 << a)) {
fprintf(f, " [%s_released]", action_name(a));
}
}
fprintf(f, "\n");
/* Player. */
fprintf(f, "Player: pos=(%.1f, %.1f) vel=(%.1f, %.1f) hp=%d",
snap.player_x, snap.player_y,
snap.player_vx, snap.player_vy,
snap.player_health);
fprintf(f, " on_ground=%d", (snap.player_flags >> 0) & 1);
fprintf(f, " jumping=%d", (snap.player_flags >> 1) & 1);
fprintf(f, " dashing=%d", (snap.player_flags >> 2) & 1);
fprintf(f, " inv=%d", (snap.player_flags >> 3) & 1);
fprintf(f, " has_gun=%d", (snap.player_flags >> 4) & 1);
fprintf(f, " aim=%s", aim_dir_name(snap.player_aim_dir));
fprintf(f, " dash_charges=%d dash_timer=%.2f inv_timer=%.2f coyote=%.3f\n",
snap.player_dash_charges, snap.player_dash_timer,
snap.player_inv_timer, snap.player_coyote);
/* Camera + physics. */
fprintf(f, "Camera: (%.1f, %.1f)\n", snap.cam_x, snap.cam_y);
fprintf(f, "Physics: gravity=%.1f wind=%.1f\n",
snap.gravity, snap.wind);
if (snap.level_name[0]) {
fprintf(f, "Level: %s\n", snap.level_name);
}
/* Entities. */
fprintf(f, "Entities (%u active):\n", snap.entity_count);
uint32_t ent_offset = (read_pos + sizeof(TickSnapshot)) % cap;
uint16_t count = snap.entity_count;
if (count > MAX_ENTITIES) count = MAX_ENTITIES;
for (uint16_t i = 0; i < count; i++) {
EntitySnapshot es;
ring_read(ent_offset, &es, sizeof(EntitySnapshot));
ent_offset = (ent_offset + sizeof(EntitySnapshot)) % cap;
fprintf(f, " [%u] %-12s pos=(%.0f, %.0f) vel=(%.0f, %.0f) hp=%d\n",
i, entity_type_name(es.type),
es.pos_x, es.pos_y,
es.vel_x, es.vel_y,
es.health);
}
fprintf(f, "\n");
read_pos = (read_pos + snap.frame_size) % cap;
bytes_read += snap.frame_size;
snapshots_dumped++;
}
/* Write summary at the top. */
if (snapshots_dumped > 0) {
float seconds = (float)(last_tick - first_tick) / TICK_RATE;
fprintf(f, "=== Summary: ticks %u to %u (%u snapshots, %.1f seconds) ===\n",
first_tick, last_tick, snapshots_dumped, seconds);
}
fclose(f);
printf("Debug log dumped to %s (%u snapshots)\n", path, snapshots_dumped);
}

91
src/engine/debuglog.h Normal file
View File

@@ -0,0 +1,91 @@
#ifndef JNR_DEBUGLOG_H
#define JNR_DEBUGLOG_H
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
/* Forward declaration — game layer type, registered via pointer. */
typedef struct Level Level;
/* ═══════════════════════════════════════════════════
* Snapshot data structures
* ═══════════════════════════════════════════════════ */
/* Packed input snapshot — 3 bytes (ACTION_COUNT actions × 3 states). */
typedef struct InputSnapshot {
uint8_t held; /* bitmask of ACTION_* held this tick */
uint8_t pressed; /* bitmask of ACTION_* pressed this tick */
uint8_t released; /* bitmask of ACTION_* released this tick */
} InputSnapshot;
/* Per-entity summary — 20 bytes each. */
typedef struct EntitySnapshot {
uint8_t type; /* EntityType */
uint8_t flags; /* active, facing, dead, invincible */
int16_t health;
float pos_x, pos_y;
float vel_x, vel_y;
} EntitySnapshot;
/* Full tick snapshot — fixed-size header + variable entity list.
* Written sequentially into the ring buffer; frame_size stores
* the total byte count so the reader can skip forward. */
typedef struct TickSnapshot {
uint32_t tick; /* g_engine.tick */
uint32_t frame_size; /* total bytes of this record */
/* Input */
InputSnapshot input; /* 3 bytes */
uint8_t _pad0; /* align to 4 bytes */
/* Player state (expanded) */
float player_x, player_y;
float player_vx, player_vy;
int8_t player_health;
uint8_t player_flags; /* on_ground, jumping, dashing, inv, has_gun */
uint8_t player_aim_dir;
int8_t player_dash_charges;
float player_dash_timer;
float player_inv_timer;
float player_coyote;
/* Camera */
float cam_x, cam_y;
/* Physics globals */
float gravity;
float wind;
/* Level info */
char level_name[32]; /* truncated level path/tag */
/* Entity summary */
uint16_t entity_count; /* number of active entities */
uint16_t _pad1; /* alignment padding */
/* EntitySnapshot entities[] follows immediately in the buffer. */
} TickSnapshot;
/* ═══════════════════════════════════════════════════
* Public API
* ═══════════════════════════════════════════════════ */
/* Allocate ring buffer. Safe to call even if logging stays disabled. */
void debuglog_init(void);
/* Final flush + free. */
void debuglog_shutdown(void);
/* Turn on recording. */
void debuglog_enable(void);
/* Query state. */
bool debuglog_is_enabled(void);
/* Register the active Level pointer so the debuglog can read game state.
* Pass NULL before freeing a level.
* name is an optional label (e.g. file path); NULL or "" to clear. */
void debuglog_set_level(Level *lvl, const char *name);
/* Called once per tick from engine_frame, after update, before consume.
* Captures input + game state into the ring buffer. */
void debuglog_record_tick(void);
/* Dump the ring buffer contents to a human-readable text file. */
void debuglog_dump(const char *path);
#endif /* JNR_DEBUGLOG_H */

View File

@@ -1,4 +1,5 @@
#include "engine/input.h"
#include "engine/debuglog.h"
#include <string.h>
static bool s_current[ACTION_COUNT];
@@ -196,6 +197,19 @@ bool input_key_held(SDL_Scancode key) {
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 */
}

View File

@@ -50,4 +50,10 @@ int input_mouse_scroll(void);
bool input_key_pressed(SDL_Scancode key);
bool input_key_held(SDL_Scancode key);
/* Pack current input state into a compact bitmask snapshot.
* Used by the debug log to record per-tick input without
* exposing internal arrays. */
typedef struct InputSnapshot InputSnapshot; /* defined in debuglog.h */
void input_get_snapshot(InputSnapshot *out);
#endif /* JNR_INPUT_H */

View File

@@ -1,6 +1,7 @@
#include "engine/core.h"
#include "engine/input.h"
#include "engine/font.h"
#include "engine/debuglog.h"
#include "game/level.h"
#include "game/levelgen.h"
#include "game/editor.h"
@@ -34,6 +35,7 @@ static GameMode s_mode = MODE_PLAY;
static bool s_use_procgen = false;
static bool s_dump_lvl = false;
static bool s_use_editor = false;
static bool s_use_debuglog = false;
static uint32_t s_gen_seed = 0;
static char s_edit_path[256] = {0};
static char s_level_path[ASSET_PATH_MAX] = {0}; /* path of active play-mode level */
@@ -94,6 +96,7 @@ static const char *theme_name(LevelTheme t) {
static bool load_level_file(const char *path) {
if (!level_load(&s_level, path)) return false;
snprintf(s_level_path, sizeof(s_level_path), "%s", path);
debuglog_set_level(&s_level, path);
return true;
}
@@ -192,6 +195,7 @@ static void load_generated_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0'; /* generated levels have no file path */
debuglog_set_level(&s_level, "generated");
}
static void load_station_level(void) {
@@ -217,6 +221,7 @@ static void load_station_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0'; /* generated levels have no file path */
debuglog_set_level(&s_level, "generated:station");
}
static void load_mars_base_level(void) {
@@ -248,6 +253,7 @@ static void load_mars_base_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0';
debuglog_set_level(&s_level, "generated:mars_base");
}
/* ── Analytics session helpers ── */
@@ -268,6 +274,7 @@ static void end_session(const char *reason) {
/* ── Switch to editor mode ── */
static void enter_editor(void) {
if (s_mode == MODE_PLAY) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
}
s_mode = MODE_EDITOR;
@@ -307,6 +314,7 @@ static void enter_test_play(void) {
/* ── Return from test play to editor ── */
static void return_to_editor(void) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
s_mode = MODE_EDITOR;
s_testing_from_editor = false;
@@ -315,6 +323,7 @@ static void return_to_editor(void) {
/* ── Restart current level (file-based or generated) ── */
static void restart_level(void) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
if (s_level_path[0]) {
if (!load_level_file(s_level_path)) {
@@ -329,6 +338,7 @@ static void restart_level(void) {
/* ── Level load dispatch — loads the next level based on target string ── */
static void dispatch_level_load(const char *target) {
debuglog_set_level(NULL, NULL);
if (target[0] == '\0') {
/* Empty target = victory / end of game. */
printf("Level complete! (no next level)\n");
@@ -457,6 +467,7 @@ static void game_update(float dt) {
/* Tear down whatever mode we are in. */
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|| s_mode == MODE_TRANSITION) {
debuglog_set_level(NULL, NULL);
transition_reset(&s_transition);
level_free(&s_level);
}
@@ -524,6 +535,11 @@ static void game_update(float dt) {
/* ── Play mode ── */
/* F12: dump debug log to text file. */
if (input_key_pressed(SDL_SCANCODE_F12) && debuglog_is_enabled()) {
debuglog_dump("debug_log.txt");
}
/* Pause on escape (return to editor during test play) */
if (input_pressed(ACTION_PAUSE)) {
if (s_testing_from_editor) {
@@ -539,6 +555,7 @@ static void game_update(float dt) {
if (!s_testing_from_editor && input_key_pressed(SDL_SCANCODE_E)) {
/* Load the current level file into the editor if available */
snprintf(s_edit_path, sizeof(s_edit_path), "%s", s_level_path);
debuglog_set_level(NULL, NULL);
level_free(&s_level);
enter_editor();
return;
@@ -550,6 +567,7 @@ static void game_update(float dt) {
if (r_pressed && !r_was_pressed) {
printf("\n=== Regenerating level ===\n");
end_session("quit");
debuglog_set_level(NULL, NULL);
level_free(&s_level);
s_gen_seed = (uint32_t)time(NULL);
s_use_procgen = true;
@@ -647,6 +665,7 @@ static void game_render(float interpolation) {
static void game_shutdown(void) {
end_session("quit");
debuglog_set_level(NULL, NULL);
/* Always free both — editor may have been initialized even if we're
* currently in play mode (e.g. shutdown during test play). editor_free
@@ -675,6 +694,8 @@ int main(int argc, char *argv[]) {
if (i + 1 < argc) {
s_gen_seed = (uint32_t)atoi(argv[++i]);
}
} else if (strcmp(argv[i], "--debug-log") == 0) {
s_use_debuglog = true;
} else if (strcmp(argv[i], "--edit") == 0 || strcmp(argv[i], "-e") == 0) {
s_use_editor = true;
/* Optional: next arg is a file path */
@@ -687,6 +708,7 @@ int main(int argc, char *argv[]) {
printf(" --dump, -d Dump generated level to assets/levels/generated.lvl\n");
printf(" --seed N, -s N Set RNG seed for generation\n");
printf(" --edit [file], -e [file] Open level editor (optionally load a .lvl file)\n");
printf(" --debug-log Record game state every tick (F12 to dump)\n");
printf("\nIn-game:\n");
printf(" R Regenerate level with new random seed\n");
printf(" E Open level editor\n");
@@ -739,6 +761,11 @@ int main(int argc, char *argv[]) {
return 1;
}
debuglog_init();
if (s_use_debuglog) {
debuglog_enable();
}
engine_set_callbacks((GameCallbacks){
.init = game_init,
.update = game_update,
@@ -747,6 +774,7 @@ int main(int argc, char *argv[]) {
});
engine_run();
debuglog_shutdown();
engine_shutdown();
return 0;