/** * @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 #include #include #include #include #include #include #include #include #include #include #include #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); }