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