Files
akbasic/src/runtime_sprite.c
Tachikoma a9059423e9 Say what was hit, which way is out, and how far
The narrowphase has been producing a contact since it went in, and the interpreter
was throwing it away. `RCOLLISION(n, f)` reports it: what was hit (a sprite or a
`SOLID` rectangle), which one, the contact normal, the penetration depth, the
contact point, and which axis to reverse.

**The normal points out of the other thing and toward this one**, so a program
moves along it by the depth and is exactly clear. That sign is the one assertion
in the new test that could not be caught any other way -- both parties of a
sprite-against-sprite hit get their own record, each pointing the way *that*
sprite has to move, and sharing one would tell both to go the same direction,
which is how two things end up stuck inside each other.

**Field 7 is the one that deletes the most BASIC.** It is the minimum translation
axis, computed from the normal in C, and it is there because doing it in BASIC
means comparing two floats -- which is exactly where this dialect's left-operand
rule catches people. `BALLBRICKS`/`TESTCELL` in the artwork breakout spend six
lines computing an overlap rectangle and comparing its width to its height to get
this number.

The record is **sticky and deepest-wins**: replaced whenever that sprite is in a
contact and otherwise left alone, so `BUMP` stays the event and this stays the
detail of it. Making it clear itself when nothing touches would break the pairing,
because `BUMP` accumulates across steps and a once-a-frame poll would find the
detail already gone. Reading `BUMP` clears both, so they cannot disagree.

Deliberately narrower than `akgl_Contact`: no actor pointers, because BASIC has no
actor; no tile fields, because there is no tilemap; no z, because every test is
planar; and **no `dt` and no `sensor`**, which libakgl documents as filled in by
the resolver. This interpreter never resolves anything, so those two come back
zero and mean nothing, and an always-zero field in a reference table is a lie.

Documented with the two caveats that matter: fields 2, 3 and 4 are floats and want
a `%` variable, and the contact *point* is exact only for two boxes -- libakgl's
solver returns a point on the portal it converged to, while the normal and depth
are exact for every pair.

Chapter 8's collision section stops claiming only type 1 exists, which has been
false since the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 10:53:26 -04:00

1039 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 <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;
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;
}
memset(&obj->sprite_state.solids[i], 0, sizeof(obj->sprite_state.solids[i]));
/* PASS rather than CATCH: this is a loop. */
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 ) {
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);
}