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);
|
||
|
|
}
|