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
1083 lines
41 KiB
C
1083 lines
41 KiB
C
/**
|
|
* @file sprite_akgl.c
|
|
* @brief Wires the sprite backend record to libakgl's actors.
|
|
*
|
|
* Each of BASIC's eight sprites becomes a full libakgl object graph -- a
|
|
* spritesheet holding one texture, a sprite cutting one frame from it, a
|
|
* character mapping state 0 to that sprite, and an actor instantiating the
|
|
* character -- so a host game sees BASIC's sprites in libakgl's actor registry
|
|
* alongside its own and can do anything to them it can do to an actor.
|
|
*
|
|
* **None of that comes from JSON.** akgl_sprite_load_json() is a thin wrapper
|
|
* over the same four initializers plus writes to public struct fields, and every
|
|
* field it fills from a document -- frame list, animation speed, loop flags,
|
|
* state-to-sprite map -- is something a Commodore sprite does not have. The one
|
|
* exception is the image, and that is exactly where this file *does* call
|
|
* libakgl: `SPRSAV "ship.png", 1` resolves through akgl_path_relative() and
|
|
* loads through akgl_spritesheet_initialize(), the same two calls
|
|
* akgl_sprite_load_json_spritesheet() makes.
|
|
*
|
|
* Every actor gets a renderfunc of its own rather than akgl_actor_render(). It
|
|
* was two reasons, both filed upstream, and **libakgl 0.5.0 fixed one of them**:
|
|
* the default used to compute its destination height from the sprite's *width*,
|
|
* drawing a 24x21 Commodore sprite as a 24x24 square, and it now uses the
|
|
* height. That was libakgl defect 26.
|
|
*
|
|
* The remaining reason is the one that still requires this file: an akgl_Actor
|
|
* carries a single scalar `scale` applied to both axes, which cannot express
|
|
* SPRITE's separate x- and y-expand bits. libakgl records that as an open item
|
|
* and names this interpreter as the caller that needs it; the shape it takes --
|
|
* scale_x/scale_y beside scale, or replacing scale outright -- is a design
|
|
* decision over there rather than a patch. Until it lands, installing a function
|
|
* pointer is libakgl's own extension point for exactly this, so nothing is
|
|
* forked.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
|
|
#include <SDL3_image/SDL_image.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/staticstring.h>
|
|
#include <akgl/util.h>
|
|
|
|
#include <akbasic/akgl.h>
|
|
#include <akbasic/error.h>
|
|
|
|
/** @brief The colour conversion, same as src/graphics_akgl.c's. */
|
|
static SDL_Color to_sdl(akbasic_Color color)
|
|
{
|
|
SDL_Color out;
|
|
|
|
out.r = color.r;
|
|
out.g = color.g;
|
|
out.b = color.b;
|
|
out.a = color.a;
|
|
return out;
|
|
}
|
|
|
|
/** @brief Recover the backend's own state, or say that it has none. */
|
|
static akerr_ErrorContext *state_of(akbasic_SpriteBackend *self, akbasic_AkglSprites **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in akgl sprite backend");
|
|
*dest = (akbasic_AkglSprites *)self->self;
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
|
"akgl sprite backend has no state");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Recover the state and turn a BASIC sprite number into a slot index. */
|
|
static akerr_ErrorContext *slot_of(akbasic_SpriteBackend *self, int n, akbasic_AkglSprites **state, int *index)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, state_of(self, state));
|
|
FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
|
|
"Sprite %d is outside 1..%d", n, AKBASIC_MAX_SPRITES);
|
|
*index = n - 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Draw one BASIC sprite, in place of akgl_actor_render().
|
|
*
|
|
* Differs from the default in exactly one way now, noted at the top of this file:
|
|
* the two expansion bits scale the axes independently, where an actor's single
|
|
* `scale` cannot. Taking the destination height from the sprite's height used to
|
|
* be the second difference and is the default's behaviour as of libakgl 0.5.0 --
|
|
* the line below is no longer a correction, only agreement.
|
|
*
|
|
* Like the default, it skips rather than reports: an actor with no image, one
|
|
* that is hidden, one whose slot has been reused. A frame is not the place to
|
|
* fail.
|
|
*/
|
|
static akerr_ErrorContext *spr_render_actor(akgl_Actor *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
akgl_Sprite *sprite = NULL;
|
|
SDL_FRect src;
|
|
SDL_FRect dest;
|
|
int i = 0;
|
|
int found = -1;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL actor in sprite render");
|
|
if ( !obj->visible || obj->basechar == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
state = (akbasic_AkglSprites *)obj->actorData;
|
|
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
|
"Sprite actor \"%s\" carries no backend state", obj->name);
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
if ( state->actors[i] == obj ) {
|
|
found = i;
|
|
break;
|
|
}
|
|
}
|
|
if ( found < 0 ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
sprite = state->sprites[found];
|
|
if ( sprite == NULL || sprite->sheet == NULL || sprite->sheet->texture == NULL ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akgl_spritesheet_coords_for_frame(sprite, &src, 0));
|
|
dest.x = obj->x - (akgl_camera != NULL ? akgl_camera->x : 0.0f);
|
|
dest.y = obj->y - (akgl_camera != NULL ? akgl_camera->y : 0.0f);
|
|
dest.w = (float32_t)sprite->width * (state->xexpand[found] ? 2.0f : 1.0f);
|
|
dest.h = (float32_t)sprite->height * (state->yexpand[found] ? 2.0f : 1.0f);
|
|
|
|
PASS(errctx, state->renderer->draw_texture(state->renderer, sprite->sheet->texture,
|
|
&src, &dest, 0, NULL, SDL_FLIP_NONE));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Tear down slot @p i, releasing its texture with it. */
|
|
static akerr_ErrorContext *release_slot(akbasic_AkglSprites *state, int i)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
/*
|
|
* Actor first, then character, sprite and sheet. Each borrows the one below
|
|
* it without taking a reference, so releasing in the other order leaves a
|
|
* live object pointing at a zeroed pool slot.
|
|
*/
|
|
if ( state->actors[i] != NULL ) {
|
|
PASS(errctx, akgl_heap_release_actor(state->actors[i]));
|
|
state->actors[i] = NULL;
|
|
}
|
|
if ( state->characters[i] != NULL ) {
|
|
PASS(errctx, akgl_heap_release_character(state->characters[i]));
|
|
state->characters[i] = NULL;
|
|
}
|
|
if ( state->sprites[i] != NULL ) {
|
|
PASS(errctx, akgl_heap_release_sprite(state->sprites[i]));
|
|
state->sprites[i] = NULL;
|
|
}
|
|
if ( state->sheets[i] != NULL ) {
|
|
/* This is what destroys the texture: see akgl_heap_release_spritesheet. */
|
|
PASS(errctx, akgl_heap_release_spritesheet(state->sheets[i]));
|
|
state->sheets[i] = NULL;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Build slot @p i's object graph around a sheet that is already loaded.
|
|
*
|
|
* The names are fixed 128-byte buffers, not string literals, because
|
|
* akgl_sprite_initialize() and akgl_actor_initialize() memcpy a whole
|
|
* AKGL_*_MAX_NAME_LENGTH from whatever they are handed.
|
|
*/
|
|
static akerr_ErrorContext *build_slot(akbasic_AkglSprites *state, int i, int width, int height)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
|
|
char charname[AKGL_SPRITE_MAX_NAME_LENGTH];
|
|
char actorname[AKGL_ACTOR_MAX_NAME_LENGTH];
|
|
float32_t oldx = 0.0f;
|
|
float32_t oldy = 0.0f;
|
|
bool oldvisible = false;
|
|
|
|
if ( state->actors[i] != NULL ) {
|
|
oldx = state->actors[i]->x;
|
|
oldy = state->actors[i]->y;
|
|
oldvisible = state->actors[i]->visible;
|
|
}
|
|
|
|
memset(spritename, 0, sizeof(spritename));
|
|
memset(charname, 0, sizeof(charname));
|
|
memset(actorname, 0, sizeof(actorname));
|
|
SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1);
|
|
SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1);
|
|
SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1);
|
|
|
|
PASS(errctx, akgl_heap_next_sprite(&state->sprites[i]));
|
|
PASS(errctx, akgl_sprite_initialize(state->sprites[i], spritename, state->sheets[i]));
|
|
state->sprites[i]->frames = 1;
|
|
state->sprites[i]->frameids[0] = 0;
|
|
state->sprites[i]->width = (uint32_t)width;
|
|
state->sprites[i]->height = (uint32_t)height;
|
|
|
|
PASS(errctx, akgl_heap_next_character(&state->characters[i]));
|
|
PASS(errctx, akgl_character_initialize(state->characters[i], charname));
|
|
PASS(errctx, akgl_character_sprite_add(state->characters[i], state->sprites[i], 0));
|
|
|
|
PASS(errctx, akgl_heap_next_actor(&state->actors[i]));
|
|
PASS(errctx, akgl_actor_initialize(state->actors[i], actorname));
|
|
PASS(errctx, akgl_actor_set_character(state->actors[i], charname));
|
|
state->actors[i]->state = 0;
|
|
state->actors[i]->actorData = state;
|
|
state->actors[i]->renderfunc = spr_render_actor;
|
|
/*
|
|
* Position and visibility survive a redefinition. SPRSAV changes what a
|
|
* sprite looks like, not where it is or whether it is on -- a program that
|
|
* animates by swapping patterns in a loop would otherwise have to re-issue
|
|
* SPRITE and MOVSPR after every frame.
|
|
*/
|
|
state->actors[i]->x = oldx;
|
|
state->actors[i]->y = oldy;
|
|
state->actors[i]->visible = oldvisible;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Take ownership of @p surface as slot @p i's image, replacing whatever was there. */
|
|
static akerr_ErrorContext *install_surface(akbasic_AkglSprites *state, int i, SDL_Surface *surface)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Texture *texture = NULL;
|
|
char sheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
|
|
int width = 0;
|
|
int height = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (surface != NULL), AKERR_NULLPOINTER, "NULL surface for a sprite");
|
|
width = surface->w;
|
|
height = surface->h;
|
|
texture = SDL_CreateTextureFromSurface(state->renderer->sdl_renderer, surface);
|
|
FAIL_ZERO_RETURN(errctx, (texture != NULL), AKGL_ERR_SDL,
|
|
"Could not upload a sprite pattern: %s", SDL_GetError());
|
|
/*
|
|
* Nearest-neighbour. A sprite is a 24x21 bitmap and an expanded one is drawn
|
|
* at twice the size; smoothing it would turn a pixel into a smudge, which is
|
|
* not what anybody typing SPRSAV into a BASIC has in mind.
|
|
*/
|
|
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
|
|
|
|
PASS(errctx, release_slot(state, i));
|
|
|
|
/*
|
|
* The sheet is assembled rather than initialized: akgl_spritesheet_initialize
|
|
* loads an image from a path, and this pattern came from bytes or from a
|
|
* saved region. Everything about it still matches what that function
|
|
* produces, so akgl_heap_release_spritesheet() tears it down the same way --
|
|
* which is what makes the texture's ownership one rule rather than two.
|
|
*/
|
|
PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i]));
|
|
memset(state->sheets[i], 0, sizeof(*state->sheets[i]));
|
|
memset(sheetname, 0, sizeof(sheetname));
|
|
SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1);
|
|
SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name));
|
|
state->sheets[i]->texture = texture;
|
|
state->sheets[i]->refcount = 1;
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, state->sheets[i]->name, state->sheets[i]);
|
|
|
|
PASS(errctx, build_slot(state, i, width, height));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Write one unpacked pattern into @p surface, a set bit in @p fg and a clear one in @p bg. */
|
|
static akerr_ErrorContext *paint_pattern(SDL_Surface *surface, const uint8_t *pixels, akbasic_Color fg, akbasic_Color bg)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int x = 0;
|
|
int y = 0;
|
|
|
|
for ( y = 0; y < AKBASIC_SPRITE_HEIGHT; y++ ) {
|
|
for ( x = 0; x < AKBASIC_SPRITE_WIDTH; x++ ) {
|
|
akbasic_Color c = (pixels[(y * AKBASIC_SPRITE_WIDTH) + x] != 0 ? fg : bg);
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
SDL_WriteSurfacePixel(surface, x, y, c.r, c.g, c.b, c.a),
|
|
AKGL_ERR_SDL,
|
|
"Could not write sprite pixel %d,%d: %s", x, y, SDL_GetError());
|
|
}
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
SDL_Surface *surface = NULL;
|
|
uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT];
|
|
int index = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
PASS(errctx, akbasic_sprite_unpack(pattern, bytes, pixels));
|
|
|
|
surface = SDL_CreateSurface(AKBASIC_SPRITE_WIDTH, AKBASIC_SPRITE_HEIGHT,
|
|
SDL_PIXELFORMAT_RGBA32);
|
|
FAIL_ZERO_RETURN(errctx, (surface != NULL), AKGL_ERR_SDL,
|
|
"Could not create a sprite surface: %s", SDL_GetError());
|
|
ATTEMPT {
|
|
/*
|
|
* The pixel loop is its own function so that it can be reached with a
|
|
* single CATCH. Written inline it would need a FAIL inside two nested
|
|
* loops inside this ATTEMPT, and the _BREAK forms expand to a C break
|
|
* that escapes only the inner loop.
|
|
*/
|
|
CATCH(errctx, paint_pattern(surface, pixels, fg, bg));
|
|
CATCH(errctx, install_surface(state, index, surface));
|
|
} CLEANUP {
|
|
SDL_DestroySurface(surface);
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_define_shape(akbasic_SpriteBackend *self, int n, int handle)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int index = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
FAIL_ZERO_RETURN(errctx, (state->graphics != NULL), AKBASIC_ERR_DEVICE,
|
|
"SPRSAV from a saved shape needs a graphics backend, and this host supplied none");
|
|
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->graphics->shapecount),
|
|
AKBASIC_ERR_VALUE,
|
|
"Shape handle %d names no saved region", handle);
|
|
FAIL_ZERO_RETURN(errctx, (state->graphics->shapes[handle] != NULL), AKBASIC_ERR_VALUE,
|
|
"Shape handle %d has been released", handle);
|
|
PASS(errctx, install_surface(state, index, state->graphics->shapes[handle]));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
akgl_String *resolved = NULL;
|
|
int index = 0;
|
|
bool missing = false;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
FAIL_ZERO_RETURN(errctx, (path != NULL && path[0] != '\0'), AKBASIC_ERR_VALUE,
|
|
"SPRSAV was given an empty file name");
|
|
|
|
PASS(errctx, akgl_heap_next_string(&resolved));
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_string_initialize(resolved, NULL));
|
|
/*
|
|
* The same resolution a sprite document gets for the spritesheet it
|
|
* names: the working directory first, then the directory the file doing
|
|
* the naming lives in -- here, the directory the BASIC program was
|
|
* loaded from. A program stored beside its art therefore works whether
|
|
* it was launched from its own directory or from anywhere else.
|
|
*/
|
|
CATCH(errctx, akgl_path_relative((char *)(root != NULL ? root : "."),
|
|
(char *)path, resolved));
|
|
CATCH(errctx, release_slot(state, index));
|
|
CATCH(errctx, akgl_heap_next_spritesheet(&state->sheets[index]));
|
|
/*
|
|
* And the same load. The two frame-size arguments are dead upstream --
|
|
* akgl_spritesheet_initialize does not write them -- and a Commodore
|
|
* sprite has no frame grid anyway, so the image is one whole frame.
|
|
*/
|
|
CATCH(errctx, akgl_spritesheet_initialize(state->sheets[index], 0, 0,
|
|
(char *)&resolved->data));
|
|
FAIL_ZERO_BREAK(errctx, (state->sheets[index]->texture != NULL), AKGL_ERR_SDL,
|
|
"Loaded %s but it produced no texture", path);
|
|
SDL_SetTextureScaleMode(state->sheets[index]->texture, SDL_SCALEMODE_NEAREST);
|
|
/*
|
|
* The image's own size, not 24x21. A Commodore sprite is 24x21 because
|
|
* that is what the hardware could address; there is no reason to throw
|
|
* away art that is not sprite-shaped on a machine with no such limit.
|
|
*/
|
|
CATCH(errctx, build_slot(state, index,
|
|
state->sheets[index]->texture->w,
|
|
state->sheets[index]->texture->h));
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(resolved));
|
|
} PROCESS(errctx) {
|
|
} HANDLE(errctx, ENOENT) {
|
|
/*
|
|
* Recorded rather than raised here. Returning from inside a HANDLE block
|
|
* leaves FINISH's bookkeeping undone, so the flag is set and the new
|
|
* error is raised below where an ordinary return is safe.
|
|
*/
|
|
missing = true;
|
|
} FINISH(errctx, true);
|
|
FAIL_NONZERO_RETURN(errctx, missing, AKBASIC_ERR_VALUE, "No such sprite image: %s", path);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_show(akbasic_SpriteBackend *self, int n, bool visible)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int index = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
if ( state->actors[index] != NULL ) {
|
|
state->actors[index]->visible = visible;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_move(akbasic_SpriteBackend *self, int n, double x, double y)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int index = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
if ( state->actors[index] != NULL ) {
|
|
state->actors[index]->x = (float32_t)x;
|
|
state->actors[index]->y = (float32_t)y;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
SDL_Color sdl = to_sdl(color);
|
|
int index = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &index));
|
|
state->xexpand[index] = xexpand;
|
|
state->yexpand[index] = yexpand;
|
|
/*
|
|
* Colour is a texture modulation rather than a repaint: a sprite defined
|
|
* from a pattern was drawn in the colour SPRSAV was given, and SPRITE's
|
|
* colour argument tints it. For a sprite loaded from an image that means a
|
|
* white image takes the colour and a coloured one is shaded by it, which is
|
|
* what texture modulation does everywhere else.
|
|
*/
|
|
if ( state->sheets[index] != NULL && state->sheets[index]->texture != NULL ) {
|
|
SDL_SetTextureColorMod(state->sheets[index]->texture, sdl.r, sdl.g, sdl.b);
|
|
}
|
|
/*
|
|
* Priority is not honoured. A C128 draws a low-priority sprite behind the
|
|
* bitmap screen; here the text layer and the drawing surface are the same
|
|
* render target and sprites are composited on top of it every frame, so
|
|
* there is nothing to go behind. Recorded in TODO.md section 5.
|
|
*/
|
|
(void)behind;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
/*
|
|
* Nothing to do yet, and saying so is better than pretending. Multicolour
|
|
* mode packs two bitmap bits per pixel and selects between the sprite's own
|
|
* colour and these two shared registers; SPRSAV here takes one bit per pixel,
|
|
* so there is no second bit to select with. The state is kept by the
|
|
* interpreter, RSPCOLOR reads it back, and the day a multicolour pattern
|
|
* format exists this is where it lands.
|
|
*/
|
|
(void)c1;
|
|
(void)c2;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Is slot @p i a thing that can be collided with at all?
|
|
*
|
|
* The three guards the pair loop used to repeat inline. A slot with no actor was
|
|
* never defined; a slot with no sprite has no frame and therefore no size; and an
|
|
* invisible sprite is out of the world entirely, which is the cheapest way a
|
|
* program has of switching one off without forgetting where it was.
|
|
*/
|
|
static bool slot_collidable(akbasic_AkglSprites *state, int i)
|
|
{
|
|
return (state->actors[i] != NULL
|
|
&& state->sprites[i] != NULL
|
|
&& state->actors[i]->visible
|
|
&& state->shapekind[i] != AKBASIC_SHAPE_NONE);
|
|
}
|
|
|
|
/**
|
|
* @brief Point slot @p i's proxy at where the sprite is and what size it is now.
|
|
*
|
|
* The shape is rebuilt every scan rather than cached against a dirty flag. It is
|
|
* derived from the sprite's frame and the two expansion bits, both of which a
|
|
* program may change at any statement, and building a box is a handful of
|
|
* arithmetic -- cheaper than being wrong about what invalidates it.
|
|
*
|
|
* **The mask override is the load-bearing line.** akgl_collision_shape_box()
|
|
* leaves a shape on the ACTOR layer responding to STATIC only, so that giving a
|
|
* town full of NPCs hitboxes does not make them shove each other around. That is
|
|
* the right default for a tile game and exactly wrong here: BASIC's collision is
|
|
* sprite-against-sprite first, and taking the library's default would make
|
|
* BUMP(1) report nothing at all, silently.
|
|
*/
|
|
/**
|
|
* @brief Record the shape SPRHIT asked for. The next scan builds it.
|
|
*
|
|
* Nothing is built here, and the reason is worth stating: a frame-fitting shape
|
|
* depends on the picture and the expansion bits, both of which may change after
|
|
* this call and before the next scan, so deriving it now would be deriving it
|
|
* from the wrong numbers. Zeroing the synced rectangle is what makes the next
|
|
* scan rebuild rather than recognise the position as unchanged and skip.
|
|
*/
|
|
static akerr_ErrorContext *spr_shape(akbasic_SpriteBackend *self, int n, int kind,
|
|
double x1, double y1, double x2, double y2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int i = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &i));
|
|
state->shapekind[i] = kind;
|
|
state->shapex1[i] = (float32_t)x1;
|
|
state->shapey1[i] = (float32_t)y1;
|
|
state->shapex2[i] = (float32_t)x2;
|
|
state->shapey2[i] = (float32_t)y2;
|
|
state->shapeexplicit[i] = (x2 > x1 && y2 > y1);
|
|
memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i]));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The rectangle slot @p i collides with, in device pixels.
|
|
*
|
|
* Either what SPRHIT was given, offset to where the sprite is, or the whole
|
|
* drawn frame -- which is what a sprite nobody has shaped has always collided
|
|
* with, and is the default this must keep reproducing exactly.
|
|
*/
|
|
static void collision_box(akbasic_AkglSprites *state, int i, SDL_FRect *dest)
|
|
{
|
|
dest->x = state->actors[i]->x;
|
|
dest->y = state->actors[i]->y;
|
|
if ( state->shapeexplicit[i] ) {
|
|
dest->x += state->shapex1[i];
|
|
dest->y += state->shapey1[i];
|
|
dest->w = state->shapex2[i] - state->shapex1[i];
|
|
dest->h = state->shapey2[i] - state->shapey1[i];
|
|
return;
|
|
}
|
|
dest->w = (float32_t)state->sprites[i]->width * (state->xexpand[i] ? 2.0f : 1.0f);
|
|
dest->h = (float32_t)state->sprites[i]->height * (state->yexpand[i] ? 2.0f : 1.0f);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *sync_proxy(akbasic_AkglSprites *state, int i,
|
|
SDL_FRect *box)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_FRect body;
|
|
|
|
/*
|
|
* **Nothing to do if nothing moved.** The scan runs at the top of every
|
|
* interpreter step -- up to 256 times a rendered frame -- and a sprite moves
|
|
* at most once in that time, so the overwhelming majority of syncs would
|
|
* rewrite a proxy with what it already holds. Four float comparisons to find
|
|
* that out are far cheaper than the shape build and the sync they skip.
|
|
*
|
|
* Compared against the last synced rectangle rather than tracked with a dirty
|
|
* flag a verb sets. A flag would be smaller and would be wrong: this file's
|
|
* own header says a host game sees BASIC's sprites in libakgl's actor
|
|
* registry "and can do anything to them it can do to an actor", which
|
|
* includes moving one behind the interpreter's back. Comparing the answer
|
|
* cannot miss that; flagging the question can.
|
|
*/
|
|
if ( state->syncedbox[i].x == box->x && state->syncedbox[i].y == box->y
|
|
&& state->syncedbox[i].w == box->w && state->syncedbox[i].h == box->h ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
body.x = 0.0f;
|
|
body.y = 0.0f;
|
|
body.w = box->w;
|
|
body.h = box->h;
|
|
/*
|
|
* A zero-sized frame would be refused by the shape builder, and a sprite can
|
|
* legitimately have one before its sheet is loaded. Treat it as not
|
|
* collidable rather than raising: the program has not done anything wrong.
|
|
*/
|
|
if ( body.w <= 0.0f || body.h <= 0.0f ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
switch ( state->shapekind[i] ) {
|
|
case AKBASIC_SHAPE_CIRCLE:
|
|
/*
|
|
* Inscribed, and in the *smaller* of the two half-extents. A circle that
|
|
* fitted the larger one would poke out of the rectangle the program
|
|
* asked for, which is the opposite of what "give this sprite a circle"
|
|
* means to somebody looking at a disc drawn inside a square frame.
|
|
*/
|
|
PASS(errctx, akgl_collision_shape_circle(&state->shapes[i],
|
|
body.w / 2.0f, body.h / 2.0f,
|
|
(body.w < body.h ? body.w : body.h) / 2.0f,
|
|
0.0f));
|
|
break;
|
|
case AKBASIC_SHAPE_CAPSULE_X:
|
|
case AKBASIC_SHAPE_CAPSULE_Y:
|
|
PASS(errctx, akgl_collision_shape_capsule(&state->shapes[i], &body,
|
|
(uint8_t)state->shapekind[i], 0.0f));
|
|
break;
|
|
default:
|
|
PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &body, 0.0f));
|
|
break;
|
|
}
|
|
state->shapes[i].collidemask = AKGL_COLLISION_LAYER_ACTOR | AKGL_COLLISION_LAYER_STATIC;
|
|
PASS(errctx, akgl_collision_proxy_sync(state->proxies[i], &state->shapes[i],
|
|
box->x, box->y, 0.0f));
|
|
state->syncedbox[i] = *box;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Keep @p contact for slot @p i if it is deeper than what is there.
|
|
*
|
|
* A sprite may be in several overlaps at once -- a ball touching two bricks at a
|
|
* corner -- and only one can be reported. The deepest is the one that has to be
|
|
* undone, so it is the one worth keeping.
|
|
*
|
|
* The normal comes out of libakgl pointing out of the *other* shape and toward
|
|
* this one, which is the direction a program wants: move along it by the depth
|
|
* and you are clear.
|
|
*/
|
|
static void record_contact(akbasic_AkglSprites *state, int i, int what, int other,
|
|
akgl_Contact *contact)
|
|
{
|
|
akbasic_Contact *dest = &state->contacts[i];
|
|
|
|
if ( state->hascontact[i] && dest->depth >= (double)contact->depth ) {
|
|
return;
|
|
}
|
|
dest->what = what;
|
|
dest->other = other;
|
|
dest->nx = (double)contact->nx;
|
|
dest->ny = (double)contact->ny;
|
|
dest->depth = (double)contact->depth;
|
|
dest->px = (double)contact->px;
|
|
dest->py = (double)contact->py;
|
|
/*
|
|
* The minimum translation axis, from the normal rather than from an overlap
|
|
* rectangle: whichever component is larger is the axis the shapes are least
|
|
* far through, and therefore the one to reverse. Computed here because doing
|
|
* it in BASIC means comparing two floats, which is exactly where this
|
|
* dialect's left-operand rule bites.
|
|
*/
|
|
dest->axis = ((contact->nx < 0.0f ? -contact->nx : contact->nx)
|
|
>= (contact->ny < 0.0f ? -contact->ny : contact->ny)) ? 1 : 2;
|
|
state->hascontact[i] = true;
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_contact(akbasic_SpriteBackend *self, int n,
|
|
akbasic_Contact *dest, bool *found)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int i = 0;
|
|
|
|
PASS(errctx, slot_of(self, n, &state, &i));
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL && found != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in contact");
|
|
*found = state->hascontact[i];
|
|
if ( *found ) {
|
|
*dest = state->contacts[i];
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Run the scan if anything has changed, and leave both masks on the state.
|
|
*
|
|
* One worker behind two entry points, because both masks come out of the same
|
|
* pass and computing them separately would be computing the sprite boxes twice
|
|
* for one answer. `spr_collisions()` reports the sprite-against-sprite mask and
|
|
* `spr_solids()` the sprite-against-static one; either may be called first, and
|
|
* the cache below is what makes that true rather than merely usually true.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Contact contact;
|
|
SDL_FRect box[AKBASIC_MAX_SPRITES];
|
|
bool live[AKBASIC_MAX_SPRITES];
|
|
bool synced[AKBASIC_MAX_SPRITES];
|
|
bool hit = false;
|
|
int i = 0;
|
|
int j = 0;
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
live[i] = slot_collidable(state, i);
|
|
synced[i] = false;
|
|
memset(&box[i], 0, sizeof(box[i]));
|
|
if ( !live[i] ) {
|
|
continue;
|
|
}
|
|
collision_box(state, i, &box[i]);
|
|
}
|
|
|
|
/*
|
|
* **Nothing moved, so nothing can have changed.**
|
|
*
|
|
* The scan's inputs are exactly these boxes and which slots are collidable;
|
|
* static geometry invalidates the cache where it is registered. So if all
|
|
* eight match what the last scan saw, the answer is the answer it gave, and
|
|
* eight rectangle comparisons are far cheaper than recomputing it.
|
|
*
|
|
* This is what makes static geometry affordable. The scan runs at the top of
|
|
* every interpreter step -- up to 256 times a rendered frame -- and eight
|
|
* sprites against sixty-four rectangles is five hundred and twelve tests.
|
|
* That is fine once a frame and ruinous 256 times, and a sprite moves at
|
|
* most once in that window.
|
|
*
|
|
* It changes nothing a program can observe: the same mask comes back, so
|
|
* BUMP accumulates the same bits and the interrupt is raised on the same
|
|
* steps.
|
|
*/
|
|
if ( state->lastvalid ) {
|
|
bool same = true;
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
if ( live[i] != state->lastlive[i]
|
|
|| box[i].x != state->lastbox[i].x || box[i].y != state->lastbox[i].y
|
|
|| box[i].w != state->lastbox[i].w || box[i].h != state->lastbox[i].h ) {
|
|
same = false;
|
|
break;
|
|
}
|
|
}
|
|
if ( same ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
}
|
|
state->lastmask = 0;
|
|
state->lastsolidmask = 0;
|
|
memset(state->hascontact, 0, sizeof(state->hascontact));
|
|
|
|
/*
|
|
* **Two phases, and the split is the whole performance story.**
|
|
*
|
|
* The obvious implementation -- sync all eight proxies, then run
|
|
* akgl_collision_test() on all twenty-eight pairs -- was measured at 984 ns a
|
|
* scan against 96 ns for the four-comparison loop it replaced. At 256 scans a
|
|
* frame that is 21% of a frame, and it bought nothing: the mask came back
|
|
* bit-identical and the contact was thrown away.
|
|
*
|
|
* So: reject on the bounding boxes first, in the four comparisons that were
|
|
* always here, and pay for the narrowphase only on a pair that survives.
|
|
* Nearly every scan of a real game rejects all twenty-eight, and a pair that
|
|
* does overlap is worth an exact answer. This is not a workaround for a slow
|
|
* library -- it is what a broad phase *is*, and akgl_collision_test() does the
|
|
* same thing internally with the proxies' own bounds before it commits to a
|
|
* solver.
|
|
*
|
|
* **The narrowphase still decides.** The box test only says "maybe"; the bit
|
|
* is set by what the library answers. That matters for a shape that is not a
|
|
* box -- a circle inscribed in a frame overlaps a smaller region than the
|
|
* frame does -- so the prefilter can over-report and must never be the final
|
|
* word. Today every shape is the whole frame and the two always agree, which
|
|
* is exactly why this is the moment to get the ordering right rather than
|
|
* the moment it starts to matter.
|
|
*
|
|
* AKGL_COLLISION_TEST_PLANAR because a Commodore sprite has no z. Without it
|
|
* the extrusion the shape builder chose takes part in the test and the
|
|
* minimum axis can come back as a depth along z, which no BASIC program can
|
|
* act on.
|
|
*
|
|
* Still box against box, not pixel against pixel: a C128's VIC-II collides on
|
|
* set pixels, so two sprites whose boxes overlap but whose art does not are
|
|
* reported as colliding here and would not be there. TODO.md section 5.
|
|
*/
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
if ( !live[i] ) {
|
|
continue;
|
|
}
|
|
for ( j = i + 1; j < AKBASIC_MAX_SPRITES; j++ ) {
|
|
if ( !live[j] ) {
|
|
continue;
|
|
}
|
|
if ( !(box[i].x < box[j].x + box[j].w && box[j].x < box[i].x + box[i].w
|
|
&& box[i].y < box[j].y + box[j].h && box[j].y < box[i].y + box[i].h) ) {
|
|
continue;
|
|
}
|
|
/*
|
|
* PASS rather than CATCH throughout: this is a loop, and CATCH
|
|
* expands to a break, which would leave the remaining pairs untested
|
|
* and the mask half built.
|
|
*/
|
|
if ( !synced[i] ) {
|
|
PASS(errctx, sync_proxy(state, i, &box[i]));
|
|
synced[i] = true;
|
|
}
|
|
if ( !synced[j] ) {
|
|
PASS(errctx, sync_proxy(state, j, &box[j]));
|
|
synced[j] = true;
|
|
}
|
|
hit = false;
|
|
PASS(errctx, akgl_collision_test(state->proxies[i], state->proxies[j],
|
|
AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
if ( hit ) {
|
|
state->lastmask |= (uint16_t)(1u << i);
|
|
state->lastmask |= (uint16_t)(1u << j);
|
|
record_contact(state, i, 1, j + 1, &contact);
|
|
/*
|
|
* And the mirror for the other sprite. The normal points out of
|
|
* b toward a, so b's own copy has to be negated or both sprites
|
|
* would be told to move the same way.
|
|
*/
|
|
contact.nx = -contact.nx;
|
|
contact.ny = -contact.ny;
|
|
record_contact(state, j, 1, i + 1, &contact);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Every live sprite against every registered rectangle, same shape: reject
|
|
* on the boxes, and only then ask the library. A wall is motionless, so its
|
|
* proxy was synced when SOLID registered it and there is nothing to refresh
|
|
* here.
|
|
*/
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
if ( !live[i] ) {
|
|
continue;
|
|
}
|
|
for ( j = 0; j < AKBASIC_MAX_SOLIDS; j++ ) {
|
|
if ( !state->solidactive[j] ) {
|
|
continue;
|
|
}
|
|
if ( !(box[i].x < state->solidbox[j].x + state->solidbox[j].w
|
|
&& state->solidbox[j].x < box[i].x + box[i].w
|
|
&& box[i].y < state->solidbox[j].y + state->solidbox[j].h
|
|
&& state->solidbox[j].y < box[i].y + box[i].h) ) {
|
|
continue;
|
|
}
|
|
if ( !synced[i] ) {
|
|
PASS(errctx, sync_proxy(state, i, &box[i]));
|
|
synced[i] = true;
|
|
}
|
|
hit = false;
|
|
PASS(errctx, akgl_collision_test(state->proxies[i], state->solidproxies[j],
|
|
AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
if ( hit ) {
|
|
state->lastsolidmask |= (uint16_t)(1u << i);
|
|
record_contact(state, i, 2, j + 1, &contact);
|
|
}
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
state->lastbox[i] = box[i];
|
|
state->lastlive[i] = live[i];
|
|
}
|
|
state->lastvalid = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Register, replace or retire static rectangle @p id.
|
|
*
|
|
* A proxy with a **NULL owner**: there is no actor behind a wall, and libakgl's
|
|
* proxy carries the owner only so a resolver can push something. Nothing here
|
|
* resolves anything, so the field stays empty and the shape is what matters.
|
|
*
|
|
* Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and
|
|
* responds to nothing, which is the asymmetry libakgl's masks exist for: the
|
|
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
|
|
* walls never test against each other. Sixty-four motionless rectangles
|
|
* therefore cost nothing per scan beyond the boxes a sprite is compared to.
|
|
*/
|
|
static akerr_ErrorContext *spr_solid(akbasic_SpriteBackend *self, int id, bool define,
|
|
double x1, double y1, double x2, double y2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
SDL_FRect body;
|
|
int i = 0;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
|
|
"Static shape %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
|
|
i = id - 1;
|
|
|
|
/*
|
|
* Anything that changes the geometry invalidates the cached answer. Cheapest
|
|
* possible correctness: the scan's own change detection watches the sprites,
|
|
* and this is the one input it cannot see moving.
|
|
*/
|
|
state->lastvalid = false;
|
|
|
|
if ( !define ) {
|
|
if ( state->solidproxies[i] != NULL ) {
|
|
PASS(errctx, akgl_heap_release_collision_proxy(state->solidproxies[i]));
|
|
state->solidproxies[i] = NULL;
|
|
}
|
|
state->solidactive[i] = false;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
body.x = 0.0f;
|
|
body.y = 0.0f;
|
|
body.w = (float32_t)(x2 - x1);
|
|
body.h = (float32_t)(y2 - y1);
|
|
PASS(errctx, akgl_collision_shape_box(&state->solidshapes[i], &body, 0.0f));
|
|
state->solidshapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
|
state->solidshapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
|
state->solidshapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
|
|
|
if ( state->solidproxies[i] == NULL ) {
|
|
/* Adjacent, with nothing between: the slot is free until initialize takes it. */
|
|
PASS(errctx, akgl_heap_next_collision_proxy(&state->solidproxies[i]));
|
|
PASS(errctx, akgl_collision_proxy_initialize(state->solidproxies[i], NULL,
|
|
&state->solidshapes[i],
|
|
(float32_t)x1, (float32_t)y1, 0.0f));
|
|
} else {
|
|
PASS(errctx, akgl_collision_proxy_sync(state->solidproxies[i], &state->solidshapes[i],
|
|
(float32_t)x1, (float32_t)y1, 0.0f));
|
|
}
|
|
state->solidbox[i].x = (float32_t)x1;
|
|
state->solidbox[i].y = (float32_t)y1;
|
|
state->solidbox[i].w = body.w;
|
|
state->solidbox[i].h = body.h;
|
|
state->solidactive[i] = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
|
|
PASS(errctx, run_scan(state));
|
|
*mask = state->lastmask;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
|
|
PASS(errctx, run_scan(state));
|
|
*mask = state->lastsolidmask;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL && renderer != NULL),
|
|
AKERR_NULLPOINTER, "NULL argument in sprite_init_akgl");
|
|
/* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */
|
|
PASS(errctx, akgl_error_init());
|
|
|
|
memset(state, 0, sizeof(*state));
|
|
state->renderer = renderer;
|
|
state->graphics = graphics;
|
|
|
|
/*
|
|
* The eight collision proxies, claimed here and held for the life of the
|
|
* backend.
|
|
*
|
|
* Up front rather than on demand because the pool is shared: a host game
|
|
* embedding this interpreter draws its own shaped actors from the same
|
|
* AKGL_MAX_HEAP_COLLISION_PROXY. Claiming late would mean a collision scan
|
|
* discovering mid-game that a host had taken the last slot, and reporting it
|
|
* against whichever BASIC line was unlucky. Claiming here makes it an
|
|
* initialization failure that names the pool, before a program has run.
|
|
*
|
|
* A default box shape so a proxy is never initialized from an uninitialized
|
|
* shape -- libakgl fixed exactly that defect in 3a6569e, where filing a proxy
|
|
* under half-extents that had not been written yet was invisible to its
|
|
* tests and visible only to a memory checker. Real geometry arrives on the
|
|
* first sync_proxy(); this is only ever a placeholder.
|
|
*/
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
SDL_FRect placeholder = { 0.0f, 0.0f, 1.0f, 1.0f };
|
|
|
|
/*
|
|
* A box fitting the frame, which is what every sprite collided with
|
|
* before SPRHIT existed and is what one nobody has shaped must go on
|
|
* collided with.
|
|
*/
|
|
state->shapekind[i] = AKBASIC_SHAPE_BOX;
|
|
|
|
PASS(errctx, akgl_collision_shape_box(&state->shapes[i], &placeholder, 0.0f));
|
|
/*
|
|
* Acquire and initialize adjacent, with nothing between them: the slot
|
|
* is free until initialize takes the reference, and libakgl's heap says
|
|
* so where next_collision_proxy is declared.
|
|
*/
|
|
PASS(errctx, akgl_heap_next_collision_proxy(&state->proxies[i]));
|
|
PASS(errctx, akgl_collision_proxy_initialize(state->proxies[i], NULL,
|
|
&state->shapes[i], 0.0f, 0.0f, 0.0f));
|
|
}
|
|
|
|
memset(obj, 0, sizeof(*obj));
|
|
obj->self = state;
|
|
obj->define = spr_define;
|
|
obj->define_shape = spr_define_shape;
|
|
obj->define_file = spr_define_file;
|
|
obj->show = spr_show;
|
|
obj->move = spr_move;
|
|
obj->configure = spr_configure;
|
|
obj->shared_colors = spr_shared_colors;
|
|
obj->collisions = spr_collisions;
|
|
obj->shape = spr_shape;
|
|
obj->solid = spr_solid;
|
|
obj->solids = spr_solids;
|
|
obj->contact = spr_contact;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_AkglSprites *state = NULL;
|
|
int i = 0;
|
|
|
|
PASS(errctx, state_of(obj, &state));
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
if ( state->actors[i] == NULL ) {
|
|
continue;
|
|
}
|
|
/*
|
|
* PASS rather than CATCH: this is a loop, and CATCH expands to a break.
|
|
* Through the actor's own renderfunc rather than calling spr_render_actor
|
|
* directly, so a host that has replaced it gets what it asked for.
|
|
*/
|
|
PASS(errctx, state->actors[i]->renderfunc(state->actors[i]));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj)
|
|
{
|
|
akbasic_AkglSprites *state = NULL;
|
|
int i = 0;
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
return;
|
|
}
|
|
state = (akbasic_AkglSprites *)obj->self;
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
|
IGNORE(release_slot(state, i));
|
|
/*
|
|
* The proxies go back too. They are not registered with any partitioner,
|
|
* so there is nothing to unlink first -- which is the one ordering trap
|
|
* here, and the reason worth writing down: releasing a proxy that *is*
|
|
* registered leaves stale cell entries holding a pool index, and the
|
|
* damage only shows when the slot is handed out again.
|
|
*/
|
|
if ( state->proxies[i] != NULL ) {
|
|
IGNORE(akgl_heap_release_collision_proxy(state->proxies[i]));
|
|
state->proxies[i] = NULL;
|
|
}
|
|
}
|
|
for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
|
|
if ( state->solidproxies[i] != NULL ) {
|
|
IGNORE(akgl_heap_release_collision_proxy(state->solidproxies[i]));
|
|
state->solidproxies[i] = NULL;
|
|
}
|
|
}
|
|
}
|