Files
libakgl/examples/jrpg/CMakeLists.txt

78 lines
3.6 KiB
CMake
Raw Permalink Normal View History

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
# The top-down JRPG tutorial game, quoted by docs/20-tutorial-jrpg.md.
#
# It is a real target built by `all`, not a snippet: a tutorial whose program
# does not compile is the failure this whole documentation effort exists to
# stop, and the chapter's `c excerpt=` blocks fail the moment this source moves
# under them.
add_executable(jrpg
jrpg.c
world.c
textbox.c
)
target_link_libraries(jrpg
PRIVATE akstdlib::akstdlib akerror::akerror akgl
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer
jansson::jansson -lm)
target_include_directories(jrpg PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_options(jrpg PRIVATE ${AKGL_WARNING_FLAGS})
# The game finds its data through two absolute paths baked in here. A game that
# ships would install its assets and resolve them against SDL_GetBasePath(); a
# tutorial has to run from the build tree, from the source tree, and from
# whatever working directory CTest hands it, so the paths are compiled in.
get_filename_component(JRPG_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
target_compile_definitions(jrpg PRIVATE
JRPG_ASSET_DIR="${JRPG_REPO_ROOT}/docs/tutorials/assets/jrpg"
# tests/assets/akgl_test_mono.ttf, which ships beside its licence file.
JRPG_FONT_FILE="${JRPG_REPO_ROOT}/tests/assets/akgl_test_mono.ttf"
)
# The vendored SDL satellite libraries land in per-project subdirectories that
# are not on the loader's default search path, exactly as they do for the test
# suites. AKGL_VENDORED_RPATH is set by the top-level CMakeLists.txt and is only
# defined when something was actually vendored.
if(AKGL_VENDORED_DEPENDENCIES)
set_target_properties(jrpg PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
endif()
# The headless smoke run. `--demo` drives the arrow keys from a script and steps
# the physics clock by a fixed 1/60 s, so the run is deterministic and finishes
# in well under a second; `--frames` bounds it. Between them the run walks the
# player into a wall, opens the text box, and exercises the follower's render
# hook -- rather than just proving that main() returns.
#
# add_test() reaches the real command here: the top-level CMakeLists.txt shadows
# it to suppress the vendored projects' registrations and lifts the suppression
# again long before examples/ is added.
add_test(NAME example_jrpg COMMAND jrpg --frames 320 --demo)
Stop set_tests_properties from eating the test environment lists The example games failed on the CI runner with "ALSA: Couldn't open audio device" despite SDL_AUDIODRIVER=dummy sitting right there in their CMake -- because it never reached the process. set_tests_properties parses its PROPERTIES arguments as name/value pairs, so a semicolon-separated value list is split and every element after the first is consumed as a bogus property name: ENVIRONMENT kept only SDL_VIDEODRIVER=dummy, and the LD_LIBRARY_PATH prepend list kept only its first directory. ctest --show-only=json-v1 shows the truncation plainly. Nobody could see it. Video survived because SDL3 falls through its driver list to dummy on a headless machine with no hint at all; audio survived on every developer machine because the real device works there; the library paths survived because RPATH covered them. A runner with no sound card was the first environment where any of it mattered. Every list-valued test property now goes through set_property(TEST ...), which takes real list arguments -- the examples' ENVIRONMENT triplets and vendored-path modifications, the suites' LD_LIBRARY_PATH prepends, and the docs harness's driver variables. Verified by property dump (3 environment entries and all 7 prepends present) and by running the example tests with ALSA_CONFIG_PATH=/dev/null, which now pass where a real ALSA open would fail: the dummy driver is finally the one being asked. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 14:19:05 -04:00
set_tests_properties(example_jrpg PROPERTIES TIMEOUT 120)
# set_property, not set_tests_properties: the latter parses its PROPERTIES
# arguments as name/value *pairs*, so a semicolon-separated value is split and
# every element after the first is consumed as a bogus property name. The
# audio and render entries of this list were silently lost that way --
# invisible on any machine whose real audio device works, and found the first
# time CI ran on a runner without one.
set_property(TEST example_jrpg PROPERTY ENVIRONMENT
"SDL_VIDEODRIVER=dummy"
"SDL_RENDER_DRIVER=software"
"SDL_AUDIODRIVER=dummy"
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
)
Stop set_tests_properties from eating the test environment lists The example games failed on the CI runner with "ALSA: Couldn't open audio device" despite SDL_AUDIODRIVER=dummy sitting right there in their CMake -- because it never reached the process. set_tests_properties parses its PROPERTIES arguments as name/value pairs, so a semicolon-separated value list is split and every element after the first is consumed as a bogus property name: ENVIRONMENT kept only SDL_VIDEODRIVER=dummy, and the LD_LIBRARY_PATH prepend list kept only its first directory. ctest --show-only=json-v1 shows the truncation plainly. Nobody could see it. Video survived because SDL3 falls through its driver list to dummy on a headless machine with no hint at all; audio survived on every developer machine because the real device works there; the library paths survived because RPATH covered them. A runner with no sound card was the first environment where any of it mattered. Every list-valued test property now goes through set_property(TEST ...), which takes real list arguments -- the examples' ENVIRONMENT triplets and vendored-path modifications, the suites' LD_LIBRARY_PATH prepends, and the docs harness's driver variables. Verified by property dump (3 environment entries and all 7 prepends present) and by running the example tests with ALSA_CONFIG_PATH=/dev/null, which now pass where a real ALSA open would fail: the dummy driver is finally the one being asked. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 14:19:05 -04:00
# LD_LIBRARY_PATH for the vendored satellite libraries. Only needed when they
# were vendored; an installed build resolves them normally.
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
if(AKGL_VENDORED_DEPENDENCIES)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
set(JRPG_TEST_ENV_MOD "")
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
list(APPEND JRPG_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
endforeach()
Stop set_tests_properties from eating the test environment lists The example games failed on the CI runner with "ALSA: Couldn't open audio device" despite SDL_AUDIODRIVER=dummy sitting right there in their CMake -- because it never reached the process. set_tests_properties parses its PROPERTIES arguments as name/value pairs, so a semicolon-separated value list is split and every element after the first is consumed as a bogus property name: ENVIRONMENT kept only SDL_VIDEODRIVER=dummy, and the LD_LIBRARY_PATH prepend list kept only its first directory. ctest --show-only=json-v1 shows the truncation plainly. Nobody could see it. Video survived because SDL3 falls through its driver list to dummy on a headless machine with no hint at all; audio survived on every developer machine because the real device works there; the library paths survived because RPATH covered them. A runner with no sound card was the first environment where any of it mattered. Every list-valued test property now goes through set_property(TEST ...), which takes real list arguments -- the examples' ENVIRONMENT triplets and vendored-path modifications, the suites' LD_LIBRARY_PATH prepends, and the docs harness's driver variables. Verified by property dump (3 environment entries and all 7 prepends present) and by running the example tests with ALSA_CONFIG_PATH=/dev/null, which now pass where a real ALSA open would fail: the dummy driver is finally the one being asked. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 14:19:05 -04:00
set_property(TEST example_jrpg PROPERTY ENVIRONMENT_MODIFICATION ${JRPG_TEST_ENV_MOD})
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
else()
string(REPLACE ";" ":" JRPG_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
Stop set_tests_properties from eating the test environment lists The example games failed on the CI runner with "ALSA: Couldn't open audio device" despite SDL_AUDIODRIVER=dummy sitting right there in their CMake -- because it never reached the process. set_tests_properties parses its PROPERTIES arguments as name/value pairs, so a semicolon-separated value list is split and every element after the first is consumed as a bogus property name: ENVIRONMENT kept only SDL_VIDEODRIVER=dummy, and the LD_LIBRARY_PATH prepend list kept only its first directory. ctest --show-only=json-v1 shows the truncation plainly. Nobody could see it. Video survived because SDL3 falls through its driver list to dummy on a headless machine with no hint at all; audio survived on every developer machine because the real device works there; the library paths survived because RPATH covered them. A runner with no sound card was the first environment where any of it mattered. Every list-valued test property now goes through set_property(TEST ...), which takes real list arguments -- the examples' ENVIRONMENT triplets and vendored-path modifications, the suites' LD_LIBRARY_PATH prepends, and the docs harness's driver variables. Verified by property dump (3 environment entries and all 7 prepends present) and by running the example tests with ALSA_CONFIG_PATH=/dev/null, which now pass where a real ALSA open would fail: the dummy driver is finally the one being asked. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 14:19:05 -04:00
set_property(TEST example_jrpg APPEND PROPERTY ENVIRONMENT
"LD_LIBRARY_PATH=${JRPG_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}")
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
endif()
endif()