Commit Graph

2 Commits

Author SHA1 Message Date
943bf4324e Let a control map listen to any keyboard, and fix the dead arrow keys
The sidescroller's controls did nothing. `akgl_controller_handle_event` matched
`event->key.which == curmap->kbid` exactly, with no way to say "whatever
keyboard the player is typing on", so the game did the obvious thing and bound
`SDL_GetKeyboards()[0]`.

That cannot work. The id a key event *carries* is chosen by the video backend
and is not required to be an id `SDL_GetKeyboards()` reports. On X11 without
XInput2 every key event carries `SDL_GLOBAL_KEYBOARD_ID`, which is 0, while
`SDL_AddKeyboard` registers the attached keyboard as `SDL_DEFAULT_KEYBOARD_ID`,
which is 1; with XInput2 the events carry the physical slave device's
`sourceid` while `SDL_GetKeyboards()[0]` may be the master. Either way the map
matched nothing and every key press was dropped.

A `kbid` or `jsid` of 0 now matches any device of that kind. 0 is safe to spend:
SDL documents `which` as 0 when the source is unknown or virtual, and joystick
ids start at 1, so no real device is 0. A non-zero id still matches only that
device, which is what keeps two local players on two keyboards apart, and there
is a test for that half too. `util/charviewer.c` already passed 0 and depended
on the old behaviour by accident; it works under XInput2 now as well.

Two reasons this shipped, both closed:

- Every test in tests/controller.c dispatched an event whose id equalled the id
  the map was bound with, so none of them could see it.
- The sidescroller's smoke test called the control handlers directly instead of
  dispatching events, so it passed against a control map that matched nothing.
  It now pushes synthetic events through akgl_controller_handle_event from a
  deliberately non-zero device id and fails if the press does not arrive.
  Verified by breaking the binding and watching the run exit non-zero.

`test_controller_wildcard_device_ids` was written first and failed against the
unfixed library with "a map bound to keyboard 0 ignored a key press from device
11", which is the whole reason to trust it now that it passes.

The controls were documented, in one sentence. Chapter 19 has a proper table of
them, and chapter 15 explains why not to reach for SDL_GetKeyboards(). The
header said the match was exact and now says what it does.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 21:23:13 -04:00
88aaa184e4 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>
2026-08-01 20:59:00 -04:00