Deliver GALAGA tutorial and akbasic enemy scripting example #39

Closed
logikoma wants to merge 7 commits from 34 into feature/reduce_memory_usage
36 changed files with 2532 additions and 0 deletions
Showing only changes of commit 27837aeabc - Show all commits

View File

@@ -268,6 +268,69 @@ if(AKBASIC_BUILD_EXAMPLES)
endforeach()
endif()
# The galaga example: a C game on libakgl with the interpreter embedded as its
# enemy-behavior engine. Chapters 20 and 21 build it from an empty file, so it
# is compiled and run by every AKGL build rather than rotting in a document.
# The asset, script and font paths are baked in so the smoke test can launch
# from any working directory; --assets and --script override them at runtime.
if(AKBASIC_BUILD_EXAMPLES AND AKBASIC_WITH_AKGL)
add_executable(akbasic_example_galaga
examples/galaga/main.c
examples/galaga/script.c
examples/galaga/enemies.c
examples/galaga/player.c)
target_compile_options(akbasic_example_galaga PRIVATE -Wall -Wextra)
target_compile_definitions(akbasic_example_galaga PRIVATE
GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/assets"
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas"
GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
akbasic_instrument(akbasic_example_galaga)
# Ten seconds of scripted play under the headless drivers: the script boots,
# a wave enters and forms, the autoplay pilot shoots at it, and the program
# tears down and exits 0. A tutorial that stops working fails here rather
# than in front of a reader.
_add_test(NAME example_galaga COMMAND akbasic_example_galaga --frames 600 --autoplay)
_set_tests_properties(example_galaga PROPERTIES TIMEOUT 120
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
# The boundary's round-trip test: links the real script.c and the real
# galaga.bas, and fails the moment the two sides of the interop disagree.
add_executable(akbasic_example_galaga_interop
examples/galaga/interop_test.c
examples/galaga/script.c)
target_compile_options(akbasic_example_galaga_interop PRIVATE -Wall -Wextra)
target_compile_definitions(akbasic_example_galaga_interop PRIVATE
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
SDL3::SDL3)
akbasic_instrument(akbasic_example_galaga_interop)
_add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
_set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)
# Regenerating the game figures in docs/ is a deliberate act, never part of
# a build, for the same reason docs_screenshots is: the PNGs are checked in.
# Wall-clock dt makes each regeneration differ by a few pixels of starfield,
# so expect a binary diff every time this runs; commit one only when the
# content changed on purpose. (docs_galaga_figures, not docs_game_figures:
# the libakgl submodule already owns that target name.)
add_custom_target(docs_galaga_figures
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
SDL_RENDER_DRIVER=software
$<TARGET_FILE:akbasic_example_galaga> --frames 40
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-title.png"
--screenshot-frame 30
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
SDL_RENDER_DRIVER=software
$<TARGET_FILE:akbasic_example_galaga> --autoplay --frames 370
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-wave.png"
--screenshot-frame 360
DEPENDS akbasic_example_galaga
COMMENT "Regenerating the galaga figures in docs/images"
VERBATIM)
endif()
# ---------------------------------------------------------------------------
# Tests.
#

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
docs/images/galaga-wave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

63
examples/galaga/README.md Normal file
View File

@@ -0,0 +1,63 @@
# GALAGA — a C engine with akbasic embedded as its enemy brain
A GALAGA-style fixed shooter whose core engine is C on libakgl, with akbasic
linked in as the scripting engine that owns every enemy's behavior. One BASIC
script — `galaga.bas`, nothing but `DEF` functions and an `END` — is called
once per enemy per frame through a custom `akgl_Actor` update hook. Bullets,
collision, scoring and screens are C forever; everything an enemy *decides* is
BASIC.
This is the checked-in example behind two tutorial chapters, and the chapters
are the intended way in:
* **[Chapter 20](../../docs/20-tutorial-galaga.md)** — the engine and the
boundary: from an empty file to a C game that boots a script and hands one
actor to BASIC.
* **[Chapter 21](../../docs/21-tutorial-galaga-enemies.md)** — the data
structures and the AI: from `SELF@` to a full attacking wave.
It is an academic exercise demonstrating *how* such an embed is done, not a
claim that it is the best way to write a GALAGA.
## Building and running
The example builds when both example and graphics builds are on:
```
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
cmake --build build-akgl --target akbasic_example_galaga
build-akgl/akbasic_example_galaga
```
| Key | Does |
|---|---|
| Left / Right | move the ship |
| Space | fire (two shots on screen, the classic rule) |
| Return | choose a menu entry |
## Flags
```
akbasic_example_galaga [--assets DIR] [--script PATH] [--frames N]
[--autoplay] [--screenshot PATH] [--screenshot-frame N]
```
`--script` points at a different enemy script, which is the whole point of the
architecture: edit `galaga.bas`, run again, no rebuild. `--frames N` with
`--autoplay` is the headless smoke test CI runs under the dummy SDL drivers;
the final log line reports frames, score, kills and shots per kind, and the
script-error count — a wave of dumb enemies still exits 0, and that count is
how you notice.
## The files
| File | Owns |
|---|---|
| `galaga.h` | the shared structs — the whole boundary in one header |
| `main.c` | startup order, the frame loop, screens, the starfield |
| `script.c` | everything that touches the interpreter |
| `enemies.c` | wave table, formation grid, the enemy update hook |
| `player.c` | the ship, both bullet kinds, every collision |
| `galaga.bas` | every decision an enemy makes |
| `interop_test.c` | round-trip proof the boundary works, run by CTest |
| `assets/` | sprite/character JSON, and Kenney CC0 art under `assets/art/` |

View File

@@ -0,0 +1,14 @@
###############################################################################
Space Shooter (Remastered, plus fonts and sounds) by Kenney Vleugels (www.kenney.nl)
------------------------------
License (CC0)
http://creativecommons.org/publicdomain/zero/1.0/
You may use these graphics in personal and commercial projects.
Credit (Kenney or www.kenney.nl) would be nice but is not mandatory.
###############################################################################

View File

@@ -0,0 +1,39 @@
# Where this art came from
Every PNG in this directory is from **Kenney's Space Shooter (Remastered)**, released
into the public domain under
[Creative Commons Zero](http://creativecommons.org/publicdomain/zero/1.0/).
`License.txt` is the pack's own licence file, copied here unedited.
* Source: <https://kenney.nl/assets/space-shooter-remastered>
* Downloaded: 2026-08-04, `kenney_space-shooter-remastered.zip`
* Author: Kenney (<https://www.kenney.nl>)
* Licence: CC0 1.0. Crediting is not required; it is here because it should be.
The files are the pack's `PNG/` versions, byte for byte — nothing is resized,
recoloured or re-encoded, so the checksum of any of them still matches the
distributed archive. `playerShip1_blue.png` sits at the top of `PNG/`; the enemies
are from `PNG/Enemies/` and the lasers from `PNG/Lasers/`.
| File | Size | Used for |
|---|---|---|
| `playerShip1_blue.png` | 99x75 | the player's ship |
| `enemyBlue1.png` | 93x84 | the bee |
| `enemyRed2.png` | 104x84 | the butterfly |
| `enemyGreen3.png` | 103x84 | the boss, at full health |
| `enemyBlack3.png` | 103x84 | the boss at one hit point — same silhouette, drained colour |
| `laserBlue01.png` | 9x54 | the player's shot |
| `laserRed01.png` | 9x54 | an enemy's shot |
| `laserBlue08.png` | 48x46 | the explosion burst |
The sprites are used at their distributed size: libakgl draws a sprite at the
sprite's own dimensions (`akgl_Actor.scale` is overwritten every frame — libakgl
docs/12-actors.md), so there is no way to draw these smaller, and the game's
1280x960 view is sized to fit a ten-column formation of them instead. The boss's
damage state is the same shape in a different colour deliberately: the swap has to
read at a glance from the top of the screen.
Everything else on screen — the starfield and the HUD — is drawn by the program
with `akgl_draw_point()` and the UI layer. See `../../README.md` for the run
instructions and the two tutorial chapters (docs/20, docs/21) for why only the
things that move are artwork.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_bee",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_bee"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_boom",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_boom"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"name": "galaga_boss",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_boss"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_UNDEFINED_13"
],
"sprite": "galaga_boss_hurt"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_butterfly",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_butterfly"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_enemyshot",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_enemyshot"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_player",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_player"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"name": "galaga_playershot",
"speedtime": 200,
"speed_x": 0.0,
"speed_y": 0.0,
"acceleration_x": 0.0,
"acceleration_y": 0.0,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE"
],
"sprite": "galaga_playershot"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/enemyBlue1.png",
"frame_width": 93,
"frame_height": 84
},
"name": "galaga_bee",
"width": 93,
"height": 84,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/laserBlue08.png",
"frame_width": 48,
"frame_height": 46
},
"name": "galaga_boom",
"width": 48,
"height": 46,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/enemyGreen3.png",
"frame_width": 103,
"frame_height": 84
},
"name": "galaga_boss",
"width": 103,
"height": 84,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/enemyBlack3.png",
"frame_width": 103,
"frame_height": 84
},
"name": "galaga_boss_hurt",
"width": 103,
"height": 84,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/enemyRed2.png",
"frame_width": 104,
"frame_height": 84
},
"name": "galaga_butterfly",
"width": 104,
"height": 84,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/laserRed01.png",
"frame_width": 9,
"frame_height": 54
},
"name": "galaga_enemyshot",
"width": 9,
"height": 54,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/playerShip1_blue.png",
"frame_width": 99,
"frame_height": 75
},
"name": "galaga_player",
"width": 99,
"height": 75,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

View File

@@ -0,0 +1,16 @@
{
"spritesheet": {
"filename": "art/laserBlue01.png",
"frame_width": 9,
"frame_height": 54
},
"name": "galaga_playershot",
"width": 9,
"height": 54,
"speed": 200,
"loop": false,
"loopReverse": false,
"frames": [
0
]
}

314
examples/galaga/enemies.c Normal file
View File

@@ -0,0 +1,314 @@
/**
* @file enemies.c
* @brief The formation, the wave, and the hook that hands each enemy to BASIC.
*
* C owns the grid, the wave table and the spawn timing; BASIC owns everything
* an enemy does after it exists. The formation slot arrives in SELF@.HOMEX% /
* HOMEY%, so even the idle breathing of the grid is the script's, computed
* relative to home.
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include "galaga.h"
galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
/* Explosion lifetimes, indexed by heap slot. An explosion is an actor with
* nothing to decide, so its whole state is one countdown. */
static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR];
/* A spawn serial per shot so registry names never collide while two shots
* with the same slot number are briefly both alive. */
static uint32_t SHOT_SERIAL = 0;
static uint32_t BOOM_SERIAL = 0;
/*
* The wave, one row per formation row. Columns are 0..9 at GALAGA_COL_PITCH;
* `first` and `count` say which columns the row fills. 40 enemies: 4 bosses,
* 16 butterflies, 20 bees -- 59 of the 64 actor heap slots at peak, counting
* the player, two player shots, eight enemy shots and eight explosions.
*/
static const struct
{
int32_t kind; /* GALAGA_ENEMY_* */
int row; /* formation row */
int first; /* first column filled */
int count; /* columns filled */
int32_t hp;
}
WAVE_ROWS[] = {
/* kind row first count hp */
{ GALAGA_ENEMY_BOSS, 0, 3, 4, 2 },
{ GALAGA_ENEMY_BUTTERFLY, 1, 1, 8, 1 },
{ GALAGA_ENEMY_BUTTERFLY, 2, 1, 8, 1 },
{ GALAGA_ENEMY_BEE, 3, 0, 10, 1 },
{ GALAGA_ENEMY_BEE, 4, 0, 10, 1 }
};
#define WAVE_ROW_COUNT ((int)(sizeof(WAVE_ROWS) / sizeof(WAVE_ROWS[0])))
/* Enemy kind -> character name, the render half of the dispatch table. */
static char *ENEMY_CHARACTER[GALAGA_ENEMY_KINDS] = {
"galaga_bee",
"galaga_butterfly",
"galaga_boss"
};
/* --------------------------------------------------------------- random --- */
/*
* No RND verb exists (issue #16), so the engine is the script's only source
* of randomness: it refreshes GAME@.RND% each frame and SELF@.RND% each call
* from this PRNG. A hand-rolled LCG rather than rand() so a headless run is
* the same game on every libc.
*/
static uint32_t PRNG_STATE = 0x12345678u;
float galaga_random(void)
{
PRNG_STATE = PRNG_STATE * 1664525u + 1013904223u;
return (float)(PRNG_STATE >> 8) / (float)0x01000000u;
}
/* -------------------------------------------------------------- helpers --- */
static akerr_ErrorContext *release_actor(akgl_Actor *actor)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
PASS(errctx, akgl_heap_release_actor(actor));
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- shots --- */
/**
* @brief Move an enemy shot; release it once it has left the screen.
*/
static akerr_ErrorContext *enemy_shot_update(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
obj->y += 380.0f * galaga_game.dt;
if ( obj->y > (float)GALAGA_VIEW_HEIGHT + 60.0f ) {
galaga_game.enemy_shots_live -= 1;
PASS(errctx, release_actor(obj));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Consume an enemy's fire flag: take an actor and aim it downward.
*
* The script only raises a flag. Spawning takes a slot from the actor heap,
* and pool exhaustion must be a C-side refusal with the house error context --
* so C consumes the flag and does the spawn. The engine also enforces the
* eight-shot cap by simply not consuming the flag's wish.
*/
static akerr_ErrorContext *enemy_fire(galaga_Enemy *enemy, akgl_Actor *from)
{
akgl_Actor *shot = NULL;
char name[32];
int count = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
FAIL_ZERO_RETURN(errctx, from, AKERR_NULLPOINTER, "from");
enemy->fire = 0;
if ( galaga_game.enemy_shots_live >= GALAGA_MAX_ENEMY_SHOTS ) {
SUCCEED_RETURN(errctx);
}
SHOT_SERIAL += 1;
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "eshot%u", SHOT_SERIAL));
PASS(errctx, akgl_heap_next_actor(&shot));
PASS(errctx, akgl_actor_initialize(shot, name));
PASS(errctx, akgl_actor_set_character(shot, "galaga_enemyshot"));
/* AFTER initialize: it resets all seven hooks. */
shot->updatefunc = &enemy_shot_update;
shot->movement_controls_face = false;
shot->state = AKGL_ACTOR_STATE_ALIVE;
/* akgl_actor_initialize() does not raise `visible`; a hand-spawned actor
* that skips this line exists, moves and collides -- invisibly. */
shot->visible = true;
/* Actor x/y is a sprite's top-left corner; the shot leaves the enemy's
* midline. Enemy sprites run 93..104 wide, the shot is 9. */
shot->x = from->x + 46.0f;
shot->y = from->y + 60.0f;
galaga_game.enemy_shots_live += 1;
galaga_game.shots[enemy->kind] += 1;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------- explosions --- */
static akerr_ErrorContext *boom_update(akgl_Actor *obj)
{
ptrdiff_t slot = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
slot = obj - akgl_heap_actors;
BOOM_TTL[slot] -= galaga_game.dt;
if ( BOOM_TTL[slot] <= 0.0f ) {
PASS(errctx, release_actor(obj));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_boom_spawn(float x, float y)
{
akgl_Actor *boom = NULL;
char name[32];
int count = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&boom));
BOOM_SERIAL += 1;
CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL));
CATCH(errctx, akgl_actor_initialize(boom, name));
CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom"));
boom->updatefunc = &boom_update;
boom->movement_controls_face = false;
boom->state = AKGL_ACTOR_STATE_ALIVE;
boom->visible = true;
boom->x = x;
boom->y = y;
BOOM_TTL[boom - akgl_heap_actors] = 0.25f;
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKGL_ERR_HEAP) {
/* Explosions are decoration. When the heap is momentarily full the
* right outcome is no explosion, not a dead frame -- this is the one
* spawn that absorbs exhaustion. */
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/* --------------------------------------------------------------- enemies --- */
/**
* @brief The custom update hook: one enemy, once per frame, thought in BASIC.
*
* The whole body is the protocol from docs/20: refresh the inbox, hand the
* pair to the script, consume the outbox. akgl_game_update() calls this in
* place of akgl_actor_update() because spawn replaced the hook.
*/
static akerr_ErrorContext *enemy_update(akgl_Actor *obj)
{
galaga_Enemy *enemy = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
enemy = (galaga_Enemy *)obj->actorData;
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached");
enemy->rnd = galaga_random();
PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt));
if ( enemy->fire != 0 ) {
PASS(errctx, enemy_fire(enemy, obj));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_wave_spawn(void)
{
akgl_Actor *actor = NULL;
galaga_Enemy *enemy = NULL;
char name[32];
int row = 0;
int col = 0;
int index = 0;
int count = 0;
PREPARE_ERROR(errctx);
for ( row = 0; row < WAVE_ROW_COUNT; row++ ) {
for ( col = 0; col < WAVE_ROWS[row].count; col++ ) {
FAIL_NONZERO_RETURN(errctx, (index >= GALAGA_MAX_ENEMIES), AKERR_OUTOFBOUNDS,
"The wave table places more than %d enemies", GALAGA_MAX_ENEMIES);
enemy = &galaga_enemies[index];
memset(enemy, 0, sizeof(*enemy));
enemy->kind = WAVE_ROWS[row].kind;
enemy->state = GALAGA_ES_ENTERING;
enemy->homex = (float)(GALAGA_FORM_LEFT
+ (WAVE_ROWS[row].first + col) * GALAGA_COL_PITCH);
enemy->homey = (float)(GALAGA_FORM_TOP + WAVE_ROWS[row].row * GALAGA_ROW_PITCH);
enemy->hp = WAVE_ROWS[row].hp;
/* Stagger the entries: each enemy's clock starts in the past, and
* the script holds still until its own t crosses zero. */
enemy->t = -0.08f * (float)index;
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "enemy%02d", index));
PASS(errctx, akgl_heap_next_actor(&actor));
PASS(errctx, akgl_actor_initialize(actor, name));
PASS(errctx, akgl_actor_set_character(actor, ENEMY_CHARACTER[enemy->kind]));
/* AFTER initialize: it resets all seven hooks. */
actor->updatefunc = &enemy_update;
actor->actorData = enemy;
/* Nothing here moves by state bits, and an actor whose state word
* matches no character mapping is silently not drawn -- so facing
* stays entirely out of the state word. */
actor->movement_controls_face = false;
actor->state = AKGL_ACTOR_STATE_ALIVE;
/* akgl_actor_initialize() does not raise `visible` -- the map
* loader copies it from map data, and there is no map here. Skip
* this and the whole wave exists, moves, fires and dies without
* ever being drawn. */
actor->visible = true;
/* Off screen above, pouring in from whichever side is closer. */
actor->x = (enemy->homex < (float)GALAGA_VIEW_WIDTH / 2.0f)
? -80.0f : (float)GALAGA_VIEW_WIDTH + 80.0f;
actor->y = -80.0f;
galaga_enemy_actors[index] = actor;
index += 1;
}
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_wave_release(void)
{
int i = 0;
PREPARE_ERROR(errctx);
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] != NULL ) {
PASS(errctx, release_actor(galaga_enemy_actors[i]));
galaga_enemy_actors[i] = NULL;
}
}
SUCCEED_RETURN(errctx);
}
int galaga_enemies_alive(void)
{
int i = 0;
int alive = 0;
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] != NULL ) {
alive += 1;
}
}
return alive;
}

119
examples/galaga/galaga.bas Normal file
View File

@@ -0,0 +1,119 @@
REM GALAGA enemy behavior. The C engine loads this file, runs it once so the
REM definitions exist, and then calls one UPDATE function per enemy per frame.
REM There is no top-level code: definitions, then END.
REM
REM Names the engine binds before every call:
REM SELF@ - this enemy's record (ENEMY): the state machine's memory
REM ACTOR@ - the engine's live actor (ACTOR): position is the real thing
REM GAME@ - shared frame state (GAME): player position, wave, randomness
REM
REM SELF@.STATE# bits: 1 = entering 2 = in formation 4 = diving
REM
REM Two rules of this dialect that bite here, both from docs/03:
REM - the LEFT operand decides integer or float arithmetic, so a float
REM always goes first: SELF@.T% * 150 + 260, never 260 + 150 * SELF@.T%
REM - RETURN at the start of a line ends the DEF body, so every early
REM return rides an IF ... THEN, and only the last RETURN starts a line
REM Ease toward the formation slot, with a little entry swirl.
REM Answers 1 once the slot is reached, else 0.
DEF GLIDEHOME(DT%)
DX% = SELF@.HOMEX% - ACTOR@.X%
DY% = SELF@.HOMEY% - ACTOR@.Y%
K% = DT% * 4.5
IF K% > 1 THEN K% = 1
ACTOR@.X% = ACTOR@.X% + DX% * K% + SIN(SELF@.T% * 6) * 90 * DT%
ACTOR@.Y% = ACTOR@.Y% + DY% * K%
IF ABS(DX%) < 3 AND ABS(DY%) < 3 THEN RETURN 1
RETURN 0
REM One frame of a dive: accelerate downward, weave, lean toward the
REM player's column, and glide back in from the top after falling out.
DEF DIVESTEP(DT%, WEAVE%, LEAD%)
SPD% = SELF@.T% * 150 + 260
ACTOR@.Y% = ACTOR@.Y% + SPD% * DT%
ACTOR@.X% = ACTOR@.X% + SIN(SELF@.T% * 4) * WEAVE% * DT%
DX% = GAME@.PLAYERX% - ACTOR@.X%
IF DX% > 220 THEN DX% = 220
IF DX% < -220 THEN DX% = -220
ACTOR@.X% = ACTOR@.X% + DX% * LEAD% * DT%
IF ACTOR@.Y% > 1040 THEN BEGIN
ACTOR@.Y% = 0.0 - 90
SELF@.STATE# = 1
SELF@.T% = 0
BEND
RETURN 0
REM Raise the fire flag when diving roughly above the player. The engine
REM consumes FIRE# and does the spawning; the script only wishes.
DEF DECIDEFIRE(DT%)
DX% = GAME@.PLAYERX% - ACTOR@.X%
IF ABS(DX%) > 140 THEN RETURN 0
IF ACTOR@.Y% > GAME@.PLAYERY% THEN RETURN 0
IF SELF@.RND% < DT% * 1.5 THEN SELF@.FIRE# = 1
RETURN 0
REM Bee: enter, breathe in formation, occasionally dive nearly straight.
DEF UPDATEBEE(DT%)
SELF@.T% = SELF@.T% + DT%
IF SELF@.T% < 0 THEN RETURN 0
S# = SELF@.STATE#
IF (S# AND 1) > 0 THEN BEGIN
R# = GLIDEHOME(DT%)
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
BEND
IF (S# AND 2) > 0 THEN BEGIN
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
ACTOR@.Y% = SELF@.HOMEY%
IF SELF@.RND% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
BEND
IF (S# AND 4) > 0 THEN BEGIN
R# = DIVESTEP(DT%, 130, 0.2)
R# = DECIDEFIRE(DT%)
BEND
RETURN 0
REM Butterfly: the same machine with a wide lateral weave on the dive.
DEF UPDATEBFLY(DT%)
SELF@.T% = SELF@.T% + DT%
IF SELF@.T% < 0 THEN RETURN 0
S# = SELF@.STATE#
IF (S# AND 1) > 0 THEN BEGIN
R# = GLIDEHOME(DT%)
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
BEND
IF (S# AND 2) > 0 THEN BEGIN
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 2.1) * 24
ACTOR@.Y% = SELF@.HOMEY%
IF SELF@.RND% < DT% * 0.05 THEN SELF@.STATE# = 4 : SELF@.T% = 0
BEND
IF (S# AND 4) > 0 THEN BEGIN
R# = DIVESTEP(DT%, 260, 0.1)
R# = DECIDEFIRE(DT%)
BEND
RETURN 0
REM Boss: two hit points, a slow sway, and a dive that leads the player.
REM At one hit point it raises actor state bit 13 (8192), and the engine's
REM character mapping swaps the sprite - the boundary crossed the other way.
DEF UPDATEBOSS(DT%)
SELF@.T% = SELF@.T% + DT%
IF SELF@.T% < 0 THEN RETURN 0
IF SELF@.HP# = 1 THEN ACTOR@.STATE# = ACTOR@.STATE# OR 8192
S# = SELF@.STATE#
IF (S# AND 1) > 0 THEN BEGIN
R# = GLIDEHOME(DT%)
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
BEND
IF (S# AND 2) > 0 THEN BEGIN
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.1) * 10
ACTOR@.Y% = SELF@.HOMEY%
IF SELF@.RND% < DT% * 0.03 THEN SELF@.STATE# = 4 : SELF@.T% = 0
BEND
IF (S# AND 4) > 0 THEN BEGIN
R# = DIVESTEP(DT%, 60, 0.9)
R# = DECIDEFIRE(DT%)
BEND
RETURN 0
END

155
examples/galaga/galaga.h Normal file
View File

@@ -0,0 +1,155 @@
/**
* @file galaga.h
* @brief Shared declarations for the GALAGA embedding example.
*
* The engine is C on libakgl; the enemies think in BASIC. Everything the two
* sides share crosses in exactly one place: the three structures below, which
* script.c registers as host types so a script reads and writes them directly.
* docs/20-tutorial-galaga.md and docs/21-tutorial-galaga-enemies.md build this
* program from an empty file; the split between files follows the split
* between chapters.
*/
#ifndef _GALAGA_H_
#define _GALAGA_H_
#include <stdbool.h>
#include <stdint.h>
#include <akerror.h>
#include <akgl/actor.h>
/* ------------------------------------------------------------- geometry --- */
/*
* The view is sized to the artwork rather than the other way round: the Kenney
* sprites are ~100 pixels wide, libakgl has no way to draw a sprite smaller
* than it is (akgl_Actor.scale is overwritten every frame -- libakgl
* docs/12-actors.md), and a ten-column formation of them needs 1120 pixels.
*/
#define GALAGA_VIEW_WIDTH 1280
#define GALAGA_VIEW_HEIGHT 960
#define GALAGA_FORM_COLUMNS 10 /* formation width, in slots */
#define GALAGA_FORM_LEFT 136 /* x of column 0, map pixels */
#define GALAGA_FORM_TOP 120 /* y of row 0, map pixels */
#define GALAGA_COL_PITCH 112
#define GALAGA_ROW_PITCH 100
#define GALAGA_PLAYER_Y 860.0f
#define GALAGA_PLAYER_SPEED 420.0f /* map pixels per second */
#define GALAGA_PLAYER_MARGIN 60.0f /* how close to the edge it may go */
/* ------------------------------------------------------------- entities --- */
#define GALAGA_ENEMY_BEE 0
#define GALAGA_ENEMY_BUTTERFLY 1
#define GALAGA_ENEMY_BOSS 2
#define GALAGA_ENEMY_KINDS 3
#define GALAGA_MAX_ENEMIES 40
#define GALAGA_MAX_PLAYER_SHOTS 2 /* the classic two-on-screen rule */
#define GALAGA_MAX_ENEMY_SHOTS 8
/*
* galaga_Enemy.state bits. The script owns these transitions; the engine only
* writes the word at spawn and when a script error forces an enemy dumb.
* galaga.bas spells the same three values as literals, with a REM naming them.
*
* 8 0
* 0 0 0 0 0 1 1 1
* | | `-- ENTERING: flying its entry path toward the formation slot
* | `---- FORMATION: holding (and breathing around) homex/homey
* `------ DIVING: attacking, off the grid until it leaves the screen
*/
#define GALAGA_ES_ENTERING (1 << 0)
#define GALAGA_ES_FORMATION (1 << 1)
#define GALAGA_ES_DIVING (1 << 2)
/** @brief One enemy, as both sides see it. Hangs off akgl_Actor.actorData. */
typedef struct galaga_Enemy
{
int32_t kind; /* GALAGA_ENEMY_BEE / BUTTERFLY / BOSS */
int32_t state; /* GALAGA_ES_* bit flags */
float homex; /* formation slot, in map pixels */
float homey;
float t; /* parametric clock for the current maneuver */
int32_t hp;
int32_t fire; /* outbox: script sets 1, engine consumes */
float rnd; /* inbox: engine writes fresh 0..1 each call */
} galaga_Enemy;
/** @brief Frame state every enemy may read. Bound once as GAME@. */
typedef struct galaga_Shared
{
float playerx; /* the player actor's position, this frame */
float playery;
int32_t wave;
float rnd; /* fresh 0..1 each frame; the issue #16 route */
} galaga_Shared;
/* --------------------------------------------------------------- screens --- */
typedef enum
{
GALAGA_SCREEN_TITLE = 0,
GALAGA_SCREEN_PLAY,
GALAGA_SCREEN_GAMEOVER,
GALAGA_SCREEN_VICTORY
} galaga_Screen;
/* ------------------------------------------------------------ game state --- */
typedef struct galaga_Game
{
galaga_Screen screen;
int frame;
float dt; /* seconds, clamped; see main.c */
bool autoplay;
int score;
int lives;
int kills[GALAGA_ENEMY_KINDS];
int shots[GALAGA_ENEMY_KINDS]; /* shots each kind fired */
int script_errors;
akgl_Actor *player;
float fire_cooldown;
float respawn_timer; /* > 0 while the player is invulnerable */
bool firing;
bool moveleft;
bool moveright;
int player_shots_live;
int enemy_shots_live;
} galaga_Game;
extern galaga_Game galaga_game;
extern galaga_Shared galaga_shared;
extern galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
extern akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
/* ---------------------------------------------------------------- script --- */
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_boot(char *path);
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt);
/* --------------------------------------------------------------- enemies --- */
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_spawn(void);
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_release(void);
int galaga_enemies_alive(void);
akerr_ErrorContext AKERR_NOIGNORE *galaga_boom_spawn(float x, float y);
/* ---------------------------------------------------------------- player --- */
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_spawn(void);
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_controls(void);
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_autoplay(int frame);
/** @brief A 0..1 random draw from the engine's own PRNG (see enemies.c). */
float galaga_random(void);
#endif // _GALAGA_H_

View File

@@ -0,0 +1,132 @@
/**
* @file interop_test.c
* @brief Round-trip test for the galaga boundary, hoststruct.c-style.
*
* Links the real script.c and the real galaga.bas -- not copies -- so this
* fails the moment the boundary and the script disagree. The four claims it
* pins:
*
* 1. The script writes the engine's actor memory: a formation enemy's sway
* lands in akgl_Actor.x with no marshalling step.
* 2. The outbox works: a diving enemy above the player raises FIRE# and the
* C side reads it.
* 3. The boss flips actor state bit 13 at one hit point -- the boundary
* crossed engine-ward.
* 4. Sustained calling holds: 24000 calls through the per-call
* akbasic_environment_zero() regime, the load a 40-enemy wave puts on
* the runtime in ten seconds.
*
* Exit status equals the number of failed claims.
*/
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akgl/actor.h>
#include "galaga.h"
#ifndef GALAGA_SCRIPT_PATH
#define GALAGA_SCRIPT_PATH "galaga.bas"
#endif
/* script.c reads these; main.c usually defines them. This test is the host. */
galaga_Game galaga_game;
galaga_Shared galaga_shared;
static int FAILURES = 0;
#define CLAIM(__cond, __text) \
if ( !(__cond) ) { \
fprintf(stderr, "FAILED: %s\n", __text); \
FAILURES += 1; \
} else { \
printf("ok: %s\n", __text); \
}
static akerr_ErrorContext *run_claims(void)
{
galaga_Enemy enemy;
akgl_Actor actor;
int i = 0;
PREPARE_ERROR(errctx);
PASS(errctx, galaga_script_boot((char *)GALAGA_SCRIPT_PATH));
/* --- 1: formation sway lands in the actor ---------------------------- */
memset(&enemy, 0, sizeof(enemy));
memset(&actor, 0, sizeof(actor));
enemy.kind = GALAGA_ENEMY_BEE;
enemy.state = GALAGA_ES_FORMATION;
enemy.homex = 400.0f;
enemy.homey = 300.0f;
enemy.hp = 1;
actor.x = 0.0f;
actor.y = 0.0f;
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
CLAIM((fabsf(actor.x - enemy.homex) <= 16.5f) && (actor.y == enemy.homey),
"a formation bee's sway is written into akgl_Actor.x/y by the script");
/* --- 2: the fire outbox ---------------------------------------------- */
memset(&enemy, 0, sizeof(enemy));
memset(&actor, 0, sizeof(actor));
enemy.kind = GALAGA_ENEMY_BEE;
enemy.state = GALAGA_ES_DIVING;
enemy.rnd = 0.0f; /* 0.0 < DT% * 1.5: always willing */
actor.x = 600.0f;
actor.y = 200.0f;
galaga_shared.playerx = 610.0f; /* just off the shot's column */
galaga_shared.playery = 860.0f; /* well below */
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
CLAIM(enemy.fire == 1,
"a diving bee above the player raises FIRE# for the engine to consume");
/* --- 3: the boss's hurt bit ------------------------------------------ */
memset(&enemy, 0, sizeof(enemy));
memset(&actor, 0, sizeof(actor));
enemy.kind = GALAGA_ENEMY_BOSS;
enemy.state = GALAGA_ES_FORMATION;
enemy.homex = 500.0f;
enemy.homey = 120.0f;
enemy.hp = 1;
actor.state = AKGL_ACTOR_STATE_ALIVE;
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
CLAIM((actor.state & AKGL_ACTOR_STATE_UNDEFINED_13) != 0,
"a boss at one hit point raises actor state bit 13 from BASIC");
/* --- 4: sustained calling --------------------------------------------- */
memset(&enemy, 0, sizeof(enemy));
memset(&actor, 0, sizeof(actor));
enemy.kind = GALAGA_ENEMY_BEE;
enemy.state = GALAGA_ES_FORMATION;
enemy.homex = 400.0f;
enemy.homey = 300.0f;
for ( i = 0; i < 24000; i++ ) {
enemy.rnd = 0.9f; /* never dive: keep the state put */
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
}
CLAIM(galaga_game.script_errors == 0,
"24000 calls survive the per-call akbasic_environment_zero() regime");
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, run_claims());
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "the interop test could not run");
FAILURES += 1;
} FINISH_NORETURN(errctx);
return FAILURES;
}

698
examples/galaga/main.c Normal file
View File

@@ -0,0 +1,698 @@
/**
* @file main.c
* @brief Startup, the frame loop, the screens, and teardown.
*
* The startup order is libakgl's one sequence that works (deps/libakgl
* include/akgl/game.h): metadata, akgl_game_init(), screen properties,
* akgl_render_2d_init(), a physics backend -- and then, new in this example,
* the interpreter. The scripts compute, the engine draws; the interpreter is
* lent no devices at all, so a script that tries SPRITE is refused by name.
*/
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/text.h>
#include <akgl/ui.h>
#include "galaga.h"
/** @brief Where the example's assets live. CMake defines it; `--assets` overrides. */
#ifndef GALAGA_ASSET_DIR
#define GALAGA_ASSET_DIR "."
#endif
/** @brief The enemy script. CMake defines it; `--script` overrides. */
#ifndef GALAGA_SCRIPT_PATH
#define GALAGA_SCRIPT_PATH "galaga.bas"
#endif
/** @brief The HUD font. CMake defines it; headless runs still need it for the UI. */
#ifndef GALAGA_FONT_PATH
#define GALAGA_FONT_PATH "font.ttf"
#endif
#define GALAGA_PATH_MAX 1024
galaga_Game galaga_game;
galaga_Shared galaga_shared;
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
static char *SHOTPATH = NULL;
static int SHOTFRAME = 0;
/** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main. */
static int FAILED = 0;
/* ------------------------------------------------------------- starfield --- */
/*
* No parallax facility exists in libakgl and none is needed: a fixed array of
* stars advanced per frame and drawn with akgl_draw_point() between
* frame_start and akgl_game_update(). Two speed bands give the depth for
* free -- the slow band reads as far away.
*/
#define GALAGA_STARS 96
static struct
{
float x;
float y;
float speed;
Uint8 bright;
} STARS[GALAGA_STARS];
static void starfield_seed(void)
{
int i = 0;
for ( i = 0; i < GALAGA_STARS; i++ ) {
STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH;
STARS[i].y = galaga_random() * (float)GALAGA_VIEW_HEIGHT;
if ( (i % 2) == 0 ) {
STARS[i].speed = 40.0f; /* the far band */
STARS[i].bright = 110;
} else {
STARS[i].speed = 110.0f; /* the near band */
STARS[i].bright = 220;
}
}
}
static akerr_ErrorContext *starfield_draw(void)
{
SDL_Color color = { 255, 255, 255, 255 };
int i = 0;
PREPARE_ERROR(errctx);
for ( i = 0; i < GALAGA_STARS; i++ ) {
STARS[i].y += STARS[i].speed * galaga_game.dt;
if ( STARS[i].y > (float)GALAGA_VIEW_HEIGHT ) {
STARS[i].y -= (float)GALAGA_VIEW_HEIGHT;
STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH;
}
color.r = STARS[i].bright;
color.g = STARS[i].bright;
color.b = STARS[i].bright;
PASS(errctx, akgl_draw_point(akgl_renderer, STARS[i].x, STARS[i].y, color));
}
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------ screenshots --- */
/**
* @brief Read the render target back and write it out as a PNG.
*
* Called after everything has drawn and before the frame is presented,
* because SDL_RenderPresent is where the target stops being readable. The
* figures in docs/20 and docs/21 are output from this program rather than
* pictures somebody took once, so they cannot show a game that no longer
* exists.
*/
static akerr_ErrorContext *save_screenshot(char *path)
{
SDL_Surface *shot = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());
ATTEMPT {
FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
"IMG_SavePNG(%s): %s", path, SDL_GetError());
} CLEANUP {
SDL_DestroySurface(shot);
} PROCESS(errctx) {
} FINISH(errctx, true);
SDL_Log("Wrote %s", path);
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- assets --- */
static char *SPRITE_FILES[] = {
"sprite_galaga_player.json",
"sprite_galaga_bee.json",
"sprite_galaga_butterfly.json",
"sprite_galaga_boss.json",
"sprite_galaga_boss_hurt.json",
"sprite_galaga_playershot.json",
"sprite_galaga_enemyshot.json",
"sprite_galaga_boom.json",
NULL
};
static char *CHARACTER_FILES[] = {
"character_galaga_player.json",
"character_galaga_bee.json",
"character_galaga_butterfly.json",
"character_galaga_boss.json",
"character_galaga_playershot.json",
"character_galaga_enemyshot.json",
"character_galaga_boom.json",
NULL
};
static akerr_ErrorContext *asset_path(char *dir, char *name, char *dest, size_t size)
{
int count = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "dir");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
PASS(errctx, aksl_snprintf(&count, dest, size, "%s/%s", dir, name));
SUCCEED_RETURN(errctx);
}
/**
* @brief Sprites first, characters second. Not a preference: a character's
* JSON names its sprites by registry name, so a character loaded first fails
* on the first sprite it cannot find.
*/
static akerr_ErrorContext *load_assets(char *assetdir)
{
char path[GALAGA_PATH_MAX];
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir");
for ( i = 0; SPRITE_FILES[i] != NULL; i++ ) {
PASS(errctx, asset_path(assetdir, SPRITE_FILES[i], (char *)&path, sizeof(path)));
PASS(errctx, akgl_sprite_load_json((char *)&path));
}
for ( i = 0; CHARACTER_FILES[i] != NULL; i++ ) {
PASS(errctx, asset_path(assetdir, CHARACTER_FILES[i], (char *)&path, sizeof(path)));
PASS(errctx, akgl_character_load_json((char *)&path));
}
SUCCEED_RETURN(errctx);
}
/* --------------------------------------------------------------- startup --- */
/** @brief Replacement for akgl_game.lowfpsfunc, which logs a line per frame. */
static void galaga_lowfps(void)
{
}
static akerr_ErrorContext *startup(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, aksl_strncpy((char *)&akgl_game.name, sizeof(akgl_game.name),
"akbasic galaga tutorial", sizeof(akgl_game.name) - 1));
PASS(errctx, aksl_strncpy((char *)&akgl_game.version, sizeof(akgl_game.version),
"1.0.0", sizeof(akgl_game.version) - 1));
PASS(errctx, aksl_strncpy((char *)&akgl_game.uri, sizeof(akgl_game.uri),
"net.aklabs.akbasic.galaga", sizeof(akgl_game.uri) - 1));
PASS(errctx, akgl_game_init());
akgl_game.lowfpsfunc = &galaga_lowfps;
/* Properties before the renderer: akgl_render_2d_init reads both, and an
* unset one defaults to the string "0" -- a zero-sized window. */
PASS(errctx, akgl_set_property("game.screenwidth", "1280"));
PASS(errctx, akgl_set_property("game.screenheight", "960"));
PASS(errctx, akgl_render_2d_init(akgl_renderer));
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderLogicalPresentation(
akgl_renderer->sdl_renderer,
GALAGA_VIEW_WIDTH,
GALAGA_VIEW_HEIGHT,
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE),
AKGL_ERR_SDL,
"%s",
SDL_GetError()
);
/* The view is what the camera looks through, so it says the same thing. */
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = (float)GALAGA_VIEW_WIDTH;
akgl_camera->h = (float)GALAGA_VIEW_HEIGHT;
/*
* akgl_game_init does NOT install a physics backend, whatever physics.h's
* file comment says (libakgl docs/14-physics.md). Null physics accepts
* every call and moves nothing: whatever writes x and y directly is the
* mover, and in this game that is BASIC writing through ACTOR@.
*/
PASS(errctx, akgl_physics_init_null(akgl_physics));
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- the UI --- */
static akgl_UiMenu TITLE_MENU = {
"titlemenu", { "START", "QUIT" }, 2, 0, false, NULL
};
static akgl_UiMenu AGAIN_MENU = {
"againmenu", { "PLAY AGAIN", "QUIT" }, 2, 0, false, NULL
};
/* Clay borrows label text until frame_end, so these cannot be locals. */
static char HUD_SCORE[64];
static char HUD_LIVES[64];
/**
* @brief Draw a headline centred above the menu, in the banner font.
*
* Direct text rather than a ui label: the menu owns AKGL_UI_ANCHOR_CENTER,
* and a label anchored there disappears behind it -- there is no
* top-centre anchor to reach for. Drawn before the UI bracket, so the menu
* still paints over it if the two ever meet.
*/
static akerr_ErrorContext *draw_banner(char *text)
{
SDL_Color ink = { 235, 235, 235, 255 };
TTF_Font *font = NULL;
int w = 0;
int h = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text");
font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "banner", NULL);
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "the banner font is not loaded");
PASS(errctx, akgl_text_measure(font, text, &w, &h));
PASS(errctx, akgl_text_rendertextat(font, text, ink, 0,
(GALAGA_VIEW_WIDTH - w) / 2, 280));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *declare_title(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_ui_menu(&TITLE_MENU));
SUCCEED_RETURN(errctx);
}
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);
}
static akerr_ErrorContext *declare_end(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_ui_menu(&AGAIN_MENU));
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------ transitions --- */
static akerr_ErrorContext *start_game(void)
{
PREPARE_ERROR(errctx);
galaga_game.score = 0;
galaga_game.lives = 3;
memset(galaga_game.kills, 0, sizeof(galaga_game.kills));
memset(galaga_game.shots, 0, sizeof(galaga_game.shots));
galaga_game.respawn_timer = 0.0f;
galaga_shared.wave = 1;
galaga_game.player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
PASS(errctx, galaga_wave_spawn());
galaga_game.screen = GALAGA_SCREEN_PLAY;
SUCCEED_RETURN(errctx);
}
/**
* @brief End-of-round bookkeeping: notice a cleared wave or a spent ship.
*/
static akerr_ErrorContext *check_transitions(bool *running)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
(void)running;
if ( galaga_game.screen != GALAGA_SCREEN_PLAY ) {
SUCCEED_RETURN(errctx);
}
if ( galaga_game.lives <= 0 ) {
PASS(errctx, galaga_wave_release());
galaga_game.screen = GALAGA_SCREEN_GAMEOVER;
SUCCEED_RETURN(errctx);
}
if ( galaga_enemies_alive() == 0 ) {
galaga_game.screen = GALAGA_SCREEN_VICTORY;
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Consume a menu activation. The menu never clears `activated`; the
* state machine that acts on it does.
*/
static akerr_ErrorContext *consume_menus(bool *running)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
if ( galaga_game.screen == GALAGA_SCREEN_TITLE && TITLE_MENU.activated ) {
TITLE_MENU.activated = false;
if ( TITLE_MENU.selected == 0 ) {
PASS(errctx, start_game());
} else {
*running = false;
}
}
if ( (galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|| galaga_game.screen == GALAGA_SCREEN_VICTORY)
&& AGAIN_MENU.activated ) {
AGAIN_MENU.activated = false;
if ( AGAIN_MENU.selected == 0 ) {
PASS(errctx, galaga_wave_release());
PASS(errctx, start_game());
} else {
*running = false;
}
}
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------- the frame --- */
/**
* @brief Route one event: the UI gets first refusal, then the menus, then
* the controller. A consumed event goes no further.
*/
static akerr_ErrorContext *route_event(SDL_Event *event, bool *running)
{
bool consumed = false;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
if ( event->type == SDL_EVENT_QUIT ) {
*running = false;
SUCCEED_RETURN(errctx);
}
PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
if ( consumed ) {
SUCCEED_RETURN(errctx);
}
if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) {
PASS(errctx, akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed));
} else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|| galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
PASS(errctx, akgl_ui_menu_handle_event(&AGAIN_MENU, event, &consumed));
}
if ( consumed ) {
SUCCEED_RETURN(errctx);
}
/* Every event, unconditionally: one that no control map binds is not an
* error, it is a call that did nothing. */
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, event));
SUCCEED_RETURN(errctx);
}
/** @brief The previous frame's timestamp, for dt. Stamped once at loop start. */
static uint64_t LAST_NS = 0;
static akerr_ErrorContext *frame(bool *running)
{
SDL_Event event;
uint64_t now = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
while ( SDL_PollEvent(&event) == true ) {
PASS(errctx, route_event(&event, running));
}
galaga_game.frame += 1;
if ( galaga_game.autoplay ) {
if ( galaga_game.screen == GALAGA_SCREEN_TITLE && galaga_game.frame >= 8 ) {
PASS(errctx, start_game());
}
/*
* On an end screen the pilot presses Return, which drives the real
* menu path -- declare, handle, activate, restart -- so a headless
* run that dies keeps exercising the game instead of idling.
*/
if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|| galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
if ( (galaga_game.frame % 30) == 0 ) {
SDL_Event press;
memset(&press, 0, sizeof(press));
press.type = SDL_EVENT_KEY_DOWN;
press.key.key = SDLK_RETURN;
PASS(errctx, route_event(&press, running));
}
}
PASS(errctx, galaga_player_autoplay(galaga_game.frame));
}
/*
* dt from the wall clock, clamped: a debugger pause or a stalled runner
* must not become one frame of teleporting enemies. The clamp is a 30 Hz
* frame, the slowest game this is still worth playing at.
*/
now = SDL_GetTicksNS();
galaga_game.dt = (float)(now - LAST_NS) / 1e9f;
LAST_NS = now;
if ( galaga_game.dt > (1.0f / 30.0f) ) {
galaga_game.dt = 1.0f / 30.0f;
}
/* The shared frame state, refreshed before any enemy thinks. The engine
* filling GAME@.RND% is the issue #16 route: no RND verb exists. */
galaga_shared.playerx = galaga_game.player->x + 50.0f;
galaga_shared.playery = galaga_game.player->y;
galaga_shared.rnd = galaga_random();
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
PASS(errctx, starfield_draw());
/*
* akgl_game_update is update-every-actor, step-the-physics, draw-the-
* world. Updating every actor is where the forty scripts run: each
* enemy's updatefunc is the hook in enemies.c, and that hook is a BASIC
* call. Held back on the menu screens so the world stands still there.
*/
if ( galaga_game.screen == GALAGA_SCREEN_PLAY ) {
PASS(errctx, akgl_game_update(NULL));
PASS(errctx, check_transitions(running));
}
/* The banner is direct text, drawn before the UI bracket. */
if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) {
PASS(errctx, draw_banner("GALAGA"));
} else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER ) {
PASS(errctx, draw_banner("GAME OVER"));
} else if ( galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
PASS(errctx, draw_banner("VICTORY"));
}
/* The UI bracket sits between akgl_game_update and frame_end, exactly as
* libakgl docs/22-ui.md draws it. */
PASS(errctx, akgl_ui_frame_begin());
switch ( galaga_game.screen ) {
case GALAGA_SCREEN_TITLE:
PASS(errctx, declare_title());
break;
case GALAGA_SCREEN_PLAY:
PASS(errctx, declare_play());
break;
case GALAGA_SCREEN_GAMEOVER:
case GALAGA_SCREEN_VICTORY:
PASS(errctx, declare_end());
break;
}
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
PASS(errctx, consume_menus(running));
if ( (SHOTPATH != NULL) && (galaga_game.frame == SHOTFRAME) ) {
PASS(errctx, save_screenshot(SHOTPATH));
}
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *run(int frames)
{
bool running = true;
PREPARE_ERROR(errctx);
LAST_NS = SDL_GetTicksNS();
while ( running == true ) {
PASS(errctx, frame(&running));
if ( (frames > 0) && (galaga_game.frame >= frames) ) {
running = false;
}
/* A crude frame limiter. A game on a real display should ask SDL for
* vsync; this one has to work under the dummy video driver, where
* there is nothing to sync to. */
SDL_Delay(16);
}
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- teardown --- */
/**
* @brief Give back what the process is holding.
*
* There is no akgl_game_shutdown; teardown is the application's. Fonts have
* to unload before TTF_Quit destroys them underneath the registry. IGNORE()
* on every call: a teardown failure must not mask whatever error is already
* being reported.
*/
static void shutdown_game(void)
{
int i = 0;
IGNORE(akgl_ui_shutdown());
IGNORE(akgl_text_unloadallfonts());
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( akgl_heap_actors[i].refcount > 0 ) {
IGNORE(akgl_heap_release_actor(&akgl_heap_actors[i]));
}
}
TTF_Quit();
SDL_Quit();
}
/* ------------------------------------------------------------------ args --- */
static akerr_ErrorContext *parse_args(int argc, char *argv[], char **assetdir,
char **script, int *frames)
{
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir");
FAIL_ZERO_RETURN(errctx, script, AKERR_NULLPOINTER, "script");
FAIL_ZERO_RETURN(errctx, frames, AKERR_NULLPOINTER, "frames");
for ( i = 1; i < argc; i++ ) {
if ( strcmp(argv[i], "--autoplay") == 0 ) {
galaga_game.autoplay = true;
} else if ( strcmp(argv[i], "--frames") == 0 ) {
i += 1;
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--frames needs a count");
PASS(errctx, aksl_atoi(argv[i], frames));
} else if ( strcmp(argv[i], "--assets") == 0 ) {
i += 1;
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--assets needs a directory");
*assetdir = argv[i];
} else if ( strcmp(argv[i], "--script") == 0 ) {
i += 1;
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--script needs a path");
*script = argv[i];
} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
i += 1;
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot needs a path");
SHOTPATH = argv[i];
} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
i += 1;
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE,
"--screenshot-frame needs a number");
PASS(errctx, aksl_atoi(argv[i], &SHOTFRAME));
} else {
FAIL_RETURN(
errctx,
AKERR_VALUE,
"usage: galaga [--assets DIR] [--script PATH] [--frames N]"
" [--autoplay] [--screenshot PATH] [--screenshot-frame N]"
);
}
}
SUCCEED_RETURN(errctx);
}
int main(int argc, char *argv[])
{
char *assetdir = GALAGA_ASSET_DIR;
char *script = GALAGA_SCRIPT_PATH;
int frames = 0;
uint16_t fontid = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, parse_args(argc, argv, &assetdir, &script, &frames));
CATCH(errctx, startup());
CATCH(errctx, load_assets(assetdir));
/* The engine refuses to start when the script will not boot: a game
* whose enemies cannot think is not a game missing a feature. */
CATCH(errctx, galaga_script_boot(script));
CATCH(errctx, akgl_ui_init(GALAGA_VIEW_WIDTH, GALAGA_VIEW_HEIGHT));
CATCH(errctx, akgl_text_loadfont("hud", GALAGA_FONT_PATH, 28));
CATCH(errctx, akgl_text_loadfont("banner", GALAGA_FONT_PATH, 84));
CATCH(errctx, akgl_ui_font_register("hud", &fontid));
CATCH(errctx, galaga_player_spawn());
CATCH(errctx, galaga_player_controls());
starfield_seed();
galaga_game.screen = GALAGA_SCREEN_TITLE;
galaga_game.lives = 3;
CATCH(errctx, run(frames));
} CLEANUP {
shutdown_game();
} 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_ERROR and leaks the context's pool slot. */
FAILED = 1;
/* FINISH_NORETURN rather than FINISH: FINISH expands a return that an
* int-returning function cannot compile. */
} FINISH_NORETURN(errctx);
/*
* The readout is the evidence: exiting 0 is not proof the wave flew. A
* headless CI log gets the same line a reader's terminal does, and the
* script-error count is the line's whole reason to exist -- a wave of
* dumb enemies still exits 0.
*/
SDL_Log(
"galaga: %d frames, screen %d, score %d, alive %d, kills bee %d bfly %d boss %d,"
" shots bee %d bfly %d boss %d, script errors %d",
galaga_game.frame,
(int)galaga_game.screen,
galaga_game.score,
galaga_enemies_alive(),
galaga_game.kills[GALAGA_ENEMY_BEE],
galaga_game.kills[GALAGA_ENEMY_BUTTERFLY],
galaga_game.kills[GALAGA_ENEMY_BOSS],
galaga_game.shots[GALAGA_ENEMY_BEE],
galaga_game.shots[GALAGA_ENEMY_BUTTERFLY],
galaga_game.shots[GALAGA_ENEMY_BOSS],
galaga_game.script_errors);
return FAILED;
}

407
examples/galaga/player.c Normal file
View File

@@ -0,0 +1,407 @@
/**
* @file player.c
* @brief The player's ship, its shots, and every collision in the game.
*
* Bullets and collision are C forever -- they are engine, not behavior. The
* per-frame budget for the script is spent on the forty things that think;
* nothing here thinks, it just moves and intersects.
*/
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/controller.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/util.h>
#include "galaga.h"
/* Points per kill, indexed by enemy kind. */
static const int KILL_SCORE[GALAGA_ENEMY_KINDS] = {
/* bee butterfly boss */
50, 80, 150
};
static uint32_t PSHOT_SERIAL = 0;
/* -------------------------------------------------------------- hitboxes --- */
/*
* Actor x/y is a sprite's top-left corner. Every box is inset from the
* artwork's rectangle, because the PNGs carry transparent margin and wing
* tips that should not kill anybody.
*/
static void player_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x + 12.0f;
dest->y = actor->y + 8.0f;
dest->w = 75.0f;
dest->h = 60.0f;
}
static void enemy_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x + 8.0f;
dest->y = actor->y + 8.0f;
dest->w = 78.0f;
dest->h = 68.0f;
}
static void shot_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x;
dest->y = actor->y;
dest->w = 9.0f;
dest->h = 54.0f;
}
/* ---------------------------------------------------------- player shots --- */
/**
* @brief Kill one enemy: score it, blow it up, free its slot.
*/
static akerr_ErrorContext *kill_enemy(int index)
{
akgl_Actor *actor = NULL;
galaga_Enemy *enemy = NULL;
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(errctx, (index < 0 || index >= GALAGA_MAX_ENEMIES),
AKERR_OUTOFBOUNDS, "enemy index %d", index);
actor = galaga_enemy_actors[index];
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "enemy %d is already gone", index);
enemy = &galaga_enemies[index];
galaga_game.score += KILL_SCORE[enemy->kind];
galaga_game.kills[enemy->kind] += 1;
PASS(errctx, galaga_boom_spawn(actor->x + 20.0f, actor->y + 20.0f));
PASS(errctx, akgl_heap_release_actor(actor));
galaga_enemy_actors[index] = NULL;
SUCCEED_RETURN(errctx);
}
/**
* @brief Move a player shot and test it against every live enemy.
*
* The classic O(shots x enemies) sweep: at most 2 x 40 rectangle tests a
* frame, which is noise. A hit costs the enemy a point of hp; the boss's
* second point is the script's business to survive, not this file's.
*/
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
{
SDL_FRect mine;
SDL_FRect theirs;
bool hit = false;
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
obj->y -= 900.0f * galaga_game.dt;
if ( obj->y < -60.0f ) {
galaga_game.player_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(obj));
SUCCEED_RETURN(errctx);
}
shot_box(obj, &mine);
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] == NULL ) {
continue;
}
enemy_box(galaga_enemy_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( !hit ) {
continue;
}
galaga_enemies[i].hp -= 1;
if ( galaga_enemies[i].hp <= 0 ) {
PASS(errctx, kill_enemy(i));
}
galaga_game.player_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(obj));
SUCCEED_RETURN(errctx);
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *player_fire(akgl_Actor *player)
{
akgl_Actor *shot = NULL;
char name[32];
int count = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
PSHOT_SERIAL += 1;
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "pshot%u", PSHOT_SERIAL));
PASS(errctx, akgl_heap_next_actor(&shot));
PASS(errctx, akgl_actor_initialize(shot, name));
PASS(errctx, akgl_actor_set_character(shot, "galaga_playershot"));
/* AFTER initialize: it resets all seven hooks. */
shot->updatefunc = &player_shot_update;
shot->movement_controls_face = false;
shot->state = AKGL_ACTOR_STATE_ALIVE;
shot->visible = true;
shot->x = player->x + 45.0f;
shot->y = player->y - 44.0f;
galaga_game.player_shots_live += 1;
galaga_game.fire_cooldown = 0.22f;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------- player hit --- */
static akerr_ErrorContext *player_hit(akgl_Actor *player)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
galaga_game.lives -= 1;
galaga_game.respawn_timer = 2.0f;
PASS(errctx, galaga_boom_spawn(player->x + 25.0f, player->y + 10.0f));
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
SUCCEED_RETURN(errctx);
}
/**
* @brief The player's own update hook: motion, fire, and what can kill it.
*/
static akerr_ErrorContext *player_update(akgl_Actor *obj)
{
SDL_FRect mine;
SDL_FRect theirs;
bool hit = false;
float dx = 0.0f;
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
dx = 0.0f;
if ( galaga_game.moveleft ) {
dx -= GALAGA_PLAYER_SPEED;
}
if ( galaga_game.moveright ) {
dx += GALAGA_PLAYER_SPEED;
}
obj->x += dx * galaga_game.dt;
if ( obj->x < GALAGA_PLAYER_MARGIN ) {
obj->x = GALAGA_PLAYER_MARGIN;
}
if ( obj->x > (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f ) {
obj->x = (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f;
}
galaga_game.fire_cooldown -= galaga_game.dt;
if ( galaga_game.firing
&& galaga_game.fire_cooldown <= 0.0f
&& galaga_game.player_shots_live < GALAGA_MAX_PLAYER_SHOTS
&& galaga_game.screen == GALAGA_SCREEN_PLAY ) {
PASS(errctx, player_fire(obj));
}
/*
* Respawn grace: two seconds of blinking invulnerability. The blink is
* the `visible` flag, which is deliberate hiding -- the actor still
* updates, it just is not drawn on the off frames.
*/
if ( galaga_game.respawn_timer > 0.0f ) {
galaga_game.respawn_timer -= galaga_game.dt;
obj->visible = ((galaga_game.frame / 6) % 2) == 0;
SUCCEED_RETURN(errctx);
}
obj->visible = true;
player_box(obj, &mine);
/* Enemy shots. */
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( akgl_heap_actors[i].refcount == 0 ) {
continue;
}
if ( strncmp(akgl_heap_actors[i].name, "eshot", 5) != 0 ) {
continue;
}
shot_box(&akgl_heap_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( hit ) {
galaga_game.enemy_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(&akgl_heap_actors[i]));
PASS(errctx, player_hit(obj));
SUCCEED_RETURN(errctx);
}
}
/* Diving enemies. The formation never reaches this low, so testing all
* forty is the same answer as testing the divers, without a state read. */
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] == NULL ) {
continue;
}
enemy_box(galaga_enemy_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( hit ) {
PASS(errctx, kill_enemy(i));
PASS(errctx, player_hit(obj));
SUCCEED_RETURN(errctx);
}
}
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- controls --- */
static akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveleft = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveleft = false;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *right_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveright = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *right_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveright = false;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *fire_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.firing = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *fire_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.firing = false;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_player_controls(void)
{
akgl_Control control;
PREPARE_ERROR(errctx);
memset(&control, 0, sizeof(control));
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.key = SDLK_LEFT;
control.handler_on = &left_on;
control.handler_off = &left_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
control.key = SDLK_RIGHT;
control.handler_on = &right_on;
control.handler_off = &right_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
control.key = SDLK_SPACE;
control.handler_on = &fire_on;
control.handler_off = &fire_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
akgl_controlmaps[0].target = galaga_game.player;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- spawn --- */
akerr_ErrorContext *galaga_player_spawn(void)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
PASS(errctx, akgl_heap_next_actor(&player));
PASS(errctx, akgl_actor_initialize(player, "player"));
PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
/* AFTER initialize: it resets all seven hooks. */
player->updatefunc = &player_update;
player->movement_controls_face = false;
player->state = AKGL_ACTOR_STATE_ALIVE;
player->visible = true;
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
player->y = GALAGA_PLAYER_Y;
galaga_game.player = player;
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- autoplay --- */
/**
* @brief Send one synthetic key event through the controller.
*
* Through akgl_controller_handle_event(), never the handlers directly: the
* point of autoplay is to exercise the same path a keyboard does.
*/
static akerr_ErrorContext *synth_key(SDL_Keycode key, bool down)
{
SDL_Event event;
PREPARE_ERROR(errctx);
memset(&event, 0, sizeof(event));
event.type = (down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP);
event.key.key = key;
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
SUCCEED_RETURN(errctx);
}
/**
* @brief The scripted pilot for headless runs: hold fire, sweep the floor.
*/
akerr_ErrorContext *galaga_player_autoplay(int frame)
{
int phase = 0;
PREPARE_ERROR(errctx);
/* Hold fire until the wave has mostly assembled: shooting the entry
* stream point-blank empties the formation before it exists, which makes
* both the game and its figure worse. */
if ( frame == 300 ) {
PASS(errctx, synth_key(SDLK_SPACE, true));
}
phase = frame % 240;
if ( phase == 30 ) {
PASS(errctx, synth_key(SDLK_LEFT, true));
} else if ( phase == 90 ) {
PASS(errctx, synth_key(SDLK_LEFT, false));
PASS(errctx, synth_key(SDLK_RIGHT, true));
} else if ( phase == 210 ) {
PASS(errctx, synth_key(SDLK_RIGHT, false));
}
SUCCEED_RETURN(errctx);
}

281
examples/galaga/script.c Normal file
View File

@@ -0,0 +1,281 @@
/**
* @file script.c
* @brief The boundary: everything that touches the interpreter lives here.
*
* One runtime, one script, three host types, three bindings. The engine calls
* exactly one thing per enemy per frame -- galaga_script_update_enemy() -- and
* that function is the whole protocol: rebind, call, recover, reset.
*
* The structure types are declared once, in C, right below. The script never
* declares a TYPE of its own; akbasic_host_register_type() makes these structs
* *be* the BASIC types, offsets taken from offsetof() so the two sides cannot
* drift (include/akbasic/host.h).
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akbasic/environment.h>
#include <akbasic/error.h>
#include <akbasic/host.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
#include "galaga.h"
/* The interpreter. Static because an akbasic_Runtime is far too big for a
* stack frame -- 2.40 MiB on this branch. */
static akbasic_Runtime SCRIPT;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
/** @brief Longest galaga.bas this loader will accept. */
#define GALAGA_MAX_SCRIPT_BYTES 16384
static char SOURCE[GALAGA_MAX_SCRIPT_BYTES];
/*
* Enemy kind -> BASIC function name. Dispatch is a table, not a conditional:
* adding a kind is one row here and one DEF in galaga.bas.
*/
static const char *UPDATE_FUNCTION[GALAGA_ENEMY_KINDS] = {
"UPDATEBEE", /* GALAGA_ENEMY_BEE */
"UPDATEBFLY", /* GALAGA_ENEMY_BUTTERFLY */
"UPDATEBOSS" /* GALAGA_ENEMY_BOSS */
};
/* ---------------------------------------------------------- host types --- */
static const akbasic_HostField ENEMY_FIELDS[] = {
/* struct member BASIC name C representation */
AKBASIC_HOST_FIELD( galaga_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, homex, "HOMEX%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Enemy, homey, "HOMEY%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Enemy, t, "T%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, fire, "FIRE#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType ENEMY_TYPE = {
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
};
/*
* The live actor itself. This is the demonstrative point of the whole
* example: the script writes the engine's *real* actor memory -- the same x
* the renderer reads -- with no copy in either direction.
*/
static const akbasic_HostField ACTOR_FIELDS[] = {
AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( akgl_Actor, y, "Y%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( akgl_Actor, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( akgl_Actor, visible, "VISIBLE#", AKBASIC_HOSTFIELD_BOOL )
};
static const akbasic_HostType ACTOR_TYPE = {
"ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 4
};
static const akbasic_HostField GAME_FIELDS[] = {
AKBASIC_HOST_FIELD( galaga_Shared, playerx, "PLAYERX%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Shared, playery, "PLAYERY%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Shared, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType GAME_TYPE = {
"GAME", sizeof(galaga_Shared), GAME_FIELDS, 4
};
/* Placeholders the boot bindings point at until the first real rebind. A
* binding is borrowed, never copied, so these must be static storage. */
static galaga_Enemy SCRATCH_ENEMY;
static akgl_Actor SCRATCH_ACTOR;
/* ---------------------------------------------------------------- boot --- */
static akerr_ErrorContext *read_script(char *path)
{
FILE *fp = NULL;
size_t got = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
fp = fopen(path, "rb");
FAIL_ZERO_RETURN(errctx, fp, AKERR_IO, "Cannot open the enemy script %s", path);
ATTEMPT {
got = fread(SOURCE, 1, sizeof(SOURCE) - 1, fp);
SOURCE[got] = '\0';
FAIL_NONZERO_BREAK(errctx, (got >= sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
"%s does not fit in the %d byte script buffer",
path, GALAGA_MAX_SCRIPT_BYTES);
} CLEANUP {
fclose(fp);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief One dry call of every dispatch-table function, at boot.
*
* A missing or misspelled DEF fails here, at startup, with the function's name
* in the message -- not on frame one of the first wave. The scratch enemy's
* state word is zero, so no maneuver block runs and nothing moves.
*/
static akerr_ErrorContext *dry_run(void)
{
akbasic_Value dt;
akbasic_Value *argp[1];
akbasic_Value *result = NULL;
int i = 0;
PREPARE_ERROR(errctx);
memset(&SCRATCH_ENEMY, 0, sizeof(SCRATCH_ENEMY));
memset(&dt, 0, sizeof(dt));
dt.valuetype = AKBASIC_TYPE_FLOAT;
dt.floatval = 0.0;
argp[0] = &dt;
for ( i = 0; i < GALAGA_ENEMY_KINDS; i++ ) {
PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", &SCRATCH_ENEMY));
PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", &SCRATCH_ACTOR));
PASS(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[i],
argp, 1, &result));
/*
* A body that died reports through the sink and answers zero; the
* dropped mode is the only signal C gets. At boot that must be fatal
* and must say which function -- not frame one of the first wave.
*/
FAIL_NONZERO_RETURN(errctx, (SCRIPT.mode != AKBASIC_MODE_RUN), AKERR_VALUE,
"%s died during the boot dry run; the interpreter's report"
" is above", UPDATE_FUNCTION[i]);
PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_script_boot(char *path)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
PASS(errctx, akbasic_error_register());
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
PASS(errctx, akbasic_runtime_init(&SCRIPT, &SINK));
PASS(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE));
PASS(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE));
PASS(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE));
PASS(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY));
PASS(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR));
PASS(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared));
PASS(errctx, read_script(path));
PASS(errctx, akbasic_runtime_load(&SCRIPT, SOURCE));
/*
* A "no top level code" script still has to run once: executing the DEF
* statements is what files the functions. The run is bounded because a
* script that is all definitions has no business taking more than a step
* per line, and an accidental loop at boot should be a diagnosis, not a
* hang.
*/
PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&SCRIPT, 4 * AKBASIC_MAX_SOURCE_LINES));
/*
* The program has now ended and the runtime sits in QUIT mode, where a
* multi-line DEF called from the host returns a silent zero. Forcing the
* mode back makes the bodies run, and it stays put because nothing here
* ever steps the runtime again. Issue #8 tracks making this unnecessary.
*/
PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
PASS(errctx, dry_run());
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------ per frame --- */
akerr_ErrorContext *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
{
akbasic_Value dtval;
akbasic_Value *argp[1];
akbasic_Value *result = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
FAIL_NONZERO_RETURN(errctx, (enemy->kind < 0 || enemy->kind >= GALAGA_ENEMY_KINDS),
AKERR_VALUE, "Enemy kind %d has no update function", enemy->kind);
PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy));
PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor));
memset(&dtval, 0, sizeof(dtval));
dtval.valuetype = AKBASIC_TYPE_FLOAT;
dtval.floatval = (double)dt;
argp[0] = &dtval;
/*
* An error in an enemy's function is that script's problem, not the
* engine's: the enemy goes dumb -- cleared to a formation hold it will
* never leave -- and the frame lives. HANDLE_DEFAULT absorbs whatever the
* interpreter raised; the first failure is logged with the function's
* name, the rest are counted, because sixty a second of the same message
* is how a log stops being read.
*/
ATTEMPT {
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[enemy->kind],
argp, 1, &result));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
if ( galaga_game.script_errors == 0 ) {
LOG_ERROR_WITH_MESSAGE(errctx, "first script error; this enemy is now dumb");
}
galaga_game.script_errors += 1;
enemy->state = GALAGA_ES_FORMATION;
enemy->fire = 0;
} FINISH(errctx, true);
/*
* A BASIC-level error in the body is quieter than an interpreter error:
* it reports through the sink, the call answers a stale value, and the
* runtime falls out of RUN mode -- after which every later call is a
* silent no-op. The mode is the tell. Revival is two calls:
* clear_error(), because a run's first error latches and every line is
* skipped while it stands, and the same set_mode(RUN) the boot needed
* (issue #8's mechanics). The enemy is treated exactly like the
* interpreter-error case above.
*/
if ( SCRIPT.mode != AKBASIC_MODE_RUN ) {
if ( galaga_game.script_errors == 0 ) {
SDL_Log("first script error (reported by the interpreter above);"
" enemy %s is now dumb", UPDATE_FUNCTION[enemy->kind]);
}
galaga_game.script_errors += 1;
enemy->state = GALAGA_ES_FORMATION;
enemy->fire = 0;
PASS(errctx, akbasic_runtime_clear_error(&SCRIPT));
PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
}
/*
* Load-bearing: akbasic_runtime_call_function() parks every result in the
* caller environment's per-line value scratch, and a host calling in a
* loop never crosses the line boundary that would reset it. Without this
* the pool drains in under two frames of a 40-enemy wave.
*/
PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
SUCCEED_RETURN(errctx);
}