diff --git a/docs/20-tutorial-galaga.md b/docs/20-tutorial-galaga.md index 39561e3..72f9e68 100644 --- a/docs/20-tutorial-galaga.md +++ b/docs/20-tutorial-galaga.md @@ -125,7 +125,38 @@ writes `x` and `y` directly is the mover, and in this game that will be BASIC. **Error handling is the house protocol.** Every function returns `akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter -just uses it. The status codes this game raises are `AKERR_NULLPOINTER`, +just uses it, with two rules that keep the fragments compiling: **`CATCH` is +only legal inside an `ATTEMPT` block, and `PASS` everywhere else** — swap them +and the compiler objects about a stray `break` — and `main()` alone ends its +block with `FINISH_NORETURN(errctx)` instead of `FINISH`, because `FINISH` +expands a `return` of the context that an `int`-returning function cannot +compile: + +```c wrap=galagatypes requires=akgl +static int FAILED = 0; + +int main(int argc, char *argv[]) +{ + PREPARE_ERROR(errctx); + + (void)argc; (void)argv; + ATTEMPT { + /* CATCH each stage in order: startup, assets, the script boot, + * the spawns, then the frame loop. */ + } CLEANUP { + /* ...teardown, every call wrapped in IGNORE()... */ + } PROCESS(errctx) { + } HANDLE_DEFAULT(errctx) { + LOG_ERROR_WITH_MESSAGE(errctx, "galaga could not run"); + /* Set a flag rather than returning: leaving a HANDLE block early + * skips FINISH's release and leaks the context's pool slot. */ + FAILED = 1; + } FINISH_NORETURN(errctx); + return FAILED; +} +``` + +The status codes this game raises are `AKERR_NULLPOINTER`, `AKERR_VALUE`, `AKERR_KEY`, `AKERR_IO`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` and `AKGL_ERR_HEAP` — there is no code this tutorial invents. @@ -285,8 +316,32 @@ Each of the four lines under the comment closes a trap: invisibly. This one line cost this example its first screenshot. Input goes through a control map: push a control per key with handlers that set -flags, and let the actor's update hook read the flags. The full recipe is in -`examples/galaga/player.c` and libakgl docs/16-input.md; the shape is: +flags, and let the actor's update hook read the flags. A handler receives the +map's target actor and the event, and returns through the error protocol like +everything else — this pair is the whole pattern, repeated per key: + +```c wrap=galagagame requires=akgl +static bool MOVELEFT = false; + +akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event) +{ + PREPARE_ERROR(errctx); + (void)obj; (void)event; + MOVELEFT = true; + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event) +{ + PREPARE_ERROR(errctx); + (void)obj; (void)event; + MOVELEFT = false; + SUCCEED_RETURN(errctx); +} +``` + +(The example keeps the flags in its `galaga_Game` struct rather than statics; +either works.) The bindings themselves are pushes onto map 0: ```c wrap=galagagame requires=akgl static akerr_ErrorContext *galaga_player_controls(void) @@ -673,10 +728,53 @@ case GALAGA_SCREEN_VICTORY: CATCH(errctx, akgl_ui_frame_end(akgl_renderer)); ``` -The playing screen is two `akgl_ui_label()` calls — score top-left, lives and -wave top-right — formatted into `static` buffers, because the UI borrows label +The playing screen is two `akgl_ui_label()` calls — a widget call per label, +not a struct — formatted into `static` buffers, because the UI borrows label text until `frame_end` and a local buffer would be dangling by the time it -draws. The title and end screens are an `akgl_ui_menu()` at the center. +draws: + +```c wrap=galagagame requires=akgl +static char HUD_SCORE[64]; +static char HUD_LIVES[64]; + +static akerr_ErrorContext *declare_play(void) +{ + int count = 0; + PREPARE_ERROR(errctx); + + PASS(errctx, aksl_snprintf(&count, HUD_SCORE, sizeof(HUD_SCORE), + "SCORE %06d", galaga_game.score)); + PASS(errctx, aksl_snprintf(&count, HUD_LIVES, sizeof(HUD_LIVES), + "LIVES %d WAVE %d", galaga_game.lives, galaga_shared.wave)); + PASS(errctx, akgl_ui_label("score", HUD_SCORE, AKGL_UI_ANCHOR_TOP_LEFT, NULL)); + PASS(errctx, akgl_ui_label("lives", HUD_LIVES, AKGL_UI_ANCHOR_TOP_RIGHT, NULL)); + SUCCEED_RETURN(errctx); +} +``` + +The title and end screens are an `akgl_ui_menu()` at the center, fed an +`akgl_UiMenu` that lives in a `static` for the same borrowing reason. The +struct is an id, the item strings, a count, the selected index, the +`activated` output flag, and a style (`NULL` for the default): + +```c wrap=galagatypes requires=akgl +static akgl_UiMenu TITLE_MENU = { + "titlemenu", { "START", "QUIT" }, 2, 0, false, NULL +}; + +static akerr_ErrorContext *declare_title(void) +{ + PREPARE_ERROR(errctx); + + PASS(errctx, akgl_ui_menu(&TITLE_MENU)); + SUCCEED_RETURN(errctx); +} +``` + +Route events to the menu with +`akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed)` — the menu for +whichever screen is up, a `bool` out-parameter reporting whether the event was +taken. Up and down move `selected`, return sets `activated`. The big **GALAGA** headline is direct text rather than a label: diff --git a/docs/21-tutorial-galaga-enemies.md b/docs/21-tutorial-galaga-enemies.md index 7a7df0b..ff14dc7 100644 --- a/docs/21-tutorial-galaga-enemies.md +++ b/docs/21-tutorial-galaga-enemies.md @@ -157,6 +157,24 @@ between the script's decision and the engine's pixel. Null physics (Chapter 20, Step 1) is what makes that safe: nothing else is trying to move the actor. +Registration and the first binding happen at boot, before the script loads — +between `akbasic_runtime_init()` and `akbasic_runtime_load()` in Chapter 20's +boot sequence. A binding is **borrowed, never copied**, so the placeholders it +points at must be static storage: + +```c wrap=galagacalls requires=akgl +CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE)); +CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE)); +CATCH(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE)); + +CATCH(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY)); +CATCH(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR)); +CATCH(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared)); +``` + +`akbasic_host_bind()` takes the script name, the registered type's name, and +the instance; after that, `SELF@` and `ACTOR@` are only ever *re*bound. + The per-frame call binds both names to *this* enemy before dispatching — one binding per name, pointed at forty enemies in turn, which is what `akbasic_host_rebind()` is for: diff --git a/tests/docs_preludes/galagacalls.post b/tests/docs_preludes/galagacalls.post index ce04880..e6d321b 100644 --- a/tests/docs_preludes/galagacalls.post +++ b/tests/docs_preludes/galagacalls.post @@ -4,5 +4,7 @@ (void)SCRIPT; (void)SINK; (void)SINKSTATE; (void)SOURCE; (void)args; (void)argp; (void)dtval; (void)result; (void)enemy; (void)actor; (void)dt; + (void)SCRATCH_ENEMY; (void)SCRATCH_ACTOR; (void)galaga_shared; + (void)ENEMY_TYPE; (void)ACTOR_TYPE; (void)GAME_TYPE; SUCCEED_RETURN(errctx); } diff --git a/tests/docs_preludes/galagacalls.pre b/tests/docs_preludes/galagacalls.pre index 4ebab03..781efa5 100644 --- a/tests/docs_preludes/galagacalls.pre +++ b/tests/docs_preludes/galagacalls.pre @@ -36,11 +36,42 @@ typedef struct galaga_docs_Enemy float rnd; } galaga_docs_Enemy; +typedef struct galaga_docs_Shared +{ + float playerx; + float playery; + int32_t wave; + float rnd; +} galaga_docs_Shared; + static akbasic_Runtime SCRIPT; static akbasic_TextSink SINK; static akbasic_StdioSink SINKSTATE; static char SOURCE[16384]; +static galaga_docs_Enemy SCRATCH_ENEMY; +static akgl_Actor SCRATCH_ACTOR; +static galaga_docs_Shared galaga_shared; + +static const akbasic_HostField ENEMY_FIELDS[] = { + AKBASIC_HOST_FIELD( galaga_docs_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 ) +}; +static const akbasic_HostType ENEMY_TYPE = { + "ENEMY", sizeof(galaga_docs_Enemy), ENEMY_FIELDS, 1 +}; +static const akbasic_HostField ACTOR_FIELDS[] = { + AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT ) +}; +static const akbasic_HostType ACTOR_TYPE = { + "ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 1 +}; +static const akbasic_HostField GAME_FIELDS[] = { + AKBASIC_HOST_FIELD( galaga_docs_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 ) +}; +static const akbasic_HostType GAME_TYPE = { + "GAME", sizeof(galaga_docs_Shared), GAME_FIELDS, 1 +}; + akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt); akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt) {