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.
275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
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
|
|
```c
|
|
#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](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md),
|
|
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 `if`s up and `for`s 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 |
|