586 lines
22 KiB
C
586 lines
22 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(). Two
|
||
|
|
* reasons, both defects filed upstream: the default computes its destination
|
||
|
|
* height from the sprite's *width*, which draws a 24x21 Commodore sprite as a
|
||
|
|
* 24x24 square; and an actor carries one scalar `scale`, which cannot express
|
||
|
|
* SPRITE's separate x- and y-expand bits. 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 two ways, both noted at the top of this
|
||
|
|
* file: the destination height comes from the sprite's height rather than its
|
||
|
|
* width, and the two expansion bits scale the axes independently.
|
||
|
|
*
|
||
|
|
* 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_sprite_sheet_coords_for_frame(sprite, &src, 0));
|
||
|
|
dest.x = obj->x - (camera != NULL ? camera->x : 0.0f);
|
||
|
|
dest.y = obj->y - (camera != NULL ? 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);
|
||
|
|
}
|
||
|
|
|
||
|
|
static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_AkglSprites *state = NULL;
|
||
|
|
int i = 0;
|
||
|
|
int j = 0;
|
||
|
|
|
||
|
|
PASS(errctx, state_of(self, &state));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
|
||
|
|
*mask = 0;
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Axis-aligned bounding boxes, compared here rather than through
|
||
|
|
* akgl_collide_rectangles(): that function has a documented
|
||
|
|
* corner-containment defect -- it misses a plus-shaped overlap where neither
|
||
|
|
* rectangle contains a corner of the other -- and nothing in libakgl calls
|
||
|
|
* it. Four comparisons are both correct and smaller.
|
||
|
|
*
|
||
|
|
* Box against box, not pixel against pixel. A C128 collides on set pixels,
|
||
|
|
* so two sprites whose boxes touch but whose art does not are reported as
|
||
|
|
* colliding here and would not be there. Recorded in TODO.md section 5.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
||
|
|
for ( j = i + 1; j < AKBASIC_MAX_SPRITES; j++ ) {
|
||
|
|
float32_t ax = 0.0f, ay = 0.0f, aw = 0.0f, ah = 0.0f;
|
||
|
|
float32_t bx = 0.0f, by = 0.0f, bw = 0.0f, bh = 0.0f;
|
||
|
|
|
||
|
|
if ( state->actors[i] == NULL || state->actors[j] == NULL ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( !state->actors[i]->visible || !state->actors[j]->visible ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( state->sprites[i] == NULL || state->sprites[j] == NULL ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
ax = state->actors[i]->x;
|
||
|
|
ay = state->actors[i]->y;
|
||
|
|
aw = (float32_t)state->sprites[i]->width * (state->xexpand[i] ? 2.0f : 1.0f);
|
||
|
|
ah = (float32_t)state->sprites[i]->height * (state->yexpand[i] ? 2.0f : 1.0f);
|
||
|
|
bx = state->actors[j]->x;
|
||
|
|
by = state->actors[j]->y;
|
||
|
|
bw = (float32_t)state->sprites[j]->width * (state->xexpand[j] ? 2.0f : 1.0f);
|
||
|
|
bh = (float32_t)state->sprites[j]->height * (state->yexpand[j] ? 2.0f : 1.0f);
|
||
|
|
|
||
|
|
if ( ax < bx + bw && bx < ax + aw && ay < by + bh && by < ay + ah ) {
|
||
|
|
*mask |= (uint16_t)(1u << i);
|
||
|
|
*mask |= (uint16_t)(1u << j);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
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;
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
}
|