282 lines
11 KiB
C
282 lines
11 KiB
C
|
|
/**
|
||
|
|
* @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);
|
||
|
|
}
|