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
598 lines
19 KiB
C
598 lines
19 KiB
C
/**
|
|
* @file mockdevice.h
|
|
* @brief Recording graphics, audio and input backends for the device tests.
|
|
*
|
|
* The graphics and audio verbs produce no output a golden file can compare, so
|
|
* the assertions have to be about what reached the backend. These record every
|
|
* call as a formatted line, which a test compares against the sequence it
|
|
* expected -- what was drawn, in what order, in what color.
|
|
*
|
|
* Nothing here needs SDL, which is the point of the backend records in the first
|
|
* place: the whole of groups G, I and E is testable in the default build.
|
|
*/
|
|
|
|
#ifndef _AKBASIC_TEST_MOCKDEVICE_H_
|
|
#define _AKBASIC_TEST_MOCKDEVICE_H_
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/audio.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/graphics.h>
|
|
#include <akbasic/input.h>
|
|
#include <akbasic/sprite.h>
|
|
|
|
/** @brief Room for the call log. Longer than any test needs; overflow truncates loudly. */
|
|
#define MOCK_LOG_SIZE 8192
|
|
/** @brief How many keystrokes mock_input can be primed with. */
|
|
#define MOCK_MAX_KEYS 32
|
|
/** @brief How many regions mock_graphics will hand out handles for. */
|
|
#define MOCK_MAX_SHAPES 8
|
|
|
|
/** @brief State shared by all three mock backends, so one log records the lot. */
|
|
typedef struct
|
|
{
|
|
char log[MOCK_LOG_SIZE];
|
|
size_t used;
|
|
bool overflowed;
|
|
|
|
/* Graphics */
|
|
int shapes; /* handles handed out so far */
|
|
bool paint_exhausts; /* when true, paint reports a partial fill */
|
|
int devwidth; /* what size() reports; see mock_devices_init */
|
|
int devheight;
|
|
|
|
/* Audio */
|
|
bool voice_active[AKBASIC_AUDIO_VOICES];
|
|
|
|
/* Input */
|
|
int keys[MOCK_MAX_KEYS];
|
|
int keycount;
|
|
int keynext;
|
|
|
|
/* Sprites */
|
|
uint16_t collisions; /* what the next collisions() call reports */
|
|
uint16_t solidcollisions; /* and what the next solids() call reports */
|
|
akbasic_Contact contact; /* what the next contact() call describes */
|
|
bool hascontact;
|
|
int patternbytes[AKBASIC_MAX_SPRITES]; /* bytes the last define() carried, per sprite */
|
|
} akbasic_MockDevice;
|
|
|
|
static akbasic_MockDevice MOCK;
|
|
static akbasic_GraphicsBackend MOCK_GRAPHICS;
|
|
static akbasic_AudioBackend MOCK_AUDIO;
|
|
static akbasic_InputBackend MOCK_INPUT;
|
|
static akbasic_SpriteBackend MOCK_SPRITES;
|
|
|
|
/**
|
|
* @brief Append one formatted call to the log.
|
|
*
|
|
* Deliberately not an akerr_ErrorContext * function. It is called from inside
|
|
* the vtable entry points, and a test whose log overflowed should say so through
|
|
* the assertion on the log contents rather than by raising an error that the
|
|
* verb under test would then report as a device failure.
|
|
*/
|
|
__attribute__((format(printf, 1, 2)))
|
|
static void mock_log(const char *format, ...)
|
|
{
|
|
va_list args;
|
|
int written = 0;
|
|
|
|
if ( MOCK.used >= sizeof(MOCK.log) - 1 ) {
|
|
MOCK.overflowed = true;
|
|
return;
|
|
}
|
|
va_start(args, format);
|
|
written = vsnprintf(MOCK.log + MOCK.used, sizeof(MOCK.log) - MOCK.used, format, args);
|
|
va_end(args);
|
|
if ( written < 0 || (size_t)written >= sizeof(MOCK.log) - MOCK.used ) {
|
|
MOCK.overflowed = true;
|
|
MOCK.used = sizeof(MOCK.log) - 1;
|
|
return;
|
|
}
|
|
MOCK.used += (size_t)written;
|
|
}
|
|
|
|
/* --------------------------------------------------------------- graphics -- */
|
|
|
|
/**
|
|
* @brief Report the recorded surface size, and log that it was asked.
|
|
*
|
|
* Logged like every other call because *when* the interpreter asks matters: it
|
|
* re-asks before each verb that draws, deliberately, so a resized window is not
|
|
* drawn to at its old size.
|
|
*/
|
|
static akerr_ErrorContext *mock_size(akbasic_GraphicsBackend *self, int *width, int *height)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER,
|
|
"NULL destination in mock_size");
|
|
*width = MOCK.devwidth;
|
|
*height = MOCK.devheight;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("point %.1f,%.1f #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("line %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("rect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("fillrect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("paint %d,%d #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
|
|
/*
|
|
* Stands in for the akgl flood fill running out of its fixed span stack,
|
|
* which reports AKERR_OUTOFBOUNDS having filled part of the region. PAINT has
|
|
* to surface that rather than treat it as success.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (!MOCK.paint_exhausts), AKERR_OUTOFBOUNDS,
|
|
"mock span stack exhausted");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("clear #%02x%02x%02x\n", color.r, color.g, color.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER, "NULL handle in save_shape");
|
|
FAIL_ZERO_RETURN(errctx, (MOCK.shapes < MOCK_MAX_SHAPES), AKBASIC_ERR_DEVICE,
|
|
"mock shape pool exhausted");
|
|
*handle = MOCK.shapes;
|
|
MOCK.shapes += 1;
|
|
mock_log("save %d,%d-%d,%d -> %d\n", x1, y1, x2, y2, *handle);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < MOCK.shapes), AKBASIC_ERR_BOUNDS,
|
|
"no such mock shape %d", handle);
|
|
mock_log("paste %d at %.1f,%.1f\n", handle, x, y);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_free_shapes(akbasic_GraphicsBackend *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
MOCK.shapes = 0;
|
|
mock_log("freeshapes\n");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ audio -- */
|
|
|
|
static akerr_ErrorContext *mock_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
MOCK.voice_active[voice] = true;
|
|
mock_log("tone v%d %.1fhz %dms\n", voice, hz, ms);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_hz, int ms)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
MOCK.voice_active[voice] = true;
|
|
mock_log("sweep v%d %.1fhz->%.1fhz step %.1fhz %dms\n",
|
|
voice, from_hz, to_hz, step_hz, ms);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_audio_stop(akbasic_AudioBackend *self, int voice)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
MOCK.voice_active[voice] = false;
|
|
mock_log("stop v%d\n", voice);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
mock_log("wave v%d %d\n", voice, (int)waveform);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
mock_log("env v%d %d/%d/%.2f/%d\n", voice, attack, decay, sustain, release);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_volume(akbasic_AudioBackend *self, double level)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
mock_log("vol %.2f\n", level);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (active != NULL), AKERR_NULLPOINTER, "NULL active in voice_active");
|
|
FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
|
|
"mock voice %d out of range", voice);
|
|
*active = MOCK.voice_active[voice];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ input -- */
|
|
|
|
static akerr_ErrorContext *mock_poll_key(akbasic_InputBackend *self, int *keycode, bool *available)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (keycode != NULL && available != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in poll_key");
|
|
if ( MOCK.keynext >= MOCK.keycount ) {
|
|
/* An empty buffer is success, not an error -- see input.h. */
|
|
*available = false;
|
|
*keycode = 0;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
*keycode = MOCK.keys[MOCK.keynext];
|
|
*available = true;
|
|
MOCK.keynext += 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_flush_keys(akbasic_InputBackend *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
MOCK.keynext = MOCK.keycount;
|
|
mock_log("flushkeys\n");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- sprites -- */
|
|
|
|
/**
|
|
* @brief Refuse a sprite number the way a real backend has to.
|
|
*
|
|
* Sprites are 1-based at this boundary -- see sprite.h -- so an off-by-one in a
|
|
* verb handler shows up here as AKBASIC_ERR_BOUNDS rather than as a silently
|
|
* mis-addressed sprite.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *mock_sprite_range(int n)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
|
|
"mock sprite %d out of range", n);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
FAIL_ZERO_RETURN(errctx, (pattern != NULL), AKERR_NULLPOINTER, "NULL pattern in define");
|
|
MOCK.patternbytes[n - 1] = bytes;
|
|
/*
|
|
* The first and last bytes rather than all 63: enough to tell one pattern
|
|
* from another and to catch a byte order that came through reversed,
|
|
* without a log line no human can read.
|
|
*/
|
|
mock_log("sprdef %d %d bytes %02x..%02x fg=%d,%d,%d bg=%d,%d,%d\n",
|
|
n, bytes, pattern[0], pattern[bytes - 1],
|
|
fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_define_shape(akbasic_SpriteBackend *self, int n, int handle)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
mock_log("sprshape %d from %d\n", n, handle);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER, "NULL path in define_file");
|
|
/*
|
|
* The root is logged as well as the path. Whether the interpreter passed the
|
|
* running program's directory through is the whole of what this repository
|
|
* owns about relative asset paths -- resolving one is libakgl's job, and
|
|
* there is no way to see it happen from a mock.
|
|
*/
|
|
mock_log("sprfile %d %s root=%s\n", n, path, (root != NULL ? root : "(none)"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_show(akbasic_SpriteBackend *self, int n, bool visible)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
mock_log("sprshow %d %d\n", n, (visible ? 1 : 0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_move(akbasic_SpriteBackend *self, int n, double x, double y)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
mock_log("sprmove %d %.2f,%.2f\n", n, x, y);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
PASS(errctx, mock_sprite_range(n));
|
|
mock_log("sprcfg %d %d,%d,%d x%d y%d b%d\n", n, color.r, color.g, color.b,
|
|
(xexpand ? 1 : 0), (yexpand ? 1 : 0), (behind ? 1 : 0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
mock_log("sprshared %d,%d,%d %d,%d,%d\n", c1.r, c1.g, c1.b, c2.r, c2.g, c2.b);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
|
|
*mask = MOCK.collisions;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
|
|
*mask = MOCK.solidcollisions;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_shape(akbasic_SpriteBackend *self, int n, int kind,
|
|
double x1, double y1, double x2, double y2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
mock_log("sprhit %d,%d,%g,%g,%g,%g\n", n, kind, x1, y1, x2, y2);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_solid(akbasic_SpriteBackend *self, int id, bool define,
|
|
double x1, double y1, double x2, double y2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
if ( !define ) {
|
|
mock_log("solid %d off\n", id);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
mock_log("solid %d,%g,%g,%g,%g\n", id, x1, y1, x2, y2);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext *mock_spr_contact(akbasic_SpriteBackend *self, int n,
|
|
akbasic_Contact *dest, bool *found)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)self;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL && found != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in contact");
|
|
*found = MOCK.hascontact;
|
|
if ( *found ) {
|
|
*dest = MOCK.contact;
|
|
dest->other = n;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- fixture -- */
|
|
|
|
/** @brief Reset the recorder and populate all three vtables. */
|
|
__attribute__((unused))
|
|
static void mock_devices_init(void)
|
|
{
|
|
memset(&MOCK, 0, sizeof(MOCK));
|
|
/*
|
|
* 640x400 rather than 320x200, on purpose: it is exactly twice the fallback,
|
|
* so a scaled coordinate that came out right by accident -- because nothing
|
|
* consulted the device at all -- is off by a factor of two rather than
|
|
* indistinguishable. A test that wants the fallback clears MOCK_GRAPHICS.size,
|
|
* the way the SOUND test clears MOCK_AUDIO.sweep.
|
|
*/
|
|
MOCK.devwidth = 640;
|
|
MOCK.devheight = 400;
|
|
|
|
MOCK_GRAPHICS.self = &MOCK;
|
|
MOCK_GRAPHICS.size = mock_size;
|
|
MOCK_GRAPHICS.point = mock_point;
|
|
MOCK_GRAPHICS.line = mock_line;
|
|
MOCK_GRAPHICS.rect = mock_rect;
|
|
MOCK_GRAPHICS.filled_rect = mock_filled_rect;
|
|
MOCK_GRAPHICS.paint = mock_paint;
|
|
MOCK_GRAPHICS.clear = mock_clear;
|
|
MOCK_GRAPHICS.save_shape = mock_save_shape;
|
|
MOCK_GRAPHICS.paste_shape = mock_paste_shape;
|
|
MOCK_GRAPHICS.free_shapes = mock_free_shapes;
|
|
|
|
MOCK_AUDIO.self = &MOCK;
|
|
MOCK_AUDIO.tone = mock_tone;
|
|
/*
|
|
* Set here, and deliberately cleared again by the one test that needs a
|
|
* backend without it: `sweep` may be NULL, which is what a host written
|
|
* against libakgl 0.2.0 looks like, and SOUND has to refuse rather than
|
|
* crash on one.
|
|
*/
|
|
MOCK_AUDIO.sweep = mock_sweep;
|
|
MOCK_AUDIO.stop = mock_audio_stop;
|
|
MOCK_AUDIO.waveform = mock_waveform;
|
|
MOCK_AUDIO.envelope = mock_envelope;
|
|
MOCK_AUDIO.volume = mock_volume;
|
|
MOCK_AUDIO.voice_active = mock_voice_active;
|
|
|
|
MOCK_INPUT.self = &MOCK;
|
|
MOCK_INPUT.poll_key = mock_poll_key;
|
|
MOCK_INPUT.flush_keys = mock_flush_keys;
|
|
|
|
MOCK_SPRITES.self = &MOCK;
|
|
MOCK_SPRITES.define = mock_spr_define;
|
|
MOCK_SPRITES.define_shape = mock_spr_define_shape;
|
|
MOCK_SPRITES.define_file = mock_spr_define_file;
|
|
MOCK_SPRITES.show = mock_spr_show;
|
|
MOCK_SPRITES.move = mock_spr_move;
|
|
MOCK_SPRITES.configure = mock_spr_configure;
|
|
MOCK_SPRITES.shared_colors = mock_spr_shared_colors;
|
|
MOCK_SPRITES.collisions = mock_spr_collisions;
|
|
MOCK_SPRITES.solids = mock_spr_solids;
|
|
MOCK_SPRITES.shape = mock_spr_shape;
|
|
MOCK_SPRITES.solid = mock_spr_solid;
|
|
MOCK_SPRITES.contact = mock_spr_contact;
|
|
}
|
|
|
|
/** @brief Set what the next collisions() call will report. Bit n-1 is sprite n. */
|
|
__attribute__((unused))
|
|
static void mock_set_collisions(uint16_t mask)
|
|
{
|
|
MOCK.collisions = mask;
|
|
}
|
|
|
|
/** @brief Set what the next solids() call will report. Bit n-1 is sprite n. */
|
|
__attribute__((unused))
|
|
static void mock_set_solid_collisions(uint16_t mask)
|
|
{
|
|
MOCK.solidcollisions = mask;
|
|
}
|
|
|
|
/** @brief Set the contact the next contact() call will report. */
|
|
__attribute__((unused))
|
|
static void mock_set_contact(int what, double nx, double ny, double depth, int axis)
|
|
{
|
|
memset(&MOCK.contact, 0, sizeof(MOCK.contact));
|
|
MOCK.contact.what = what;
|
|
MOCK.contact.nx = nx;
|
|
MOCK.contact.ny = ny;
|
|
MOCK.contact.depth = depth;
|
|
MOCK.contact.axis = axis;
|
|
MOCK.hascontact = (what != 0);
|
|
}
|
|
|
|
/** @brief Prime the input backend with keystrokes GET will drain in order. */
|
|
__attribute__((unused))
|
|
static void mock_push_keys(const char *keys)
|
|
{
|
|
size_t i = 0;
|
|
|
|
MOCK.keycount = 0;
|
|
MOCK.keynext = 0;
|
|
for ( i = 0; keys != NULL && keys[i] != '\0' && MOCK.keycount < MOCK_MAX_KEYS; i++ ) {
|
|
MOCK.keys[MOCK.keycount] = (int)(unsigned char)keys[i];
|
|
MOCK.keycount += 1;
|
|
}
|
|
}
|
|
|
|
/** @brief Forget everything logged so far, keeping the primed state. */
|
|
__attribute__((unused))
|
|
static void mock_log_reset(void)
|
|
{
|
|
memset(MOCK.log, 0, sizeof(MOCK.log));
|
|
MOCK.used = 0;
|
|
MOCK.overflowed = false;
|
|
}
|
|
|
|
#endif // _AKBASIC_TEST_MOCKDEVICE_H_
|