/** * @file sprite.h * @brief Declares the sprite backend: where SPRITE, MOVSPR and SPRSAV land. * * The same shape as akbasic/graphics.h and for the same reason -- the core * library builds with no SDL and no libakgl present, so a sprite verb calls * through a record of function pointers rather than reaching for akgl_actor_*. * akbasic_sprite_init_akgl() lives in the separate akbasic_akgl target. * * **A sprite is a Commodore sprite, not a libakgl sprite.** libakgl's has a * sheet, a frame list, an animation speed and a state-to-sprite map, all of * which arrive from a JSON document; BASIC 7.0 has a word for none of it. What * BASIC has is eight numbered slots, each holding one still image with a * position, a colour, a visibility flag and two expansion bits. So the JSON * layer is bypassed rather than reached through: the adaptor builds the same * structures directly and hands the interpreter this much smaller surface. * * The one place the two overlap is loading an image from a file, which is why * `define_file` is here: a program that has art on disk should be able to say so, * and libakgl already resolves a relative asset path and decodes the image. That * path is not bypassed -- it is exactly akgl_spritesheet_initialize(). * * A runtime with no sprite backend is the normal case, so every verb that needs * one refuses with AKBASIC_ERR_DEVICE. The verbs that only touch interpreter * state -- SPRCOLOR, and the RSP* readbacks -- work regardless, which is what * makes them testable without a device. */ #ifndef _AKBASIC_SPRITE_H_ #define _AKBASIC_SPRITE_H_ #include #include #include /** @brief Sprites a program has. Eight, as on a C128; the verbs number them 1-8. */ #define AKBASIC_MAX_SPRITES 8 /** * @brief The collision shapes SPRHIT understands. * * **Deliberately libakgl's own numbers**, so the two documents agree and nobody * has to maintain a mapping between them. A kind this interpreter has no use for * is still refused by name rather than silently accepted. */ #define AKBASIC_SHAPE_NONE 0 #define AKBASIC_SHAPE_BOX 1 #define AKBASIC_SHAPE_CIRCLE 2 #define AKBASIC_SHAPE_CAPSULE_X 3 #define AKBASIC_SHAPE_CAPSULE_Y 4 /** @brief One past the last kind, for the range check. */ #define AKBASIC_SHAPE_LAST 5 /** * @brief Static collision rectangles a program can register with SOLID. * * Sixty-four, which is the smallest power of two that holds a sixty-brick wall * with headroom. The ceiling matters because these come out of libakgl's * collision proxy pool, which is shared with whatever host this interpreter is * embedded in: eight sprites plus sixty-four solids is seventy-two of * #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's. */ #ifndef AKBASIC_MAX_SOLIDS #define AKBASIC_MAX_SOLIDS 64 #endif /** @brief Width of a Commodore sprite in pixels. */ #define AKBASIC_SPRITE_WIDTH 24 /** @brief Height of a Commodore sprite in pixels. */ #define AKBASIC_SPRITE_HEIGHT 21 /** * @brief Bytes in a sprite pattern: three per row, twenty-one rows. * * The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21, * which is 63 bytes of bitmap plus a four-byte header. Only the bitmap is * carried here -- the header records a width and height that are constants for a * sprite -- so a DIMmed integer array of this many elements is a whole pattern. */ #define AKBASIC_SPRITE_PATTERN_BYTES 63 /** * @brief Fastest MOVSPR speed, and how far one unit of it travels. * * `MOVSPR n, angle # speed` takes 0-15 on a C128 and the reference manual does * not say what a unit is in pixels; it says 15 is fastest and 0 stops. So the * mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND device * pixels per second, which puts speed 15 at four seconds to cross a 320-pixel * screen and proportionally longer across a wider one. Recorded as a deviation * in TODO.md section 5 rather than presented as fidelity. */ #define AKBASIC_SPRITE_MAX_SPEED 15 /** @brief Pixels per second one unit of MOVSPR speed is worth. See #AKBASIC_SPRITE_MAX_SPEED. */ #define AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND 5.0 /** * @brief One sprite, as BASIC sees it. * * Every field here is something a BASIC verb sets and an RSP* function reads * back, which is why the whole thing lives on the runtime rather than on the * device: a host that swaps one renderer for another does not expect the * program's sprite positions to go with it. The device is told about changes; it * is never asked what they were. */ typedef struct { bool defined; /* SPRSAV has given this slot an image */ bool enabled; /* SPRITE n, 1 */ bool behind; /* priority: drawn behind the drawing surface */ bool xexpand; /* SPRITE's x-expand bit */ bool yexpand; bool multicolour; int colorindex; /* 1-16, the sprite's own colour */ double x; /* position in device pixels; see graphics.h */ double y; double angle; /* MOVSPR's continuous motion: degrees */ int speed; /* clockwise from vertical, and 0-15 */ /** * SPRHIT's collision shape: one of the #AKBASIC_SHAPE_* kinds, and a * rectangle measured from the sprite's top-left corner. * * `shapeexplicit` is what separates "the program asked for the whole frame" * from "the program has not said". They are the same shape today and will * not stay that way: a frame-fitting shape has to be rebuilt when the * picture or an expansion bit changes, and one the program gave in pixels * must not be. */ int shapekind; double shapex1; double shapey1; double shapex2; double shapey2; bool shapeexplicit; } akbasic_Sprite; /** * @brief One rectangle registered by SOLID, as BASIC sees it. * * The id is the array index plus one and is the program's own number, not a * minted handle. A brick in a sixty-brick wall has a natural index; making the * program carry one back out of the interpreter would be inventing a second * numbering for something that already has one. */ typedef struct { bool active; double x1; double y1; double x2; double y2; } akbasic_Solid; /** * @brief What sprite n last collided with, and how. * * Narrower than libakgl's `akgl_Contact` on purpose. 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 * they would come back zero and mean nothing. An always-zero field in a reference * table is a lie. * * `axis` is the one field that is not the library's. It is the minimum * translation axis -- which way to reverse -- and it is here because working it * out from the normal is exactly where this dialect's left-operand arithmetic * rule catches people, and because it is the single field that deletes the most * BASIC. */ typedef struct { int what; /* 0 nothing yet, 1 a sprite, 2 static geometry */ int other; /* the other sprite's number, or the SOLID id */ double nx; /* unit normal, out of the other and toward this */ double ny; double depth; /* how far in, in pixels */ double px; /* where they touched */ double py; int axis; /* 1 reverse x, 2 reverse y */ } akbasic_Contact; /** * @brief The sprite verbs' own state, which lives on the runtime. * * `bumped` accumulates rather than reporting an instant. A collision that * happens between two BUMP() calls is still news when the second one asks, and a * program polling once a frame would otherwise miss anything that started and * ended inside a frame. BUMP() clears what it reports, which is what a C128 does * and what makes "has this sprite hit anything since I last looked" answerable. */ typedef struct { akbasic_Sprite sprites[AKBASIC_MAX_SPRITES]; akbasic_Solid solids[AKBASIC_MAX_SOLIDS]; akbasic_Contact contacts[AKBASIC_MAX_SPRITES]; int sharedcolor1; /* SPRCOLOR's two multicolour registers, 1-16 */ int sharedcolor2; uint16_t bumped; /* bit n-1 set when sprite n has collided */ uint16_t bumpedsolid; /* and when it has met static geometry */ int64_t lastservicems; /* when MOVSPR's continuous motion last moved */ } akbasic_SpriteState; /** * @brief Where the sprite verbs land. * * Sprites are numbered 1 through #AKBASIC_MAX_SPRITES at this boundary, the same * way a BASIC program numbers them. Translating to a zero-based slot is the * backend's business, and doing it here would mean two numbering schemes in one * file for no gain. * * There is deliberately no entry point for the RSP* readbacks. Everything they * report is in #akbasic_SpriteState, which the interpreter owns, so asking the * device would be asking it to repeat what it was told. */ typedef struct akbasic_SpriteBackend { void *self; /** * Install a pattern into sprite @p n from packed bitmap bytes. * * Three bytes per row, most significant bit leftmost, twenty-one rows -- * the layout a C128 keeps in its sprite block, and what a type-in listing's * DATA statements hold. @p fg is what a set bit draws and @p bg what a clear * one does; a clear bit is normally transparent, which is @p bg with a zero * alpha. */ akerr_ErrorContext AKERR_NOIGNORE *(*define)(struct akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg); /** * Install a pattern into sprite @p n from a region SSHAPE saved. * * The handle is the graphics backend's, not this one's -- SSHAPE puts it in * a BASIC string and SPRSAV hands it straight back. An adaptor that serves * both devices resolves it; one that does not may refuse with * AKBASIC_ERR_DEVICE. */ akerr_ErrorContext AKERR_NOIGNORE *(*define_shape)(struct akbasic_SpriteBackend *self, int n, int handle); /** * Install a pattern into sprite @p n by loading an image file. * * @p path is what the program wrote, resolved by the backend rather than * here: the interpreter has no idea what a path means to the host, and the * akgl adaptor resolves it exactly as libakgl resolves a spritesheet named * by a sprite document. @p root is the directory to fall back to when the * path does not resolve against the working directory, normally the * directory the running program was loaded from; it may be NULL. * * Unlike the other two, this one does not force 24x21: the image's own size * is the sprite's size. A modern PC has no reason to throw away art that * does not happen to be sprite-shaped. */ akerr_ErrorContext AKERR_NOIGNORE *(*define_file)(struct akbasic_SpriteBackend *self, int n, const char *path, const char *root); /** Show or hide sprite @p n. */ akerr_ErrorContext AKERR_NOIGNORE *(*show)(struct akbasic_SpriteBackend *self, int n, bool visible); /** Put sprite @p n at (@p x, @p y), in device pixels. */ akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akbasic_SpriteBackend *self, int n, double x, double y); /** * Set sprite @p n's own colour, its expansion bits and its priority. * * One call rather than four because `SPRITE` sets them together and a * backend that has to rebuild a texture to change a colour would otherwise * do it four times for one statement. */ akerr_ErrorContext AKERR_NOIGNORE *(*configure)(struct akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind); /** Set the two multicolour registers every multicolour sprite shares. */ akerr_ErrorContext AKERR_NOIGNORE *(*shared_colors)(struct akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2); /** * Report which sprites are currently overlapping another sprite. * * Bit n-1 of @p mask is set when sprite n overlaps at least one other. The * interpreter calls this once per step, ORs the result into * akbasic_SpriteState::bumped, and raises the collision interrupt when * anything is set -- so a backend answers about *now* and does not have to * remember anything. */ akerr_ErrorContext AKERR_NOIGNORE *(*collisions)(struct akbasic_SpriteBackend *self, uint16_t *mask); /** * Give sprite @p n a collision shape. * * @p kind is an #AKBASIC_SHAPE_NONE .. #AKBASIC_SHAPE_CAPSULE_Y, and the * rectangle is two corners measured from the sprite's top-left, in device * pixels -- the same form `BOX` and `SSHAPE` take, because a dialect with two * spellings for a rectangle is a dialect nobody can write from memory. * * Optional. A backend that withholds it makes `SPRHIT` refuse by name, which * is what a device that cannot do what was asked is supposed to do. */ akerr_ErrorContext AKERR_NOIGNORE *(*shape)(struct akbasic_SpriteBackend *self, int n, int kind, double x1, double y1, double x2, double y2); /** * Register, replace or retire static collision geometry @p id. * * @p define false retires it and the rectangle is ignored. Rectangles are in * window pixels -- the coordinates `BOX` draws in and `MOVSPR` positions in, * with `SCALE` not applying, exactly as for a sprite. * * Optional, like `shape`. A backend that withholds it makes `SOLID` refuse * by name. */ akerr_ErrorContext AKERR_NOIGNORE *(*solid)(struct akbasic_SpriteBackend *self, int id, bool define, double x1, double y1, double x2, double y2); /** * Report which sprites are overlapping static geometry. * * Bit n-1 is set when sprite n overlaps at least one rectangle `solid` * registered. Answered from the same pass as `collisions`, so the two are * always about the same instant whichever order they are called in. * * Optional. A runtime whose backend withholds it reports no static * collisions rather than refusing, exactly as one with no backend at all * reports none -- a step is not a statement and has no line to blame. */ akerr_ErrorContext AKERR_NOIGNORE *(*solids)(struct akbasic_SpriteBackend *self, uint16_t *mask); /** * Describe sprite @p n's deepest collision from the scan just run. * * @p found comes back false when that sprite hit nothing, and @p dest is * then untouched. Deepest rather than first, because the deepest is the one * a program has to undo. * * A pure accessor: it reads what the same pass that answered `collisions` * and `solids` already worked out. That is why the contract on `collisions` * says the two are about the same instant -- this depends on it. * * Optional. Withholding it makes `RCOLLISION` report nothing rather than * refuse, the way a runtime with no backend at all does. */ akerr_ErrorContext AKERR_NOIGNORE *(*contact)(struct akbasic_SpriteBackend *self, int n, akbasic_Contact *dest, bool *found); } akbasic_SpriteBackend; /** * @brief Reset the sprite state to its power-on values. * * Eight undefined, hidden sprites at the origin in white, the two shared * multicolour registers at their C128 defaults, and nothing bumped. NEW comes * back through here. * * @param obj Object to initialize, inspect, or modify. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When `obj` is NULL. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_state_init(akbasic_SpriteState *obj); /** * @brief Unpack 63 bytes of sprite bitmap into one pixel per byte. * * Three bytes per row, most significant bit leftmost. @p dest receives * #AKBASIC_SPRITE_WIDTH * #AKBASIC_SPRITE_HEIGHT entries, each 0 or 1. Shared by * every backend that has to turn a pattern into pixels, and separate from them * so the bit order is asserted once rather than trusted eight times. * * @param pattern Packed bitmap; at least @p bytes readable. * @param bytes How many bytes @p pattern holds; must be #AKBASIC_SPRITE_PATTERN_BYTES. * @param dest Output destination populated by the function; 504 entries. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When either pointer is NULL. * @throws AKBASIC_ERR_BOUNDS When `bytes` is not #AKBASIC_SPRITE_PATTERN_BYTES. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_unpack(const uint8_t *pattern, int bytes, uint8_t *dest); struct akbasic_Runtime; /** * @brief Move every sprite MOVSPR set in continuous motion. * * Called once per akbasic_runtime_step(), before the line runs, and paced off * akbasic_runtime_settime() -- exactly the way the PLAY queue is paced, and for * the same reason: this library owns no loop and must not block, so the caller * that owns the frame owns the clock. * * A host that never sets the time leaves it at zero and nothing moves, which is * a still picture rather than a hang. * * @param obj Object to initialize, inspect, or modify. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When `obj` is NULL. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_service(struct akbasic_Runtime *obj); /** * @brief Ask the device what is overlapping, and raise the collision interrupt. * * Also called once per step. What it finds is ORed into * akbasic_SpriteState::bumped for BUMP() to report, and raised as * #AKBASIC_INTERRUPT_SPRITE -- unconditionally, because an unarmed interrupt * records nothing and there is therefore nothing to ask first. * * A runtime with no sprite device, or one whose backend withholds `collisions`, * does nothing here rather than refusing: a step is not a statement and has no * program line to blame. * * @param obj Object to initialize, inspect, or modify. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When `obj` is NULL. */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_collision_service(struct akbasic_Runtime *obj); #endif // _AKBASIC_SPRITE_H_