Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
756 lines
28 KiB
C
756 lines
28 KiB
C
/**
|
|
* @file runtime_sprite.c
|
|
* @brief The group H verb implementations: SPRITE, MOVSPR, SPRCOLOR, SPRSAV,
|
|
* COLLISION, and the BUMP / RSPPOS / RSPRITE / RSPCOLOR readbacks.
|
|
*
|
|
* Nothing here includes an SDL or a libakgl header. Every device call goes
|
|
* through the akbasic_SpriteBackend record the host attached, which is what lets
|
|
* the whole group be tested in a build with no SDL on the machine.
|
|
*
|
|
* These verbs are not in the Go reference -- it lists all of them as
|
|
* unimplemented -- so the semantics come from Commodore BASIC 7.0 rather than
|
|
* from a port. Where 7.0 does something a modern renderer cannot, or where the
|
|
* manual leaves a unit undefined, the decision is recorded in TODO.md section 5
|
|
* rather than presented as fidelity.
|
|
*
|
|
* The verbs split three ways, and the split is what makes them testable:
|
|
*
|
|
* - SPRCOLOR and the four readbacks touch only interpreter state and work with
|
|
* no device at all.
|
|
* - SPRITE and MOVSPR update interpreter state *and* tell the device. With no
|
|
* device they still update the state and then refuse, so a program that
|
|
* positions a sprite and asks RSPPOS where it is gets the right answer
|
|
* either way.
|
|
* - SPRSAV and COLLISION need the device outright.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/args.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "verbs.h"
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
do { \
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
} while ( 0 )
|
|
|
|
/** @brief What SSHAPE writes into a string variable, ahead of the slot number. */
|
|
#define SHAPE_PREFIX "SHAPE:"
|
|
|
|
/** @brief MOVSPR's four forms, as src/parser_commands.c encodes them. */
|
|
#define MOVSPR_ABSOLUTE 0
|
|
#define MOVSPR_RELATIVE 1
|
|
#define MOVSPR_POLAR 2
|
|
#define MOVSPR_CONTINUOUS 3
|
|
|
|
/**
|
|
* @brief Refuse politely when the host lent us no sprite device.
|
|
*
|
|
* Names the verb, because "no sprite device" on its own tells a program author
|
|
* nothing about which line to look at. The standalone stdio driver attaches no
|
|
* backend at all, so this is a common path rather than an edge case.
|
|
*/
|
|
static akerr_ErrorContext *require_sprites(akbasic_Runtime *obj, const char *verb)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in require_sprites");
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites != NULL), AKBASIC_ERR_DEVICE,
|
|
"%s needs a sprite device and this runtime has none", verb);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Check a sprite number and turn it into a slot index. */
|
|
static akerr_ErrorContext *sprite_index(int n, const char *verb, int *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
|
|
"%s: sprite %d is outside 1..%d", verb, n, AKBASIC_MAX_SPRITES);
|
|
*dest = n - 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Push one sprite's colour, expansion and priority at the device.
|
|
*
|
|
* One call rather than four, matching the backend record: SPRITE sets them
|
|
* together and a backend that has to rebuild a texture to change a colour should
|
|
* not do it four times for one statement.
|
|
*/
|
|
static akerr_ErrorContext *push_configuration(akbasic_Runtime *obj, int index)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = &obj->sprite_state.sprites[index];
|
|
akbasic_Color color;
|
|
|
|
if ( obj->sprites == NULL || obj->sprites->configure == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, akbasic_graphics_palette(sprite->colorindex, &color));
|
|
PASS(errctx, obj->sprites->configure(obj->sprites, index + 1, color,
|
|
sprite->xexpand, sprite->yexpand, sprite->behind));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- SPRITE -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sprite(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = NULL;
|
|
double args[7];
|
|
int count = 0;
|
|
int index = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SPRITE");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SPRITE", args, 7, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
|
|
"SPRITE expected a sprite number");
|
|
PASS(errctx, sprite_index((int)args[0], "SPRITE", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
/*
|
|
* Every argument after the number is optional and an omitted one leaves that
|
|
* attribute alone -- which is BASIC 7.0's rule and is what makes
|
|
* `SPRITE 1,1` and `SPRITE 1,,,,1` both useful.
|
|
*/
|
|
if ( count >= 2 ) {
|
|
sprite->enabled = (args[1] != 0.0);
|
|
}
|
|
if ( count >= 3 ) {
|
|
FAIL_ZERO_RETURN(errctx, (args[2] >= 1.0 && args[2] <= 16.0), AKBASIC_ERR_BOUNDS,
|
|
"SPRITE: colour %d is outside 1..16", (int)args[2]);
|
|
sprite->colorindex = (int)args[2];
|
|
}
|
|
if ( count >= 4 ) {
|
|
sprite->behind = (args[3] != 0.0);
|
|
}
|
|
if ( count >= 5 ) {
|
|
sprite->xexpand = (args[4] != 0.0);
|
|
}
|
|
if ( count >= 6 ) {
|
|
sprite->yexpand = (args[5] != 0.0);
|
|
}
|
|
if ( count >= 7 ) {
|
|
/*
|
|
* The multicolour bit is recorded and RSPRITE reads it back, but no
|
|
* pattern format here carries the second bit per pixel that multicolour
|
|
* selects with -- see TODO.md section 5. Recording it costs nothing and
|
|
* keeps the argument position honest for the day one does.
|
|
*/
|
|
sprite->multicolour = (args[6] != 0.0);
|
|
}
|
|
|
|
/*
|
|
* State first, device second, and the refusal last of all. A program with no
|
|
* sprite device still gets its state updated, so RSPRITE and RSPPOS answer
|
|
* correctly -- and it still gets told, by name, that SPRITE could not reach
|
|
* a device.
|
|
*/
|
|
PASS(errctx, require_sprites(obj, "SPRITE"));
|
|
PASS(errctx, push_configuration(obj, index));
|
|
if ( obj->sprites->show != NULL ) {
|
|
PASS(errctx, obj->sprites->show(obj->sprites, index + 1, sprite->enabled));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- MOVSPR -- */
|
|
|
|
/**
|
|
* @brief Put a sprite where the state says it is, if there is a device to tell.
|
|
*
|
|
* Shared by MOVSPR and by the continuous-motion service, which are the only two
|
|
* things that move a sprite.
|
|
*/
|
|
static akerr_ErrorContext *push_position(akbasic_Runtime *obj, int index)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = &obj->sprite_state.sprites[index];
|
|
|
|
if ( obj->sprites == NULL || obj->sprites->move == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, obj->sprites->move(obj->sprites, index + 1, sprite->x, sprite->y));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_movspr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = NULL;
|
|
double args[4];
|
|
int count = 0;
|
|
int index = 0;
|
|
int form = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in MOVSPR");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "MOVSPR", args, 4, &count));
|
|
/* form, n, a, b -- the parse handler guarantees all four or it raised. */
|
|
FAIL_ZERO_RETURN(errctx, (count == 4), AKBASIC_ERR_SYNTAX,
|
|
"MOVSPR expected a sprite number and two values");
|
|
form = (int)args[0];
|
|
PASS(errctx, sprite_index((int)args[1], "MOVSPR", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
switch ( form ) {
|
|
case MOVSPR_ABSOLUTE:
|
|
sprite->x = args[2];
|
|
sprite->y = args[3];
|
|
break;
|
|
case MOVSPR_RELATIVE:
|
|
sprite->x += args[2];
|
|
sprite->y += args[3];
|
|
break;
|
|
case MOVSPR_POLAR:
|
|
/*
|
|
* Distance first, then angle -- that order is BASIC 7.0's and it is the
|
|
* opposite of the continuous form's, which is a trap worth naming. The
|
|
* angle is degrees clockwise from vertical, so 0 is straight up and 90 is
|
|
* to the right; that is a compass bearing, not the mathematical
|
|
* convention, and it is why the sine and cosine look swapped.
|
|
*/
|
|
sprite->x += args[2] * sin(args[3] * M_PI / 180.0);
|
|
sprite->y -= args[2] * cos(args[3] * M_PI / 180.0);
|
|
break;
|
|
case MOVSPR_CONTINUOUS:
|
|
FAIL_ZERO_RETURN(errctx,
|
|
(args[3] >= 0.0 && args[3] <= (double)AKBASIC_SPRITE_MAX_SPEED),
|
|
AKBASIC_ERR_BOUNDS,
|
|
"MOVSPR: speed %d is outside 0..%d",
|
|
(int)args[3], AKBASIC_SPRITE_MAX_SPEED);
|
|
sprite->angle = args[2];
|
|
sprite->speed = (int)args[3];
|
|
/*
|
|
* Nothing moves here. The sprite starts moving on the next step, paced
|
|
* off the host's clock by akbasic_sprite_service() -- exactly the way the
|
|
* PLAY queue is, and for the same reason: this library owns no loop and
|
|
* must not block.
|
|
*/
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
default:
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX, "MOVSPR: unknown argument form %d", form);
|
|
}
|
|
|
|
PASS(errctx, require_sprites(obj, "MOVSPR"));
|
|
PASS(errctx, push_position(obj, index));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_sprite_service(akbasic_Runtime *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
double elapsed = 0.0;
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in sprite_service");
|
|
|
|
if ( obj->sprite_state.lastservicems == 0 ) {
|
|
obj->sprite_state.lastservicems = obj->timems;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
elapsed = (double)(obj->timems - obj->sprite_state.lastservicems) / 1000.0;
|
|
if ( elapsed <= 0.0 ) {
|
|
/*
|
|
* A host that never calls akbasic_runtime_settime() leaves the clock at
|
|
* zero, and a host that stepped twice inside one millisecond has not
|
|
* moved. Neither is an error and neither should move a sprite by a
|
|
* garbage amount; a clock that went backwards lands here too.
|
|
*/
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
obj->sprite_state.lastservicems = obj->timems;
|
|
|
|
/*
|
|
* A loop, so CATCH and the _BREAK macros are banned in here -- they expand to
|
|
* a C break and would leave the loop with an error still pending.
|
|
*/
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
akbasic_Sprite *sprite = &obj->sprite_state.sprites[i];
|
|
double distance = 0.0;
|
|
|
|
if ( sprite->speed <= 0 ) {
|
|
continue;
|
|
}
|
|
distance = (double)sprite->speed * AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND * elapsed;
|
|
sprite->x += distance * sin(sprite->angle * M_PI / 180.0);
|
|
sprite->y -= distance * cos(sprite->angle * M_PI / 180.0);
|
|
PASS(errctx, push_position(obj, i));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* -------------------------------------------------------------- SPRCOLOR -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sprcolor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Color c1;
|
|
akbasic_Color c2;
|
|
double args[2];
|
|
int count = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SPRCOLOR");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SPRCOLOR", args, 2, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
|
|
"SPRCOLOR expected at least one colour");
|
|
FAIL_ZERO_RETURN(errctx, (args[0] >= 1.0 && args[0] <= 16.0), AKBASIC_ERR_BOUNDS,
|
|
"SPRCOLOR: colour %d is outside 1..16", (int)args[0]);
|
|
obj->sprite_state.sharedcolor1 = (int)args[0];
|
|
if ( count >= 2 ) {
|
|
FAIL_ZERO_RETURN(errctx, (args[1] >= 1.0 && args[1] <= 16.0), AKBASIC_ERR_BOUNDS,
|
|
"SPRCOLOR: colour %d is outside 1..16", (int)args[1]);
|
|
obj->sprite_state.sharedcolor2 = (int)args[1];
|
|
}
|
|
|
|
/*
|
|
* No device required. The two registers are the program's state, RSPCOLOR
|
|
* reads them back, and a runtime with no sprite device is still entitled to
|
|
* remember what it was told -- the same rule GRAPHIC's mode follows.
|
|
*/
|
|
if ( obj->sprites != NULL && obj->sprites->shared_colors != NULL ) {
|
|
PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sharedcolor1, &c1));
|
|
PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sharedcolor2, &c2));
|
|
PASS(errctx, obj->sprites->shared_colors(obj->sprites, c1, c2));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- SPRSAV -- */
|
|
|
|
/**
|
|
* @brief Read a DIMmed integer array out into packed sprite pattern bytes.
|
|
*
|
|
* The array form is ours rather than BASIC 7.0's, and it exists because a value
|
|
* in this interpreter carries a NUL-terminated `char[256]`: a C128 puts the
|
|
* 63 raw bytes of a pattern in a string, and a string that cannot hold a zero
|
|
* byte cannot hold a sprite. An integer array can, and `DIM P#(63)` is exactly
|
|
* the right size -- see TODO.md section 5.
|
|
*/
|
|
static akerr_ErrorContext *pattern_from_array(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, uint8_t *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Variable *variable = NULL;
|
|
int64_t subscript[1];
|
|
int64_t length = 0;
|
|
int i = 0;
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, &variable));
|
|
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
|
|
"SPRSAV could not reach the array %s", arg->identifier);
|
|
length = (int64_t)variable->valuecount;
|
|
FAIL_ZERO_RETURN(errctx, (length >= AKBASIC_SPRITE_PATTERN_BYTES), AKBASIC_ERR_BOUNDS,
|
|
"SPRSAV: %s holds %" PRId64 " elements and a sprite pattern is %d",
|
|
arg->identifier, length, AKBASIC_SPRITE_PATTERN_BYTES);
|
|
|
|
for ( i = 0; i < AKBASIC_SPRITE_PATTERN_BYTES; i++ ) {
|
|
akbasic_Value *element = NULL;
|
|
|
|
subscript[0] = (int64_t)i;
|
|
PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &element));
|
|
FAIL_NONZERO_RETURN(errctx, (element->valuetype == AKBASIC_TYPE_STRING),
|
|
AKBASIC_ERR_TYPE,
|
|
"SPRSAV: %s(%d) is a string, and a pattern is bytes",
|
|
arg->identifier, i);
|
|
/*
|
|
* Masked rather than refused. A pattern byte is eight pixels and there
|
|
* are only eight to set, so anything outside 0..255 is a program that
|
|
* has miscounted -- but a DATA statement full of 256s should draw
|
|
* something recognisable rather than stop the program dead.
|
|
*/
|
|
dest[i] = (uint8_t)(((element->valuetype == AKBASIC_TYPE_FLOAT)
|
|
? (int64_t)element->floatval : element->intval) & 0xff);
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Install sprite @p index from whatever the first argument turned out to be.
|
|
*
|
|
* Three shapes of source, told apart by what the leaf *is* rather than by an
|
|
* argument position:
|
|
*
|
|
* - an unsubscripted integer-array identifier -- 63 pattern bytes
|
|
* - a string beginning "SHAPE:" -- a region SSHAPE saved
|
|
* - any other string -- a path to an image file
|
|
*/
|
|
static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, int index)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Value *value = NULL;
|
|
akbasic_Color fg;
|
|
akbasic_Color bg;
|
|
uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
|
|
|
|
if ( arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT &&
|
|
akbasic_leaf_first_subscript(arg) == NULL ) {
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->define != NULL), AKBASIC_ERR_DEVICE,
|
|
"This sprite device cannot be given a pattern");
|
|
PASS(errctx, pattern_from_array(obj, arg, pattern));
|
|
PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sprites[index].colorindex, &fg));
|
|
/*
|
|
* A clear bit is transparent, not black. On a C128 the background shows
|
|
* through a sprite's clear pixels; drawing them opaque would put a
|
|
* 24x21 black rectangle behind every sprite.
|
|
*/
|
|
bg.r = 0;
|
|
bg.g = 0;
|
|
bg.b = 0;
|
|
bg.a = 0;
|
|
PASS(errctx, obj->sprites->define(obj->sprites, index + 1, pattern,
|
|
AKBASIC_SPRITE_PATTERN_BYTES, fg, bg));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"SPRSAV expected a sprite number, an image path, a saved shape or an integer array");
|
|
|
|
if ( strncmp(value->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0 ) {
|
|
int handle = atoi(value->stringval + strlen(SHAPE_PREFIX));
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE,
|
|
"This sprite device cannot take a saved shape");
|
|
PASS(errctx, obj->sprites->define_shape(obj->sprites, index + 1, handle));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->define_file != NULL), AKBASIC_ERR_DEVICE,
|
|
"This sprite device cannot load an image file");
|
|
/*
|
|
* The running program's directory goes with the path. The backend tries the
|
|
* working directory first and this second, which is what makes a `.bas`
|
|
* stored beside its art work from anywhere -- and it is exactly how libakgl
|
|
* resolves a spritesheet named by a sprite document.
|
|
*/
|
|
PASS(errctx, obj->sprites->define_file(obj->sprites, index + 1, value->stringval,
|
|
(obj->sourcepath[0] != '\0'
|
|
? obj->sourcepath : NULL)));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sprsav(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *source = NULL;
|
|
akbasic_ASTLeaf *target = NULL;
|
|
akbasic_Value *value = NULL;
|
|
int index = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SPRSAV");
|
|
PASS(errctx, require_sprites(obj, "SPRSAV"));
|
|
|
|
source = akbasic_leaf_first_argument(expr);
|
|
target = (source != NULL ? source->next : NULL);
|
|
FAIL_ZERO_RETURN(errctx, (source != NULL && target != NULL), AKBASIC_ERR_SYNTAX,
|
|
"SPRSAV expected a source and a destination");
|
|
FAIL_NONZERO_RETURN(errctx, (target->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"SPRSAV takes exactly two arguments");
|
|
|
|
/*
|
|
* Only one direction. A C128's SPRSAV also copies a sprite *out* into a
|
|
* string, which here would mean writing an image file -- a disk operation,
|
|
* and group F is unimplemented as a whole. Refused by name rather than
|
|
* half-built; see TODO.md section 5.
|
|
*/
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, target, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_DEVICE,
|
|
"SPRSAV cannot copy a sprite out to a string or a file; that is a disk operation and this interpreter has none");
|
|
PASS(errctx, sprite_index((int)value->intval, "SPRSAV", &index));
|
|
|
|
PASS(errctx, define_from_leaf(obj, source, index));
|
|
obj->sprite_state.sprites[index].defined = true;
|
|
PASS(errctx, push_configuration(obj, index));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------- COLLISION -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_ASTLeaf *handler = NULL;
|
|
akbasic_Value *value = NULL;
|
|
int type = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in COLLISION");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
"COLLISION expected a collision type");
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"COLLISION expected a number");
|
|
type = (int)value->intval;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (type >= 1 && type <= 3), AKBASIC_ERR_BOUNDS,
|
|
"COLLISION: type %d is outside 1..3", type);
|
|
/*
|
|
* Type 1 only. Sprite-to-background would need the whole render target read
|
|
* back and compared against each sprite every frame, and a light pen has no
|
|
* meaning on a machine with no light pen; both are refused by name rather
|
|
* than silently accepted and never fired. TODO.md section 5.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (type == 1), AKBASIC_ERR_DEVICE,
|
|
"COLLISION type %d is not implemented: %s",
|
|
type,
|
|
(type == 2
|
|
? "sprite-to-background collision needs the screen read back every frame"
|
|
: "there is no light pen"));
|
|
|
|
handler = arg->next;
|
|
if ( handler == NULL ) {
|
|
/* No target disarms it, which is how a C128 turns a handler off. */
|
|
PASS(errctx, akbasic_runtime_disarm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_NONZERO_RETURN(errctx, (handler->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"COLLISION takes a type and at most one handler");
|
|
|
|
/*
|
|
* A label is taken by *name*, not by evaluating it. Evaluating would resolve
|
|
* it here and now, and a handler is normally written on a line the program
|
|
* has not reached yet -- so the name is what is stored and it is resolved
|
|
* when the interrupt fires. A line number is an ordinary expression and is
|
|
* evaluated, so `COLLISION 1, L#` works too.
|
|
*/
|
|
if ( handler->leaftype == AKBASIC_LEAF_IDENTIFIER &&
|
|
akbasic_leaf_first_subscript(handler) == NULL ) {
|
|
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE,
|
|
0, handler->identifier));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, handler, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"COLLISION expected a line number or a label");
|
|
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE,
|
|
value->intval, NULL));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_collision_service(akbasic_Runtime *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
uint16_t mask = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
"NULL runtime in collision_service");
|
|
if ( obj->sprites == NULL || obj->sprites->collisions == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, obj->sprites->collisions(obj->sprites, &mask));
|
|
if ( mask == 0 ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
/*
|
|
* Accumulated, not replaced. BUMP() answers "has this sprite hit anything
|
|
* since I last looked", so a collision that starts and ends between two
|
|
* polls is still news; BUMP clears what it reports.
|
|
*/
|
|
obj->sprite_state.bumped |= mask;
|
|
/*
|
|
* Raised unconditionally. An unarmed source records nothing, so there is
|
|
* nothing to ask before raising -- see akbasic_runtime_raise_interrupt.
|
|
*/
|
|
PASS(errctx, akbasic_runtime_raise_interrupt(obj, AKBASIC_INTERRUPT_SPRITE));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------- readbacks -- */
|
|
|
|
/**
|
|
* @brief Evaluate a function's nth argument.
|
|
*
|
|
* The dispatch table hands a function handler `lval` and `rval` as NULL and its
|
|
* whole argument list on the leaf, so every function in the interpreter digs its
|
|
* arguments out this way. src/runtime_functions.c has the same helper and keeps
|
|
* it static; a third copy would be the point at which it moved to args.h, and
|
|
* this is the second.
|
|
*/
|
|
static akerr_ErrorContext *nth_number(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int n, int64_t *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Value *value = NULL;
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
for ( i = 0; i < n && arg != NULL; i++ ) {
|
|
arg = arg->next;
|
|
}
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
"%s is missing argument %d", fname, n + 1);
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"%s expected a number in argument %d", fname, n + 1);
|
|
*dest = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
|
? (int64_t)value->floatval : value->intval;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Take a scratch integer value from the per-line pool for a function result. */
|
|
static akerr_ErrorContext *integer_result(akbasic_Runtime *obj, int64_t value, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Value *out = NULL;
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
|
PASS(errctx, akbasic_value_zero(out));
|
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
|
out->intval = value;
|
|
*dest = out;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_fn_bump(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
uint16_t reported = 0;
|
|
int64_t type = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in BUMP");
|
|
PASS(errctx, nth_number(obj, expr, "BUMP", 0, &type));
|
|
FAIL_ZERO_RETURN(errctx, (type == 1), AKBASIC_ERR_DEVICE,
|
|
"BUMP type %" PRId64 " is not implemented; only sprite-to-sprite collision is",
|
|
type);
|
|
|
|
/*
|
|
* Reading clears, which is what a C128 does and what makes the question
|
|
* answerable at all: without it a program that polls in a loop would see the
|
|
* same collision forever.
|
|
*/
|
|
reported = obj->sprite_state.bumped;
|
|
obj->sprite_state.bumped = 0;
|
|
PASS(errctx, integer_result(obj, (int64_t)reported, dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_fn_rsppos(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
akbasic_Sprite *sprite = NULL;
|
|
PREPARE_ERROR(errctx);
|
|
int64_t n = 0;
|
|
int64_t field = 0;
|
|
int index = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RSPPOS");
|
|
PASS(errctx, nth_number(obj, expr, "RSPPOS", 0, &n));
|
|
PASS(errctx, nth_number(obj, expr, "RSPPOS", 1, &field));
|
|
PASS(errctx, sprite_index((int)n, "RSPPOS", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
switch ( field ) {
|
|
case 0:
|
|
PASS(errctx, integer_result(obj, (int64_t)sprite->x, dest));
|
|
break;
|
|
case 1:
|
|
PASS(errctx, integer_result(obj, (int64_t)sprite->y, dest));
|
|
break;
|
|
case 2:
|
|
PASS(errctx, integer_result(obj, (int64_t)sprite->speed, dest));
|
|
break;
|
|
default:
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
|
|
"RSPPOS: field %" PRId64 " is outside 0..2 (x, y, speed)", field);
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_fn_rsprite(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
akbasic_Sprite *sprite = NULL;
|
|
PREPARE_ERROR(errctx);
|
|
int64_t n = 0;
|
|
int64_t field = 0;
|
|
int index = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RSPRITE");
|
|
PASS(errctx, nth_number(obj, expr, "RSPRITE", 0, &n));
|
|
PASS(errctx, nth_number(obj, expr, "RSPRITE", 1, &field));
|
|
PASS(errctx, sprite_index((int)n, "RSPRITE", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
/* The five fields are SPRITE's own arguments, in SPRITE's own order. */
|
|
switch ( field ) {
|
|
case 0:
|
|
PASS(errctx, integer_result(obj, (sprite->enabled ? 1 : 0), dest));
|
|
break;
|
|
case 1:
|
|
PASS(errctx, integer_result(obj, (int64_t)sprite->colorindex, dest));
|
|
break;
|
|
case 2:
|
|
PASS(errctx, integer_result(obj, (sprite->behind ? 1 : 0), dest));
|
|
break;
|
|
case 3:
|
|
PASS(errctx, integer_result(obj, (sprite->xexpand ? 1 : 0), dest));
|
|
break;
|
|
case 4:
|
|
PASS(errctx, integer_result(obj, (sprite->yexpand ? 1 : 0), dest));
|
|
break;
|
|
case 5:
|
|
PASS(errctx, integer_result(obj, (sprite->multicolour ? 1 : 0), dest));
|
|
break;
|
|
default:
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
|
|
"RSPRITE: field %" PRId64 " is outside 0..5", field);
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_fn_rspcolor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int64_t field = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RSPCOLOR");
|
|
PASS(errctx, nth_number(obj, expr, "RSPCOLOR", 0, &field));
|
|
FAIL_ZERO_RETURN(errctx, (field == 1 || field == 2), AKBASIC_ERR_BOUNDS,
|
|
"RSPCOLOR: register %" PRId64 " is 1 or 2", field);
|
|
PASS(errctx, integer_result(obj,
|
|
(field == 1
|
|
? (int64_t)obj->sprite_state.sharedcolor1
|
|
: (int64_t)obj->sprite_state.sharedcolor2),
|
|
dest));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|