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