Add two tutorial games that build, run in CI, and cannot drift
examples/sidescroller and examples/jrpg are complete programs, built with the library and exercised headless by ctest. The chapters quote them with `c excerpt=examples/...` blocks rather than restating the code, so a chapter cannot drift from a program that compiles -- the excerpt check fails the moment the source moves. 34 excerpts in one chapter, 21 in the other. The two are complementary. The sidescroller is the physics tutorial: gravity, a jump, coins, hazards. The JRPG is the content-pipeline tutorial: a town map, NPCs spawned from map objects, four-way per-facing animation, a text box, a follower. Both smoke tests drive real SDL_Events through akgl_controller_handle_event and step the physics clock at a fixed 1/60s rather than sleeping, so a scripted run is deterministic and finishes in under five seconds. Writing them is what turned up most of the defects recorded in the next commit, because a game exercises paths a unit test does not. Each workaround says in the chapter which library gap forced it: - collision is written in a custom movementlogicfunc, because akgl_physics_arcade_collide raises AKERR_API and akgl_physics_simulate never calls collide at all; - the sidescroller cancels the step's own gravity when it blocks downward, because otherwise a quarter-pixel of penetration makes the *horizontal* sweep report blocked and the character walks backwards a tile at a time; - both clear movement_controls_face on every map-spawned actor, because the default facefunc leaves a stopped actor with no facing bit, no sprite, and no draw; - the JRPG's follower gets a renderfunc that nulls obj->parent for the duration of the draw, because a child's offset is counted twice. Assets are CC0 from three Kenney packs, vendored with per-pack licence text, per-file provenance, and the geometry contract in docs/tutorials/assets/README.md. CC0 specifically rather than merely free: a reader who copies a tutorial into their own game inherits no obligation. scripts/fetch_tutorial_assets.sh refreshes them in the shape mkcontrollermappings.sh was fixed into for 0.5.0 -- it checks curl's status, refuses a pack page that does not say CC0, verifies the archive and the staged dimensions, and leaves the tracked bytes untouched on any failure. Both failure paths were tested, and a no-op refresh is byte-identical. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
151
docs/tutorials/assets/PROVENANCE.md
Normal file
151
docs/tutorials/assets/PROVENANCE.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Provenance of the tutorial assets
|
||||
|
||||
Everything under `docs/tutorials/assets/` is **CC0 1.0** or was written for this
|
||||
repository. Nothing here carries an attribution requirement, a share-alike
|
||||
clause, or a field-of-use restriction, which is the whole point: a reader who
|
||||
copies a tutorial into their own game inherits whatever obligation these assets
|
||||
carry, and CC0 carries none. "Free to download" is not the same thing and was
|
||||
not accepted -- each pack's licence was read on its Kenney page *and* in the
|
||||
`License.txt` it ships, and `scripts/fetch_tutorial_assets.sh` re-checks both on
|
||||
every refresh and refuses to vendor a pack that fails either.
|
||||
|
||||
`LICENSE` is the CC0 1.0 legal code. The three `LICENSE.kenney_*.txt` files are
|
||||
the licence statements the packs themselves ship, kept beside the art in the
|
||||
same way `tests/assets/akgl_test_mono.LICENSE.txt` sits beside its font.
|
||||
|
||||
The two tables below account for all 56 asset files: 14 carry upstream bytes and
|
||||
42 were written here. This file and `README.md` are the remaining two, and are
|
||||
documentation of the same kind as the rest of `docs/`.
|
||||
|
||||
## Upstream packs
|
||||
|
||||
| Pack | URL | Licence | Licence file in this tree |
|
||||
|-----------------------|------------------------------------------------|---------|--------------------------------------------|
|
||||
| Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | `LICENSE.kenney_pixel-line-platformer.txt` |
|
||||
| RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | `LICENSE.kenney_rpg-urban-pack.txt` |
|
||||
| Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | `LICENSE.kenney_music-jingles.txt` |
|
||||
|
||||
Kenney asks for credit and does not require it. Credit is given here and in the
|
||||
tutorial chapters.
|
||||
|
||||
## Files carrying upstream bytes
|
||||
|
||||
| File | Pack | Source URL | Licence | Cropped / repacked into |
|
||||
|--------------------------------------------|-----------------------|-----------------------------------------------------------------|---------|----------------------------------------------------------------------------------------------------|
|
||||
| `LICENSE` | creativecommons.org | https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt | CC0 1.0 | The CC0 1.0 Universal legal code, fetched verbatim |
|
||||
| `LICENSE.kenney_pixel-line-platformer.txt` | Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | The pack's own `License.txt`, copied verbatim |
|
||||
| `LICENSE.kenney_rpg-urban-pack.txt` | RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | The pack's own `License.txt`, copied verbatim |
|
||||
| `LICENSE.kenney_music-jingles.txt` | Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | The pack's own `License.txt`, copied verbatim |
|
||||
| `sidescroller/tiles.png` | Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | `Tilemap/tilemap_packed.png` copied verbatim: 160x96, 10x6 tiles of 16x16, no spacing, no margin |
|
||||
| `sidescroller/player.png` | Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tiles 40, 41, 42 and their horizontal mirrors, bottom-centred in six 32x32 cells |
|
||||
| `sidescroller/coin.png` | Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tile 44, bottom-centred in one 32x32 cell |
|
||||
| `sidescroller/hazard.png` | Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tiles 55, 56, 51, 52, bottom-centred in four 32x32 cells |
|
||||
| `sidescroller/jingle_start.ogg` | Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | `Audio/8-Bit jingles/jingles_NES00.ogg`, copied verbatim (1.8 s) |
|
||||
| `jrpg/tiles.png` | RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | `Tilemap/tilemap_packed.png` copied verbatim: 432x288, 27x18 tiles of 16x16, no spacing, no margin |
|
||||
| `jrpg/player.png` | RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 0 (tileset columns 23-26, rows 0-2) regrouped into twelve 32x32 cells |
|
||||
| `jrpg/npc_shopkeeper.png` | RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 3 (tileset columns 23-26, rows 9-11) regrouped into twelve 32x32 cells |
|
||||
| `jrpg/npc_elder.png` | RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 2 (tileset columns 23-26, rows 6-8) regrouped into twelve 32x32 cells |
|
||||
| `jrpg/jingle_start.ogg` | Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | `Audio/Pizzicato jingles/jingles_PIZZI07.ogg`, copied verbatim (1.3 s) |
|
||||
|
||||
## Files written for this repository
|
||||
|
||||
These contain no upstream content. They are libakgl source in JSON form: they
|
||||
name the art above, they do not embed it.
|
||||
|
||||
| File | Kind | What it is |
|
||||
|-------------------------------------------------|-----------|---------------------------------------------------------------------------------|
|
||||
| `jrpg/character_jrpg_elder.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `jrpg/character_jrpg_player.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `jrpg/character_jrpg_shopkeeper.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `jrpg/sprite_jrpg_elder_idle_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_idle_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_idle_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_idle_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_walk_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_walk_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_walk_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_elder_walk_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_idle_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_idle_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_idle_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_idle_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_walk_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_walk_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_walk_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_player_walk_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_idle_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_idle_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_idle_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_idle_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_walk_down.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_walk_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_walk_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/sprite_jrpg_shopkeeper_walk_up.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `jrpg/town.tmj` | map | Tiled 1.8 TMJ, tileset **embedded**. 30x20 cells, ground + decoration + actors |
|
||||
| `sidescroller/character_ss_coin.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `sidescroller/character_ss_hazard_blob.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `sidescroller/character_ss_hazard_moth.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `sidescroller/character_ss_player.json` | character | State-name to sprite-name bindings; contains no upstream content |
|
||||
| `sidescroller/level1.tmj` | map | Tiled 1.8 TMJ, tileset **embedded**. 40x15 cells, background + terrain + actors |
|
||||
| `sidescroller/sprite_ss_coin.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_hazard_blob.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_hazard_moth.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_idle_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_idle_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_jump_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_jump_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_run_left.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
| `sidescroller/sprite_ss_player_run_right.json` | sprite | Frame list into the sheet named by its own `spritesheet.filename` |
|
||||
|
||||
## How the repacking works, and why
|
||||
|
||||
**Tilesets are copied verbatim.** Both packs already ship a 16x16 grid with zero
|
||||
spacing and zero margin, which is the only geometry
|
||||
`akgl_tilemap_compute_tileset_offsets` gets right -- it adds `spacing` to the
|
||||
tile pitch but sets the *first* row's y offset to `spacing` rather than zero,
|
||||
and it ignores `margin` completely. A packed sheet with a 1 px gutter, which is
|
||||
what most of Kenney's older packs ship, would render every tile in row 0 one
|
||||
pixel low. That ruled several otherwise-suitable packs out.
|
||||
|
||||
**Character sheets are repacked into one row of 32x32 cells.** The asset
|
||||
contract for these tutorials is 16x16 tiles and 32x32 character frames, and
|
||||
neither pack ships 32x32 art, so each 16x16 source tile is composited at (8, 16)
|
||||
inside its cell -- horizontally centred, sitting on the cell's bottom edge. The
|
||||
art is not resampled: a 2x upscale would put 2x2 pixel blocks next to 1x1 tiles,
|
||||
and every sprite in the repository would stop matching the source pixel for
|
||||
pixel. The cost is that three quarters of each cell is empty, and the benefit is
|
||||
one rule the tutorials can rely on: **an actor drawn into a 32x32 destination
|
||||
rectangle has its feet on the bottom edge of that rectangle and its body
|
||||
centred**, whichever sheet it came from.
|
||||
|
||||
**Left-facing frames are stored, not mirrored at draw time.**
|
||||
`akgl_actor_render` calls `draw_texture` with `SDL_FLIP_NONE` hard-coded
|
||||
(`src/actor.c`), so there is no way to ask for a mirrored blit. The sidescroller
|
||||
sheet therefore carries frames 0-2 facing right and frames 3-5 as their
|
||||
horizontal mirrors. The JRPG sheets need no mirroring: the RPG Urban Pack draws
|
||||
both side views.
|
||||
|
||||
**One frame is used twice.** Pixel Line Platformer has no dedicated jump pose.
|
||||
Frame 1 is the passing position of the run cycle, which is the airborne one, and
|
||||
`ss_player_jump_right` reuses it. Said here rather than left for a reader to
|
||||
notice.
|
||||
|
||||
**The coin does not animate.** Pixel Line Platformer ships one gold pickup tile
|
||||
and no rotation frames, so `sprite_ss_coin.json` is a single frame. Nothing was
|
||||
invented to pad it out.
|
||||
|
||||
## Reproducing this
|
||||
|
||||
```sh
|
||||
scripts/fetch_tutorial_assets.sh
|
||||
```
|
||||
|
||||
Fetches all three packs, checks each page and each shipped `License.txt` says
|
||||
CC0, verifies the source images are the size the frame arithmetic assumes,
|
||||
repacks into a temporary directory, checks every staged file's dimensions, and
|
||||
only then moves anything into place. A failure at any step leaves the tracked
|
||||
copies untouched and exits non-zero. The output is byte-for-byte reproducible --
|
||||
ImageMagick's wall-clock `date:create` and `date:modify` chunks are stripped, so
|
||||
a refresh that changes nothing produces no diff.
|
||||
|
||||
The script does not touch the JSON or the TMJ maps. Those are hand-maintained.
|
||||
Reference in New Issue
Block a user