akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1050 lines
39 KiB
C
1050 lines
39 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 <akstdlib.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];
|
|
size_t prefixlen = 0;
|
|
int handle = 0;
|
|
int cmp = 0;
|
|
|
|
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");
|
|
|
|
PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen));
|
|
PASS(errctx, aksl_strncmp(value->stringval, SHAPE_PREFIX, prefixlen, &cmp));
|
|
if ( cmp == 0 ) {
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE,
|
|
"This sprite device cannot take a saved shape");
|
|
/*
|
|
* aksl_atoi refuses a slot number that is not one, where atoi(3) answered
|
|
* a silent 0 -- so `SPRSAV "SHAPE:", 1` is now told what is wrong with it
|
|
* rather than being handed shape 0.
|
|
*/
|
|
PASS(errctx, aksl_atoi(value->stringval + prefixlen, &handle));
|
|
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;
|
|
int source = AKBASIC_INTERRUPT_SPRITE;
|
|
|
|
(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);
|
|
/*
|
|
* Types 1 and 2 both exist now. A C128's type 2 is a sprite against the set
|
|
* pixels of the bitmap screen, which was refused here because reading the
|
|
* render target back every frame is not affordable -- but SOLID gives this
|
|
* interpreter a background made of rectangles instead of pixels, and that is
|
|
* the same question asked in a form it can answer. The same move SPRSAV made
|
|
* when it learned to take an image path.
|
|
*
|
|
* Type 3 stays refused. There is still no light pen.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (type == 1 || type == 2), AKBASIC_ERR_DEVICE,
|
|
"COLLISION type %d is not implemented: there is no light pen", type);
|
|
source = (type == 2 ? AKBASIC_INTERRUPT_BACKGROUND : AKBASIC_INTERRUPT_SPRITE);
|
|
|
|
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, source));
|
|
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, source,
|
|
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, source,
|
|
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;
|
|
uint16_t solidmask = 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));
|
|
/*
|
|
* Static geometry is a second question with a second accumulator and a
|
|
* second handler, answered from the same scan the device just ran. A backend
|
|
* that cannot do it withholds the entry point and reports nothing, the same
|
|
* way a runtime with no backend at all does.
|
|
*/
|
|
if ( obj->sprites->solids != NULL ) {
|
|
PASS(errctx, obj->sprites->solids(obj->sprites, &solidmask));
|
|
}
|
|
/*
|
|
* Pull the detail for whatever collided, while the scan that produced it is
|
|
* still the current one. RCOLLISION answers from here rather than from the
|
|
* device, because by the time a handler runs the sprites have moved on --
|
|
* akbasic_sprite_service() runs immediately before this.
|
|
*/
|
|
if ( (mask | solidmask) != 0 && obj->sprites->contact != NULL ) {
|
|
uint16_t both = (uint16_t)(mask | solidmask);
|
|
int i = 0;
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
bool found = false;
|
|
|
|
if ( (both & (uint16_t)(1u << i)) == 0 ) {
|
|
continue;
|
|
}
|
|
/* PASS rather than CATCH: this is a loop. */
|
|
PASS(errctx, obj->sprites->contact(obj->sprites, i + 1,
|
|
&obj->sprite_state.contacts[i], &found));
|
|
}
|
|
}
|
|
if ( solidmask != 0 ) {
|
|
obj->sprite_state.bumpedsolid |= solidmask;
|
|
PASS(errctx, akbasic_runtime_raise_interrupt(obj, AKBASIC_INTERRUPT_BACKGROUND));
|
|
}
|
|
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 || type == 2), AKBASIC_ERR_DEVICE,
|
|
"BUMP type %" PRId64 " is not implemented; sprite-to-sprite is 1 and static geometry is 2",
|
|
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.
|
|
*/
|
|
if ( type == 2 ) {
|
|
reported = obj->sprite_state.bumpedsolid;
|
|
obj->sprite_state.bumpedsolid = 0;
|
|
} else {
|
|
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);
|
|
}
|
|
|
|
/** @brief Take a scratch float value from the per-line pool for a function result. */
|
|
static akerr_ErrorContext *float_result(akbasic_Runtime *obj, double 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_FLOAT;
|
|
out->floatval = value;
|
|
*dest = out;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- SOLID -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Solid *solid = NULL;
|
|
double args[5];
|
|
int count = 0;
|
|
int id = 0;
|
|
int i = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SOLID");
|
|
PASS(errctx, require_sprites(obj, "SOLID"));
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->solid != NULL), AKBASIC_ERR_DEVICE,
|
|
"SOLID needs a sprite device that can carry static collision geometry, and this one cannot");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SOLID", args, 5, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count == 0 || count == 1 || count == 5), AKBASIC_ERR_SYNTAX,
|
|
"SOLID takes nothing, an id, or an id and four corners, not %d arguments",
|
|
count);
|
|
|
|
/*
|
|
* No arguments clears the lot, which is how `TRAP`, `COLLISION` and `DCLOSE`
|
|
* all read absence -- and is the verb a level change wants.
|
|
*/
|
|
if ( count == 0 ) {
|
|
for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
|
|
if ( !obj->sprite_state.solids[i].active ) {
|
|
continue;
|
|
}
|
|
/* PASS rather than CATCH: this is a loop. */
|
|
PASS(errctx, aksl_memset(&obj->sprite_state.solids[i], 0,
|
|
sizeof(obj->sprite_state.solids[i])));
|
|
PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
id = (int)args[0];
|
|
FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
|
|
"SOLID: %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
|
|
solid = &obj->sprite_state.solids[id - 1];
|
|
|
|
if ( count == 1 ) {
|
|
PASS(errctx, aksl_memset(solid, 0, sizeof(*solid)));
|
|
PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
FAIL_ZERO_RETURN(errctx, (args[3] > args[1] && args[4] > args[2]), AKBASIC_ERR_VALUE,
|
|
"SOLID: (%g, %g) to (%g, %g) is not a rectangle with a positive width and height",
|
|
args[1], args[2], args[3], args[4]);
|
|
solid->active = true;
|
|
solid->x1 = args[1];
|
|
solid->y1 = args[2];
|
|
solid->x2 = args[3];
|
|
solid->y2 = args[4];
|
|
PASS(errctx, obj->sprites->solid(obj->sprites, id, true,
|
|
solid->x1, solid->y1, solid->x2, solid->y2));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- SPRHIT -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sprhit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = NULL;
|
|
double args[6];
|
|
int count = 0;
|
|
int index = 0;
|
|
int kind = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in SPRHIT");
|
|
PASS(errctx, require_sprites(obj, "SPRHIT"));
|
|
FAIL_ZERO_RETURN(errctx, (obj->sprites->shape != NULL), AKBASIC_ERR_DEVICE,
|
|
"SPRHIT needs a sprite device that can carry a collision shape, and this one cannot");
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SPRHIT", args, 6, &count));
|
|
FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
|
|
"SPRHIT expected a sprite number and a shape kind");
|
|
FAIL_ZERO_RETURN(errctx, (count == 2 || count == 6), AKBASIC_ERR_SYNTAX,
|
|
"SPRHIT takes a kind alone or a kind and four corners, not %d arguments",
|
|
count);
|
|
PASS(errctx, sprite_index((int)args[0], "SPRHIT", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
kind = (int)args[1];
|
|
FAIL_ZERO_RETURN(errctx, (kind >= AKBASIC_SHAPE_NONE && kind < AKBASIC_SHAPE_LAST),
|
|
AKBASIC_ERR_BOUNDS,
|
|
"SPRHIT: shape kind %d is outside 0..%d", kind, AKBASIC_SHAPE_LAST - 1);
|
|
sprite->shapekind = kind;
|
|
|
|
/*
|
|
* Two arguments means "fit the frame", which is what a sprite loaded from a
|
|
* file needs: `SPRSAV "ship.png", 1` gives it the image's own size and the
|
|
* program never learns what that was. The device recomputes it from the
|
|
* picture, so the rectangle recorded here stays zero and RSPHIT reports what
|
|
* the device worked out rather than what was not said.
|
|
*/
|
|
sprite->shapeexplicit = (count == 6);
|
|
if ( count == 6 ) {
|
|
FAIL_ZERO_RETURN(errctx, (args[4] > args[2] && args[5] > args[3]), AKBASIC_ERR_VALUE,
|
|
"SPRHIT: (%g, %g) to (%g, %g) is not a rectangle with a positive width and height",
|
|
args[2], args[3], args[4], args[5]);
|
|
sprite->shapex1 = args[2];
|
|
sprite->shapey1 = args[3];
|
|
sprite->shapex2 = args[4];
|
|
sprite->shapey2 = args[5];
|
|
} else {
|
|
sprite->shapex1 = 0.0;
|
|
sprite->shapey1 = 0.0;
|
|
sprite->shapex2 = 0.0;
|
|
sprite->shapey2 = 0.0;
|
|
}
|
|
|
|
PASS(errctx, obj->sprites->shape(obj->sprites, index + 1, sprite->shapekind,
|
|
sprite->shapex1, sprite->shapey1,
|
|
sprite->shapex2, sprite->shapey2));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_fn_rsphit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Sprite *sprite = NULL;
|
|
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 RSPHIT");
|
|
PASS(errctx, nth_number(obj, expr, "RSPHIT", 0, &n));
|
|
PASS(errctx, nth_number(obj, expr, "RSPHIT", 1, &field));
|
|
PASS(errctx, sprite_index((int)n, "RSPHIT", &index));
|
|
sprite = &obj->sprite_state.sprites[index];
|
|
|
|
/*
|
|
* The five fields are SPRHIT's own arguments in SPRHIT's own order, which is
|
|
* the rule RSPRITE and RSPPOS already follow. No device is needed: this
|
|
* answers from interpreter state, because asking the device would be asking
|
|
* it to repeat what it was told.
|
|
*/
|
|
switch ( field ) {
|
|
case 0:
|
|
PASS(errctx, integer_result(obj, (int64_t)sprite->shapekind, dest));
|
|
break;
|
|
case 1:
|
|
PASS(errctx, float_result(obj, sprite->shapex1, dest));
|
|
break;
|
|
case 2:
|
|
PASS(errctx, float_result(obj, sprite->shapey1, dest));
|
|
break;
|
|
case 3:
|
|
PASS(errctx, float_result(obj, sprite->shapex2, dest));
|
|
break;
|
|
case 4:
|
|
PASS(errctx, float_result(obj, sprite->shapey2, dest));
|
|
break;
|
|
default:
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
|
|
"RSPHIT: field %" PRId64 " is outside 0..4", field);
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief One field of the contact sprite n is in.
|
|
*
|
|
* Sticky: the record is replaced whenever that sprite is in a contact and
|
|
* otherwise left alone, so `BUMP` remains the event -- "has anything hit me since
|
|
* I last looked" -- and this is the detail of it. Making it clear itself when
|
|
* nothing is touching would break the pairing, because `BUMP` accumulates across
|
|
* steps and a once-a-frame poll would find the detail already gone.
|
|
*
|
|
* Reading `BUMP` clears it, so the two stay in step.
|
|
*/
|
|
akerr_ErrorContext *akbasic_fn_rcollision(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Contact *contact = NULL;
|
|
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 RCOLLISION");
|
|
PASS(errctx, nth_number(obj, expr, "RCOLLISION", 0, &n));
|
|
PASS(errctx, nth_number(obj, expr, "RCOLLISION", 1, &field));
|
|
PASS(errctx, sprite_index((int)n, "RCOLLISION", &index));
|
|
contact = &obj->sprite_state.contacts[index];
|
|
|
|
switch ( field ) {
|
|
case 0:
|
|
PASS(errctx, integer_result(obj, (int64_t)contact->what, dest));
|
|
break;
|
|
case 1:
|
|
PASS(errctx, integer_result(obj, (int64_t)contact->other, dest));
|
|
break;
|
|
case 2:
|
|
PASS(errctx, float_result(obj, contact->nx, dest));
|
|
break;
|
|
case 3:
|
|
PASS(errctx, float_result(obj, contact->ny, dest));
|
|
break;
|
|
case 4:
|
|
PASS(errctx, float_result(obj, contact->depth, dest));
|
|
break;
|
|
case 5:
|
|
PASS(errctx, float_result(obj, contact->px, dest));
|
|
break;
|
|
case 6:
|
|
PASS(errctx, float_result(obj, contact->py, dest));
|
|
break;
|
|
case 7:
|
|
PASS(errctx, integer_result(obj, (int64_t)contact->axis, dest));
|
|
break;
|
|
default:
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
|
|
"RCOLLISION: field %" PRId64 " is outside 0..7", 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);
|
|
}
|