Files
major_tom/AGENTS.md
Thomas 6b32199f25 Disable origin cache headers to prevent stale JS/WASM mismatch
static-web-server's default cache-control sends max-age=31536000 (1 year)
for .js files but only 1 day for .wasm. After redeployment, Cloudflare CDN
serves the cached old .js with a fresh .wasm, causing EM_ASM address table
mismatches and runtime crashes. Disable built-in cache headers at the origin
so Cloudflare respects new content on each deploy.

Also update AGENTS.md: add deploy commands, fix emsdk path, document the
Cloudflare cache-purge requirement, and correct stale MAX_ENTITY_SPAWNS
and MAX_EXIT_ZONES values.
2026-03-02 21:33:07 +00:00

12 KiB

AGENTS.md — JNR Engine

Important Files

Before starting any task, always read:

  • DESIGN.md — Game design document with vision, mechanics, and level plans
  • TODO.md — Current roadmap and next tasks to implement

Project Overview

2D side-scrolling platformer (run-and-gun) written in C11 using SDL2, SDL2_image, and SDL2_mixer. Binary: jnr. Targets Linux (native), WebAssembly (Emscripten), Windows (MinGW cross-compile).

Build Commands

make              # Release build (Linux) → ./jnr
make run          # Build + run
make debug        # Debug build: -g -O0 -DDEBUG
make DEBUG=1      # Alternative debug flag
make web          # WASM build → dist-web/
make web-serve    # WASM build + HTTP server on :8080
make windows      # Cross-compile → dist-win64/
make k8s          # Build web + container image + deploy to local k3s
make clean        # Remove all build artifacts
./deploy.sh       # Full deploy: clean build → container → k3s rollout

Compiler flags: -Wall -Wextra -std=c11 -I include -I src

There are no test or lint targets. Verify changes by building with make and confirming zero warnings.

Cross-platform prerequisites

  • WASM builds require the Emscripten SDK. The emsdk/ directory in the project root is gitignored; source the environment before building:
    source emsdk/emsdk_env.sh
    make web
    
  • Windows cross-compilation requires MinGW (x86_64-w64-mingw32-gcc) and vendored SDL2 development libraries in deps/win64/ (also gitignored).
  • Web deployment goes through Cloudflare CDN (jnr.schick-web.site). After deploying a new build, purge the Cloudflare cache so stale .js/.wasm files are not served. Emscripten's .js and .wasm outputs are tightly coupled (EM_ASM address tables must match); serving a cached .js with a fresh .wasm causes runtime crashes.

Project Structure

include/             Global headers (config.h)
src/
  engine/            Engine subsystems (.c/.h pairs): physics, tilemap, entity, camera, etc.
  game/              Game logic (.c/.h pairs): player, enemy, level, levelgen, editor, etc.
  util/              Header-only utilities: vec2.h, darray.h
  main.c             Entry point, game mode switching, level transitions
assets/
  levels/            .lvl level files (plain text)
  sounds/            .wav/.ogg audio
  sprites/           PNG spritesheets
  tiles/             Tileset PNGs
web/                 Emscripten HTML shell

Engine code lives in src/engine/, game code in src/game/. Each subsystem is a .c/.h pair. Header-only utilities use static inline functions.

Code Style

Formatting

  • 4 spaces for indentation (no tabs in source; Makefile uses tabs)
  • K&R brace style: opening brace on same line
  • Pointer declaration: Type *name (space before *)
  • const for input-only pointer params: const Tilemap *map
  • No-parameter functions use void: void physics_init(void)
  • Unused parameters: (void)param;

Naming Conventions

Kind Convention Example
Functions snake_case, module-prefixed player_update(), tilemap_load()
Types/Structs PascalCase Entity, PlayerData, Tilemap
Enums PascalCase type, UPPER_SNAKE values EntityType / ENT_PLAYER
Macros/Constants UPPER_SNAKE_CASE MAX_ENTITIES, TILE_SIZE
Static (file-scope) vars s_ prefix s_gravity, s_renderer
Global vars g_ prefix g_engine, g_spritesheet
Local vars Short snake_case dt, pos, em, tx
Function pointer types PascalCase + Fn EntityUpdateFn

Includes

Order within each file:

  1. Own module header ("game/player.h")
  2. Other project headers ("engine/physics.h", "game/sprites.h")
  3. Standard library (<stdlib.h>, <string.h>, <math.h>)
  4. SDL headers (<SDL2/SDL.h>)
  5. Platform-conditional (#ifdef __EMSCRIPTEN__)

Paths are forward-slash, relative to src/ or include/: "engine/core.h", "config.h".

Comments

  • Section headers: /* ═══...═══ */ box-drawing block
  • Subsections: /* ── Name ──────── */ light-line style
  • Inline/doc comments: /* ... */ (C89-style, not //)
  • Struct field comments: trailing, aligned with whitespace padding

Header Guards

#ifndef JNR_MODULE_H
#define JNR_MODULE_H
...
#endif /* JNR_MODULE_H */

Types

  • float for positions, velocities, timers, physics (not double)
  • Vec2 (float x, y) for all 2D quantities
  • bool from <stdbool.h>
  • uint16_t for tile IDs; uint32_t for bitfield flags and seeds
  • SDL_Color for colors; SDL_Rect for integer rectangles

Error Handling

  • Return bool for success/failure
  • Return NULL from creation functions on failure
  • Errors to stderr via fprintf(stderr, "...")
  • Info to stdout via printf(...)
  • Warnings use "Warning: ..." prefix
  • Early return on failure; no goto-based cleanup

Memory Management

  • calloc(1, sizeof(T)) for entity data (zero-initialized)
  • free(ptr); ptr = NULL; in destroy callbacks
  • memset(ptr, 0, sizeof(*ptr)) for struct re-initialization
  • Fixed-size arrays for most collections (entity pool, tile defs)
  • Dynamic allocation only for tile layers (uint16_t *)
  • snprintf / strncpy with explicit size limits for strings
  • ASSET_PATH_MAX (256) for path buffers

TigerStyle Guidelines

Follow the principles from TigerStyle, adapted for this C11 codebase. The design goals are safety, performance, and developer experience, in that order.

Safety

  • Simple, explicit control flow. No recursion. Minimal abstractions — only where they genuinely model the domain. Every abstraction has a cost; prefer straightforward code.

  • Put a limit on everything. All loops and queues must have a fixed upper bound. Use MAX_ENTITIES, MAX_ENTITY_SPAWNS, MAX_EXIT_ZONES, etc. Where a loop cannot terminate (e.g. the game loop), document why.

  • Assert preconditions, postconditions, and invariants. Validate function arguments and return values. A function must not operate blindly on unchecked data. In C, use assert() or early-return checks with fprintf(stderr, ...) for runtime-recoverable cases. Split compound conditions: prefer assert(a); assert(b); over assert(a && b);.

  • Assert the positive and negative space. Check what you expect AND what you do not expect. Bugs live at the boundary between valid and invalid data.

  • Static allocation after initialization. Fixed-size arrays for collections (MAX_ENTITIES pool, tile defs). Dynamic allocation (calloc) only during level loading for tile layers. No allocation or reallocation during gameplay.

  • Smallest possible scope for variables. Declare variables close to where they are used. Minimize the number of live variables at any point.

  • Hard limit of ~70 lines per function. When splitting, centralize control flow (switch/if) in the parent function and push pure computation into helpers. Keep leaf functions pure. "Push ifs up and fors down."

  • Zero compiler warnings. All builds must pass -Wall -Wextra with zero warnings.

  • Handle all errors. Every fopen, calloc, snprintf return must be checked. Early return on failure.

Performance

  • Think about performance in the design phase. The biggest wins (1000x) come from structural decisions, not micro-optimization after the fact.

  • Back-of-the-envelope sketches for the four resources: network, disk, memory, CPU. For a game: frame budget is ~16ms at 60 Hz. Know where time goes.

  • Optimize for the slowest resource first (disk > memory > CPU), weighted by frequency. A cache miss that happens every frame matters more than a disk read that happens once at level load.

  • Batch and amortize. Sprite batching via renderer_submit(). Particle pools. Tile culling to the viewport. Don't iterate what you don't need to.

  • Be explicit with the compiler. Don't rely on the optimizer to fix structural problems. Extract hot inner loops into standalone helpers with primitive arguments when it helps clarity and performance.

Developer Experience

  • Get the nouns and verbs right. Names should capture what a thing is or does. Take time to find the right name. Module-prefixed functions (player_update, tilemap_load) already enforce this.

  • Add units or qualifiers last, sorted by descending significance. For example, latency_ms_max not max_latency_ms. Related variables then align: latency_ms_min, latency_ms_avg.

  • Always say why. Comments explain rationale, not just what the code does. If a design decision isn't obvious, explain the tradeoff. Code alone is not documentation.

  • Comments are sentences. Capital letter, full stop, space after /*. End-of-line comments can be phrases without punctuation.

  • Order matters for readability. Put important things near the top of a file. Public API first, then helpers. In structs, fields before methods.

  • Don't duplicate variables or alias them. One source of truth per value. Reduces the chance of state getting out of sync.

  • Calculate or check variables close to where they are used. Don't introduce variables before they are needed. Minimize the gap between place-of-check and place-of-use.

  • Descriptive commit messages. Imperative mood. Inform the reader about the why, not just the what. The commit message is the permanent record — PR descriptions are not stored in git.

Zero Technical Debt

Do it right the first time. Don't allow known issues to slip through — exponential-complexity algorithms, unbounded loops, potential buffer overflows. What we ship is solid. We may lack features, but what we have meets our design goals. This is the only way to make steady incremental progress.

Architecture Patterns

Entity System

  • Fixed pool of MAX_ENTITIES (512) in EntityManager
  • Dispatch tables: update_fn[type], render_fn[type], destroy_fn[type]
  • void *data for type-specific data (cast in callbacks)
  • Each entity type: _register() sets callbacks, _spawn() creates instances
  • Entity registry maps string names to spawn functions for level loading

Module Pattern

  • Public API: declared in header, module-prefixed (camera_init, camera_follow)
  • Private helpers: static in .c only
  • File-scope state: static variables with s_ prefix
  • Forward declarations to break circular includes

Level System

  • .lvl files: plain-text directives + tile grid data
  • level_load() for handcrafted levels from files
  • level_load_generated() for procedural levels from Tilemap structs
  • Exit zones trigger transitions; target strings: file path, "generate", "generate:station", or empty (victory)
  • Procedural generator: segment-based, theme-driven, difficulty-scaled

Rendering

  • Sprite batching: submit to queue via renderer_submit(), flush layer-by-layer
  • Draw layers: BG → entities → FG → particles → HUD
  • Camera transforms world coords to screen coords

Commit Messages

  • Imperative mood, concise
  • No co-authored-by or AI attribution
  • Example: "Add in-game level editor with auto-discovered tile/entity palettes"

Key Constants (config.h)

Constant Value Notes
SCREEN_WIDTH 640 Logical resolution
SCREEN_HEIGHT 360
TILE_SIZE 16 Pixels per tile
TICK_RATE 60 Fixed timestep Hz
DEFAULT_GRAVITY 980.0f px/s²
MAX_ENTITIES 512 Entity pool size
MAX_ENTITY_SPAWNS 512 Per-level spawn slots
MAX_EXIT_ZONES 16 Per-level exit zones