18 Commits

Author SHA1 Message Date
42b60f725d Close the test gaps mutation testing found in audio and draw
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / mutation_test (push) Failing after 17s
A mutation smoke run scored src/audio.c at 50% and src/draw.c at 70% and named
three real holes:

- Nothing asserted that an unconfigured voice is audible, which is the entire
  reason the voice table defaults to a square wave at full level instead of a
  zeroed struct. Deleting the defaults survived.
- No envelope test used a non-zero attack and decay together, so the decay
  measuring from the start of the note rather than from the end of the attack
  survived.
- The circle was only checked at its four axis points, which a mis-signed
  octant reflection survives. It now checks that every plotted pixel has a
  mirror in the other three quadrants.

Also adds a backend that exists but was never given an SDL_Renderer, which is
the state a host is in between allocating one and initializing it; every draw
entry point has to report it rather than dereference it.

audio 50% -> 75%, draw 70% -> 90%. The survivors are recorded in TODO.md and
are honestly untestable from here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:30:18 -04:00
4208d9d471 Regenerate the coverage numbers
77.2% line and 83.1% function coverage across 2637 lines, up from 72.2% / 78.6%,
with the three new suites at 91% (audio), 95% (draw) and 58% (text). All 19
suites pass: the note recording character as an intentional failure was stale,
and neither tests/character.c nor src/character.c has changed since it was
written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:17:18 -04:00
f56f88710f Initialize the flood fill's dirty rectangle at its declaration
GCC cannot see that it is only read on a path where flood_region() wrote it,
and warns under -Wmaybe-uninitialized.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:15:08 -04:00
2a3ca48d8f Synthesise tones on three voices
There was no audio API at all: SDL3_mixer was vendored and AKGL_REGISTRY_MUSIC
was declared, but nothing opened a mixer, and a mixer plays recordings anyway.
BASIC 7.0's SOUND, PLAY, ENVELOPE and VOL describe notes, so this adds a tone
generator -- three voices, five waveforms, an ADSR envelope each, mixed to one
float stream and fed to an SDL_AudioStream.

The voice table works whether or not a device is open. akgl_audio_init connects
it to one; a host that owns its own audio pipeline can call akgl_audio_mix
instead. That is also what makes the tests deterministic: a device pulls samples
on SDL's audio thread whenever it likes, so tests/audio.c mixes by hand and only
opens a device in its last test.

Phase is computed from the frame counter rather than accumulated, because a
float increment of hz/44100 added 44100 times a second walks a held note off
pitch. A voice that has never been configured defaults to a square wave at full
level, since a zeroed voice would have a sustain of 0.0 and make no sound with
no error to explain why. Voices summing past full scale are clamped rather than
scaled, so one voice plays at the level it was asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:13:18 -04:00
dba0f8db89 Draw shapes immediately against a renderer
BASIC 7.0's graphics verbs -- DRAW, BOX, CIRCLE, PAINT, SSHAPE and GSHAPE -- all
plot against the current screen right now rather than adding to a scene, and
draw.h had exactly one function in it. This adds akgl_draw_point, _line, _rect,
_filled_rect, _circle, _flood_fill, _copy_region and _paste_region, each taking
the akgl_RenderBackend the host already initialized.

Color is an argument rather than a current-color global, so there is no second
copy of state to disagree with the caller's own, and each call restores the
renderer's draw color when it is done. SDL3 has no circle and no flood fill: the
circle is a midpoint circle, and the fill reads the target back, walks the
region with a fixed 4096-entry span stack, and blits back only the bounding box
of what changed. Exhausting that stack reports AKERR_OUTOFBOUNDS and says in the
header that the region is left partially filled.

tests/draw.c draws into a 64x64 software renderer under the dummy video driver
and reads the pixels back, so it needs no display and no offscreen harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:05:36 -04:00
1ddc64010a Let a caller take keystrokes without owning the event loop
akbasic's GET and GETKEY ask "is there a keystroke waiting, yes or no" and must
not require the interpreter to pump SDL events itself. akgl_controller_poll_key
drains one key per call from a fixed 32-entry ring that
akgl_controller_handle_event fills, so the host keeps pumping and the embedded
interpreter reads at its own pace. akgl_controller_flush_keys discards a
backlog.

The recording happens before the control-map scan, not after: that scan returns
as soon as a binding claims the event, so recording afterwards would have
dropped exactly the keys a game also acts on. A full buffer refuses the newest
keystroke rather than overwriting the oldest, so a caller reading a line gets
what was typed first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:57:50 -04:00
17e6e04c79 Measure rendered text without drawing it
akbasic needs the advance width of one character cell to build a terminal-style
text surface, and the wrapped size to know where a string crosses the right
margin. akgl_text_measure and akgl_text_measure_wrapped report both over
TTF_GetStringSize and TTF_GetStringSizeWrapped; neither needs a renderer, so
this half of the text subsystem is testable without the offscreen harness.

A negative wrap length is refused with AKERR_OUTOFBOUNDS: SDL_ttf reads it as a
very large unsigned width and quietly stops wrapping, which would return a
measurement that is wrong rather than an error the caller can see.

Also fixes akgl_text_loadfont, which checked name twice and passed an unchecked
filepath to TTF_OpenFont (TODO item 39).

The fixture font is a 10 KB monospaced ASCII subset of Liberation Mono, renamed
because "Liberation" is a Reserved Font Name; see
tests/assets/akgl_test_mono.LICENSE.txt. Monospaced so the suite can assert
width("AAAA") == 4 * width("A") rather than hardcode glyph metrics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:55:25 -04:00
5f03475e0f Record the API gaps blocking akbasic
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / mutation_test (push) Failing after 17s
akbasic is a C port of the BASIC interpreter being built to link into libakgl as
a scripting engine for game authors. Its interpreter core is complete and passes
its whole acceptance corpus against a stdio text sink; four gaps here block the
libakgl-backed sink and the graphics, sound and console verbs of Commodore
BASIC 7.0.

Filed rather than worked around downstream, because growing libakgl to serve a
consumer is the wanted outcome. Each entry says what the BASIC verb needs, what
the akgl_* entry point should look like, and what would cover it.

Only the first blocks work already designed and waiting. text.h can load a font
and render a string and cannot say how large that string will be, so there is no
way to build a terminal-style surface on it -- a cursor needs the advance width
of a cell and wrapping needs to know where a string crosses the margin. The
reference interpreter derived its whole character grid from SDL2_ttf's
SizeUTF8("A"). akgl_text_measure() over TTF_GetStringSize closes it.

The other three block verbs not yet started: draw.h declares one function and
src/draw.c is at 0%, so DRAW/BOX/CIRCLE/PAINT have nothing to plot with; there
is no audio API at all despite SDL3_mixer being vendored, and PLAY/ENVELOPE want
a synthesised voice SDL3_mixer does not provide; and controller.h is built
around event handlers the host pumps, which suits a game loop but not GET/GETKEY
-- those ask whether a keystroke is waiting and must not require the interpreter
to own the event loop, which goal 3 forbids anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:29:17 -04:00
6dfe7487ae Record the stale-build-tree coverage defect
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / mutation_test (push) Failing after 16s
Defects #13: gcovr searches for gcov data under --root, which
CMakeLists.txt:208 and :223 set to the source directory, so any
instrumented build tree left there is merged into the report. The
--object-directory argument beside each does not narrow the search; it
only maps gcda files back to the compiler's working directory.

A leftover build-coverage/ therefore fails coverage_reset before any
test runs, with gcovr unable to reconcile a function reported on two
different lines. The build*/ entry in .gitignore keeps the offending
tree out of git status, so the state is easy to get into and hard to
spot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:58:55 -04:00
c2b16d3c18 Upgrade to libakstdlib 0.1.0
Bump deps/libakstdlib five commits to 95e5002, which versions the library
at 0.1.0 with an soname and a ConfigVersion file, upgrades its own
libakerror to 1.0.0, namespaces its coverage target when embedded, and
raises its libc-wrapper coverage. The public header diff is purely
additive -- nothing removed, no signature changed -- so libakgl's nine
aksl_* call sites needed no edits. What broke was the build.

A default `cmake -S . -B build` stopped configuring:

    add_executable cannot create target "test_version" because another
    target with the same name already exists. The existing target is an
    executable created in source directory ".../deps/libakstdlib"

The versioning commit added tests/test_version.c to libakstdlib.
EXCLUDE_FROM_ALL keeps that target from being built, but
add_subdirectory still creates it, so it collided with the version suite
added in the preceding commit. Build libakgl's test programs as
akgl_test_<name> instead. The CTest names are unchanged -- ctest -R
version still selects it -- and a dependency is now free to ship a test
of any name.

Ask find_package for libakstdlib 0.1 rather than any version. 0.1.0
ships akstdlibConfigVersion.cmake with SameMinorVersion compatibility,
mirroring its libakstdlib.so.0.1 soname, so this accepts any 0.1.x and
refuses 0.2 and 1.0. Unversioned, the non-embedded path would have taken
an ABI-incompatible libakstdlib without complaint.

libakerror is deliberately left unversioned in find_package. It installs
akerrorConfig.cmake and akerrorTargets.cmake but no
akerrorConfigVersion.cmake, so find_package(akerror 1.0) fails against a
correct install rather than a stale one. Its floor stays with the #error
in include/akgl/error.h. Worth fixing upstream.

Verified: clean configure, build and 16/16 ctest; a -DAKGL_COVERAGE=ON
tree configures, builds and passes 18/18, so libakstdlib's embedded
coverage-target rename does not collide here. Against a temp-prefix
install of 95e5002, find_package(akstdlib) accepts 0.1 and 0.1.0 and
refuses 0.2 and 1.0, and find_package(akerror 1.0) fails for the missing
version file as described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:55:18 -04:00
2be9831c0c Ignore out-of-tree build directories
The first .gitignore entry was "./build/*". A leading ./ is not a valid
gitignore pattern, so it matched nothing and build/ turned up as
untracked in every git status. Use build*/, which also covers the
instrumented trees scripts/coverage.py and -DAKGL_COVERAGE=ON create.

Nothing under a build* path is tracked, so this hides no existing file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:42:41 -04:00
c5a7b6053d Derive the library version from one place and give the .so a soname
The version was written down twice and agreed with itself by luck.
AKGL_VERSION was hand-maintained in include/akgl/game.h, and nothing
connected it to the build, which had no version at all:

- project() declared no VERSION, so the @PROJECT_VERSION@ substitution
  in akgl.pc.in expanded to nothing and every installed akgl.pc shipped
  an empty Version field. pkg-config --modversion returned "" and
  --atleast-version could not work at all.
- libakgl.so had no VERSION or SOVERSION, so there was no soname and no
  symlink chain. A stale libakgl could be paired with new headers
  silently, which is the failure libakerror added its own soname to
  prevent.
- MY_LIBRARY_VERSION at CMakeLists.txt:288 was never set anywhere. It
  built main_lib_dest as "lib/akgl-", and main_lib_dest was then never
  read. Dead line, removed.

project(akgl VERSION 0.1.0) is now the only place the number appears. It
drives the generated include/akgl/version.h, the shared library VERSION
and SOVERSION, and the Version field in akgl.pc. Held at 0.1.0 to match
the constant it replaces, so akgl_game_load_versioncmp still compares
savegames against the same value.

include/akgl/version.h is generated by configure_file from version.h.in
and publishes AKGL_VERSION, AKGL_VERSION_MAJOR/MINOR/PATCH and
AKGL_VERSION_AT_LEAST(major, minor, patch). That macro is the point:
libakstdlib could not write that test against libakerror, which
publishes no version macro, and had to feature-test on
AKERR_FIRST_CONSUMER_STATUS instead. game.h now includes the generated
header rather than defining the version itself, so consumers see
AKGL_VERSION exactly where they saw it before.

While the major version is 0 the soname carries major.minor, giving
libakgl.so.0.1. A plain SOVERSION 0 would assert that 0.1.0 and 0.2.0
are ABI-interchangeable, and they are not -- the preceding commit alone
added akgl_error_init. At 1.0.0 the soname becomes the major alone,
matching libakerror.

akgl_version() in src/version.c reports the version of the libakgl that
is actually linked, where AKGL_VERSION reports what the caller compiled
against. Comparing the two is the only way to catch a stale shared
library on the loader path; the macro alone cannot see it. It returns a
string rather than an akerr_ErrorContext * because it cannot fail, for
the same reason akerr_name_for_status does.

Add tests/version.c: the string and the numeric components must describe
one version, the linked library must agree with the headers, and
AKGL_VERSION_AT_LEAST must order correctly at the major, minor and patch
boundaries. Verified out of band that bumping project() to 1.2.3 moves
version.h, akgl.pc and the soname together, and that an install produces
the libakgl.so -> .so.0.1 -> .so.0.1.0 chain with SONAME libakgl.so.0.1.

Also ignore include/akgl/version.h. include/ precedes the build tree on
the include path, so a stray copy there would shadow the generated one
and pin every consumer to whatever version it was generated at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:36:50 -04:00
9b124f2e27 Migrate to the libakerror 1.0.0 status registry
libakerror 1.0.0 replaced the consumer-sized status-name array with a
private registry and made status-code ownership explicit and enforced.
AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, and the registry
entry points raise akerr_ErrorContext * instead of returning int. See
deps/libakerror/UPGRADING.md.

The break was not only source-level. libakgl's codes sat at
AKERR_LAST_ERRNO_VALUE + 18 through + 22, and 1.0.0 claimed exactly
those five offsets for its own AKERR_STATUS_* registry codes, so every
AKGL_ERR_* was aliasing a libakerror status. HANDLE(e,
AKGL_ERR_LOGICINTERRUPT) at physics.c:222 would have swallowed a
foreign-name refusal.

- Move the band to AKERR_FIRST_CONSUMER_STATUS (256) as fixed offsets,
  so a libc that grows an errno cannot move the codes, and add
  AKGL_ERR_OWNER, AKGL_ERR_LIMIT and AKGL_ERR_COUNT to describe it.
- Reserve the range and register the names through the owned entry
  points, PASS-ing each: these are AKERR_NOIGNORE, and the old
  akerr_name_for_status calls discarded failure silently.
- Drop the AKERR_MAX_ERR_VALUE=256 compile definition.
- Guard on AKERR_FIRST_CONSUMER_STATUS in include/akgl/error.h, which
  now includes <akerror.h> so the guard is reliable. The embedded build
  is fine, but the find_package path can pick up a stale installed
  header, and 1.0.0 has an soname, so that pairing is an ABI mismatch
  rather than a compile problem. Same guard libakstdlib already carries.

Registration also moves out of akgl_heap_init into a new akgl_error_init
in src/error.c. It was in the heap pool's initializer only because that
was the first thing akgl_game_init called, and the upgrade turned five
fire-and-forget name calls into a library-wide ownership claim that can
fail. That placement was hiding a defect: game.c raises AKGL_ERR_SDL
when SDL_CreateMutex fails, five lines before akgl_heap_init ran, so the
earliest error path in the library was guaranteed to print "Unknown
Error". akgl_error_init is now the first statement in akgl_game_init.

Callers that drive subsystems directly must call akgl_error_init first;
it is idempotent, so ordering it precisely is not required. The eleven
test suites that relied on akgl_heap_init to name their statuses now
call it explicitly, or their failure messages would have degraded to
"Unknown Error".

Add tests/error.c: assert every code reads back its registered name,
that the name table and AKGL_ERR_COUNT agree, that a foreign owner is
refused with AKERR_STATUS_NAME_FOREIGN and AKERR_STATUS_RANGE_OVERLAP,
and that repeating the init is a no-op. That last one is a live
constraint, not a triviality -- libakerror treats only an identical
reservation as a repeat, so a subset or superset raises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:20:28 -04:00
b014eb2360 Document git hook installation in the README
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 19s
libakgl CI Build / mutation_test (push) Failing after 16s
The pre-commit hook is version controlled under scripts/hooks/, but Git
configuration is not cloned, so the hook does nothing until each clone is
pointed at it. Nothing said so anywhere a new contributor would look.

Cover installation, verification, what the hook actually does, the Emacs
requirement, bypassing, and uninstalling.

Two things worth calling out explicitly:

- core.hooksPath replaces the hooks directory outright rather than adding
  to it, so any pre-existing .git/hooks entries silently stop firing.
  Document a symlink alternative for anyone who keeps local hooks.
- The documented --check exit codes are 0 conforming, 1 non-conforming,
  2 environment failure, so a CI job failing on any non-zero status will
  not mistake a missing Emacs for a clean tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:22:47 -04:00
e423f9594e Accept absolute paths in reindent.sh --check
`--check` prefixed every path with the repository root unconditionally, so
passing an absolute path produced a bogus `$root//abs/path`, cp failed, and
the subsequent cmp reported the file as non-conforming. The exit status
happened to be 1, which looked correct while being reached for the wrong
reason.

In-place mode was unaffected -- it cds to the root and hands the list
straight to Emacs, where absolute paths resolve normally, which is why the
pre-commit hook worked.

Normalize both the copy and the compare through an abspath helper so
relative and absolute paths behave identically in either mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:22:47 -04:00
772f960865 Record internal consistency findings and the controller DB defect
Add an "Internal consistency" section to TODO.md: 41 numbered findings
from a sweep of src/ and include/, each cited by file:line and grouped by
blast radius. These are convention problems, not a re-audit of behavior,
but several have live functional consequences:

- AKGL_TIME_ONESEC_MS is misnamed. Its value is nanoseconds-per-
  millisecond, but akgl_game_state_lock uses it as a one-second budget
  against a 100ms delay loop, so the lock blocks ~16 minutes.
- SUCCEED_RETURN inside an ATTEMPT block at tilemap.c:52 bypasses the
  CLEANUP two lines below, leaking two heap strings on every successful
  tilemap property lookup, against a 256-entry pool.
- AKGL_ACTOR_STATE_STRING_NAMES is declared [33] and defined [32], and
  names bits 11 and 12 as UNDEFINED where actor.h defines MOVING_IN and
  MOVING_OUT, so no character JSON can bind a sprite to those states.
- 19 non-static functions are defined in src/ but declared in no header,
  and akgl_game_init_screen is declared but never defined.
- AKGL_COLLIDE_RECTANGLES has unbalanced parentheses and cannot compile.
- text.c:19 checks `name` twice and never validates `filepath`.

Item 30 is marked resolved by the reindent in the preceding commit.

Also add Defects #12: a failed controller-DB fetch silently destroys the
tracked fallback. include/akgl/SDL_GameControllerDB.h is committed on
purpose so the library still builds if upstream disappears, but
mkcontrollermappings.sh has no `set -e` and never checks curl's exit
status -- on failure it overwrites the good header with
AKGL_SDL_GAMECONTROLLER_DB_LEN 0 and an empty initializer, and exits 0.
Verified against an unresolvable host. CMakeLists.txt:89-93 compounds
this: the custom command's OUTPUT is relative, so CMake resolves it
against the binary dir while the script writes to the source dir, leaving
the command permanently out of date and re-running on every build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:58 -04:00
28bde4176d Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.

Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.

Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.

The tree is now a fixed point of `scripts/reindent.sh --check`.

include/akgl/SDL_GameControllerDB.h is generated and excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
8d21d5c7dd Codify the canonical C style and add reindent tooling
AGENTS.md described style in one paragraph that said little more than
"follow the surrounding code", which was not actionable once files had
drifted apart. Replace it with an explicit specification.

The canonical style is Emacs cc-mode "stroustrup" with tabs enabled: a
4-column offset with 8-column tabs, so depth 1 is four spaces, depth 2 is
one tab, depth 3 is a tab plus four spaces. Most of src/ already followed
this; what looked like randomly mixed tabs and spaces was simply cc-mode
output. Document the byte ladder explicitly, since editors that expand
tabs or assume a 4-column tab silently corrupt it.

Rules beyond indentation are derived from counted majorities in the
existing code rather than invented: errctx over e (92 to 45), padded
control parens (140 to 7), pointer binding to the identifier (486 to 5),
and always-brace (only four unbraced bodies exist). Brace and else
placement are called out as house conventions that cc-mode does not
enforce, so "run the indenter" and "follow the guide" cannot conflict.

Also record the naming, error-handling, and API-surface rules that a
consistency sweep showed were being broken silently -- in particular that
a *_RETURN macro inside an ATTEMPT block skips CLEANUP.

Add the tooling to apply it:

  .dir-locals.el          applies the style to every c-mode buffer
  scripts/reindent.el     batch reindent via Emacs
  scripts/reindent.sh     reindent or --check the tree
  scripts/hooks/pre-commit  reindents staged sources

reindent.el deliberately avoids Emacs' tabify: its tabify-regexp is
" [ \t]+", which is not anchored to the start of a line, so it rewrites
runs of spaces anywhere and destroys the hand-aligned value columns in
the bit-flag tables in actor.h and iterator.h. Only leading whitespace is
ever rewritten, and lines starting inside a string literal are skipped.

The hook checks staged content rather than the working tree, so what is
committed is what was verified. It re-stages a fixed file only when the
index and working tree agree, otherwise a partial `git add -p` would
sweep unstaged work into the commit.

Enable with: git config core.hooksPath scripts/hooks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:26 -04:00
63 changed files with 5571 additions and 749 deletions

16
.dir-locals.el Normal file
View File

@@ -0,0 +1,16 @@
;;; Directory Local Variables -*- no-byte-compile: t -*-
;;; See AGENTS.md -> "Coding Style" for the rationale.
;;;
;;; The canonical style is cc-mode "stroustrup" with tabs enabled: 4 columns per
;;; level, tabs 8 columns wide, so depth 1 is four spaces, depth 2 is one tab,
;;; depth 3 is a tab plus four spaces. A correctly formatted file is a fixed
;;; point of `indent-region' under these settings.
;;;
;;; Reindent a whole file with C-x h C-M-\, or the tree with
;;; `scripts/reindent.sh'.
((c-mode . ((c-file-style . "stroustrup")
(indent-tabs-mode . t)
(tab-width . 8)
(fill-column . 100)
(require-final-newline . t))))

10
.gitignore vendored
View File

@@ -1,3 +1,11 @@
./build/* # A leading ./ is not a valid gitignore pattern, so "./build/*" matched nothing
# and every out-of-tree build tree showed up as untracked. This also covers the
# instrumented trees scripts/coverage.py and the AKGL_COVERAGE option create.
build*/
.aider* .aider*
*~ *~
# Generated by configure_file into the build tree. include/ precedes the build
# tree on the include path, so a stray copy here would silently shadow the real
# one and pin every consumer to whatever version it was generated at.
include/akgl/version.h

259
AGENTS.md
View File

@@ -2,7 +2,43 @@
## Project Structure & Module Organization ## Project Structure & Module Organization
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/`, generated `akgl.pc` files, and `include/akgl/SDL_GameControllerDB.h` as build outputs rather than hand-maintained source. This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/` and generated `akgl.pc` files as build outputs rather than hand-maintained source; `include/akgl/SDL_GameControllerDB.h` is generated too, but is tracked on purpose — see below.
## Generated and Vendored Sources
### `include/akgl/SDL_GameControllerDB.h` is generated, and is tracked deliberately
`mkcontrollermappings.sh` regenerates this header by fetching the community
controller database from `raw.githubusercontent.com`. **It is committed to the
repository on purpose**: it is the offline fallback that keeps the library
buildable if upstream is renamed, rate-limited, taken down, or simply
unreachable from the build machine. Do not delete it, do not add it to
`.gitignore`, and do not "clean up" the fact that a generated file is tracked.
Working rules:
- **Never hand-edit it.** It is machine-written; changes belong in
`mkcontrollermappings.sh`.
- **Do not commit incidental regeneration churn.** The build regenerates the
header on every run, and the script stamps `$(date)` into a comment, so an
ordinary build leaves the file modified with nothing of substance changed.
Revert that before committing: `git checkout -- include/akgl/SDL_GameControllerDB.h`.
- **Update it as a deliberate, standalone commit** when you actually want newer
mappings, so the diff is reviewable and bisectable.
- **Never commit a copy with `AKGL_SDL_GAMECONTROLLER_DB_LEN 0`.** That is the
signature of a failed fetch, not an empty upstream — see the defect note
below. Check the constant before staging this file.
- Formatting tooling ignores it: `scripts/reindent.sh` and the pre-commit hook
both skip it.
> **Known defect (tracked in `TODO.md`).** The safety net is currently
> self-defeating. `mkcontrollermappings.sh` has no `set -e` and does not check
> `curl`'s exit status, so when the fetch fails it writes a header with
> `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array **over the good tracked
> copy, and exits 0**. Because CMake re-runs the generator on every build, an
> offline build silently destroys the very fallback the file exists to provide.
> Until that is fixed, treat a dirty `SDL_GameControllerDB.h` after a build as
> suspect and check the length constant before committing it.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
@@ -30,9 +66,226 @@ Run mutation testing with `cmake --build build --target mutation`. For a quick s
Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang. Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang.
## Coding Style & Naming Conventions ## Coding Style
Follow the surrounding C style: braces on the next line for function bodies, spaces inside control-flow parentheses, and short, focused functions. Preserve the indentation of the file being edited; older files contain both spaces and tabs. Public symbols use the `akgl_` prefix, public types use `akgl_TypeName`, and constants/macros use `AKGL_UPPER_SNAKE_CASE`. Name matching header/source pairs by feature, such as `include/akgl/sprite.h` and `src/sprite.c`. No repository-wide formatter or linter is configured, so avoid unrelated formatting churn. The canonical style is Emacs `cc-mode` **`stroustrup`**, with tabs enabled. This
is not advisory — new and edited code must match what `cc-mode` produces, so that
reindenting a region never shows up as a diff.
### Emacs setup
`.dir-locals.el` in the repository root already applies the style to every
`c-mode` buffer, so nothing needs configuring by hand:
```elisp
((c-mode . ((c-file-style . "stroustrup")
(indent-tabs-mode . t)
(tab-width . 8)
(fill-column . 100)
(require-final-newline . t))))
```
Interactively: `C-x h C-M-\` (`mark-whole-buffer` + `indent-region`) reindents
the buffer. A correctly formatted file is a fixed point of that operation —
reindenting must produce no change.
### Reindenting from the command line
```sh
scripts/reindent.sh # reindent every tracked C source in place
scripts/reindent.sh src/tilemap.c # reindent only the named files
scripts/reindent.sh --check # list non-conforming files, change nothing
```
`--check` exits 1 when something needs reindenting and 2 if Emacs is missing, so
it is usable from CI. The script drives Emacs in batch mode through
`scripts/reindent.el`, which reindents, normalises leading whitespace to the
canonical tab/space mix, strips trailing whitespace, and ensures a final
newline. `include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
Note that `reindent.el` deliberately does **not** use Emacs' `tabify`: that
function's `tabify-regexp` is `" [ \t]+"`, which is not anchored to the start of
a line, so it rewrites runs of spaces anywhere and destroys the hand-aligned
value columns in the bit-flag tables in `actor.h` and `iterator.h`. Only leading
whitespace is ever rewritten.
### The pre-commit hook
`scripts/hooks/pre-commit` reindents staged C sources before the commit is
written. Enable it once per clone:
```sh
git config core.hooksPath scripts/hooks
```
It inspects the *staged* content rather than the working tree, so what lands in
the commit is what was checked. When a file needs reindenting it is fixed in the
working tree and re-staged — but only if the index and working tree agree for
that file. If they differ, re-staging would sweep unstaged work into the commit,
so the hook stops and tells you to reindent and stage it yourself. It steps
aside during a merge, and warns rather than blocking if Emacs is unavailable.
`git commit --no-verify` bypasses it.
For reference, `cc-mode` defines the style as:
```elisp
("stroustrup"
(c-basic-offset . 4)
(c-comment-only-line-offset . 0)
(c-offsets-alist . ((statement-block-intro . +)
(substatement-open . 0)
(substatement-label . 0)
(label . 0)
(statement-cont . +))))
```
### Indentation and whitespace
- **4 columns per level** (`c-basic-offset` 4).
- **Tabs are 8 columns wide and are used for indentation** (`indent-tabs-mode`
`t`, `tab-width` 8). Emacs emits the largest possible run of tabs and pads the
remainder with spaces, which produces this ladder. Editors that expand tabs, or
that assume a 4-column tab, will silently corrupt it:
| Depth | Columns | Bytes |
|---|---|---|
| 1 | 4 | 4 spaces |
| 2 | 8 | `TAB` |
| 3 | 12 | `TAB` + 4 spaces |
| 4 | 16 | `TAB` `TAB` |
| 5 | 20 | `TAB` `TAB` + 4 spaces |
- No trailing whitespace. Files end with a single newline.
- `case` labels sit at the same column as their `switch` (`label . 0`).
- Continuation lines of a wrapped expression indent one level (`statement-cont . +`).
Most of `src/` already conforms. The known non-conforming files are
`src/json_helpers.c`, `src/util.c` (from `akgl_rectangle_points` onward),
`src/assets.c`, `src/staticstring.c`, parts of `src/actor.c`, and the headers
`include/akgl/util.h` and `include/akgl/staticstring.h` — all of which use a
2-column offset. Convert a file to the canonical style in its own commit, not
mixed into a behavioral change.
### Braces
- **Function bodies open on their own line, in column 0.**
- **Control statements keep the brace on the same line**, one space before it.
- **`else`, `else if`, and `while` of a `do`/`while` stay on the closing-brace
line**: `} else {`, not a bare `else` on the next line. Note that this is a
house convention rather than something `cc-mode` enforces — the `stroustrup`
style governs indentation only and will not move a brace or an `else` for you.
- **Always brace, even single-statement bodies.**
```c
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
{
if ( obj->curSpriteFrameId == 0 ) {
obj->curSpriteReversing = false;
} else if ( obj->parent != NULL ) {
obj->curSpriteFrameId -= 1;
} else {
obj->curSpriteFrameId += 1;
}
}
```
### Spacing
- **Spaces inside control-flow parentheses**: `if ( x == y ) {`, `while ( done == false ) {`,
`for ( i = 0; i < len; i++ ) {`. This is the dominant existing convention
(140 sites to 7) and `cc-mode` will not add or remove it.
- No space between a function name and its argument list, at both call and
definition sites, and no padding inside call parentheses: `SDL_Log("x %d", n)`.
- Binary operators and assignment are surrounded by single spaces; unary
operators are not separated from their operand.
- **The pointer `*` binds to the identifier**, not the type: `char *name`,
`akgl_Actor **dest`. Never `char* name`.
- When a call is too long for one line, put each argument on its own line
indented one level past the callee, with the closing `)` on its own line. This
is the established shape for the `FAIL_*` and `CATCH` macros:
```c
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
AKERR_KEY,
"Unable to add actor to registry"
);
```
### No repository-wide formatter
There is no `clang-format` or linter wired into the build, and `cc-mode`'s
indentation engine is not exactly reproducible by `clang-format`. Do not
introduce one without discussion, and do not reformat code you are not otherwise
changing — unrelated whitespace churn makes review harder and is the reason the
style drifted in the first place.
## Naming Conventions
- **Public functions**: `akgl_<subsystem>_<verb>`, all lower snake_case —
`akgl_actor_set_character`, `akgl_heap_next_string`. No camelCase, and never
embed a type name (`akgl_Actor_cmhf_left_on` is wrong; it should be
`akgl_actor_cmhf_left_on`).
- **Public types**: `akgl_TypeName` — `akgl_Actor`, `akgl_SpriteSheet`,
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix;
bare names like `point` and `RectanglePoints` are defects, not precedent.
- **Constants and macros**: `AKGL_UPPER_SNAKE_CASE`. A constant belongs to the
subsystem it describes: a character limit is `AKGL_CHARACTER_MAX_*`, not
`AKGL_SPRITE_MAX_CHARACTER_*`. Name a constant for what its value *is* — a
nanoseconds-per-millisecond scale factor is `AKGL_TIME_ONEMS_NS`.
- **Exported globals** take the `akgl_` prefix like any other public symbol. Do
not add bare names (`renderer`, `camera`, `window`), leading-underscore names
(reserved at file scope), or SCREAMING_SNAKE names for mutable objects —
`AKGL_UPPER_SNAKE_CASE` is for constants.
- **`static` helpers drop the `akgl_` prefix**, which exists only to avoid
external collisions.
- **Include guards**: `_AKGL_<FILE>_H_`, matching the file name. Guard names
without the project prefix risk colliding with system headers.
- **Parameter names must match between the declaration and the definition** —
Doxygen publishes the header spelling. Conventional names: `dest` for an
output parameter (not `dst`), `self` for a backend receiver, `obj` for the
instance being initialized or inspected, `e` for an incoming error context to
be inspected.
- **The local error context is named `errctx`** (92 sites to 45). New code uses
`errctx`; convert `e` when you touch a function for other reasons.
- **Header/source pairs are named by feature**: `include/akgl/sprite.h` and
`src/sprite.c`.
## Error-Handling Protocol
The `akerror` control-flow macros have a shape that must be followed exactly,
because the failure mode is silent.
- Inside an `ATTEMPT` block use the **`_BREAK`** variants (`FAIL_ZERO_BREAK`,
`FAIL_NONZERO_BREAK`, `FAIL_BREAK`) and `CATCH`. Outside it use the
**`_RETURN`** variants.
- **Never use a `*_RETURN` macro inside an `ATTEMPT` block.** It returns past the
`CLEANUP` block, so every release, `fclose`, and free in `CLEANUP` is skipped.
This has already caused a heap-string leak on the success path of
`akgl_get_json_tilemap_property`.
- `CLEANUP` must precede `PROCESS`. Transposing them moves the cleanup body into
the `PROCESS` switch, where it runs only when an error context exists.
- `CATCH` reports failure by `break`ing, which binds to the innermost enclosing
loop or `switch`. A `CATCH` written directly inside a `while` exits the loop
rather than the function — put the `ATTEMPT` block inside the loop.
- Validate every pointer parameter before dereferencing it, including the ones a
sibling function happens not to check.
## API Surface
Every function with external linkage must be declared in a header, and every
declaration must have a definition. A non-`static` function that appears in no
header is still in the ABI but is unreachable by callers; a declaration with no
definition is a link error for anyone compiling against the header alone. If a
function is exposed only so tests can reach it, declare it under the existing
"part of the internal API" comment block in the relevant header.
Headers must be self-contained: include what you use, so that a translation unit
including a single `akgl` header compiles without a particular include order.
Within the project use the angled form, `#include <akgl/sibling.h>`.
Declare no-argument functions as `(void)`, not `()`.
## Rules ## Rules

View File

@@ -1,5 +1,8 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
project(akgl LANGUAGES C) # The single source of truth for the library version. It drives the generated
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
# Version field in akgl.pc. Bump it here and nowhere else.
project(akgl VERSION 0.1.0 LANGUAGES C)
include(CTest) include(CTest)
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF) option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
@@ -39,10 +42,8 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
# Reserve error-name table entries for libakgl-specific status codes. # libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
target_compile_definitions(akerror PUBLIC # libakgl claims its status codes at runtime in akgl_heap_init() instead.
AKERR_MAX_ERR_VALUE=256
)
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE) set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
@@ -62,11 +63,19 @@ else()
if(NOT TARGET SDL3_ttf::SDL3_ttf) if(NOT TARGET SDL3_ttf::SDL3_ttf)
find_package(SDL3_ttf REQUIRED) find_package(SDL3_ttf REQUIRED)
endif() endif()
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
# for one makes find_package reject every install. The floor is enforced by
# the #error in include/akgl/error.h instead, which feature-tests
# AKERR_FIRST_CONSUMER_STATUS.
if(NOT TARGET akerror::akerror) if(NOT TARGET akerror::akerror)
find_package(akerror REQUIRED) find_package(akerror REQUIRED)
endif() endif()
# 0.1 rather than bare: libakstdlib 0.1.0 ships an akstdlibConfigVersion.cmake
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
# any 0.1.x and refuses 0.2 and 1.0. Unversioned, this path would silently
# accept an ABI-incompatible libakstdlib.
if(NOT TARGET akstdlib::akstdlib) if(NOT TARGET akstdlib::akstdlib)
find_package(akstdlib REQUIRED) find_package(akstdlib 0.1 REQUIRED)
endif() endif()
if(NOT TARGET jansson::jansson) if(NOT TARGET jansson::jansson)
find_package(jansson) find_package(jansson)
@@ -80,6 +89,8 @@ set(exec_prefix "\${prefix}")
set(libdir "\${exec_prefix}/lib") set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include") set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside # Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
# test executables in every out-of-tree build. # test executables in every out-of-tree build.
@@ -98,10 +109,12 @@ add_library(akgl SHARED
deps/semver/semver.c deps/semver/semver.c
src/actor.c src/actor.c
src/actor_state_string_names.c src/actor_state_string_names.c
src/audio.c
src/text.c src/text.c
src/assets.c src/assets.c
src/character.c src/character.c
src/draw.c src/draw.c
src/error.c
src/game.c src/game.c
src/controller.c src/controller.c
src/heap.c src/heap.c
@@ -113,21 +126,41 @@ add_library(akgl SHARED
src/staticstring.c src/staticstring.c
src/tilemap.c src/tilemap.c
src/util.c src/util.c
src/version.c
${GAMECONTROLLERDB_H} ${GAMECONTROLLERDB_H}
) )
# While the major version is 0 the ABI is not stable across minor releases, so
# the soname carries major.minor -- libakgl.so.0.1. A plain SOVERSION 0 would
# claim 0.1.0 and 0.2.0 are interchangeable, which is exactly the silent
# mispairing the soname is here to prevent. At 1.0.0 this becomes the major
# alone, matching libakerror.
if(PROJECT_VERSION_MAJOR EQUAL 0)
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
else()
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}")
endif()
set_target_properties(akgl PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${AKGL_SOVERSION}
)
add_library(akgl::akgl ALIAS akgl) add_library(akgl::akgl ALIAS akgl)
add_executable(charviewer util/charviewer.c) add_executable(charviewer util/charviewer.c)
add_executable(test_semver_unit deps/semver/semver_unit.c) add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
# Every suite here is a standalone C program named tests/<name>.c, built as # Every suite here is a standalone C program named tests/<name>.c, built as
# test_<name> and registered with CTest under <name>. # akgl_test_<name> and registered with CTest under <name>.
set(AKGL_TEST_SUITES set(AKGL_TEST_SUITES
actor actor
audio
bitmasks bitmasks
character character
controller controller
draw
error
game game
heap heap
json_helpers json_helpers
@@ -135,16 +168,22 @@ set(AKGL_TEST_SUITES
registry registry
sprite sprite
staticstring staticstring
text
tilemap tilemap
util util
version
) )
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
# it from being built, so an unprefixed test_version here is a configure error.
foreach(suite IN LISTS AKGL_TEST_SUITES) foreach(suite IN LISTS AKGL_TEST_SUITES)
add_executable(test_${suite} tests/${suite}.c) add_executable(akgl_test_${suite} tests/${suite}.c)
add_test(NAME ${suite} COMMAND test_${suite}) add_test(NAME ${suite} COMMAND akgl_test_${suite})
endforeach() endforeach()
add_test(NAME semver_unit COMMAND test_semver_unit) add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
set_tests_properties( set_tests_properties(
${AKGL_TEST_SUITES} semver_unit ${AKGL_TEST_SUITES} semver_unit
@@ -155,6 +194,9 @@ set_tests_properties(
target_include_directories(akgl PUBLIC target_include_directories(akgl PUBLIC
include/ include/
deps/semver/ deps/semver/
# akgl/version.h is generated by configure_file, so it lives in the build tree
# rather than beside the headers it is included from.
${CMAKE_CURRENT_BINARY_DIR}/include/
) )
if(AKGL_COVERAGE) if(AKGL_COVERAGE)
@@ -209,8 +251,8 @@ target_link_libraries(akgl
) )
foreach(suite IN LISTS AKGL_TEST_SUITES) foreach(suite IN LISTS AKGL_TEST_SUITES)
target_link_libraries(test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm) target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
target_include_directories(test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests") target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
endforeach() endforeach()
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm) target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
@@ -230,7 +272,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
"$<TARGET_FILE_DIR:akstdlib::akstdlib>" "$<TARGET_FILE_DIR:akstdlib::akstdlib>"
) )
foreach(suite IN LISTS AKGL_TEST_SUITES) foreach(suite IN LISTS AKGL_TEST_SUITES)
set_target_properties(test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}") set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
endforeach() endforeach()
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}") set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
@@ -285,14 +327,15 @@ if(Python3_FOUND)
) )
endif() endif()
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
install(TARGETS akgl DESTINATION "lib/") install(TARGETS akgl DESTINATION "lib/")
install(FILES "deps/semver/semver.h" DESTINATION "include/") install(FILES "deps/semver/semver.h" DESTINATION "include/")
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/") install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/") install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/") install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/") install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/") install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/") install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/") install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")

View File

@@ -270,6 +270,98 @@ PASS(e, akgl_heap_release_string(width));
``` ```
## Git hooks
The repository ships a `pre-commit` hook that keeps committed C sources in the
project's canonical format (Emacs `cc-mode` "stroustrup"; see `AGENTS.md` for the
full style guide). The hook lives in `scripts/hooks/` and is version controlled,
but **Git configuration is not cloned**, so every clone has to be pointed at it
once:
```sh
git config core.hooksPath scripts/hooks
```
Confirm it took effect:
```sh
git rev-parse --git-path hooks # should print: scripts/hooks
```
That is the whole installation. The hook is already committed with its
executable bit set, so nothing needs `chmod`.
### What the hook does
On each commit it looks at the **staged** content of any added, copied, modified,
or renamed `.c`/`.h` file under `src/`, `include/`, `tests/`, or `util/`, and
reindents it if it does not already match the canonical style. Checking the
staged content rather than the working tree means what lands in the commit is
what was actually verified.
When a file needs reindenting, the hook fixes it in the working tree and
re-stages it — but only when the index and working tree agree for that file. If
they differ, you have staged part of a file with `git add -p`, and re-staging
would sweep your unstaged work into the commit. Rather than do that silently the
hook stops and prints the commands to run yourself:
```sh
scripts/reindent.sh path/to/file.c
git add path/to/file.c
```
`include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
### Requirements
The hook drives Emacs in batch mode, because `cc-mode`'s indentation engine is
the definition of the style and `clang-format` cannot reproduce it exactly. If
`emacs` is not on `PATH` the hook prints a warning and allows the commit — a
hook that refuses to run without an optional tool only teaches people to reach
for `--no-verify`. Install Emacs to get the check; without it, formatting is on
you.
### Bypassing and uninstalling
Skip the hook for a single commit:
```sh
git commit --no-verify
```
Remove it entirely:
```sh
git config --unset core.hooksPath
```
### If you already have local hooks
`core.hooksPath` **replaces** the hooks directory outright — once it is set, Git
stops reading `.git/hooks/` altogether, so any hooks you keep there will silently
stop firing. If that matters, leave `core.hooksPath` unset and symlink just this
one hook instead:
```sh
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
```
### Formatting without the hook
The hook is a convenience, not the source of truth. The same check is available
directly, and is what you would run in CI:
```sh
scripts/reindent.sh --check # list non-conforming files; exit 1 if any
scripts/reindent.sh # reindent every tracked C source in place
scripts/reindent.sh src/game.c # reindent specific files
```
`--check` exits `0` when everything conforms, `1` when a file needs reindenting,
and `2` if Emacs is missing or the script cannot run — so a CI job that treats
any non-zero status as failure will not mistake a broken toolchain for a clean
tree.
## Mutation testing ## Mutation testing
The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default. The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default.

662
TODO.md
View File

@@ -1,5 +1,372 @@
# TODO # TODO
## Internal consistency
Findings from a sweep of `src/` and `include/`. These are consistency and
convention problems, not new functional defects — where one has a functional
consequence it is called out. Ordered roughly by blast radius. Items that
overlap the existing **Defects** list are cross-referenced rather than repeated.
### 1. Public naming conventions
1. **Include guards use three different schemes.** `_AKGL_ACTOR_H_`,
`_AKGL_CHARACTER_H_`, `_AKGL_GAME_H_`, `_AKGL_HEAP_H_`, `_AKGL_ITERATOR_H_`,
`_AKGL_SPRITE_H_`, and `_AKGL_TYPES_H_` carry the project prefix; `_ASSETS_H_`,
`_CONTROLLER_H_`, `_DRAW_H_`, `_ERROR_H_`, `_JSON_HELPERS_H_`, `_PHYSICS_H_`,
`_REGISTRY_H_`, `_RENDERER_H_`, `_TEXT_H_`, `_TILEMAP_H_`, and `_UTIL_H_` do
not. Pick `_AKGL_<FILE>_H_` everywhere.
The worst case is `include/akgl/staticstring.h:6`, which guards with
`_STRING_H_` — a name several libc implementations use for their own
`<string.h>`. The same file then does `#include "string.h"` (line 9) with
quotes, which is a relative-first lookup that only reaches the system header
by accident. Rename the guard to `_AKGL_STATICSTRING_H_` and use
`#include <string.h>`.
2. **Function names contradict the stated convention.** `AGENTS.md` says public
symbols use `akgl_` and types use `akgl_TypeName`. Exceptions:
- `akgl_Actor_cmhf_left_on` and its seven siblings (`include/akgl/actor.h:196-252`)
embed the *type* name in a *function* name, unlike every other actor entry
point (`akgl_actor_*`). Rename to `akgl_actor_cmhf_*`.
- `akgl_game_updateFPS` (`include/akgl/game.h:104`) is camelCase; every other
function is snake_case.
- `akgl_render_init2d` (`include/akgl/renderer.h:83`) puts the `2d` at the end
while the six functions it installs are `akgl_render_2d_*`. Make it
`akgl_render_2d_init`.
- `akgl_sprite_sheet_coords_for_frame` (`include/akgl/sprite.h:86`) spells it
`sprite_sheet`; `akgl_spritesheet_initialize` and `akgl_heap_next_spritesheet`
spell it `spritesheet`.
3. **Two public types have no prefix at all.** `point` and `RectanglePoints`
(`include/akgl/util.h:13-25`) are unprefixed, and `RectanglePoints` is
PascalCase with no namespace. Both are dumped into every translation unit
that includes `util.h`. Rename to `akgl_Point` / `akgl_RectanglePoints`.
4. **Global variables use four different conventions.** `window`, `bgm`, `game`,
`gamemap`, `renderer`, `physics`, and `camera` (`src/game.c:26-43`) are
unprefixed single common words exported from a shared library — `renderer`
and `camera` in particular are very likely to collide with a consuming game.
Alongside them the same file exports `akgl_mixer` and `akgl_tracks` (prefixed),
`_akgl_renderer` / `_akgl_camera` / `_akgl_physics` / `_akgl_gamemap`
(underscore-prefixed, which is reserved at file scope), and elsewhere
`HEAP_ACTOR``HEAP_STRING` (`src/heap.c:17-21`) and `GAME_ControlMaps`
(`src/controller.c:13`) are SCREAMING_SNAKE, which the convention reserves for
constants and macros. Settle on `akgl_` for all exported objects.
5. **`AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH`** (`include/akgl/character.h:13`) is a
character constant carrying the `SPRITE` prefix, and lives in `character.h`.
Rename to `AKGL_CHARACTER_MAX_NAME_LENGTH`.
6. **`AKGL_TIME_ONESEC_MS` is misnamed and the error is live.**
`include/akgl/game.h:22` defines it as `1000000`. One second in milliseconds
is `1000`; `1000000` is the number of nanoseconds in a *millisecond*. The name
says "one second" but the value means "one millisecond", so:
- `src/character.c:209` and `src/sprite.c:141` use it correctly as a
milliseconds-to-nanoseconds scale factor.
- `src/game.c:136` uses it as documented — a one-second budget for the state
lock — in a loop that advances `totaltime += 100` alongside `SDL_Delay(100)`.
The loop therefore runs 10,000 iterations of 100 ms, so `akgl_game_state_lock`
blocks for roughly 16 minutes rather than 1 second before reporting failure.
Rename to `AKGL_TIME_ONEMS_NS` to match `AKGL_TIME_ONESEC_NS`, and give
`akgl_game_state_lock` a real one-second budget.
### 2. Header/implementation surface drift
7. **Nineteen non-static functions are defined in `src/` but declared in no
header.** They have external linkage and public-looking names, so they are
part of the ABI whether intended or not, and no consumer can call them:
`akgl_game_load_objectnamemap`, `akgl_game_load_versioncmp`,
`akgl_game_save_actors`, `akgl_game_save_actorname_iterator`,
`akgl_game_save_charactername_iterator`, `akgl_game_save_spritename_iterator`,
`akgl_game_save_spritesheetname_iterator` (`src/game.c`);
`akgl_get_json_properties_double`, `akgl_get_json_properties_float`,
`akgl_get_json_properties_number`, `akgl_tilemap_load_layer_image`,
`akgl_tilemap_load_layer_object_actor`, `akgl_tilemap_load_physics`
(`src/tilemap.c`); `akgl_path_relative_from`, `akgl_path_relative_root`
(`src/util.c`); `gamepad_handle_added`, `gamepad_handle_button_down`,
`gamepad_handle_button_up`, `gamepad_handle_removed` (`src/controller.c`).
Each should be either declared in its header or made `static`. Note that
`tilemap.h` already has a "part of the internal API, exposed here for unit
testing" block — the tilemap entries belong there.
8. **`akgl_game_init_screen` is declared but never defined**
(`include/akgl/game.h:100`). Same failure mode as **Defects → Known and still
open #10**, which covers the four `akgl_controller_handle_*` declarations;
fold this one into that item.
9. **Static helpers use three different naming styles.** `actor_visible`
(`src/actor.c:185`) is bare; `akgl_character_load_json_inner` and
`akgl_character_load_json_state_int_from_strings` (`src/character.c:101,132`)
and `akgl_sprite_load_json_spritesheet` (`src/sprite.c:51`) carry the full
public prefix; `gamepad_handle_*` (`src/controller.c:121+`) uses a third
subsystem word that appears nowhere else. Adopt one rule — the clearest is
that `static` helpers drop the `akgl_` prefix, since it exists to avoid
external collisions.
10. **Parameter names disagree between declaration and definition.** Doxygen
documents the header spelling, so the generated docs describe names the
implementation does not use:
| Header | Implementation |
|---|---|
| `character.h:41` `basechar` | `character.c:21` `obj` |
| `character.h:69` `props` | `character.c:75` `registry` |
| `heap.h:121` `ptr` | `heap.c:143` `basechar` |
| `registry.h:97` `value` | `registry.c:163` `src` |
| `json_helpers.h:134` `e` | `json_helpers.c:149` `err` |
| `tilemap.h:134,143` `dest` | `tilemap.c:646,767` `map` |
The `tilemap.h` pair is the most misleading: the parameter is the map being
*read* and drawn, but it is named `dest` and documented as "Output
destination populated by the function".
11. **Object-pool size macros are defined twice, and the override hook is
dead.** `heap.h:15-29` wraps `AKGL_MAX_HEAP_ACTOR`, `_SPRITE`, `_SPRITESHEET`,
`_CHARACTER`, and `_STRING` in `#ifndef` guards so a consumer can override
them, but `actor.h:65`, `sprite.h:19-20`, and `character.h:14` define the same
four unconditionally and are included from `heap.h:9-11`. Whichever header
lands first wins and the `#ifndef` never fires, so the override mechanism
cannot work. Define each pool size once — `heap.h` is the natural home.
12. **Headers rely on their includers for types.** `iterator.h` uses `uint32_t`
without `<stdint.h>`; `json_helpers.h` uses `json_t` without `<jansson.h>`;
`util.h` uses `SDL_FRect` and `bool` without any SDL include; `text.h` uses
`SDL_Color` and `akerr_ErrorContext` without including SDL or `akerror.h`;
`controller.h` uses `akgl_Actor` without including `actor.h`. Each compiles
only because of `.c`-file include ordering. Headers should be self-contained.
13. **Include spelling is split between quoted and angled forms for the same
directory.** `actor.h:10-11`, `character.h:10-11`, `controller.h:11`,
`game.h:11-14`, `json_helpers.h:10`, and `staticstring.h:9` use
`#include "sibling.h"`; `physics.h:11-13`, `registry.h:9-10`, `renderer.h:13`,
`tilemap.h:10-12`, and `util.h:10` use `#include <akgl/sibling.h>`. The
angled form is correct for an installed library.
14. **Empty parameter lists.** `akgl_heap_init()`, `akgl_heap_init_actor()`,
`akgl_registry_init*()`, `akgl_game_init()`, `akgl_game_init_screen()`, and
`akgl_game_updateFPS()` declare `()` rather than `(void)`, while
`akgl_controller_list_keyboards(void)`, `akgl_controller_open_gamepads(void)`,
`akgl_game_lowfps(void)`, and `akgl_game_state_lock(void)` use `(void)`.
Before C23 the two are not equivalent — `()` suppresses argument checking.
`akgl_heap_init_actor` is even declared `()` in `heap.h:57` and defined
`(void)` in `heap.c:48`.
15. **`AKERR_NOIGNORE` is applied inconsistently at definition sites.** Headers
use it uniformly (except `akgl_sprite_sheet_coords_for_frame`, `sprite.h:86`,
which omits it). Definitions are split even within one file: `registry.c:55,120,163,172`
repeat it, `registry.c:27,44,63,71,79,96,104,112` do not. Since the attribute
is already on the declaration, drop it from all definitions.
### 3. Error-handling pattern
16. **`*_RETURN` macros are used inside `ATTEMPT` blocks, which skips `CLEANUP`.**
The established pattern is `FAIL_*_BREAK` / `CATCH` inside `ATTEMPT` and
`*_RETURN` outside it. Violations:
- `src/tilemap.c:52``SUCCEED_RETURN` on the success path of
`akgl_get_json_tilemap_property` returns directly from inside `ATTEMPT`,
bypassing the `CLEANUP` at line 54 that releases `tmpstr` and `typestr`.
Every successful property lookup leaks two heap strings, and the string
pool is only 256 entries.
- `src/tilemap.c:620``FAIL_RETURN` inside `ATTEMPT`.
- `src/controller.c:286-289,306``FAIL_ZERO_RETURN` / `FAIL_NONZERO_RETURN`
inside `ATTEMPT`; `src/controller.c:383``SUCCEED_RETURN` inside `ATTEMPT`,
which also leaves `akgl_controller_default` with no return statement on the
path that falls out of `FINISH`.
- `src/game.c:457,462``FAIL_NONZERO_RETURN` inside `ATTEMPT`, skipping the
`fclose` in `CLEANUP` at line 482.
17. **NULL-check discipline varies by function.** In `src/json_helpers.c` only
`akgl_get_json_string_value` (line 71) and `akgl_get_json_array_index_string`
(line 128) validate `key`/`dest`; the other eight accessors validate only the
container and then dereference `dest` unconditionally. In `src/physics.c` the
arcade backends check `actor` but `akgl_physics_null_gravity`,
`_null_collide`, and `_null_move` (lines 15-34) check only `self`. In
`src/renderer.c:65,74`, `akgl_render_2d_frame_start` and `_frame_end`
dereference `self->sdl_renderer` with no check on `self`, while
`akgl_render_2d_draw_texture` (line 82) checks `self` first.
18. **Error-context variable naming is split between `errctx` and `e`,
sometimes within one file.** `src/util.c` uses `e` in the path helpers
(lines 32-116) and `errctx` in the geometry helpers (lines 118-259);
`src/heap.c` uses `errctx` everywhere except `akgl_heap_init_actor` (line 50).
`src/actor.c`, `character.c`, `json_helpers.c`, `registry.c`, `sprite.c`, and
`tilemap.c` favor `errctx`; `game.c`, `physics.c`, `renderer.c`, and
`controller.c` favor `e`. Pick one.
### 4. Types and macros
19. **`float`/`double` are used raw where `types.h` defines aliases.** `types.h`
exports `float32_t` and `float64_t`, and the actor and character structs use
`float32_t` throughout — but `akgl_get_json_number_value` takes `float *`
(`json_helpers.h:55`), `akgl_get_json_double_value` takes `double *` (line 66),
`akgl_PhysicsBackend`'s six drag/gravity fields are `double`
(`physics.h:22-27`), and `akgl_Tilemap`'s perspective fields are `float`
(`tilemap.h:105-108`). Either use the aliases consistently or delete them.
20. **`AKGL_COLLIDE_RECTANGLES` (`include/akgl/util.h:27`) has unbalanced
parentheses** — three opens, two closes — so any use is a syntax error. It has
no callers and duplicates `akgl_collide_rectangles`. Delete it.
21. **Bitmask macros are unparenthesized.** `AKGL_BITMASK_HAS(x, y)` expands to
`(x & y) == y` with no outer parens, so `!AKGL_BITMASK_HAS(a, b)` parses as
`!(a & b) == b`. No in-tree caller negates it today (`AKGL_BITMASK_HASNOT`
exists for that), so this is latent rather than live. `AKGL_BITMASK_CLEAR(x)`
(`game.h:86`) also carries a trailing semicolon inside the macro body, so
normal use produces an empty statement. Parenthesize all five and drop the
semicolon.
22. **The state and iterator bit macros mix forms.** `AKGL_ITERATOR_OP_UPDATE`
(`iterator.h:15`) is written `1` while its 31 siblings are `1 << n`, and none
of the `1 << n` values in `iterator.h` or `actor.h:15-49` are parenthesized.
The hand-maintained trailing bit-pattern comments in `actor.h:34-49` are also
wrong for the high word — they restart at `0000 0000 0000 0001` for bit 16
rather than showing the full 32-bit value.
23. **`akgl_Frame` (`game.h:27-31`) is defined and never used** anywhere in
`src/`, `include/`, `tests/`, or `util/`.
### 5. `AKGL_ACTOR_STATE_STRING_NAMES` disagrees with `actor.h`
24. **The array bound differs between declaration and definition.**
`include/akgl/actor.h:57` declares `[AKGL_ACTOR_MAX_STATES+1]` (33);
`src/actor_state_string_names.c:6` defines `[32]`. Any consumer that trusts
the declared bound and reads index 32 reads past the object.
25. **Two entries name the wrong bit.** `actor.h` assigns bit 11 to
`AKGL_ACTOR_STATE_MOVING_IN` and bit 12 to `AKGL_ACTOR_STATE_MOVING_OUT`, but
`actor_state_string_names.c:18-19` puts `"AKGL_ACTOR_STATE_UNDEFINED_11"` and
`"AKGL_ACTOR_STATE_UNDEFINED_12"` at those indices — names for macros that do
not exist. Since `akgl_registry_init_actor_state_strings`
(`src/registry.c:79-94`) builds `AKGL_REGISTRY_ACTOR_STATE_STRINGS` from this
array and `akgl_character_load_json_state_int_from_strings`
(`src/character.c:101`) resolves character-JSON state names through it, a
character JSON can never bind a sprite to `MOVING_IN` or `MOVING_OUT`.
26. **The generation comment is stale.** `actor.h:53-55` says the file "is built
by a utility script and not kept in git, see the Makefile for
lib_src/actor_state_string_names.c". There is no Makefile (the project is
CMake), no `lib_src/`, no such script under `scripts/` or `util/`, and the
file *is* tracked in git at `src/actor_state_string_names.c`. Either restore
the generator — which would fix items 24 and 25 by construction — or delete
the comment and maintain the file by hand.
### 6. Doxygen drift
27. **Three struct doc comments in `tilemap.h` are rotated by one.**
`akgl_TilemapObject` (line 31) is documented as "Stores tileset metadata,
texture, and frame offsets" (that is `akgl_Tileset`); `akgl_TilemapLayer`
(line 46) as "Represents an object embedded in a tilemap layer" (that is
`akgl_TilemapObject`); `akgl_Tileset` (line 61) as "Stores tile, image, or
object data for one map layer" (that is `akgl_TilemapLayer`).
28. **`point` is documented as "Represents a two-dimensional point"**
(`util.h:12`) but has `x`, `y`, and `z`.
29. **Doc comments live on the definition for public functions.** The convention
is header-side documentation, but `akgl_path_relative_root` (`util.c:23`),
`akgl_path_relative_from` (`util.c:97`), the four `gamepad_handle_*`
(`controller.c:114+`), the `akgl_game_save_*` iterators and
`akgl_game_load_*` helpers (`game.c:173+`), and the tilemap helpers
(`tilemap.c:90+`) are documented only in the `.c`. This is the same set as
item 7 — resolving that resolves this.
### 7. Formatting
30. **A minority of files use a 2-column offset instead of the canonical
4-column one.** The mix of tabs and spaces across most of `src/` is *not*
disorder: it is exactly what Emacs `cc-mode` emits for the `stroustrup` style
with `indent-tabs-mode t` and `tab-width 8` — depth 1 is four spaces, depth 2
is one tab, depth 3 is a tab plus four spaces, and so on. Twelve of the
seventeen `.c` files follow that ladder cleanly.
The genuine outliers indent at 2 columns: `src/json_helpers.c` (82 lines,
except `akgl_get_json_with_default` at line 149 which is 4), `src/util.c`
(37 lines, from `akgl_rectangle_points` at line 118 onward), `src/assets.c`
(9), `src/staticstring.c` (7), `src/actor.c` (8, in `akgl_actor_add_child`
at line 272), plus `include/akgl/util.h` (7) and
`include/akgl/staticstring.h` (2).
**Resolved.** `AGENTS.md` now specifies the canonical style and the exact
`cc-mode` settings; `.dir-locals.el` applies them in Emacs; and
`scripts/reindent.sh` applies them in batch. The whole tree (32 files across
`src/`, `include/`, `tests/`, and `util/`) has been reindented and is a fixed
point of `scripts/reindent.sh --check`. Verified whitespace-only: apart from
three trailing blank lines removed at EOF in `src/heap.c`, `src/registry.c`,
and `tests/tilemap.c`, `git diff -w` over the reindent is empty, and the test
results are unchanged (13/14, `character` still the intentional failure).
`scripts/hooks/pre-commit` keeps it that way — enable with
`git config core.hooksPath scripts/hooks`.
31. **Leftover debug code ships in the library.** `src/controller.c:91-97` logs
four lines whenever `event->type == 768 && event->key.which == 11 &&
event->key.key == 13` — hardcoded decimal values for a specific keyboard ID
on a specific developer's machine, inside the per-event inner loop.
32. **Large commented-out blocks.** `src/sprite.c:115-120,157,185-198,210`;
`src/character.c:192-199,215`; `src/assets.c:18-27,50`;
`src/tilemap.c:583,596-598,640`. All are the same abandoned
`SDL_GetBasePath()` path-prefixing approach, superseded by
`akgl_path_relative`. Delete them.
33. **Unused locals.** `screenwidth`/`screenheight` (`game.c:53-54`), `curTime`
(`game.c:499`), `curTime` and `j` (`renderer.c:114,116``j` is also shadowed
by the inner loop at line 128), `target` (`character.c:59`), `result`
(`util.c:37,73`), `opflags` (`heap.c:146`, declared and cleared but never
read).
34. **`akgl_game_update`'s default flags OR the same bit twice.**
`src/game.c:496` reads
`(AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK)`. Given the loop
that follows walks layers and calls `updatefunc`, the intent was almost
certainly `AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK`.
35. **`akgl_draw_background` is the only public function outside the error
protocol.** `include/akgl/draw.h:14` returns `void` and reports nothing;
every other public entry point returns `akerr_ErrorContext *`. It also calls
`SDL_SetRenderDrawColor` and `SDL_RenderFillRect` without checking `renderer`
or `renderer->sdl_renderer`.
36. **`akgl_registry_init_actor` is the only registry initializer that destroys
an existing registry** before recreating it (`src/registry.c:47-49`). The
other seven leak the old `SDL_PropertiesID` on a second call. Either all of
them should do it or none should.
37. **Redundant casts obscure the code.** `(json_t *)json` where `json` is
already `json_t *`, `(akgl_Tilemap *)dest`, `(akgl_Actor *)actorobj`,
`(char *)&obj->name` on an array that already decays — roughly 180 pointer
casts across `src/`, a large majority of them no-ops, densest in
`src/tilemap.c:150-624` and `src/character.c:144-172`.
They suppress exactly the conversion warnings that would catch a real
mismatch.
38. **`struct`-qualified parameters in two definitions.**
`akgl_physics_null_move` (`src/physics.c:29`) and `akgl_physics_arcade_move`
(`src/physics.c:80`) are defined with `struct akgl_PhysicsBackend *self`
while the header and the other eight physics functions use the typedef.
39. **`text.c:19` validates the wrong argument.**
`FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath")` checks
`name` a second time; `filepath` is never checked and is passed straight to
`TTF_OpenFont`. Copy-paste of line 18.
**Resolved** alongside the text measurement work; `tests/text.c` asserts a
NULL filepath reports `AKERR_NULLPOINTER`.
40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output
parameter.** `akgl_path_relative` and `akgl_path_relative_root` take
`akgl_String *dst`; `akgl_path_relative_from` takes `akgl_String **dst`
(`src/util.c:105`). The `**` form matches the rest of the library
(`akgl_get_json_string_value`, `akgl_get_property`, `akgl_heap_next_string`),
which allocate when `*dest` is NULL. Related: **Defects → Known and still
open #4**, which covers the fact that `akgl_path_relative_from` never writes
`*dst` at all.
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the
`akgl_path_relative*` family use `dst`; everything else uses `dest`.
## Coverage status ## Coverage status
Generated with: Generated with:
@@ -12,35 +379,67 @@ ctest --test-dir build-coverage --output-on-failure
Reports land in `build-coverage/coverage/` (`index.html`, `coverage.xml`). Reports land in `build-coverage/coverage/` (`index.html`, `coverage.xml`).
**Line coverage 72.2%, function coverage 78.6%** (1534/2125 lines), up from a **Line coverage 77.2%, function coverage 83.1%** (2035/2637 lines), up from a
39.6% / 44.3% baseline. `character` is the one intentionally failing suite; 39.6% / 44.3% baseline. All 19 suites pass. (An earlier revision of this file
everything else passes. recorded `character` as an intentionally failing suite; it passes now, and
nothing in `tests/character.c` or `src/character.c` has changed since — the fix
came from elsewhere in the tree and this note was never updated.)
| File | Lines | Functions | | File | Lines | Functions |
|---|---|---| |---|---|---|
| `src/actor.c` | 205/258 (80%) | 16/18 | | `src/actor.c` | 205/258 (79%) | 16/18 |
| `src/assets.c` | 0/21 (0%) | 0/1 | | `src/assets.c` | 0/21 (0%) | 0/1 |
| `src/audio.c` | 189/207 (91%) | 19/20 |
| `src/character.c` | 104/118 (88%) | 6/7 | | `src/character.c` | 104/118 (88%) | 6/7 |
| `src/controller.c` | 220/243 (90%) | 9/9 | | `src/controller.c` | 247/270 (91%) | 12/12 |
| `src/draw.c` | 0/13 (0%) | 0/1 | | `src/draw.c` | 253/267 (95%) | 11/12 |
| `src/game.c` | 124/230 (54%) | 10/15 | | `src/error.c` | 9/9 (100%) | 1/1 |
| `src/heap.c` | 116/116 (100%) | 12/12 | | `src/game.c` | 124/231 (54%) | 10/15 |
| `src/heap.c` | 111/111 (100%) | 12/12 |
| `src/json_helpers.c` | 111/111 (100%) | 11/11 | | `src/json_helpers.c` | 111/111 (100%) | 11/11 |
| `src/physics.c` | 140/140 (100%) | 10/10 | | `src/physics.c` | 140/140 (100%) | 10/10 |
| `src/registry.c` | 76/102 (74%) | 11/12 | | `src/registry.c` | 76/102 (75%) | 11/12 |
| `src/renderer.c` | 7/70 (10%) | 1/7 | | `src/renderer.c` | 7/70 (10%) | 1/7 |
| `src/sprite.c` | 93/101 (92%) | 5/5 | | `src/sprite.c` | 93/101 (92%) | 5/5 |
| `src/staticstring.c` | 16/17 (94%) | 2/2 | | `src/staticstring.c` | 16/17 (94%) | 2/2 |
| `src/text.c` | 0/28 (0%) | 0/2 | | `src/text.c` | 26/45 (58%) | 3/4 |
| `src/tilemap.c` | 201/426 (47%) | 10/20 | | `src/tilemap.c` | 201/426 (47%) | 10/20 |
| `src/util.c` | 121/131 (92%) | 7/8 | | `src/util.c` | 121/131 (92%) | 7/8 |
| `src/version.c` | 2/2 (100%) | 1/1 |
Branch coverage reads 18.6% and should not be used as a target. The akerror Branch coverage reads 21.2% and should not be used as a target. The akerror
control-flow macros (`ATTEMPT`/`CATCH`/`PROCESS`/`FINISH`, `FAIL_*_RETURN`) control-flow macros (`ATTEMPT`/`CATCH`/`PROCESS`/`FINISH`, `FAIL_*_RETURN`)
expand into large branch trees per call site, most of them unreachable in normal expand into large branch trees per call site, most of them unreachable in normal
operation — `src/game.c` reports over 1700 branches across 230 lines. Track line operation — `src/game.c` reports over 1700 branches across 230 lines. Track line
and function coverage; treat branch coverage as a relative signal within a file. and function coverage; treat branch coverage as a relative signal within a file.
### Mutation testing
`scripts/mutation_test.py` was run over the three new files as a smoke check
(`--max-mutants 8` to 10 each, so these are samples rather than exhaustive
scores):
| File | Score | Surviving mutants |
|---|---|---|
| `src/draw.c` | 90% | Deleting the `FAIL_ZERO_BREAK` on `SDL_CreateTextureFromSurface` in `akgl_draw_flood_fill` |
| `src/audio.c` | 75% | Deleting `SUCCEED_RETURN` from the static `check_voice`; deleting `spec.freq` before opening a device |
| `src/text.c` | 50% | Three in `akgl_text_rendertextat`, which has no test yet, plus one `SUCCEED_RETURN` deletion |
The first pass over `src/audio.c` scored 50% and named two real gaps, both of
which are now tested: nothing asserted that an *unconfigured* voice is audible
(the whole reason the table defaults to a square wave at full level rather than
a zeroed struct), and no envelope test used a non-zero attack *and* decay
together, so the decay measuring from the wrong origin survived. `src/draw.c`
scored 70% first and named one: the circle was only checked at its four axis
points, which a mis-signed octant reflection survives, so it now checks that
every plotted pixel has a mirror in the other three quadrants.
What is left is honestly untestable from here. Deleting a `SUCCEED_RETURN`
leaves a non-void function falling off its end, which is undefined rather than
observably wrong, and the surviving SDL branches are allocation failures the
suite has no way to provoke. The `src/text.c` survivors go away with the
offscreen harness.
### Suites ### Suites
Every suite is registered through the `AKGL_TEST_SUITES` list in Every suite is registered through the `AKGL_TEST_SUITES` list in
@@ -72,14 +471,24 @@ Done:
- `tests/actor.c` — extended with the eight control-map handlers, automatic - `tests/actor.c` — extended with the eight control-map handlers, automatic
facing, movement logic, the animation frame state machine, `akgl_actor_update`, facing, movement logic, the animation frame state machine, `akgl_actor_update`,
and character/sprite binding lookups. 80%. and character/sprite binding lookups. 80%.
- `tests/audio.c` — every waveform, the ADSR envelope stage by stage, gate
expiry and release, voice summing and clamping, the master level, and device
open/shutdown under the dummy driver. 91%.
- `tests/draw.c` — every primitive against a 64x64 software renderer with the
pixels read back, including flood-fill containment, save/paste roundtrip, and
that drawing restores the renderer's draw color. 95%.
- `tests/text.c` — font loading into the registry and both measurement entry
points against a monospaced fixture font. 58%; the rest of `src/text.c` is
`akgl_text_rendertextat`, which needs the harness.
## Remaining work ## Remaining work
### Needs the offscreen renderer harness ### Needs the offscreen renderer harness
`src/renderer.c` (63 lines), `src/text.c` (28), `src/draw.c` (13), `src/renderer.c` (63 lines), `akgl_text_rendertextat` in `src/text.c` (19),
`src/assets.c` (21), `akgl_actor_render`/`actor_visible` in `src/actor.c` (53), `akgl_draw_background` in `src/draw.c` (13), `src/assets.c` (21),
and the drawing half of `src/tilemap.c` all need a live `renderer` global. `akgl_actor_render`/`actor_visible` in `src/actor.c` (53), and the drawing half
of `src/tilemap.c` all need a live `renderer` global.
Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and
`akgl_test_shutdown_headless()`: set the dummy video and audio drivers, `akgl_test_shutdown_headless()`: set the dummy video and audio drivers,
@@ -97,10 +506,15 @@ Then:
rotated `draw_texture` path including `angle != 0` with a NULL center; the rotated `draw_texture` path including `angle != 0` with a NULL center; the
`draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note `draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note
`defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs. `defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs.
- **`tests/text.c`**, **`tests/draw.c`**, **`tests/assets.c`** — font loading - **`tests/assets.c`** — BGM loading into `AKGL_REGISTRY_MUSIC` under the dummy
into `AKGL_REGISTRY_FONT`, text rendering with wrap on and off, background audio driver.
drawing at zero/negative/oversized dimensions, and BGM loading into - **`tests/text.c` extensions** — text rendering with wrap on and off.
`AKGL_REGISTRY_MUSIC` under the dummy audio driver. `tests/text.c` exists and covers font loading and measurement; only
`akgl_text_rendertextat` is left.
- **`tests/draw.c` extensions** — `akgl_draw_background` at zero, negative and
oversized dimensions. `tests/draw.c` exists and covers every other primitive
against a software renderer; `akgl_draw_background` is the one function in the
file that still reads the global `renderer` rather than taking a backend.
- **`tests/tilemap.c` extensions** — `akgl_tilemap_draw`, `_draw_tileset`, and - **`tests/tilemap.c` extensions** — `akgl_tilemap_draw`, `_draw_tileset`, and
`akgl_tilemap_load_layer_image`. `akgl_tilemap_load_layer_image`.
@@ -195,6 +609,68 @@ Each was found by a test written to assert correct behavior.
11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map 11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map
ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not
`controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`. `controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`.
12. **A failed controller-DB fetch silently destroys the tracked fallback.**
`include/akgl/SDL_GameControllerDB.h` is committed deliberately so the
library still builds if upstream disappears. But `mkcontrollermappings.sh`
has no `set -e` and never checks `curl`'s exit status: on a failed fetch it
writes `mappings.txt` empty, then overwrites the good tracked header with
`AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer — and exits 0.
Verified by pointing the script at an unresolvable host.
Two compounding problems:
- `add_custom_command` at `CMakeLists.txt:89-93` declares
`OUTPUT include/akgl/SDL_GameControllerDB.h` as a *relative* path, which
CMake resolves against the binary directory, while the script writes to
the source directory. The declared output never appears, so the command is
permanently out of date and re-runs on every build — making every build
depend on network access and leaving the file dirty in the working tree
each time.
- `const char *SDL_GAMECONTROLLER_DB[] = {\n};` is an empty initializer,
which is a constraint violation in ISO C and compiles only as a GCC
extension.
Fix: have the script fetch to a temporary file, check `curl`'s status and a
plausible minimum line count, and leave the existing header untouched on
failure (exiting non-zero). Separately, make regeneration explicit — a
dedicated `controllerdb` target, or an `OUTPUT` that matches where the
script actually writes — so an ordinary build neither needs the network nor
dirties the tree.
13. **A stale build tree in the source directory breaks the coverage run.**
`CMakeLists.txt:208` and `CMakeLists.txt:223` pass
`--root "${CMAKE_CURRENT_SOURCE_DIR}"` to gcovr, and gcovr searches for
`.gcda`/`.gcno` files under the root. The `--object-directory` argument on
the line below each does *not* narrow that search: per `gcovr --help` it
only identifies "the path between gcda files and the directory where the
compiler was originally run". So every instrumented build tree left inside
the source directory is folded into the report alongside the one actually
being measured.
With a `build-coverage/` from an earlier session still present, a freshly
configured `-DAKGL_COVERAGE=ON` tree fails in `coverage_reset`, before any
test runs:
AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46.
45 and 46 are that function's line numbers before and after an unrelated
`#include` was added to `src/game.c`: gcovr found 18 stale `.gcno` files
describing the old layout, merged them with the current ones, and could not
reconcile the two. Moving `build-coverage/` aside makes the same tree pass
18/18. Verified with gcovr 7.0.
The `build*/` entry in `.gitignore` hides these trees from `git status`,
which makes the state easier to get into and no easier to notice.
A second, smaller version of the same thing: rebuilding an existing
coverage tree after editing a test leaves `.gcda` files describing the old
object layout, and `coverage_reset` -- whose whole job is to delete them --
fails with `GCOV returncode was 5` before it gets the chance. `find
build-coverage -name '*.gcda' -delete` clears it. Observed with gcovr 7.0.
Fix: pass the build tree to gcovr as an explicit search path instead of
letting it default to `--root`, so only the tree under measurement is
considered. Touches the two `add_test` blocks at `CMakeLists.txt:204-211`
and `CMakeLists.txt:220-229`; nothing outside the `AKGL_COVERAGE` branch.
## Build notes ## Build notes
@@ -206,6 +682,156 @@ not saw every test abort before `main()` with "cannot open shared object file".
`CMakeLists.txt` now sets `BUILD_RPATH` on the library, the utility, and every `CMakeLists.txt` now sets `BUILD_RPATH` on the library, the utility, and every
test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run. test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run.
## API gaps blocking akbasic
`akbasic` (the C port of the BASIC interpreter, `source.starfort.tech/andrew/akbasic`) is
being built to link into `libakgl` as a scripting engine for game authors. Its interpreter
core is complete and passes its whole acceptance corpus against a stdio text sink; the
`libakgl`-backed sink and the graphics, sound and console verbs of Commodore BASIC 7.0 are
blocked on the four gaps below.
These are filed here rather than worked around in `akbasic` because growing `libakgl` to serve
a consumer is the wanted outcome. Each entry says what the BASIC verb needs, what the
`akgl_*` entry point should look like, and what would cover it.
1. **No way to measure rendered text.** `include/akgl/text.h` exposes `akgl_text_loadfont()`
and `akgl_text_rendertextat()`, and nothing that reports how large a string will be in a
given font. A terminal-style text surface cannot be built on that: a cursor needs the
advance width of one cell, and wrapping needs to know where a string crosses the right
margin. The reference interpreter got this from SDL2_ttf's `font.SizeUTF8("A")` and derived
its whole character grid from it.
Wants `akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);`
over `TTF_GetStringSize`, and probably a companion
`akgl_text_measure_wrapped(font, text, wraplength, w, h)` matching the `wraplength`
argument `akgl_text_rendertextat` already takes. Tests: a known string in a known font at a
known size, the empty string, and a wrapped string wide enough to force two lines.
This is the only one of the four that blocks work already designed and waiting. **`akbasic`
cannot render any output through `libakgl` until it lands.**
**Resolved.** `akgl_text_measure(font, text, w, h)` and
`akgl_text_measure_wrapped(font, text, wraplength, w, h)` are in
`include/akgl/text.h`, over `TTF_GetStringSize` and `TTF_GetStringSizeWrapped`.
Neither needs a renderer. A negative `wraplength` is refused with
`AKERR_OUTOFBOUNDS` rather than passed through, because SDL_ttf reads it as a
very large unsigned width and silently stops wrapping. `tests/text.c` covers
both against `tests/assets/akgl_test_mono.ttf`, a 10 KB monospaced ASCII
subset added for the purpose — being monospaced, it lets the suite assert
`width("AAAA") == 4 * width("A")` instead of hardcoding glyph metrics that
FreeType is free to round differently. Same change fixed item 39 below:
`akgl_text_loadfont` checked `name` twice and never checked `filepath`.
2. **No immediate-mode drawing.** `include/akgl/draw.h` declares exactly one function,
`akgl_draw_background(int w, int h)`, and `src/draw.c` is at 0% coverage. BASIC 7.0's
graphics verbs are all immediate-mode plotting against the current screen: `DRAW` (line and
point), `BOX`, `CIRCLE`, `PAINT` (flood fill), `LOCATE` (set the pixel cursor), `COLOR`,
and `SSHAPE`/`GSHAPE` (save and restore a rectangle of pixels).
Wants an `akgl_draw_*` family taking the renderer the host already initialized --
`akgl_draw_line`, `_rect`, `_filled_rect`, `_circle`, `_point`, `_flood_fill`,
`_copy_region` -- in the shape of the existing `akgl_render_2d_draw_texture`. SDL3's
`SDL_RenderLine`/`SDL_RenderRect`/`SDL_RenderFillRect` cover most of it; the circle and the
flood fill do not exist in SDL3 and need writing. Tests belong with the offscreen renderer
harness described under "Remaining work": render a known shape, read the target back, and
compare against a reference surface with the existing `akgl_compare_sdl_surfaces`.
**Resolved.** `include/akgl/draw.h` now declares `akgl_draw_point`, `_line`,
`_rect`, `_filled_rect`, `_circle`, `_flood_fill`, `_copy_region` and
`_paste_region`, all taking the `akgl_RenderBackend *` the host initialized,
in the shape of `akgl_render_2d_draw_texture`. Decisions worth knowing:
- **Color is an argument, not state.** There is no current-color global to
get out of step with the caller's own. Each call saves and restores the
renderer's draw color, so drawing a line does not change what the host's
next `SDL_RenderClear` paints. `tests/draw.c` asserts that.
- **The circle is a midpoint circle**, integer arithmetic with eight-way
symmetry, plotted eight points per step through `SDL_RenderPoints`.
- **The flood fill reads the target back, fills on the CPU, and blits only
the bounding box of what changed.** It keeps a fixed
`AKGL_DRAW_MAX_FLOOD_SPANS` (4096) stack of horizontal runs at file scope
rather than recursing per pixel; running out reports `AKERR_OUTOFBOUNDS`
and leaves the region partially filled, which is stated in the header. It
is therefore not reentrant — neither is anything else that draws to a
single `SDL_Renderer`.
- `_copy_region` allocates when `*dest` is `NULL` and otherwise copies into
the caller's surface, matching `akgl_get_json_string_value` and friends. A
region that would be clipped by the target edge is refused rather than
silently returning a smaller surface.
`tests/draw.c` draws into a 64x64 software renderer under the dummy video
driver and reads pixels back, so it did not need the offscreen harness. That
harness is still wanted for `src/renderer.c`, `src/text.c` and `src/assets.c`.
`akgl_draw_background` is untouched and still outside the error protocol
(item 35).
3. **No audio API at all.** `SDL3_mixer` is a vendored dependency and `registry.h` declares
`AKGL_REGISTRY_MUSIC`, but there is no `src/audio.c`, no `include/akgl/audio.h`, and no
`akgl_*` symbol that opens a mixer, loads a chunk, or plays a note. BASIC 7.0's sound verbs
are `SOUND` (a tone on a voice, with a duration), `PLAY` (a string of notes in a
Commodore-specific notation), `ENVELOPE` (ADSR per voice), `FILTER`, `VOL` and `TEMPO`.
`PLAY` and `ENVELOPE` want a synthesised voice rather than a sample, which SDL3_mixer does
not provide directly -- the honest first step is a small tone generator feeding
`SDL_AudioStream`, with `akgl_audio_init`, `akgl_audio_tone(voice, hz, ms)`,
`akgl_audio_envelope(voice, a, d, s, r)` and `akgl_audio_volume(level)`. This is the largest
of the four and the one most worth designing before writing. Tests can run under the dummy
audio driver and assert state transitions rather than sound.
**Resolved.** `include/akgl/audio.h` and `src/audio.c` add a three-voice tone
generator over `SDL_AudioStream`: `akgl_audio_init`, `_shutdown`, `_tone`,
`_stop`, `_waveform`, `_envelope`, `_volume`, `_voice_active` and `_mix`.
It is deliberately separate from the SDL3_mixer side of the library, which
plays audio *assets*; nothing in `audio.c` reads a file. Decisions worth
knowing:
- **The voice table works with no device open.** `akgl_audio_init` connects
it to one; without that a host can still pull samples itself through
`akgl_audio_mix`. That is not only for embedding — it is what makes the
suite deterministic. A device pulls samples on SDL's audio thread whenever
it likes, so a test that opened one and then asserted on voice state would
be racing the callback. `tests/audio.c` mixes by hand and opens a device
only in its last test.
- **Phase is derived from the frame counter, not accumulated.** A float
increment of `hz / 44100` is not exact, and adding it 44100 times a second
walks a held note off pitch.
- **A voice that was never configured is audible.** A zeroed voice has a
sustain of 0.0, which is silence with no error to explain it, so the table
defaults to a square wave held at full level.
- **Three voices summing past full scale are clamped, not scaled**, so one
voice plays at the level it was asked for rather than a third of it.
- Everything that touches the table takes the stream lock when a device is
open, since the callback reads it on another thread.
Still missing for a complete BASIC sound vocabulary: `FILTER` (SDL3 has no
filter primitive; this would need writing), `TEMPO` and the `PLAY` note-string
parser, both of which belong in the interpreter rather than here.
4. **No non-blocking keystroke read.** `include/akgl/controller.h` is built around SDL event
handlers the host pumps (`akgl_controller_handle_event` and friends), which suits a game
loop and does not suit `GET` and `GETKEY` -- those ask "is there a keystroke waiting, yes or
no" and must not require the interpreter to own the event loop. Goal 3 of `akbasic` forbids
it owning one.
Wants `akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);`
reading a small ring buffer that `akgl_controller_handle_event` already fills, so the host
keeps pumping events and the interpreter drains characters at its own pace. Tests: push
synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the
empty-buffer and overflow cases.
**Resolved.** `akgl_controller_poll_key(int *keycode, bool *available)` and
`akgl_controller_flush_keys(void)` are in `include/akgl/controller.h`, over a
fixed `AKGL_CONTROLLER_KEY_BUFFER` (32) ring in `src/controller.c`.
`akgl_controller_handle_event` records every `SDL_EVENT_KEY_DOWN` *before* it
scans the control maps, so a key bound to an actor still reaches a polling
caller — the scan returns as soon as a binding claims the event, and doing it
afterwards would have lost exactly the keys a game also acts on. An empty
buffer is success with `available` false, not an error. A full buffer drops
the newest key rather than overwriting the oldest, matching the Commodore
keyboard buffer and keeping what was typed first. `tests/controller.c` covers
drain order, the release-is-not-a-keystroke case, a key shared with a control
map, flush, both NULL arguments, and overflow plus reuse afterwards.
## Carried over ## Carried over
1. **Make character-to-sprite state bindings release their references symmetrically.** 1. **Make character-to-sprite state bindings release their references symmetrically.**

204
include/akgl/audio.h Normal file
View File

@@ -0,0 +1,204 @@
/**
* @file audio.h
* @brief Declares the public audio API.
*
* A small tone generator: a fixed set of voices, each with a waveform, a
* frequency, a gate length and an ADSR envelope, mixed to one stream of float
* samples. This is what a synthesised-voice vocabulary needs -- SOUND, PLAY,
* ENVELOPE and VOL all describe a note rather than a recording -- and it is
* deliberately separate from the SDL3_mixer side of the library, which loads
* and plays audio *assets*. Nothing here reads a file.
*
* The voice table exists whether or not an audio device is open.
* akgl_audio_init() connects it to one; without that a caller can still set up
* voices and pull samples itself with akgl_audio_mix(), which is how the test
* suite exercises the synthesis without depending on a sound card's timing.
* What a caller cannot do is set up voices and expect to hear them with no
* device open.
*/
#ifndef _AKGL_AUDIO_H_
#define _AKGL_AUDIO_H_
#include <stdint.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/types.h>
/**
* @brief Voices that can sound at once.
*
* Three, because the machine this vocabulary comes from had three and its
* music is written for three. A voice is addressed here by a zero-based index;
* a language whose own voices are numbered from one maps them itself.
*/
#define AKGL_AUDIO_MAX_VOICES 3
/** @brief Sample rate of the generated stream, in frames per second. */
#define AKGL_AUDIO_SAMPLE_RATE 44100
/** @brief Frames the device callback generates per pass through the mixer. */
#define AKGL_AUDIO_MIX_FRAMES 512
/**
* @brief Shape of one voice's oscillator.
*
* The trailing comment on each is the waveform number the C128 SOUND statement
* uses for the same shape, for a caller translating one to the other.
*/
typedef enum {
AKGL_AUDIO_WAVE_TRIANGLE = 0, /** SOUND waveform 0. Soft, flute-like. */
AKGL_AUDIO_WAVE_SAWTOOTH = 1, /** SOUND waveform 1. Bright, brassy. */
AKGL_AUDIO_WAVE_SQUARE = 2, /** SOUND waveform 2. Hollow, reedy. The default. */
AKGL_AUDIO_WAVE_NOISE = 3, /** SOUND waveform 3. Unpitched; percussion. */
AKGL_AUDIO_WAVE_SINE = 4 /** No SOUND equivalent. A pure tone. */
} akgl_AudioWaveform;
/** @brief Holds one voice's oscillator, envelope, and how far through it is. */
typedef struct {
bool active;
akgl_AudioWaveform waveform;
float32_t hz;
/**
* @brief Position through one cycle, 0.0 to 1.0.
*
* Derived from `elapsed_frames` and `hz` each sample rather than
* accumulated, so a long note does not drift off pitch. Reading it is
* meaningful; writing it is not.
*/
float32_t phase;
/** @brief Frames the gate stays open, before the release begins. */
uint32_t duration_frames;
/** @brief Frames generated since the tone started, gate and release. */
uint32_t elapsed_frames;
uint32_t attack_frames;
uint32_t decay_frames;
uint32_t release_frames;
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
float32_t sustain;
} akgl_AudioVoice;
/** @brief The process-wide voice table. */
extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
/**
* @brief Open an audio device and start pulling samples from the voice table.
*
* Requires SDL's audio subsystem to be initialized. Repeating the call is a
* no-op, so a program that cannot order its initialization precisely may call
* it more than once. The voice table is reset only on the first call, so this
* does not silence a voice that is already sounding.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void);
/**
* @brief Close the audio device and silence every voice.
*
* Safe to call when no device is open.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void);
/**
* @brief Sound a note on one voice for a fixed time.
*
* The voice's envelope and waveform are whatever akgl_audio_envelope() and
* akgl_audio_waveform() last set them to. @p ms is the length of the gate: the
* voice's release runs *after* it, so a voice with a release stays audible
* slightly longer than @p ms. Sounding a voice that is already sounding
* restarts it from the beginning of its envelope.
*
* @param voice Zero-based voice index.
* @param hz Frequency in hertz.
* @param ms Gate length in milliseconds.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
/**
* @brief Silence one voice immediately, skipping its release.
* @param voice Zero-based voice index.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice);
/**
* @brief Choose the oscillator shape one voice will use.
*
* Takes effect on the next akgl_audio_tone(); it does not reshape a note that
* is already sounding.
*
* @param voice Zero-based voice index.
* @param waveform Oscillator shape to use.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform);
/**
* @brief Set one voice's ADSR envelope.
*
* @p attack, @p decay and @p release are milliseconds; @p sustain is the level
* the envelope decays to and holds while the gate is open, from 0.0 to 1.0. A
* zero-length stage is skipped rather than divided by.
*
* Takes effect on the next akgl_audio_tone().
*
* @param voice Zero-based voice index.
* @param attack Milliseconds to rise from silence to full level.
* @param decay Milliseconds to fall from full level to @p sustain.
* @param sustain Held level while the gate is open, 0.0 to 1.0.
* @param release Milliseconds to fall to silence once the gate closes.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release);
/**
* @brief Set the level every voice is scaled by.
* @param level Master level, 0.0 to 1.0.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level);
/**
* @brief Report whether a voice is still sounding.
*
* A voice goes quiet on its own when its gate and release have both elapsed, so
* this is how a caller waits out a note without keeping its own clock.
*
* @param voice Zero-based voice index.
* @param active Output destination set to `true` while the voice is sounding.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active);
/**
* @brief Generate the next @p frames mono samples from the voice table.
*
* The device callback installed by akgl_audio_init() is a loop around this. A
* host that owns its own audio pipeline can call it directly instead and never
* open a device here at all. Samples are single-precision, one channel, in the
* range -1.0 to 1.0, and every active voice is advanced by @p frames.
*
* @param dest Output destination populated with @p frames samples.
* @param frames Number of samples to generate.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
#endif // _AKGL_AUDIO_H_

View File

@@ -13,6 +13,16 @@
#define AKGL_MAX_CONTROL_MAPS 8 #define AKGL_MAX_CONTROL_MAPS 8
#define AKGL_MAX_CONTROLS 32 #define AKGL_MAX_CONTROLS 32
/**
* @brief Keystrokes akgl_controller_handle_event() will hold for a poller.
*
* The Commodore keyboard buffer this serves held ten. Thirty-two is enough that
* a program which polls once per frame never loses a key to a fast typist, and
* small enough that the buffer stays a fixed-size object in the library's data
* segment.
*/
#define AKGL_CONTROLLER_KEY_BUFFER 32
/** @brief Maps one SDL input to pressed and released callbacks. */ /** @brief Maps one SDL input to pressed and released callbacks. */
typedef struct { typedef struct {
uint32_t event_on; uint32_t event_on;
@@ -117,4 +127,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
/**
* @brief Take the oldest waiting keystroke, if there is one.
*
* The rest of this header is built around the host pumping SDL events into
* akgl_controller_handle_event(), which suits a game loop and does not suit an
* embedded interpreter asking "is there a key waiting, yes or no" without
* owning the event loop itself. Every key press that reaches
* akgl_controller_handle_event() is recorded in a fixed ring buffer first,
* whether or not a control map claims it, and this drains that buffer one
* keystroke per call.
*
* When no key is waiting the call still succeeds: @p available is set to
* `false` and @p keycode to 0. The caller polls, it does not block.
*
* A full buffer drops the *newest* keystroke rather than the oldest, so what
* was typed first is what is read first. This runs on whichever thread pumps
* events; it is not synchronized.
*
* @param keycode Output destination populated with the SDL keycode, or 0.
* @param available Output destination set to `true` when a keystroke was taken.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
/**
* @brief Discard every keystroke waiting in the buffer.
*
* For a caller that has been ignoring input and does not want a backlog acted
* on the moment it starts polling again.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
#endif // _CONTROLLER_H_ #endif // _CONTROLLER_H_

View File

@@ -1,11 +1,41 @@
/** /**
* @file draw.h * @file draw.h
* @brief Declares the public draw API. * @brief Declares the public draw API.
*
* Immediate-mode plotting against whichever renderer the caller hands in. This
* is the shape a BASIC-style graphics vocabulary needs -- DRAW, BOX, CIRCLE,
* PAINT, SSHAPE and GSHAPE all say "put this on the screen now" rather than
* "add this to the scene" -- and it sits alongside the actor and tilemap
* rendering rather than replacing it.
*
* Every entry point takes its color as an argument instead of reading a
* current-color global. A caller that has a notion of a current color (a BASIC
* COLOR statement, say) already owns that state and does not need the library
* to keep a second copy that can disagree with it. The renderer's own draw
* color is saved and restored around each call, so drawing a line never changes
* what the next SDL_RenderClear() paints.
*/ */
#ifndef _DRAW_H_ #ifndef _DRAW_H_
#define _DRAW_H_ #define _DRAW_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/renderer.h>
#include <akgl/types.h>
/**
* @brief Spans akgl_draw_flood_fill() will hold while walking a region.
*
* The fill keeps a fixed stack of horizontal runs still to be examined rather
* than recursing per pixel. A region complicated enough to need more than this
* many pending runs at once reports AKERR_OUTOFBOUNDS instead of overflowing;
* an ordinary convex or moderately concave shape needs a few dozen.
*/
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
/** /**
* @brief Draw background. * @brief Draw background.
* @param w Destination width. * @param w Destination width.
@@ -13,4 +43,129 @@
*/ */
void akgl_draw_background(int w, int h); void akgl_draw_background(int w, int h);
/**
* @brief Plot a single pixel.
* @param self Backend or object instance to operate on.
* @param x Horizontal destination coordinate.
* @param y Vertical destination coordinate.
* @param color Color to draw with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color);
/**
* @brief Draw a line between two points.
* @param self Backend or object instance to operate on.
* @param x1 Horizontal coordinate of the first endpoint.
* @param y1 Vertical coordinate of the first endpoint.
* @param x2 Horizontal coordinate of the second endpoint.
* @param y2 Vertical coordinate of the second endpoint.
* @param color Color to draw with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color);
/**
* @brief Draw the outline of a rectangle.
* @param self Backend or object instance to operate on.
* @param rect Rectangle to outline.
* @param color Color to draw with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
/**
* @brief Fill a rectangle.
* @param self Backend or object instance to operate on.
* @param rect Rectangle to fill.
* @param color Color to draw with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
/**
* @brief Draw the outline of a circle.
*
* SDL3 has no circle primitive, so this plots one with the midpoint circle
* algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A
* radius of zero draws the center pixel and nothing else.
*
* @param self Backend or object instance to operate on.
* @param x Horizontal coordinate of the center.
* @param y Vertical coordinate of the center.
* @param radius Circle radius in pixels.
* @param color Color to draw with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color);
/**
* @brief Flood the connected region containing one pixel with a color.
*
* SDL3 has no flood fill either, and unlike the shape primitives it cannot be
* done on the GPU side: the region is defined by what is already on the screen.
* This reads the render target back, walks the region on the CPU with a
* bounded span stack, and blits the result over the area it touched.
*
* Filling a region that already holds @p color is a no-op rather than an error.
* A seed outside the render target reports AKERR_OUTOFBOUNDS.
*
* @param self Backend or object instance to operate on.
* @param x Horizontal coordinate of the seed pixel.
* @param y Vertical coordinate of the seed pixel.
* @param color Color to fill with.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color);
/**
* @brief Read a rectangle of the render target into a surface.
*
* The save half of SSHAPE/GSHAPE. When `*dest` is `NULL` the function allocates
* the surface and the caller owns it from then on -- release it with
* SDL_DestroySurface(). When `*dest` already points at a surface of exactly
* @p src's dimensions the pixels are copied into it instead, so a caller
* saving the same region repeatedly does not churn allocations.
*
* @param self Backend or object instance to operate on.
* @param src Rectangle of the render target to read.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest);
/**
* @brief Draw a saved surface back onto the render target.
*
* The restore half of SSHAPE/GSHAPE, taking what akgl_draw_copy_region()
* produced. The surface is not consumed and may be pasted as many times as the
* caller likes.
*
* @param self Backend or object instance to operate on.
* @param src Surface to draw.
* @param x Horizontal destination coordinate.
* @param y Vertical destination coordinate.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
#endif //_DRAW_H_ #endif //_DRAW_H_

View File

@@ -6,21 +6,73 @@
#ifndef _ERROR_H_ #ifndef _ERROR_H_
#define _ERROR_H_ #define _ERROR_H_
#include <akerror.h>
/*
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
* registry entry points raise akerr_ErrorContext * instead of returning int, and
* the library gained an soname -- so a translation unit that pairs this header
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
*
* libakerror publishes no version macro, so this feature-tests on
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
* version number that does not exist. Without the guard an embedded build is
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
* codes below and again inside src/heap.c.
*
* See deps/libakerror/UPGRADING.md.
*/
#ifndef AKERR_FIRST_CONSUMER_STATUS
#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
#endif
// This macro is used to silence warnings on string concatenation operations that may fail. // This macro is used to silence warnings on string concatenation operations that may fail.
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH. // e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever // We have to draw a line in the sand somewhere or we will just let our buffers grow forever
// to keep the compiler happy. // to keep the compiler happy.
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \ #define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
_Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"") _Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
#define RESTORE_GCC_WARNINGS \ #define RESTORE_GCC_WARNINGS \
_Pragma("GCC diagnostic pop") _Pragma("GCC diagnostic pop")
#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18) // libakerror reserves statuses 0-255 for the host's errno values and its own
#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19) // AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20) // These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21) // so that a libc which grows an errno cannot move them out from under us.
#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22) //
// akgl_error_init() reserves this whole band in one call and registers a name
// for every code below. Add a code here and you must name it there, or it
// prints as "Unknown Error" in every stack trace that carries it.
#define AKGL_ERR_OWNER "libakgl"
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /** An SDL call failed; the message carries SDL_GetError() */
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /** A registry property or lookup operation failed */
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /** A heap pool has no free object left to hand out */
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /** A component did not behave the way its contract requires */
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /** Actor logic is telling the physics simulator to skip it */
// One past the last libakgl status. The reservation is all-or-nothing -- a
// subset or superset of an existing one is refused -- so this must stay one
// past the highest code above.
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5)
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
/**
* @brief Claim the libakgl status band and register a name for every code in it.
*
* Call this before anything else in libakgl. Every other subsystem raises
* AKGL_ERR_* codes, and a code raised before this runs carries no name into its
* stack trace. Repeating the call is a no-op, so a program that cannot order its
* initialization precisely may call it more than once.
*
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
#endif // _ERROR_H_ #endif // _ERROR_H_

View File

@@ -12,8 +12,9 @@
#include "tilemap.h" #include "tilemap.h"
#include "renderer.h" #include "renderer.h"
#include "physics.h" #include "physics.h"
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
#define AKGL_VERSION "0.1.0" // ship an empty Version field: nothing tied the two together.
#include <akgl/version.h>
#define AKGL_GAME_AUDIO_TRACK_BGM 1 #define AKGL_GAME_AUDIO_TRACK_BGM 1
#define AKGL_GAME_AUDIO_MAX_TRACKS 64 #define AKGL_GAME_AUDIO_MAX_TRACKS 64
@@ -140,5 +141,5 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
* @throws AKERR_* Propagates an error reported by a delegated operation. * @throws AKERR_* Propagates an error reported by a delegated operation.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags); akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
#endif //_AKGL_GAME_H_ #endif //_AKGL_GAME_H_

View File

@@ -26,7 +26,7 @@ typedef struct akgl_PhysicsBackend {
double gravity_y; double gravity_y;
double gravity_z; double gravity_z;
SDL_Time gravity_time; SDL_Time gravity_time;
SDL_Time timer_gravity; SDL_Time timer_gravity;
} akgl_PhysicsBackend; } akgl_PhysicsBackend;
/** /**

View File

@@ -15,8 +15,8 @@
/** @brief Provides a fixed-capacity, heap-managed string buffer. */ /** @brief Provides a fixed-capacity, heap-managed string buffer. */
typedef struct typedef struct
{ {
int refcount; int refcount;
char data[AKGL_MAX_STRING_LENGTH]; char data[AKGL_MAX_STRING_LENGTH];
} akgl_String; } akgl_String;
/** /**

View File

@@ -6,7 +6,9 @@
#ifndef _TEXT_H_ #ifndef _TEXT_H_
#define _TEXT_H_ #define _TEXT_H_
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
/** /**
* @brief Text loadfont. * @brief Text loadfont.
@@ -31,5 +33,40 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y); akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
/**
* @brief Report the size, in pixels, that @p text would occupy on one line.
*
* Nothing is drawn and no renderer is required. A caller building a character
* grid measures one cell with this -- the advance width of a single glyph in a
* monospaced font -- and derives the rest of the grid from it.
*
* @param font Font used to render the text.
* @param text UTF-8 text to measure.
* @param w Output destination populated with the rendered width in pixels.
* @param h Output destination populated with the rendered height in pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
/**
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
*
* The companion to akgl_text_measure() for the wrapping case, matching the
* @p wraplength argument akgl_text_rendertextat() already takes: a string
* longer than @p wraplength reports the height of every line it breaks onto.
* A @p wraplength of zero wraps only on newlines in @p text.
*
* @param font Font used to render the text.
* @param text UTF-8 text to measure.
* @param wraplength Maximum rendered line width; zero wraps on newlines only.
* @param w Output destination populated with the rendered width in pixels.
* @param h Output destination populated with the rendered height in pixels.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
#endif // _TEXT_H_ #endif // _TEXT_H_

View File

@@ -11,17 +11,17 @@
/** @brief Represents a two-dimensional point. */ /** @brief Represents a two-dimensional point. */
typedef struct point { typedef struct point {
int x; int x;
int y; int y;
int z; int z;
} point; } point;
/** @brief Stores the corners of an axis-aligned rectangle. */ /** @brief Stores the corners of an axis-aligned rectangle. */
typedef struct RectanglePoints { typedef struct RectanglePoints {
point topleft; point topleft;
point topright; point topright;
point bottomleft; point bottomleft;
point bottomright; point bottomright;
} RectanglePoints; } RectanglePoints;
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x) #define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)

56
include/akgl/version.h.in Normal file
View File

@@ -0,0 +1,56 @@
/**
* @file version.h
* @brief Declares the libakgl version, both as compiled against and as linked.
*
* GENERATED FILE -- edit include/akgl/version.h.in, never the copy in the build
* tree. Every value here comes from the project() call in CMakeLists.txt, which
* is also what sets the shared library's VERSION and SOVERSION and the Version
* field in akgl.pc. One number, one place, so the header, the soname and
* pkg-config cannot drift apart.
*
* AKGL_VERSION is the version you *compiled against*. akgl_version() reports the
* version of the libakgl you actually *linked*. They disagree when a stale
* shared library is ahead of the new one on the loader path -- the failure the
* soname exists to prevent and this pair exists to diagnose.
*/
#ifndef _AKGL_VERSION_H_
#define _AKGL_VERSION_H_
#define AKGL_VERSION "@PROJECT_VERSION@"
#define AKGL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define AKGL_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define AKGL_VERSION_PATCH @PROJECT_VERSION_PATCH@
/**
* @brief True when the headers on the include path are at least the given version.
*
* For consumers that must build against more than one libakgl release. This is
* the test libakstdlib could not write against libakerror, which published no
* version macro and had to feature-test on AKERR_FIRST_CONSUMER_STATUS instead.
*
* @code
* #if AKGL_VERSION_AT_LEAST(0, 2, 0)
* akgl_something_new();
* #endif
* @endcode
*/
#define AKGL_VERSION_AT_LEAST(major, minor, patch) \
((AKGL_VERSION_MAJOR > (major)) || \
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR > (minor)) || \
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR == (minor) && \
AKGL_VERSION_PATCH >= (patch)))
/**
* @brief Report the version of the libakgl that is actually linked.
*
* Returns "major.minor.patch". Compare it against AKGL_VERSION to detect a
* stale shared library. Never returns NULL, and the storage is static -- the
* caller must not free or modify it.
*
* This returns a string rather than an akerr_ErrorContext * because it cannot
* fail, the same reason akerr_name_for_status() returns a name directly.
*/
const char *akgl_version(void);
#endif // _AKGL_VERSION_H_

98
scripts/hooks/pre-commit Executable file
View File

@@ -0,0 +1,98 @@
#!/bin/sh
#
# Reindent staged C sources to the canonical style before the commit is made.
# See AGENTS.md -> "Coding Style".
#
# Enable with:
# git config core.hooksPath scripts/hooks
# Bypass a single commit with `git commit --no-verify`.
#
# The hook checks the *staged* content, not the working tree, so what gets
# committed is what was verified. When a file needs reindenting it is fixed in
# the working tree and re-staged -- but only when the working tree and the index
# agree for that file. If they disagree (a partial `git add -p`), re-staging
# would sweep unstaged work into the commit, so the hook stops and asks you to
# do it yourself.
set -eu
root=$(git rev-parse --show-toplevel)
reindent="$root/scripts/reindent.sh"
# Leave conflict resolution alone.
if [ -e "$root/.git/MERGE_HEAD" ]; then
exit 0
fi
if [ ! -x "$reindent" ]; then
echo "pre-commit: $reindent missing or not executable; skipping style check" >&2
exit 0
fi
# Without Emacs the canonical style cannot be applied or even checked. Warn
# rather than block: a hook that fails closed on a missing optional tool just
# teaches everyone to pass --no-verify.
if ! command -v emacs >/dev/null 2>&1; then
echo "pre-commit: emacs not found; skipping reindent (see AGENTS.md)" >&2
exit 0
fi
staged=$(git diff --cached --name-only --diff-filter=ACMR \
| grep -E '^(src|include|tests|util)/.*\.[ch]$' \
| grep -v -x -F 'include/akgl/SDL_GameControllerDB.h' || true)
if [ -z "$staged" ]; then
exit 0
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM
# Reindent a copy of each file's staged content and see whether it moves.
needs=''
for f in $staged; do
copy="$tmp/$(printf '%s' "$f" | tr '/' '_')"
git show ":$f" >"$copy"
cp "$copy" "$copy.orig"
if ! "$reindent" "$copy" >/dev/null 2>&1; then
echo "pre-commit: reindent failed on $f; commit aborted" >&2
exit 1
fi
if ! cmp -s "$copy" "$copy.orig"; then
needs="$needs $f"
fi
done
if [ -z "$needs" ]; then
exit 0
fi
# Refuse to touch anything if even one file is partially staged.
blocked=''
for f in $needs; do
if ! git diff --quiet -- "$f"; then
blocked="$blocked $f"
fi
done
if [ -n "$blocked" ]; then
echo "pre-commit: these files need reindenting but have unstaged changes," >&2
echo "so re-staging them would pull unstaged work into the commit:" >&2
for f in $blocked; do echo " $f" >&2; done
echo >&2
echo "Reindent and stage them yourself, then commit again:" >&2
echo " scripts/reindent.sh$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
echo " git add$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
exit 1
fi
# Safe: for every file needing work, the index and working tree agree.
# shellcheck disable=SC2086
"$reindent" $needs
# shellcheck disable=SC2086
git add $needs
echo "pre-commit: reindented and re-staged:" >&2
for f in $needs; do echo " $f" >&2; done
exit 0

87
scripts/reindent.el Normal file
View File

@@ -0,0 +1,87 @@
;;; reindent.el --- batch reindent to cc-mode "stroustrup" -*- lexical-binding: t -*-
;; Reindents each file named on the command line, in place, to the project's
;; canonical style: cc-mode "stroustrup", 4 columns per level, tabs 8 columns
;; wide. See AGENTS.md -> "Coding Style".
;;
;; The style is set explicitly here rather than read from .dir-locals.el so that
;; the result does not depend on where the file lives -- the pre-commit hook
;; runs this over temporary copies outside the project tree.
;;
;; Usage: emacs --batch -Q -l scripts/reindent.el -- FILE...
;;; Code:
(require 'cc-mode)
(setq make-backup-files nil
create-lockfiles nil
auto-save-default nil
vc-handled-backends nil
inhibit-message t)
(defun akgl-retab-leading-whitespace ()
"Rewrite every line's leading whitespace as tabs-then-spaces at `tab-width'.
`indent-region' fixes the column a line starts at, but `indent-line-to' leaves
a line alone when it is already at the right column even if the bytes are
wrong -- eight spaces where the canonical form is one tab. This pass closes
that gap.
Deliberately not `tabify': that function's `tabify-regexp' is \" [ \\t]+\",
which is NOT anchored to the line start, so it rewrites runs of spaces
anywhere on the line and destroys the hand-aligned columns in the bit-flag
tables in actor.h and iterator.h. Only leading whitespace is touched here, and
lines beginning inside a string literal are skipped outright."
(goto-char (point-min))
(while (not (eobp))
(let ((bol (point)))
(unless (nth 3 (syntax-ppss bol))
(skip-chars-forward " \t" (line-end-position))
(let ((col (current-column)))
(unless (or (zerop col) (eolp))
(let ((want (concat (make-string (/ col tab-width) ?\t)
(make-string (% col tab-width) ?\s))))
(unless (string= want (buffer-substring bol (point)))
(delete-region bol (point))
(insert want)))))))
(forward-line 1)))
(defun akgl-reindent-file (path)
"Reindent PATH in place. Returns t if the file changed on disk."
(let ((before (with-temp-buffer
(insert-file-contents path)
(buffer-string))))
(with-current-buffer (find-file-noselect path t)
(c-mode)
(c-set-style "stroustrup")
(setq indent-tabs-mode t
tab-width 8
c-basic-offset 4
require-final-newline t)
;; 1. Put every line at its correct column.
(indent-region (point-min) (point-max))
;; 2. Convert leading whitespace to the canonical tab/space mix.
(akgl-retab-leading-whitespace)
;; 3. Trailing whitespace and a single final newline.
(delete-trailing-whitespace)
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(let ((changed (not (string= before (buffer-string)))))
(when changed (save-buffer))
(kill-buffer)
changed))))
;; Emacs leaves the "--" separator in `command-line-args-left'; drop it, along
;; with any empty argument, so the remainder is exactly the file list.
(dolist (path (seq-remove (lambda (a) (or (string= a "--") (string= a "")))
command-line-args-left))
(when (akgl-reindent-file path)
(princ (format "reindented %s\n" path))))
;; Exit 0 on success whether or not anything changed, so that any non-zero
;; status from this script means a real failure. Callers detect "something
;; changed" from stdout, or by comparing files themselves.
(kill-emacs 0)
;;; reindent.el ends here

101
scripts/reindent.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/bin/sh
#
# Reindent C sources to the project's canonical style (cc-mode "stroustrup",
# 4-column offset, 8-column tabs). See AGENTS.md -> "Coding Style".
#
# scripts/reindent.sh reindent every tracked C source in place
# scripts/reindent.sh FILE... reindent only the named files
# scripts/reindent.sh --check ... report non-conforming files, change nothing
#
# Exit status: 0 if everything already conforms (or was reindented), 1 if
# --check found a file that needs reindenting, 2 on a usage or environment
# error.
set -eu
root=$(git rev-parse --show-toplevel)
elisp="$root/scripts/reindent.el"
# Directories whose C sources are hand-maintained. Anything outside these is
# vendored (deps/) or generated (include/akgl/SDL_GameControllerDB.h) and is
# left alone.
SCOPE='src include tests util'
GENERATED='include/akgl/SDL_GameControllerDB.h'
check=0
if [ "${1:-}" = "--check" ]; then
check=1
shift
fi
if ! command -v emacs >/dev/null 2>&1; then
echo "reindent: emacs not found; cannot verify or apply the canonical style" >&2
exit 2
fi
if [ ! -f "$elisp" ]; then
echo "reindent: missing $elisp" >&2
exit 2
fi
# Build the file list: explicit arguments, or every tracked C source in scope.
if [ "$#" -gt 0 ]; then
files=$(for f in "$@"; do printf '%s\n' "$f"; done)
else
files=$(cd "$root" && git ls-files $SCOPE | grep -E '\.[ch]$' || true)
fi
# Drop generated files and anything that no longer exists on disk.
files=$(printf '%s\n' "$files" | grep -v -x -F "$GENERATED" || true)
files=$(cd "$root" && for f in $files; do [ -f "$f" ] && printf '%s\n' "$f"; done)
if [ -z "$files" ]; then
exit 0
fi
if [ "$check" -eq 0 ]; then
# Reindent in place. A non-zero status here is a real failure -- never
# swallow it, or a broken Emacs would look like "everything already conforms".
# shellcheck disable=SC2086
(cd "$root" && emacs --batch -Q -l "$elisp" -- $files) || {
echo "reindent: emacs failed; no files were reindented" >&2
exit 2
}
exit 0
fi
# --check: reindent throwaway copies and report which originals differ. The
# copies keep their extension so cc-mode still selects the right major mode.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM
# Accept both repo-relative and absolute paths: the default file list is
# relative, but callers (including the pre-commit hook) pass absolute ones.
abspath() {
case "$1" in
/*) printf '%s' "$1" ;;
*) printf '%s/%s' "$root" "$1" ;;
esac
}
copies=''
for f in $files; do
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
cp "$(abspath "$f")" "$dest"
copies="$copies $dest"
done
# shellcheck disable=SC2086
emacs --batch -Q -l "$elisp" -- $copies >/dev/null || {
echo "reindent: emacs failed; cannot determine whether sources conform" >&2
exit 2
}
status=0
for f in $files; do
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
if ! cmp -s "$(abspath "$f")" "$dest"; then
echo "$f"
status=1
fi
done
exit $status

View File

@@ -28,14 +28,14 @@ akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
obj->curSpriteReversing = false; obj->curSpriteReversing = false;
obj->scale = 1.0; obj->scale = 1.0;
obj->movement_controls_face = true; obj->movement_controls_face = true;
obj->updatefunc = &akgl_actor_update; obj->updatefunc = &akgl_actor_update;
obj->renderfunc = &akgl_actor_render; obj->renderfunc = &akgl_actor_render;
obj->facefunc = &akgl_actor_automatic_face; obj->facefunc = &akgl_actor_automatic_face;
obj->movementlogicfunc = &akgl_actor_logic_movement; obj->movementlogicfunc = &akgl_actor_logic_movement;
obj->changeframefunc = &akgl_actor_logic_changeframe; obj->changeframefunc = &akgl_actor_logic_changeframe;
obj->addchild = &akgl_actor_add_child; obj->addchild = &akgl_actor_add_child;
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj), SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
@@ -111,7 +111,7 @@ akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *c
} else { } else {
// we are at the end of the animation and we either loop forward or do not loop // we are at the end of the animation and we either loop forward or do not loop
obj->curSpriteFrameId = 0; obj->curSpriteFrameId = 0;
} }
// we are not looping in reverse and we are not at the end of the animation // we are not looping in reverse and we are not at the end of the animation
} else { } else {
obj->curSpriteFrameId += 1; obj->curSpriteFrameId += 1;
@@ -149,7 +149,7 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
SDL_Time curtime = 0; SDL_Time curtime = 0;
akgl_Sprite *curSprite = NULL; akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
@@ -169,7 +169,7 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
// or changeframefunc, both of which should never return AKERR_KEY... // or changeframefunc, both of which should never return AKERR_KEY...
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -186,10 +186,10 @@ static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, boo
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akgl_Sprite *curSprite = NULL; akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite)); CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
} CLEANUP { } CLEANUP {
@@ -199,7 +199,7 @@ static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, boo
*visible = false; *visible = false;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} FINISH(errctx, true); } FINISH(errctx, true);
if ( (obj->x < (camera->x - curSprite->width)) || if ( (obj->x < (camera->x - curSprite->width)) ||
(obj->x > (camera->x + camera->w)) || (obj->x > (camera->x + camera->w)) ||
(obj->y < (camera->y - curSprite->height)) || (obj->y < (camera->y - curSprite->height)) ||
@@ -218,10 +218,10 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
bool visible = false; bool visible = false;
SDL_FRect src; SDL_FRect src;
SDL_FRect dest; SDL_FRect dest;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite)); CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
CATCH(errctx, actor_visible(obj, camera, &visible)); CATCH(errctx, actor_visible(obj, camera, &visible));
@@ -232,7 +232,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
// If an actor doesn't have a sprite for a state, just log it and move on // If an actor doesn't have a sprite for a state, just log it and move on
LOG_ERROR(errctx); LOG_ERROR(errctx);
} FINISH(errctx, true); } FINISH(errctx, true);
if ( ! visible ) { if ( ! visible ) {
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -254,7 +254,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
if ( obj->parent != NULL ) { if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera->x); dest.x = (obj->parent->x + obj->x - camera->x);
dest.y = (obj->parent->y + obj->y - camera->y); dest.y = (obj->parent->y + obj->y - camera->y);
@@ -271,21 +271,21 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child) akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
{ {
int i = 0; int i = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer"); FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent"); FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) { for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
if ( obj->children[i] == NULL ) { if ( obj->children[i] == NULL ) {
obj->children[i] = child; obj->children[i] = child;
child->parent = obj; child->parent = obj;
child->refcount += 1; child->refcount += 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
}
} }
} FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
} }
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name) void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)

View File

@@ -14,46 +14,46 @@
akerr_ErrorContext *akgl_load_start_bgm(char *fname) akerr_ErrorContext *akgl_load_start_bgm(char *fname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
//akgl_String *tmpstr = NULL; //akgl_String *tmpstr = NULL;
MIX_Track *bgmtrack = NULL; MIX_Track *bgmtrack = NULL;
SDL_PropertiesID bgmprops = 0; SDL_PropertiesID bgmprops = 0;
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename"); FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
//CATCH(errctx, akgl_heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
SDL_Log("Loading music asset from %s", fname);
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
bgmtrack = MIX_CreateTrack(akgl_mixer); //SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError()); SDL_Log("Loading music asset from %s", fname);
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack; bgmtrack = MIX_CreateTrack(akgl_mixer);
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
MIX_SetTrackAudio(bgmtrack, bgm),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1); akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
if (!MIX_PlayTrack(bgmtrack, bgmprops)) { FAIL_ZERO_BREAK(
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname); errctx,
} MIX_SetTrackAudio(bgmtrack, bgm),
} CLEANUP { AKGL_ERR_SDL,
//IGNORE(akgl_heap_release_string(tmpstr)); "%s",
if ( errctx != NULL ) { SDL_GetError());
if ( errctx->status != 0 && bgm != NULL) {
MIX_DestroyAudio(bgm); SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
}
} if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
} PROCESS(errctx) { FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
} FINISH(errctx, true); }
SUCCEED_RETURN(errctx); } CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
if ( errctx != NULL ) {
if ( errctx->status != 0 && bgm != NULL) {
MIX_DestroyAudio(bgm);
}
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
} }

431
src/audio.c Normal file
View File

@@ -0,0 +1,431 @@
/**
* @file audio.c
* @brief Implements the audio subsystem.
*/
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/audio.h>
#include <akgl/error.h>
akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
/*
* The device akgl_audio_init() opened, or NULL when the voice table is not
* connected to one. Everything that mutates a voice locks this stream when it
* is open, because the device callback reads the same table on SDL's audio
* thread. When it is NULL there is no other thread to race with.
*/
static SDL_AudioStream *audiostream = NULL;
/** @brief Level every voice is scaled by before the mix is clamped. */
static float32_t mastervolume = 1.0f;
/** @brief Scratch the device callback mixes into before handing it to SDL. */
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];
/*
* State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
* no allocation, no locking beyond what already guards the voice table, and it
* produces the same sequence every run, so a test can assert on noise output.
*/
static uint32_t noisestate = 0x13579bdfu;
/** @brief Whether the voice table has been given its defaults yet. */
static bool voicesready = false;
/**
* @brief Put every voice back to a flat, audible default.
*
* A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
* sounded a note without first describing an envelope would get nothing and no
* error saying why. The default is instead the simplest thing that makes noise:
* a square wave with no attack, no decay and no release, held at full level for
* as long as the gate is open.
*/
static void reset_voices(void)
{
int i = 0;
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
akgl_audio_voices[i].active = false;
akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
akgl_audio_voices[i].hz = 0.0f;
akgl_audio_voices[i].phase = 0.0f;
akgl_audio_voices[i].duration_frames = 0;
akgl_audio_voices[i].elapsed_frames = 0;
akgl_audio_voices[i].attack_frames = 0;
akgl_audio_voices[i].decay_frames = 0;
akgl_audio_voices[i].release_frames = 0;
akgl_audio_voices[i].sustain = 1.0f;
}
mastervolume = 1.0f;
voicesready = true;
}
/**
* @brief Give the voice table its defaults the first time anything touches it.
*
* The table is process-wide static storage, so it is reachable before
* akgl_audio_init() has run. Rather than make every entry point document an
* ordering requirement, the first one to arrive sets the defaults.
*/
static void ensure_voices(void)
{
if ( voicesready == false ) {
reset_voices();
}
}
/** @brief Lock the voice table against the device callback, if one is running. */
static void lock_voices(void)
{
if ( audiostream != NULL ) {
SDL_LockAudioStream(audiostream);
}
}
/** @brief Release the lock taken by lock_voices(). */
static void unlock_voices(void)
{
if ( audiostream != NULL ) {
SDL_UnlockAudioStream(audiostream);
}
}
/** @brief Convert a duration in milliseconds to a whole number of frames. */
static uint32_t frames_for_ms(uint32_t ms)
{
return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
}
/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
static float32_t noise_sample(void)
{
noisestate ^= noisestate << 13;
noisestate ^= noisestate >> 17;
noisestate ^= noisestate << 5;
// The top 24 bits are the well-mixed ones; scale them to -1..1.
return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
}
/** @brief One sample of @p voice's oscillator at its current phase. */
static float32_t voice_oscillator(akgl_AudioVoice *voice)
{
float32_t value = 0.0f;
switch ( voice->waveform ) {
case AKGL_AUDIO_WAVE_TRIANGLE:
if ( voice->phase < 0.5f ) {
value = (4.0f * voice->phase) - 1.0f;
} else {
value = 3.0f - (4.0f * voice->phase);
}
break;
case AKGL_AUDIO_WAVE_SAWTOOTH:
value = (2.0f * voice->phase) - 1.0f;
break;
case AKGL_AUDIO_WAVE_SQUARE:
value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
break;
case AKGL_AUDIO_WAVE_NOISE:
value = noise_sample();
break;
case AKGL_AUDIO_WAVE_SINE:
value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
break;
}
return value;
}
/**
* @brief Envelope level @p frame frames into the gate, before the release.
*
* Split out because the release has to start from wherever the gate left off,
* which for a gate shorter than attack plus decay is partway up or down a
* ramp rather than at the sustain level.
*/
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
{
uint32_t elapsed = frame;
if ( elapsed < voice->attack_frames ) {
return (float32_t)elapsed / (float32_t)voice->attack_frames;
}
elapsed -= voice->attack_frames;
if ( elapsed < voice->decay_frames ) {
return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
}
return voice->sustain;
}
/** @brief Envelope level for @p voice where it currently stands. */
static float32_t voice_envelope(akgl_AudioVoice *voice)
{
uint32_t released = 0;
float32_t gatelevel = 0.0f;
if ( voice->elapsed_frames < voice->duration_frames ) {
return voice_gate_level(voice, voice->elapsed_frames);
}
released = voice->elapsed_frames - voice->duration_frames;
if ( released >= voice->release_frames ) {
return 0.0f;
}
gatelevel = voice_gate_level(voice, voice->duration_frames);
return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
}
/** @brief Refuse a voice index that is not in the table. */
static akerr_ErrorContext *check_voice(int voice)
{
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(
errctx,
((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
AKERR_OUTOFBOUNDS,
"Voice %d is not in the range 0 to %d",
voice,
AKGL_AUDIO_MAX_VOICES - 1);
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill SDL's request from the voice table.
*
* SDL holds the stream lock for the duration of this callback, which is the
* same lock lock_voices() takes, so the voice table cannot change underneath a
* mix in progress.
*/
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
akerr_ErrorContext *errctx = NULL;
int frames = 0;
while ( additional_amount > 0 ) {
frames = additional_amount / (int)sizeof(float32_t);
if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
frames = AKGL_AUDIO_MIX_FRAMES;
}
if ( frames <= 0 ) {
return;
}
errctx = akgl_audio_mix(mixbuffer, frames);
if ( errctx != NULL ) {
// There is nobody to return an error to on the audio thread, and
// refusing to write leaves SDL underrunning. Report and go quiet.
LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
errctx->handled = true;
errctx = akerr_release_error(errctx);
return;
}
SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
additional_amount -= frames * (int)sizeof(float32_t);
}
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
{
SDL_AudioSpec spec;
PREPARE_ERROR(errctx);
if ( audiostream != NULL ) {
SUCCEED_RETURN(errctx);
}
ensure_voices();
spec.format = SDL_AUDIO_F32;
spec.channels = 1;
spec.freq = AKGL_AUDIO_SAMPLE_RATE;
audiostream = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec,
&audio_stream_callback,
NULL);
FAIL_ZERO_RETURN(
errctx,
audiostream,
AKGL_ERR_SDL,
"Couldn't open an audio device: %s",
SDL_GetError());
// Devices open paused so a caller can set a stream up before it is heard.
// Nothing here needs that, and a caller who expected akgl_audio_tone() to
// make a sound would otherwise get silence with no error to explain it.
FAIL_ZERO_RETURN(
errctx,
SDL_ResumeAudioStreamDevice(audiostream),
AKGL_ERR_SDL,
"Couldn't start the audio device: %s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
{
SDL_AudioStream *closing = audiostream;
PREPARE_ERROR(errctx);
// Clear the pointer before destroying the stream: lock_voices() checks it,
// and SDL_DestroyAudioStream can run the callback one last time.
audiostream = NULL;
if ( closing != NULL ) {
SDL_DestroyAudioStream(closing);
}
reset_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
ensure_voices();
lock_voices();
akgl_audio_voices[voice].hz = hz;
akgl_audio_voices[voice].phase = 0.0f;
akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
akgl_audio_voices[voice].elapsed_frames = 0;
akgl_audio_voices[voice].active = true;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
ensure_voices();
lock_voices();
akgl_audio_voices[voice].active = false;
akgl_audio_voices[voice].elapsed_frames = 0;
akgl_audio_voices[voice].phase = 0.0f;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(
errctx,
((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
AKERR_OUTOFBOUNDS,
"Waveform %d is not one of the %d shapes",
(int)waveform,
(int)AKGL_AUDIO_WAVE_SINE + 1);
ensure_voices();
lock_voices();
akgl_audio_voices[voice].waveform = waveform;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_NONZERO_RETURN(
errctx,
((sustain < 0.0f) || (sustain > 1.0f)),
AKERR_OUTOFBOUNDS,
"Sustain level %f is not between 0.0 and 1.0",
sustain);
ensure_voices();
lock_voices();
akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
akgl_audio_voices[voice].release_frames = frames_for_ms(release);
akgl_audio_voices[voice].sustain = sustain;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
{
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(
errctx,
((level < 0.0f) || (level > 1.0f)),
AKERR_OUTOFBOUNDS,
"Volume level %f is not between 0.0 and 1.0",
level);
ensure_voices();
lock_voices();
mastervolume = level;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
{
PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice));
FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");
ensure_voices();
lock_voices();
*active = akgl_audio_voices[voice].active;
unlock_voices();
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
{
akgl_AudioVoice *voice = NULL;
float32_t sum = 0.0f;
int i = 0;
int v = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);
for ( i = 0; i < frames; i++ ) {
sum = 0.0f;
for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
voice = &akgl_audio_voices[v];
if ( voice->active == false ) {
continue;
}
if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
// Gate and release are both spent. The voice goes quiet on its
// own so a caller does not have to keep a clock to stop it.
voice->active = false;
voice->elapsed_frames = 0;
voice->phase = 0.0f;
continue;
}
// Derived from the frame counter rather than accumulated one
// increment at a time. A float increment of hz/rate is not exact,
// and adding it 44100 times a second walks the pitch off over the
// length of a held note.
voice->phase = (float32_t)SDL_fmod(
((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
1.0);
sum += voice_oscillator(voice) * voice_envelope(voice);
voice->elapsed_frames += 1;
}
sum = sum * mastervolume;
// Three voices at full level can sum past full scale. Clamping rather
// than scaling by the voice count keeps a single voice at the level it
// was asked for instead of a third of it.
if ( sum > 1.0f ) {
sum = 1.0f;
} else if ( sum < -1.0f ) {
sum = -1.0f;
}
dest[i] = sum;
}
SUCCEED_RETURN(errctx);
}

View File

@@ -27,10 +27,10 @@ akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH); strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
obj->state_sprites = SDL_CreateProperties(); obj->state_sprites = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map"); FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
obj->sprite_add = &akgl_character_sprite_add; obj->sprite_add = &akgl_character_sprite_add;
obj->sprite_get = &akgl_character_sprite_get; obj->sprite_get = &akgl_character_sprite_get;
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
errctx, errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj), SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
@@ -106,7 +106,7 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array"); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer"); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstring)); CATCH(errctx, akgl_heap_next_string(&tmpstring));
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) { for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
@@ -140,7 +140,7 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
akgl_String *tmpstr2 = NULL; akgl_String *tmpstr2 = NULL;
int stateval = 0; int stateval = 0;
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data)); CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
@@ -155,15 +155,15 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
NULL NULL
); );
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray)); CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval)); CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
spriteptr, spriteptr,
AKERR_NULLPOINTER, AKERR_NULLPOINTER,
"Character %s for state %b references sprite %s but not found in the registry", "Character %s for state %b references sprite %s but not found in the registry",
tmpstr2->data, tmpstr2->data,
stateval, stateval,
@@ -190,7 +190,7 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
json_error_t error; json_error_t error;
akgl_Character *obj = NULL; akgl_Character *obj = NULL;
//akgl_String *tmpstr = NULL; //akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename"); FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&obj)); CATCH(errctx, akgl_heap_next_character(&obj));

View File

@@ -12,6 +12,39 @@
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS]; akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
/*
* Keystrokes waiting for akgl_controller_poll_key(). Filled by
* akgl_controller_handle_event() before it consults the control maps, so a key
* that also drives an actor is still delivered to a polling caller.
*
* head is the next slot to read, count is how many are waiting. Both index a
* fixed array rather than a queue object, which is the whole point: a host that
* never polls cannot make this grow.
*/
static SDL_Keycode keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
static int keybuffer_head = 0;
static int keybuffer_count = 0;
/**
* @brief Record one keystroke, discarding it if the buffer is already full.
*
* Dropping the newest rather than overwriting the oldest is deliberate. A
* caller reading a line of input wants the characters that were typed first;
* overwriting would hand it the tail of what the user typed and silently lose
* the head.
*/
static void keybuffer_push(SDL_Keycode key)
{
int tail = 0;
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
return;
}
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[tail] = key;
keybuffer_count += 1;
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void) akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
{ {
int count; int count;
@@ -19,7 +52,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError()); FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
const char *name = SDL_GetKeyboardNameForID(keyboards[i]); const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name); SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
@@ -65,6 +98,13 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
// Before the control maps, not after: a key bound to an actor is still a
// key an interpreter polling with akgl_controller_poll_key() wants to see,
// and the scan below returns as soon as a binding claims the event.
if ( event->type == SDL_EVENT_KEY_DOWN ) {
keybuffer_push(event->key.key);
}
ATTEMPT { ATTEMPT {
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &GAME_ControlMaps[i]; curmap = &GAME_ControlMaps[i];
@@ -80,7 +120,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
// This controlmap processes this control // This controlmap processes this control
eventButtonComboMatch = ( eventButtonComboMatch = (
((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || ((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) && event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
event->gbutton.which == curmap->jsid && event->gbutton.which == curmap->jsid &&
event->gbutton.button == curcontrol->button) || event->gbutton.button == curcontrol->button) ||
((event->type == SDL_EVENT_KEY_DOWN || ((event->type == SDL_EVENT_KEY_DOWN ||
@@ -104,7 +144,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
} }
} }
} }
_akgl_controller_handle_event_success: _akgl_controller_handle_event_success:
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -121,7 +161,7 @@ _akgl_controller_handle_event_success:
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event) akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
{ {
akgl_Actor *player = NULL; akgl_Actor *player = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
@@ -130,39 +170,39 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist"); FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) { event->key.key == SDLK_DOWN ) {
SDL_Log("Processing dpad down : state %d", player->state); SDL_Log("Processing dpad down : state %d", player->state);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) { event->key.key == SDLK_UP ) {
SDL_Log("Processing dpad up"); SDL_Log("Processing dpad up");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) { event->key.key == SDLK_LEFT ) {
SDL_Log("Processing dpad left"); SDL_Log("Processing dpad left");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT ) { event->key.key == SDLK_RIGHT ) {
SDL_Log("Processing dpad right"); SDL_Log("Processing dpad right");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
if ( !player->movement_controls_face ) { if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT); AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
} }
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} }
@@ -179,7 +219,7 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event) akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
{ {
akgl_Actor *player = NULL; akgl_Actor *player = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
@@ -188,25 +228,25 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist"); FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) { event->key.key == SDLK_DOWN ) {
SDL_Log("processing down release"); SDL_Log("processing down release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) { event->key.key == SDLK_UP ) {
SDL_Log("processing up release"); SDL_Log("processing up release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT) { event->key.key == SDLK_RIGHT) {
SDL_Log("processing right release"); SDL_Log("processing right release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state); SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT || } else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) { event->key.key == SDLK_LEFT ) {
SDL_Log("processing left release"); SDL_Log("processing left release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT); AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
player->curSpriteFrameId = 0; player->curSpriteFrameId = 0;
@@ -227,24 +267,24 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
SDL_JoystickID which; SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL; SDL_Gamepad *gamepad = NULL;
char *mapping = NULL; char *mapping = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which; which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which); gamepad = SDL_GetGamepadFromID(which);
if (!gamepad) { if (!gamepad) {
SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError()); SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
gamepad = SDL_OpenGamepad(which); gamepad = SDL_OpenGamepad(which);
SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError()); SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
mapping = SDL_GetGamepadMapping(gamepad); mapping = SDL_GetGamepadMapping(gamepad);
if ( mapping == NULL ) { if ( mapping == NULL ) {
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which); SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
} else if ( mapping != NULL ) { } else if ( mapping != NULL ) {
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping); SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
SDL_free(mapping); SDL_free(mapping);
} }
} else { } else {
SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad)); SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
@@ -270,7 +310,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
which = event->gbutton.which; which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which); gamepad = SDL_GetGamepadFromID(which);
if (gamepad) { if (gamepad) {
SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */ SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */
} }
@@ -292,14 +332,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akg
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid) akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{ {
akgl_ControlMap *controlmap; akgl_ControlMap *controlmap;
akgl_Control control; akgl_Control control;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
// set up the control map // set up the control map
@@ -308,12 +348,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
controlmap = &GAME_ControlMaps[controlmapid]; controlmap = &GAME_ControlMaps[controlmapid];
controlmap->kbid = kbid; controlmap->kbid = kbid;
controlmap->jsid = jsid; controlmap->jsid = jsid;
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL); controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname); FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
// ---- KEYBOARD CONTROLS ---- // ---- KEYBOARD CONTROLS ----
// Move down // Move down
control.key = SDLK_DOWN; control.key = SDLK_DOWN;
control.event_on = SDL_EVENT_KEY_DOWN; control.event_on = SDL_EVENT_KEY_DOWN;
@@ -329,7 +369,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_up_on; control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off; control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left // Move left
control.key = SDLK_LEFT; control.key = SDLK_LEFT;
control.event_on = SDL_EVENT_KEY_DOWN; control.event_on = SDL_EVENT_KEY_DOWN;
@@ -337,7 +377,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_left_on; control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off; control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right // Move right
control.key = SDLK_RIGHT; control.key = SDLK_RIGHT;
control.event_on = SDL_EVENT_KEY_DOWN; control.event_on = SDL_EVENT_KEY_DOWN;
@@ -363,7 +403,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_up_on; control.handler_on = &akgl_Actor_cmhf_up_on;
control.handler_off = &akgl_Actor_cmhf_up_off; control.handler_off = &akgl_Actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left // Move left
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT; control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN; control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
@@ -371,7 +411,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_left_on; control.handler_on = &akgl_Actor_cmhf_left_on;
control.handler_off = &akgl_Actor_cmhf_left_off; control.handler_off = &akgl_Actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right // Move right
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT; control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN; control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
@@ -379,10 +419,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
control.handler_on = &akgl_Actor_cmhf_right_on; control.handler_on = &akgl_Actor_cmhf_right_on;
control.handler_off = &akgl_Actor_cmhf_right_off; control.handler_off = &akgl_Actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
if ( keybuffer_count == 0 ) {
// An empty buffer is the ordinary case, not a failure: the caller is
// asking whether a key is waiting, and the answer is no.
*keycode = 0;
*available = false;
SUCCEED_RETURN(errctx);
}
*keycode = (int)keybuffer[keybuffer_head];
*available = true;
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer_count -= 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
{
PREPARE_ERROR(errctx);
keybuffer_head = 0;
keybuffer_count = 0;
SUCCEED_RETURN(errctx);
}

View File

@@ -6,14 +6,34 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <akerror.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h> #include <akgl/game.h>
/** @brief One horizontal run of pixels the flood fill has still to examine. */
typedef struct {
int x1;
int x2;
int y;
} FloodSpan;
/*
* The flood fill's working stack. File scope and fixed size rather than a local
* array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
* on the stack of a function a game may call every frame. The consequence is
* that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
* immediate-mode operation on a single render target, and so is everything else
* that touches an SDL_Renderer.
*/
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
/* Draw a Gimpish background pattern to show transparency in the image */ /* Draw a Gimpish background pattern to show transparency in the image */
void akgl_draw_background(int w, int h) void akgl_draw_background(int w, int h)
{ {
SDL_Color col[2] = { SDL_Color col[2] = {
{ 0x66, 0x66, 0x66, 0xff }, { 0x66, 0x66, 0x66, 0xff },
{ 0x99, 0x99, 0x99, 0xff }, { 0x99, 0x99, 0x99, 0xff },
}; };
int i, x, y; int i, x, y;
SDL_FRect rect; SDL_FRect rect;
@@ -22,14 +42,510 @@ void akgl_draw_background(int w, int h)
rect.w = (float)dx; rect.w = (float)dx;
rect.h = (float)dy; rect.h = (float)dy;
for (y = 0; y < h; y += dy) { for (y = 0; y < h; y += dy) {
for (x = 0; x < w; x += dx) { for (x = 0; x < w; x += dx) {
/* use an 8x8 checkerboard pattern */ /* use an 8x8 checkerboard pattern */
i = (((x ^ y) >> 3) & 1); i = (((x ^ y) >> 3) & 1);
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a); SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
rect.x = (float)x; rect.x = (float)x;
rect.y = (float)y; rect.y = (float)y;
SDL_RenderFillRect(renderer->sdl_renderer, &rect); SDL_RenderFillRect(renderer->sdl_renderer, &rect);
} }
} }
} }
/**
* @brief Remember the renderer's draw color and replace it with @p color.
*
* @p previous is written before anything that can fail, so a caller may restore
* it unconditionally from a CLEANUP block.
*/
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
{
PREPARE_ERROR(errctx);
previous->r = 0x00;
previous->g = 0x00;
previous->b = 0x00;
previous->a = SDL_ALPHA_OPAQUE;
FAIL_ZERO_RETURN(
errctx,
SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
/** @brief Put back the draw color push_draw_color() recorded. */
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
/**
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
*
* A scanline fill: each entry on the stack is a run of pixels on one row that
* still has to be examined. Finding a matching pixel expands it to the whole
* run it belongs to, fills that run, and pushes the rows above and below.
* Filled pixels no longer match @p oldpixel, which is what terminates it.
*
* @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
* 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
*
* @p dirty is set to the bounding box of everything written, so the caller can
* put back only the pixels that changed.
*
* Running out of stack leaves the region partially filled and reports
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
* a copy of the whole surface, and the caller asked for a bounded operation.
*/
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
{
uint32_t *pixels = (uint32_t *)surface->pixels;
int pitch = surface->pitch / (int)sizeof(uint32_t);
int count = 0;
int col = 0;
int left = 0;
int right = 0;
int i = 0;
int minx = surface->w;
int miny = surface->h;
int maxx = -1;
int maxy = -1;
FloodSpan span;
PREPARE_ERROR(errctx);
floodspans[0].x1 = x;
floodspans[0].x2 = x;
floodspans[0].y = y;
count = 1;
while ( count > 0 ) {
count -= 1;
span = floodspans[count];
col = span.x1;
while ( col <= span.x2 ) {
if ( pixels[(span.y * pitch) + col] != oldpixel ) {
col += 1;
continue;
}
left = col;
while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
left -= 1;
}
right = col;
while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
right += 1;
}
for ( i = left; i <= right; i++ ) {
pixels[(span.y * pitch) + i] = newpixel;
}
if ( left < minx ) {
minx = left;
}
if ( right > maxx ) {
maxx = right;
}
if ( span.y < miny ) {
miny = span.y;
}
if ( span.y > maxy ) {
maxy = span.y;
}
// Two pushes per run, so the check is for room for both.
FAIL_NONZERO_RETURN(
errctx,
((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
AKERR_OUTOFBOUNDS,
"Region needs more than %d pending spans; it is partially filled",
AKGL_DRAW_MAX_FLOOD_SPANS);
if ( span.y > 0 ) {
floodspans[count].x1 = left;
floodspans[count].x2 = right;
floodspans[count].y = span.y - 1;
count += 1;
}
if ( span.y < (surface->h - 1) ) {
floodspans[count].x1 = left;
floodspans[count].x2 = right;
floodspans[count].y = span.y + 1;
count += 1;
}
col = right + 1;
}
}
dirty->x = minx;
dirty->y = miny;
dirty->w = (maxx - minx) + 1;
dirty->h = (maxy - miny) + 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderPoint(self->sdl_renderer, x, y),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderRect(self->sdl_renderer, rect),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{
SDL_Color previous;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
FAIL_ZERO_BREAK(
errctx,
SDL_RenderFillRect(self->sdl_renderer, rect),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
{
SDL_Color previous;
SDL_FPoint octants[8];
int centerx = 0;
int centery = 0;
int r = 0;
int offsetx = 0;
int offsety = 0;
int decision = 0;
bool plotted = true;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);
centerx = (int)SDL_lroundf(x);
centery = (int)SDL_lroundf(y);
r = (int)SDL_lroundf(radius);
offsety = r;
// The midpoint decision variable, started so the first step chooses between
// (0, r) and (1, r-1) correctly.
decision = 1 - r;
ATTEMPT {
CATCH(errctx, push_draw_color(self, color, &previous));
while ( offsety >= offsetx ) {
// Eight-way symmetry: one computed point in the second octant gives
// the seven others by reflection.
octants[0].x = (float)(centerx + offsetx);
octants[0].y = (float)(centery + offsety);
octants[1].x = (float)(centerx - offsetx);
octants[1].y = (float)(centery + offsety);
octants[2].x = (float)(centerx + offsetx);
octants[2].y = (float)(centery - offsety);
octants[3].x = (float)(centerx - offsetx);
octants[3].y = (float)(centery - offsety);
octants[4].x = (float)(centerx + offsety);
octants[4].y = (float)(centery + offsetx);
octants[5].x = (float)(centerx - offsety);
octants[5].y = (float)(centery + offsetx);
octants[6].x = (float)(centerx + offsety);
octants[6].y = (float)(centery - offsetx);
octants[7].x = (float)(centerx - offsety);
octants[7].y = (float)(centery - offsetx);
// A CATCH here would break this loop rather than leave the function,
// so failure is recorded and reported once the loop is done.
if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
plotted = false;
}
offsetx += 1;
if ( decision < 0 ) {
decision += (2 * offsetx) + 1;
} else {
offsety -= 1;
decision += 2 * (offsetx - offsety) + 1;
}
}
FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
} CLEANUP {
IGNORE(pop_draw_color(self, &previous));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
{
SDL_Surface *target = NULL;
SDL_Surface *rgba = NULL;
SDL_Texture *patch = NULL;
// Only written by a successful flood_region(), and only read after one, but
// the paths in between are far enough apart that the compiler cannot see it.
SDL_Rect dirty = { 0, 0, 0, 0 };
SDL_FRect src;
SDL_FRect dest;
uint32_t *pixels = NULL;
uint32_t oldpixel = 0;
uint32_t newpixel = 0;
int width = 0;
int height = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
ATTEMPT {
FAIL_ZERO_BREAK(
errctx,
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_NONZERO_BREAK(
errctx,
((x < 0) || (y < 0) || (x >= width) || (y >= height)),
AKERR_OUTOFBOUNDS,
"Seed pixel %d,%d is outside the %dx%d render target",
x, y, width, height);
target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
// The fill works on 32-bit words, so the layout has to be known rather
// than whatever the render target happens to use.
rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());
pixels = (uint32_t *)rgba->pixels;
oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
if ( oldpixel == newpixel ) {
// Already the requested color. Walking it would compare filled
// pixels against themselves and find nothing, so say so up front.
SUCCEED_BREAK(errctx);
}
CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
// Replace rather than blend: this is a framebuffer operation, and the
// pixels being put back are the ones that were just read out of it.
FAIL_ZERO_BREAK(
errctx,
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
// Only the bounding box of what changed goes back to the target.
src.x = (float)dirty.x;
src.y = (float)dirty.y;
src.w = (float)dirty.w;
src.h = (float)dirty.h;
dest = src;
FAIL_ZERO_BREAK(
errctx,
SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
if ( patch != NULL ) {
SDL_DestroyTexture(patch);
}
if ( rgba != NULL ) {
SDL_DestroySurface(rgba);
}
if ( target != NULL ) {
SDL_DestroySurface(target);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
{
SDL_Surface *saved = NULL;
int width = 0;
int height = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
FAIL_NONZERO_BREAK(
errctx,
((src->w <= 0) || (src->h <= 0)),
AKERR_OUTOFBOUNDS,
"Region %dx%d has no area",
src->w, src->h);
FAIL_ZERO_BREAK(
errctx,
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
// SDL clips a read to the target and hands back a smaller surface than
// was asked for, which a caller pasting it back would not notice.
FAIL_NONZERO_BREAK(
errctx,
((src->x < 0) || (src->y < 0) ||
((src->x + src->w) > width) || ((src->y + src->h) > height)),
AKERR_OUTOFBOUNDS,
"Region %d,%d %dx%d does not fit inside the %dx%d render target",
src->x, src->y, src->w, src->h, width, height);
saved = SDL_RenderReadPixels(self->sdl_renderer, src);
FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());
if ( *dest == NULL ) {
*dest = saved;
// Ownership has moved to the caller; CLEANUP must not free it.
saved = NULL;
} else {
FAIL_NONZERO_BREAK(
errctx,
(((*dest)->w != src->w) || ((*dest)->h != src->h)),
AKERR_OUTOFBOUNDS,
"Destination surface is %dx%d, region is %dx%d",
(*dest)->w, (*dest)->h, src->w, src->h);
FAIL_ZERO_BREAK(
errctx,
SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
SDL_BlitSurface(saved, NULL, *dest, NULL),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
}
} CLEANUP {
if ( saved != NULL ) {
SDL_DestroySurface(saved);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
{
SDL_Texture *patch = NULL;
SDL_FRect dest;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
ATTEMPT {
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
// Replace what is on the target, the way GSHAPE does by default.
FAIL_ZERO_BREAK(
errctx,
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
dest.x = x;
dest.y = y;
dest.w = (float)src->w;
dest.h = (float)src->h;
FAIL_ZERO_BREAK(
errctx,
SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
} CLEANUP {
if ( patch != NULL ) {
SDL_DestroyTexture(patch);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}

24
src/error.c Normal file
View File

@@ -0,0 +1,24 @@
/**
* @file error.c
* @brief Implements the error subsystem: claims and names the libakgl status band.
*/
#include <akerror.h>
#include <akgl/error.h>
akerr_ErrorContext *akgl_error_init(void)
{
PREPARE_ERROR(errctx);
// Claim the whole band before naming anything in it: libakerror refuses a
// name for a status we do not own. Any collision propagates to the caller
// -- another component owning part of our range is an initialization
// failure, not a warning.
PASS(errctx, akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, AKGL_ERR_OWNER));
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_SDL, "SDL Error"));
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_REGISTRY, "Registry Error"));
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_HEAP, "Heap Error"));
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
SUCCEED_RETURN(errctx);
}

View File

@@ -21,6 +21,7 @@
#include <akgl/staticstring.h> #include <akgl/staticstring.h>
#include <akgl/iterator.h> #include <akgl/iterator.h>
#include <akgl/physics.h> #include <akgl/physics.h>
#include <akgl/error.h>
#include <akgl/SDL_GameControllerDB.h> #include <akgl/SDL_GameControllerDB.h>
SDL_Window *window = NULL; SDL_Window *window = NULL;
@@ -52,9 +53,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
{ {
int screenwidth = 0; int screenwidth = 0;
int screenheight = 0; int screenheight = 0;
int i = 0; int i = 0;
PREPARE_ERROR(e); PREPARE_ERROR(e);
// First, before anything that can raise: everything below reports through
// AKGL_ERR_* codes, and a code raised before its name is registered prints
// as "Unknown Error" in the stack trace the caller is left holding.
PASS(e, akgl_error_init());
strncpy((char *)&game.libversion, AKGL_VERSION, 32); strncpy((char *)&game.libversion, AKGL_VERSION, 32);
game.gameStartTime = SDL_GetTicksNS(); game.gameStartTime = SDL_GetTicksNS();
game.lastIterTime = game.gameStartTime; game.lastIterTime = game.gameStartTime;
@@ -75,13 +80,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
PASS(e, akgl_registry_init_music()); PASS(e, akgl_registry_init_music());
PASS(e, akgl_registry_init_properties()); PASS(e, akgl_registry_init_properties());
PASS(e, akgl_registry_init_actor_state_strings()); PASS(e, akgl_registry_init_actor_state_strings());
SDL_SetAppMetadata(game.name, game.version, game.uri); SDL_SetAppMetadata(game.name, game.version, game.uri);
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap)); memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
} }
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
e, e,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO), SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
@@ -123,7 +128,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
physics = &_akgl_physics; physics = &_akgl_physics;
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
PASS(e, akgl_game_state_unlock()); PASS(e, akgl_game_state_unlock());
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -358,27 +363,27 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
// corrupt name table as a successful load. // corrupt name table as a successful load.
while ( done == false ) { while ( done == false ) {
ATTEMPT { ATTEMPT {
CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp)); CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp));
CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp)); CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp));
// End of the map // End of the map
if ( ptr == 0x00 && objname[0] == 0x00 ) { if ( ptr == 0x00 && objname[0] == 0x00 ) {
done = true; done = true;
break; break;
} }
// The map allows us to say "Object X has a reference to object Y at // The map allows us to say "Object X has a reference to object Y at
// address Z. The object they had at address Z was named A. Our current // address Z. The object they had at address Z was named A. Our current
// instance of object named A is at address B. So we map address Z to // instance of object named A is at address B. So we map address Z to
// address B, so that we can reconnect function pointers on objects loaded // address B, so that we can reconnect function pointers on objects loaded
// from the save game state." // from the save game state."
// SDL_Properties objects can only use string keys, so we can't use the // SDL_Properties objects can only use string keys, so we can't use the
// old pointer as a key without first converting it to a string. // old pointer as a key without first converting it to a string.
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32)); CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
snprintf((char *)&ptrstring, 32, "%p", ptr); snprintf((char *)&ptrstring, 32, "%p", ptr);
SDL_SetPointerProperty( SDL_SetPointerProperty(
map, map,
ptrstring, ptrstring,
SDL_GetPointerProperty(registry, objname, NULL)); SDL_GetPointerProperty(registry, objname, NULL));
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} FINISH(e, true); } FINISH(e, true);
@@ -404,9 +409,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype,
FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument");
ATTEMPT { ATTEMPT {
// Check save game library version // Check save game library version
FAIL_NONZERO_BREAK( FAIL_NONZERO_BREAK(
e, e,
semver_parse((const char *)curversion, &current_version), semver_parse((const char *)curversion, &current_version),
@@ -445,10 +450,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
SDL_PropertiesID spritesheetmap; SDL_PropertiesID spritesheetmap;
SDL_PropertiesID charactermap; SDL_PropertiesID charactermap;
FILE *fp = NULL; FILE *fp = NULL;
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path"); FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
ATTEMPT { ATTEMPT {
CATCH(e, aksl_fopen(fpath, "rb", &fp)); CATCH(e, aksl_fopen(fpath, "rb", &fp));
CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp)); CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp));
@@ -464,28 +469,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
strncmp((char *)&savegame.uri, (char *)&game.uri, 256), strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
AKERR_API, AKERR_API,
"Savegame is not compatible with this game"); "Savegame is not compatible with this game");
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game)); memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
// Load actor name map // Load actor name map
actormap = SDL_CreateProperties(); actormap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR)); CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
// Load sprite name map // Load sprite name map
spritemap = SDL_CreateProperties(); spritemap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE)); CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
// Load spritesheet name map // Load spritesheet name map
spritesheetmap = SDL_CreateProperties(); spritesheetmap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET)); CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
// Load character name map // Load character name map
charactermap = SDL_CreateProperties(); charactermap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER)); CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers // Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
} CLEANUP { } CLEANUP {
if ( fp != NULL ) { if ( fp != NULL ) {
fclose(fp); fclose(fp);
} }
} PROCESS(e) { } PROCESS(e) {
} FINISH(e, true); } FINISH(e, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -502,7 +507,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
if ( opflags == NULL ) { if ( opflags == NULL ) {
opflags = &defflags; opflags = &defflags;
} }
PASS(e, akgl_game_state_lock()); PASS(e, akgl_game_state_lock());
akgl_game_updateFPS(); akgl_game_updateFPS();
@@ -521,8 +526,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
} else { } else {
actor->scale = 1.0; actor->scale = 1.0;
} }
PASS(e, actor->updatefunc(actor)); PASS(e, actor->updatefunc(actor));
} }
} }
PASS(e, physics->simulate(physics, NULL)); PASS(e, physics->simulate(physics, NULL));
PASS(e, renderer->draw_world(renderer, NULL)); PASS(e, renderer->draw_world(renderer, NULL));

View File

@@ -24,11 +24,6 @@ akerr_ErrorContext *akgl_heap_init()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int i = 0; int i = 0;
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
akerr_name_for_status(AKGL_ERR_REGISTRY, "Registry Error");
akerr_name_for_status(AKGL_ERR_HEAP, "Heap Error");
akerr_name_for_status(AKGL_ERR_BEHAVIOR, "Behavior Error");
akerr_name_for_status(AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt");
PASS(errctx, akgl_heap_init_actor()); PASS(errctx, akgl_heap_init_actor());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) { for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite)); memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
@@ -146,7 +141,7 @@ akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
akgl_Iterator opflags; akgl_Iterator opflags;
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
AKGL_BITMASK_CLEAR(opflags.flags); AKGL_BITMASK_CLEAR(opflags.flags);
if ( basechar->refcount > 0 ) { if ( basechar->refcount > 0 ) {
basechar->refcount -= 1; basechar->refcount -= 1;
} }
@@ -201,4 +196,3 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -15,135 +15,135 @@
akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest) akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest) akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_boolean_value(value); *dest = json_boolean_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest) akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_integer_value(value); *dest = json_integer_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest) akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value); *dest = json_number_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest) akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value); *dest = json_number_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest) akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
{ {
json_t *value = NULL; json_t *value = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
value = json_object_get(obj, key); value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
ATTEMPT { ATTEMPT {
if ( *dest == NULL ) { if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest)); CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL)); CATCH(errctx, akgl_string_initialize(*dest, NULL));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, false); } FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH); strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest) akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest) akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = value; *dest = value;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest) akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = json_integer_value(value); *dest = json_integer_value(value);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest) akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
ATTEMPT { ATTEMPT {
if ( *dest == NULL ) { if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest)); CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL)); CATCH(errctx, akgl_string_initialize(*dest, NULL));
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, false); } FINISH(errctx, false);
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH); strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize) akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
@@ -153,11 +153,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
int docopy = 0; int docopy = 0;
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err"); FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err");
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval"); FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT { ATTEMPT {
} CLEANUP { } CLEANUP {
} PROCESS(err) { } PROCESS(err) {
@@ -165,6 +165,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext
} HANDLE_GROUP(err, AKERR_INDEX) { } HANDLE_GROUP(err, AKERR_INDEX) {
memcpy(dest, defval, defsize); memcpy(dest, defval, defsize);
} FINISH(err, true); } FINISH(err, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }

View File

@@ -37,7 +37,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
self->gravity = akgl_physics_null_gravity; self->gravity = akgl_physics_null_gravity;
self->collide = akgl_physics_null_collide; self->collide = akgl_physics_null_collide;
self->move = akgl_physics_null_move; self->move = akgl_physics_null_move;
@@ -65,7 +65,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBacke
// Assume Z origin is - (behind the camera) // Assume Z origin is - (behind the camera)
actor->ez -= (self->gravity_z * dt); actor->ez -= (self->gravity_z * dt);
} }
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -84,7 +84,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsB
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor"); FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
actor->x += actor->vx * dt; actor->x += actor->vx * dt;
actor->y += actor->vy * dt; actor->y += actor->vy * dt;
actor->z += actor->vz * dt; actor->z += actor->vz * dt;
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -94,7 +94,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
PASS(e, akgl_heap_next_string(&tmp)); PASS(e, akgl_heap_next_string(&tmp));
self->gravity = akgl_physics_arcade_gravity; self->gravity = akgl_physics_arcade_gravity;
self->collide = akgl_physics_arcade_collide; self->collide = akgl_physics_arcade_collide;
self->move = akgl_physics_arcade_move; self->move = akgl_physics_arcade_move;
@@ -117,7 +117,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
IGNORE(akgl_heap_release_string(tmp)); IGNORE(akgl_heap_release_string(tmp));
} PROCESS(e) { } PROCESS(e) {
} FINISH(e, true); } FINISH(e, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -143,8 +143,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
if ( opflags == NULL ) { if ( opflags == NULL ) {
opflags = &defflags; opflags = &defflags;
} }
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
actor = &HEAP_ACTOR[i]; actor = &HEAP_ACTOR[i];
if ( actor->refcount == 0 ) { if ( actor->refcount == 0 ) {
@@ -164,21 +164,21 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
continue; continue;
} }
} }
// thrust is a function of acceleration on a given axis // thrust is a function of acceleration on a given axis
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) || if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) { AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->tx += actor->ax * dt; actor->tx += actor->ax * dt;
} }
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) || if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) { AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ty += actor->ay * dt; actor->ty += actor->ay * dt;
} }
// velocity equals thrust unless thrust exceeds max speed // velocity equals thrust unless thrust exceeds max speed
if ( fabsf(actor->tx) > fabsf(actor->sx) ) { if ( fabsf(actor->tx) > fabsf(actor->sx) ) {
if ( actor->tx < 0 ) { if ( actor->tx < 0 ) {
actor->tx = -actor->sx; actor->tx = -actor->sx;
} else { } else {
actor->tx = actor->sx; actor->tx = actor->sx;
} }
@@ -216,14 +216,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
actor->vy = actor->ey + actor->ty; actor->vy = actor->ey + actor->ty;
actor->vz = actor->ez + actor->tz; actor->vz = actor->ez + actor->tz;
PASS(e, self->move(self, actor, dt)); PASS(e, self->move(self, actor, dt));
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) { } HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
// noop // noop
} FINISH(e, true); } FINISH(e, true);
} }
self->gravity_time = curtime; self->gravity_time = curtime;
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -233,7 +233,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *sel
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, type, AKERR_NULLPOINTER, "type"); FAIL_ZERO_RETURN(e, type, AKERR_NULLPOINTER, "type");
if ( strncmp(type->data, "null", 4) == 0) { if ( strncmp(type->data, "null", 4) == 0) {
PASS(e, akgl_physics_init_null(self)); PASS(e, akgl_physics_init_null(self));
SUCCEED_RETURN(e); SUCCEED_RETURN(e);

View File

@@ -95,7 +95,7 @@ akerr_ErrorContext *akgl_registry_init_actor_state_strings()
akerr_ErrorContext *akgl_registry_init_sprite() akerr_ErrorContext *akgl_registry_init_sprite()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITE = SDL_CreateProperties(); AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry"); FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -123,7 +123,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
json_t *props = NULL; json_t *props = NULL;
const char *pkey = NULL; const char *pkey = NULL;
json_t *pvalue = NULL; json_t *pvalue = NULL;
json_error_t error; json_error_t error;
akgl_String *tmpstr; akgl_String *tmpstr;
@@ -189,4 +189,3 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
} FINISH(e, true); } FINISH(e, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }

View File

@@ -30,14 +30,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight); SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
PASS(e, akgl_heap_release_string(width)); PASS(e, akgl_heap_release_string(width));
PASS(e, akgl_heap_release_string(height)); PASS(e, akgl_heap_release_string(height));
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
e, e,
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer), SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
AKGL_ERR_SDL, AKGL_ERR_SDL,
"Couldn't create window/renderer: %s", "Couldn't create window/renderer: %s",
SDL_GetError()); SDL_GetError());
camera->x = 0; camera->x = 0;
camera->y = 0; camera->y = 0;
camera->w = screenwidth; camera->w = screenwidth;
@@ -133,9 +133,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
if ( actor->layer != i ) { if ( actor->layer != i ) {
continue; continue;
} }
PASS(e, actor->renderfunc(actor)); PASS(e, actor->renderfunc(actor));
} }
} }
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }

View File

@@ -85,10 +85,10 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
IGNORE(akgl_heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_sprite_load_json(char *filename) akerr_ErrorContext *akgl_sprite_load_json(char *filename)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -100,7 +100,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
akgl_String *spritename = NULL; akgl_String *spritename = NULL;
akgl_String *filename_copy = NULL; akgl_String *filename_copy = NULL;
int i = 0; int i = 0;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename"); FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_sprite(&obj)); CATCH(errctx, akgl_heap_next_sprite(&obj));
@@ -125,7 +125,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
AKERR_NULLPOINTER, AKERR_NULLPOINTER,
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text "Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
); );
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data))); CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx, CATCH(errctx,
@@ -134,14 +134,14 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
spritename->data, spritename->data,
(akgl_SpriteSheet *)sheet) (akgl_SpriteSheet *)sheet)
); );
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed)); CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
obj->speed = obj->speed * AKGL_TIME_ONESEC_MS; obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop)); CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse)); CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames)); CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
obj->frames = json_array_size((json_t *)frames); obj->frames = json_array_size((json_t *)frames);
for ( i = 0 ; i < obj->frames; i++ ) { for ( i = 0 ; i < obj->frames; i++ ) {
@@ -166,7 +166,7 @@ akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_Sp
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference"); FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference"); FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
memset(spr, 0x00, sizeof(akgl_Sprite)); memset(spr, 0x00, sizeof(akgl_Sprite));
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH); memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
spr->sheet = sheet; spr->sheet = sheet;
@@ -191,14 +191,14 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
memset(sheet, 0x00, sizeof(akgl_SpriteSheet)); memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
//CATCH(errctx, akgl_heap_next_string(&tmpstr)); //CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH); strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename); //snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename); sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError()); FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet), SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),

View File

@@ -9,15 +9,15 @@
akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init) akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
if ( init != NULL ) { if ( init != NULL ) {
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH); strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
} else { } else {
memset(&obj->data, 0x00, sizeof(akgl_String)); memset(&obj->data, 0x00, sizeof(akgl_String));
} }
obj->refcount = 1; obj->refcount = 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count) akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count)

View File

@@ -13,10 +13,10 @@
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size) akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
{ {
TTF_Font *font = NULL; TTF_Font *font = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath"); FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
font = TTF_OpenFont(filepath, size); font = TTF_OpenFont(filepath, size);
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN( FAIL_ZERO_RETURN(
@@ -35,7 +35,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
SDL_Surface *textsurf = NULL; SDL_Surface *textsurf = NULL;
SDL_Texture *texture = NULL; SDL_Texture *texture = NULL;
SDL_FRect dest; SDL_FRect dest;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font"); FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string"); FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
@@ -64,3 +64,46 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
SDL_DestroySurface(textsurf); SDL_DestroySurface(textsurf);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
// A zero length means "the string is null terminated", not "the empty
// string" -- an empty text measures 0 wide and one line high.
FAIL_ZERO_RETURN(
errctx,
TTF_GetStringSize(font, text, 0, w, h),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
// SDL_ttf takes the wrap width as an int and reads a negative one as a
// very large unsigned width, which silently disables wrapping instead of
// reporting anything. Refuse it here rather than return a wrong measurement.
FAIL_NONZERO_RETURN(
errctx,
(wraplength < 0),
AKERR_OUTOFBOUNDS,
"Wrap length %d is negative",
wraplength);
FAIL_ZERO_RETURN(
errctx,
TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}

View File

@@ -60,7 +60,7 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map"); FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
} }
@@ -68,12 +68,12 @@ akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *property; json_t *property;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property)); PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
PASS(errctx, akgl_heap_next_string(dest)); PASS(errctx, akgl_heap_next_string(dest));
PASS(errctx, akgl_string_initialize(*dest, NULL)); PASS(errctx, akgl_string_initialize(*dest, NULL));
PASS(errctx, akgl_get_json_string_value(property, "value", dest)); PASS(errctx, akgl_get_json_string_value(property, "value", dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -83,7 +83,7 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
json_t *property = NULL; json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property)); PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
PASS(errctx, akgl_get_json_integer_value(property, "value", dest)); PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -101,7 +101,7 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
json_t *property = NULL; json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property)); PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest)); PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -119,7 +119,7 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
json_t *property = NULL; json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property)); PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest)); PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -137,7 +137,7 @@ akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, doub
json_t *property = NULL; json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property)); PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_double_value(property, "value", dest)); PASS(errctx, akgl_get_json_double_value(property, "value", dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -146,7 +146,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
akgl_String *tmppath = NULL; akgl_String *tmppath = NULL;
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns)); PASS(e, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid)); PASS(e, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight)); PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
@@ -164,8 +164,8 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
(char *)&tmpstr->data, (char *)&tmpstr->data,
AKGL_TILEMAP_MAX_TILESET_NAME_SIZE AKGL_TILEMAP_MAX_TILESET_NAME_SIZE
); );
CATCH(e, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr)); CATCH(e, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
CATCH(e, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath)); CATCH(e, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH); strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
} CLEANUP { } CLEANUP {
@@ -173,10 +173,10 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
IGNORE(akgl_heap_release_string(tmppath)); IGNORE(akgl_heap_release_string(tmppath));
} PROCESS(e) { } PROCESS(e) {
} FINISH(e, true); } FINISH(e, true);
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename); dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError()); FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -199,7 +199,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
for (j = 0; j <= (dest->tilesets[tilesetidx].tilecount); j++) { for (j = 0; j <= (dest->tilesets[tilesetidx].tilecount); j++) {
/* /*
* For a given 8x2 tilemap like this with 10x10 tiles and 0 spacing and 0 margin * For a given 8x2 tilemap like this with 10x10 tiles and 0 spacing and 0 margin
* *
* 01234567 * 01234567
* 89ABCDEF * 89ABCDEF
* *
@@ -221,7 +221,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing)); x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing));
y_offset = dest->tilesets[tilesetidx].spacing; y_offset = dest->tilesets[tilesetidx].spacing;
} }
dest->tilesets[tilesetidx].tile_offsets[j][0] = x_offset; dest->tilesets[tilesetidx].tile_offsets[j][0] = x_offset;
dest->tilesets[tilesetidx].tile_offsets[j][1] = y_offset; dest->tilesets[tilesetidx].tile_offsets[j][1] = y_offset;
/* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)", /* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)",
@@ -233,8 +233,8 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
// SDL_Log("Processed %d total tiles for tileset", j); // SDL_Log("Processed %d total tiles for tileset", j);
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname) akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -244,7 +244,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
json_t *tilesets = NULL; json_t *tilesets = NULL;
json_t *jstileset = NULL; json_t *jstileset = NULL;
int i; int i;
dest->numtilesets = 0; dest->numtilesets = 0;
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets)) CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
@@ -257,7 +257,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -275,7 +275,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *cur
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
akgl_Actor *actorobj = NULL; akgl_Actor *actorobj = NULL;
curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR; curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
if ( strlen((char *)&curobj->name) == 0 ) { if ( strlen((char *)&curobj->name) == 0 ) {
FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name"); FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
@@ -304,7 +304,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *cur
} }
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
actorobj->layer = layerid; actorobj->layer = layerid;
actorobj->x = curobj->x; actorobj->x = curobj->x;
actorobj->y = curobj->y; actorobj->y = curobj->y;
@@ -340,7 +340,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x)); PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y)); PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible)); PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr)); PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) { if ( strcmp(tmpstr->data, "actor") == 0 ) {
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname)); PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
@@ -354,11 +354,11 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
dest->p_vanishing_y = curobj->y; dest->p_vanishing_y = curobj->y;
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h)); PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale)); PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
} }
} }
layerdatavalue = NULL; layerdatavalue = NULL;
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -387,7 +387,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -417,15 +417,15 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr)); CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));
DISABLE_GCC_WARNING_FORMAT_TRUNCATION DISABLE_GCC_WARNING_FORMAT_TRUNCATION
snprintf((char *)&fpath->data, snprintf((char *)&fpath->data,
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE, AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s/%s", "%s/%s",
dirname->data, dirname->data,
tmpstr->data tmpstr->data
); );
RESTORE_GCC_WARNINGS RESTORE_GCC_WARNINGS
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data); dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
dest->layers[layerid].width = dest->layers[layerid].texture->w; dest->layers[layerid].width = dest->layers[layerid].texture->w;
dest->layers[layerid].height = dest->layers[layerid].texture->h; dest->layers[layerid].height = dest->layers[layerid].texture->h;
@@ -434,7 +434,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
IGNORE(akgl_heap_release_string(fpath)); IGNORE(akgl_heap_release_string(fpath));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -450,7 +450,7 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
int i; int i;
int layerid = 0; int layerid = 0;
int tmpint = 0; int tmpint = 0;
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers)); CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
dest->numlayers = json_array_size((json_t *)layers); dest->numlayers = json_array_size((json_t *)layers);
@@ -500,7 +500,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
json_t *props = NULL; json_t *props = NULL;
akgl_String *tmpval = NULL; akgl_String *tmpval = NULL;
double defzero = 0.0; double defzero = 0.0;
ATTEMPT { ATTEMPT {
CATCH(e, akgl_heap_next_string(&tmpval)); CATCH(e, akgl_heap_next_string(&tmpval));
CATCH(e, akgl_get_json_array_value((json_t *)root, "properties", &props)); CATCH(e, akgl_get_json_array_value((json_t *)root, "properties", &props));
@@ -523,7 +523,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
); );
PASS(e, akgl_physics_factory(&dest->physics, tmpval)); PASS(e, akgl_physics_factory(&dest->physics, tmpval));
dest->use_own_physics = true; dest->use_own_physics = true;
CATCH(e, akgl_get_json_with_default( CATCH(e, akgl_get_json_with_default(
akgl_get_json_properties_double( akgl_get_json_properties_double(
root, "physics.gravity.x", &dest->physics.gravity_x root, "physics.gravity.x", &dest->physics.gravity_x
@@ -572,7 +572,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
} HANDLE(e, AKERR_KEY) { } HANDLE(e, AKERR_KEY) {
SDL_Log("Map uses game physics"); SDL_Log("Map uses game physics");
} FINISH(e, true); } FINISH(e, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -583,7 +583,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
//akgl_String *tmpstr = NULL; //akgl_String *tmpstr = NULL;
json_error_t error; json_error_t error;
akgl_String *dirnamestr = NULL; akgl_String *dirnamestr = NULL;
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename"); FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");
@@ -598,7 +598,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
//SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname); //SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data)); CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data); dirname((char *)&dirnamestr->data);
json = json_load_file(fname, 0, &error); json = json_load_file(fname, 0, &error);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
@@ -696,13 +696,13 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
dest.x = 0; dest.x = 0;
dest.y = 0; dest.y = 0;
src.w = map->layers[layeridx].width; src.w = map->layers[layeridx].width;
src.h = map->layers[layeridx].height; src.h = map->layers[layeridx].height;
dest.w = map->layers[layeridx].width; dest.w = map->layers[layeridx].width;
dest.h = map->layers[layeridx].height; dest.h = map->layers[layeridx].height;
PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE)); PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
dest.x = 0; dest.x = 0;
dest.y = 0; dest.y = 0;
dest.w = map->tilewidth; dest.w = map->tilewidth;
@@ -778,7 +778,7 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap"); FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap");
FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds"); FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds");
for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) { for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) {
// Render this tile to the correct screen position // Render this tile to the correct screen position
// FIXME: These conditionals are probably not very efficient. Need a better way of getting // FIXME: These conditionals are probably not very efficient. Need a better way of getting

View File

@@ -38,14 +38,14 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
int rootlen; int rootlen;
int pathlen; int pathlen;
int count; int count;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf)); PASS(e, akgl_heap_next_string(&strbuf));
PASS(e, akgl_heap_next_string(&pathbuf)); PASS(e, akgl_heap_next_string(&pathbuf));
ATTEMPT { ATTEMPT {
// Is it relative to the root? // Is it relative to the root?
rootlen = strlen(root); rootlen = strlen(root);
@@ -54,10 +54,10 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH); FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
} }
DISABLE_GCC_WARNING_FORMAT_TRUNCATION DISABLE_GCC_WARNING_FORMAT_TRUNCATION
CATCH(e, aksl_sprintf(&count, (char *)&pathbuf->data, "%s/%s", root, path)); CATCH(e, aksl_sprintf(&count, (char *)&pathbuf->data, "%s/%s", root, path));
RESTORE_GCC_WARNINGS RESTORE_GCC_WARNINGS
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data)); CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data));
CATCH(e, akgl_string_copy(strbuf, dst, 0)); CATCH(e, akgl_string_copy(strbuf, dst, 0));
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_string(strbuf)); IGNORE(akgl_heap_release_string(strbuf));
IGNORE(akgl_heap_release_string(pathbuf)); IGNORE(akgl_heap_release_string(pathbuf));
@@ -71,18 +71,18 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_String *strbuf; akgl_String *strbuf;
char *result; char *result;
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
PASS(e, akgl_heap_next_string(&strbuf)); PASS(e, akgl_heap_next_string(&strbuf));
ATTEMPT { ATTEMPT {
// Is path relative to our current working directory? // Is path relative to our current working directory?
CATCH(e, aksl_realpath(path, (char *)&strbuf->data)); CATCH(e, aksl_realpath(path, (char *)&strbuf->data));
// Yes it is. strbuf->data contains the absolute path. // Yes it is. strbuf->data contains the absolute path.
CATCH(e, akgl_string_copy(strbuf, dst, 0)); CATCH(e, akgl_string_copy(strbuf, dst, 0));
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_string(strbuf)); IGNORE(akgl_heap_release_string(strbuf));
} PROCESS(e) { } PROCESS(e) {
@@ -111,102 +111,102 @@ akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String
PASS(e, akgl_heap_next_string(&dirnamestr)); PASS(e, akgl_heap_next_string(&dirnamestr));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data)); PASS(e, aksl_realpath(from, (char *)&dirnamestr->data));
dirname((char *)&dirnamestr->data); dirname((char *)&dirnamestr->data);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect) akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference"); FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
dest->topleft.x = rect->x; dest->topleft.x = rect->x;
dest->topleft.y = rect->y; dest->topleft.y = rect->y;
dest->bottomleft.x = rect->x; dest->bottomleft.x = rect->x;
dest->bottomleft.y = rect->y + rect->h; dest->bottomleft.y = rect->y + rect->h;
dest->topright.x = rect->x + rect->w; dest->topright.x = rect->x + rect->w;
dest->topright.y = rect->y; dest->topright.y = rect->y;
dest->bottomright.x = rect->x + rect->w; dest->bottomright.x = rect->x + rect->w;
dest->bottomright.y = rect->y + rect->h; dest->bottomright.y = rect->y + rect->h;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide) akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference"); FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference"); FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) && if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) { (p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
*collide = true; *collide = true;
} else { } else {
*collide = false; *collide = false;
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide) akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
{ {
RectanglePoints r1p; RectanglePoints r1p;
RectanglePoints r2p; RectanglePoints r2p;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference"); FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference"); FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&r1p, r1));
CATCH(errctx, akgl_rectangle_points(&r2p, r2));
// is the upper left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the upper left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
} CLEANUP { ATTEMPT {
} PROCESS(errctx) { CATCH(errctx, akgl_rectangle_points(&r1p, r1));
} FINISH(errctx, true); CATCH(errctx, akgl_rectangle_points(&r2p, r2));
*collide = false; // is the upper left corner of r1 contacting r2?
SUCCEED_RETURN(errctx); CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the upper left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the top right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
// is the bottom right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
*collide = false;
SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2) akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer"); FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer"); FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal"); FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout) akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
@@ -228,7 +228,7 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE)); CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read); s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer"); FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
if ( writeout != NULL ) { if ( writeout != NULL ) {
snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout); snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
@@ -239,13 +239,13 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
(char *)&tmpstring->data, (char *)&tmpstring->data,
SDL_GetError()); SDL_GetError());
} }
SDL_RenderClear(renderer->sdl_renderer); SDL_RenderClear(renderer->sdl_renderer);
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE)); CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read); s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer"); FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2)); CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
} CLEANUP { } CLEANUP {
if ( s1 != NULL ) if ( s1 != NULL )

15
src/version.c Normal file
View File

@@ -0,0 +1,15 @@
/**
* @file version.c
* @brief Implements the runtime half of the version API.
*/
#include <akgl/version.h>
const char *akgl_version(void)
{
// AKGL_VERSION is baked in when this translation unit is compiled, so what
// comes back is the version of the shared library the caller linked -- not
// the version of the header the caller built against. That asymmetry is the
// whole point: comparing the two detects a stale libakgl on the loader path.
return AKGL_VERSION;
}

View File

@@ -1,4 +1,4 @@
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \ #define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
handle_unhandled_error(errctx); handle_unhandled_error(errctx);
#include <akerror.h> #include <akerror.h>
@@ -58,7 +58,7 @@ akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void) akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error; akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akerr_handler_unhandled_error = handle_unhandled_error_noexit; akerr_handler_unhandled_error = handle_unhandled_error_noexit;
@@ -151,7 +151,7 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
} FINISH(unhandled_error_context, true); } FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler; akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -159,7 +159,7 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
{ {
akgl_Actor *testactor = NULL; akgl_Actor *testactor = NULL;
akgl_Character *testchar = NULL; akgl_Character *testchar = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
@@ -167,10 +167,10 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n"); printf("Handled\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor)); CATCH(errctx, akgl_heap_next_actor(&testactor));
@@ -178,13 +178,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
testactor->layer = 0; testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop; testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop; testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_actor_set_character(testactor, "test")); CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_actor(testactor)); IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n"); printf("Handled\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
@@ -195,13 +195,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
testactor->layer = 0; testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop; testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop; testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_character_initialize(testchar, "test")); CATCH(errctx, akgl_character_initialize(testchar, "test"));
CATCH(errctx, akgl_actor_set_character(testactor, "test")); CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_actor(testactor)); IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_character(testchar)); IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -248,7 +248,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
// Expected behavior // Expected behavior
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent"); SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&child)); CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, parent->addchild(parent, child)); CATCH(errctx, parent->addchild(parent, child));
@@ -284,7 +284,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
"Child %s was not removed from the registry", "Child %s was not removed from the registry",
(char *)&tmpstring->data); (char *)&tmpstring->data);
} }
_test_actor_addchild_heaprelease_cleanup: _test_actor_addchild_heaprelease_cleanup:
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -309,7 +309,7 @@ _test_actor_addchild_heaprelease_cleanup:
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -903,6 +903,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_registry_init_actor()); CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite()); CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet()); CATCH(errctx, akgl_registry_init_spritesheet());

View File

@@ -0,0 +1,121 @@
tests/assets/akgl_test_mono.ttf
================================
A subset of Liberation Mono Regular, cut down to printable ASCII (U+0020 to
U+007E) so the text suite has a font fixture that is 10 KB rather than 320 KB.
It is monospaced, which is what the measurement tests rely on: the width of an
N-character string is exactly N times the width of one character, in any font
size, so the assertions do not have to hardcode glyph metrics.
Generated with:
pyftsubset /usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf \
--unicodes=U+0020-007E --layout-features='' --no-hinting \
--desubroutinize --name-IDs='*' --output-file=akgl_test_mono.ttf
then renamed to "AKGL Test Mono" through the fontTools name table. The rename is
required, not cosmetic: "Liberation" is a Reserved Font Name under the license
below, and a modified copy may not carry it.
Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.
Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo,
Tinos and Cousine.
Licensed under the SIL Open Font License, Version 1.1, reproduced in full below.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
worldwide development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to provide
a free and open framework in which fonts may be shared and improved in
partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves.
The fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such.
This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components
as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting ? in part or in whole ?
any of the components of the Original Version, by changing formats or
by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer
or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this license, and must not be distributed
under any other license. The requirement for fonts to remain under
this license does not apply to any document created using the Font
Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

491
tests/audio.c Normal file
View File

@@ -0,0 +1,491 @@
/**
* @file audio.c
* @brief Unit tests for the tone generator.
*
* Almost everything here drives akgl_audio_mix() by hand rather than opening a
* device. That is not a workaround: a device pulls samples on SDL's audio
* thread at whatever rate it likes, so a test that opened one and then asserted
* on voice state would be racing the callback. Pulling the samples ourselves
* makes the synthesis deterministic -- the same input produces the same
* waveform, sample for sample, every run.
*
* The device is opened once, at the end, to prove akgl_audio_init() and
* akgl_audio_shutdown() work against the dummy driver.
*/
#include <SDL3/SDL.h>
#include <math.h>
#include <string.h>
#include <akerror.h>
#include <akgl/audio.h>
#include <akgl/error.h>
#include "testutil.h"
/**
* @brief Frames of output most tests generate at a time.
*
* Enough to hold a 10 ms attack and a 10 ms decay back to back at 44100 frames
* per second, which is the longest single stretch any test here inspects.
*/
#define TEST_MIX_FRAMES 1024
/** @brief 441 Hz at 44100 frames per second is exactly 100 frames per cycle. */
#define TEST_TONE_HZ 441.0f
/** @brief Frames in one cycle of TEST_TONE_HZ. */
#define TEST_TONE_PERIOD 100
/** @brief Somewhere to mix into. */
static float32_t samples[TEST_MIX_FRAMES];
/** @brief Silence every voice and put the master level back. */
static akerr_ErrorContext *reset_audio(void)
{
PREPARE_ERROR(errctx);
int i = 0;
ATTEMPT {
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
CATCH(errctx, akgl_audio_stop(i));
CATCH(errctx, akgl_audio_waveform(i, AKGL_AUDIO_WAVE_SQUARE));
CATCH(errctx, akgl_audio_envelope(i, 0, 0, 1.0f, 0));
}
CATCH(errctx, akgl_audio_volume(1.0f));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/** @brief Report the largest absolute sample in the first @p frames. */
static float32_t peak_of(int frames)
{
float32_t peak = 0.0f;
int i = 0;
for ( i = 0; i < frames; i++ ) {
if ( fabsf(samples[i]) > peak ) {
peak = fabsf(samples[i]);
}
}
return peak;
}
akerr_ErrorContext *test_audio_defaults(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
// Deliberately the first thing this file does, and deliberately without
// a call to reset_audio(): a voice nobody has configured has to make a
// sound. A zeroed voice would have a sustain of 0.0 and be silent, with
// no error to say why, which is exactly the trap this defends against.
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
"an unconfigured voice mixed to %f, expected a full-level square wave",
samples[0]);
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
"an unconfigured voice is not a square wave (%f half a cycle in)",
samples[50]);
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_silence(void)
{
PREPARE_ERROR(errctx);
bool silent = true;
bool active = true;
int i = 0;
ATTEMPT {
CATCH(errctx, reset_audio());
// With nothing sounding, the mixer produces silence rather than
// whatever was left in the buffer.
SDL_memset((void *)&samples, 0xff, sizeof(samples));
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing an idle voice table");
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
TEST_ASSERT_FLAG(silent, samples[i] == 0.0f);
}
TEST_ASSERT(errctx, silent == true, "an idle voice table did not mix to silence");
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
CATCH(errctx, akgl_audio_voice_active(i, &active));
TEST_ASSERT(errctx, active == false, "voice %d reports active with nothing playing", i);
}
// Zero frames is a legal request that writes nothing.
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, 0), "mixing zero frames");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_mix(NULL, 16),
"mixing into a NULL destination");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_mix(samples, -1),
"mixing a negative number of frames");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_square_tone(void)
{
PREPARE_ERROR(errctx);
bool firsthalf = true;
bool secondhalf = true;
bool active = false;
int i = 0;
ATTEMPT {
CATCH(errctx, reset_audio());
// A square wave with a flat envelope is the one waveform whose every
// sample is known exactly: +1 for the first half of each cycle, -1 for
// the second.
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000), "sounding a tone");
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == true, "a sounded voice does not report itself active");
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing a square wave");
for ( i = 0; i < (TEST_TONE_PERIOD / 2); i++ ) {
TEST_ASSERT_FLAG(firsthalf, samples[i] == 1.0f);
}
for ( i = (TEST_TONE_PERIOD / 2); i < TEST_TONE_PERIOD; i++ ) {
TEST_ASSERT_FLAG(secondhalf, samples[i] == -1.0f);
}
TEST_ASSERT(errctx, firsthalf == true,
"the first half cycle of a square wave is not at full positive level");
TEST_ASSERT(errctx, secondhalf == true,
"the second half cycle of a square wave is not at full negative level");
// The wave repeats: the second cycle matches the first.
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD] == samples[0],
"the wave did not repeat after one period");
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD + 60] == samples[60],
"the wave did not repeat after one period");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, 0.0f, 100),
"sounding a tone at zero hertz");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, -100.0f, 100),
"sounding a tone at a negative frequency");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, TEST_TONE_HZ, 0),
"sounding a tone with no duration");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(-1, TEST_TONE_HZ, 100),
"sounding a tone on a negative voice");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_tone(AKGL_AUDIO_MAX_VOICES, TEST_TONE_HZ, 100),
"sounding a tone on a voice past the last one");
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_waveforms(void)
{
PREPARE_ERROR(errctx);
bool inrange = true;
int i = 0;
int w = 0;
akgl_AudioWaveform shapes[4] = {
AKGL_AUDIO_WAVE_TRIANGLE,
AKGL_AUDIO_WAVE_SAWTOOTH,
AKGL_AUDIO_WAVE_NOISE,
AKGL_AUDIO_WAVE_SINE,
};
ATTEMPT {
// Every shape has to stay inside full scale and actually move. The
// exact sample values differ per shape; these two properties do not.
for ( w = 0; w < 4; w++ ) {
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_waveform(0, shapes[w]));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
TEST_ASSERT_FLAG(inrange, (samples[i] >= -1.0f) && (samples[i] <= 1.0f));
}
TEST_ASSERT_FLAG(inrange, peak_of(TEST_MIX_FRAMES) > 0.5f);
}
TEST_ASSERT(errctx, inrange == true,
"a waveform either left full scale or produced nothing");
// The triangle is symmetric about the middle of its cycle, which the
// square and sawtooth are not -- enough to tell it was really selected.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_waveform(0, AKGL_AUDIO_WAVE_TRIANGLE));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[25], 0.0f,
"a triangle wave is at %f a quarter of the way up, expected 0",
samples[25]);
TEST_ASSERT_FEQ(errctx, samples[50], 1.0f,
"a triangle wave is at %f at its peak, expected 1", samples[50]);
TEST_ASSERT_FEQ(errctx, samples[75], 0.0f,
"a triangle wave is at %f three quarters through, expected 0",
samples[75]);
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_waveform(0, (akgl_AudioWaveform)(AKGL_AUDIO_WAVE_SINE + 1)),
"selecting a waveform past the last one");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_waveform(AKGL_AUDIO_MAX_VOICES, AKGL_AUDIO_WAVE_SINE),
"selecting a waveform on a voice past the last one");
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_envelope(void)
{
PREPARE_ERROR(errctx);
bool rising = true;
int i = 0;
ATTEMPT {
CATCH(errctx, reset_audio());
// A 10 ms attack at 44100 is 441 frames, so the level should climb from
// nothing to full across the first 441 samples of a square wave and
// then hold at the sustain level.
CATCH(errctx, akgl_audio_envelope(0, 10, 0, 1.0f, 0));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 0.0f,
"an attack does not start from silence (first sample %f)", samples[0]);
TEST_ASSERT(errctx, fabsf(samples[100]) > fabsf(samples[10]),
"the attack is not climbing (%f at frame 10, %f at frame 100)",
samples[10], samples[100]);
// Frames 0..49 are the positive half of the square, so their level is
// the envelope value directly.
for ( i = 1; i < 50; i++ ) {
TEST_ASSERT_FLAG(rising, samples[i] > samples[i - 1]);
}
TEST_ASSERT(errctx, rising == true, "the attack ramp is not monotonic");
// Half a millisecond of decay to a half sustain level, no attack: the
// very first sample is at full level and it settles at the sustain.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_envelope(0, 0, 10, 0.5f, 0));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
"with no attack the first sample is %f, expected full level", samples[0]);
TEST_ASSERT_FEQ(errctx, samples[450], -0.5f,
"after the decay the level is %f, expected the 0.5 sustain", samples[450]);
// Attack and decay together, which is the case where the decay has to
// measure from the end of the attack rather than from the start of the
// note. 10 ms of each is 441 frames of each at 44100.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_envelope(0, 10, 10, 0.5f, 0));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[441], 1.0f,
"at the end of the attack the level is %f, expected full", samples[441]);
TEST_ASSERT_FEQ(errctx, samples[661], -(1.0f - (0.5f * (220.0f / 441.0f))),
"halfway through the decay the level is %f, expected the halfway ramp",
samples[661]);
TEST_ASSERT_FEQ(errctx, samples[900], 0.5f,
"after the decay the level is %f, expected the 0.5 sustain", samples[900]);
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, -0.1f, 1),
"setting a negative sustain level");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, 1.5f, 1),
"setting a sustain level past full");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_envelope(AKGL_AUDIO_MAX_VOICES, 1, 1, 1.0f, 1),
"setting an envelope on a voice past the last one");
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_duration_and_release(void)
{
PREPARE_ERROR(errctx);
bool active = false;
ATTEMPT {
CATCH(errctx, reset_audio());
// A 10 ms gate is 441 frames. The voice is still sounding while they
// are being generated and goes quiet on its own afterwards, without the
// caller having to stop it.
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
CATCH(errctx, akgl_audio_mix(samples, 440));
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == true, "the voice went quiet before its gate closed");
CATCH(errctx, akgl_audio_mix(samples, 8));
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == false, "the voice is still sounding past its gate");
CATCH(errctx, akgl_audio_mix(samples, 64));
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
"a finished voice is still producing sound (peak %f)", peak_of(64));
// With a release, the voice outlives its gate and fades rather than
// stopping dead.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 10));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
CATCH(errctx, akgl_audio_mix(samples, 441));
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == true, "a voice with a release stopped when its gate closed");
// The release starts from the level the gate left off at and falls from
// there, so the end of this window has to be quieter than its start.
CATCH(errctx, akgl_audio_mix(samples, 220));
TEST_ASSERT(errctx, fabsf(samples[219]) < fabsf(samples[0]),
"the release is not attenuating (%f at its start, %f 220 frames later)",
samples[0], samples[219]);
TEST_ASSERT(errctx, peak_of(220) > 0.0f, "the release went silent immediately");
CATCH(errctx, akgl_audio_mix(samples, 250));
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == false, "the voice outlived its gate and its release");
// Stopping cuts a voice off where it stands, release and all.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 1000));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, 64));
TEST_EXPECT_OK(errctx, akgl_audio_stop(0), "stopping a sounding voice");
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == false, "a stopped voice still reports itself active");
CATCH(errctx, akgl_audio_mix(samples, 64));
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
"a stopped voice is still producing sound (peak %f)", peak_of(64));
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_stop(AKGL_AUDIO_MAX_VOICES),
"stopping a voice past the last one");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_audio_voice_active(AKGL_AUDIO_MAX_VOICES, &active),
"asking about a voice past the last one");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_voice_active(0, NULL),
"asking about a voice with nowhere to put the answer");
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_volume_and_mixing(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, reset_audio());
// The master level scales everything.
CATCH(errctx, akgl_audio_volume(0.25f));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 0.25f,
"at a quarter volume the first sample is %f, expected 0.25", samples[0]);
CATCH(errctx, akgl_audio_volume(0.0f));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, peak_of(TEST_MIX_FRAMES), 0.0f,
"at zero volume the peak is %f, expected silence",
peak_of(TEST_MIX_FRAMES));
// Voices sum, and the sum is clamped rather than wrapped: three square
// waves in phase at full level would be 3.0 without the clamp.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_tone(2, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
"three voices in phase mixed to %f, expected a clamp at 1.0", samples[0]);
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
"three voices in phase mixed to %f, expected a clamp at -1.0", samples[50]);
// Two voices at different levels sum to their total rather than to
// either one of them, which is only visible below the clamp.
CATCH(errctx, reset_audio());
CATCH(errctx, akgl_audio_volume(0.5f));
CATCH(errctx, akgl_audio_envelope(1, 0, 0, 0.5f, 0));
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
TEST_ASSERT_FEQ(errctx, samples[0], 0.75f,
"a full voice and a half voice at half volume mixed to %f, expected 0.75",
samples[0]);
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(-0.1f),
"setting a negative master volume");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(1.1f),
"setting a master volume past full");
} CLEANUP {
IGNORE(reset_audio());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_audio_device(void)
{
PREPARE_ERROR(errctx);
bool active = true;
ATTEMPT {
// Everything above ran without a device. This is the one test that
// opens one, so nothing that asserts on voice state runs after it.
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device");
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device a second time");
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10), "sounding a tone on a device");
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing the audio device");
CATCH(errctx, akgl_audio_voice_active(0, &active));
TEST_ASSERT(errctx, active == false, "shutting down left a voice sounding");
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing an audio device that is not open");
} CLEANUP {
IGNORE(akgl_audio_shutdown());
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_error_init());
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_AUDIO),
AKGL_ERR_SDL,
"Couldn't initialize SDL: %s",
SDL_GetError());
CATCH(errctx, test_audio_defaults());
CATCH(errctx, test_audio_silence());
CATCH(errctx, test_audio_square_tone());
CATCH(errctx, test_audio_waveforms());
CATCH(errctx, test_audio_envelope());
CATCH(errctx, test_audio_duration_and_release());
CATCH(errctx, test_audio_volume_and_mixing());
CATCH(errctx, test_audio_device());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -3,29 +3,29 @@
int main(void) int main(void)
{ {
int mask = 0; int mask = 0;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != AKGL_ACTOR_STATE_ALIVE ) if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1; return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) ) if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
return 1; return 1;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE); AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) ) if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
return 1; return 1;
AKGL_BITMASK_CLEAR(mask); AKGL_BITMASK_CLEAR(mask);
if ( mask != 0 ) if ( mask != 0 )
return 1; return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) ) if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
return 1; return 1;
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP; mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL); AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
if ( mask != AKGL_ACTOR_STATE_ALIVE ) if ( mask != AKGL_ACTOR_STATE_ALIVE )
return 1; return 1;
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN);
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN); AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) ) if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
return 1; return 1;
return 0; return 0;
} }

View File

@@ -86,7 +86,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
akgl_Sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
akgl_Sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0}; akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testchar)); CATCH(errctx, akgl_heap_next_character(&testchar));
@@ -126,7 +126,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(akgl_heap_release_sprite(testsprite2)); IGNORE(akgl_heap_release_sprite(testsprite2));
IGNORE(akgl_heap_release_character(testchar)); IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -140,7 +140,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
akgl_Sprite *comparesprite = NULL; akgl_Sprite *comparesprite = NULL;
int tsrc = 0; int tsrc = 0;
int tsrc2 = 0; int tsrc2 = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&testcharacter)); CATCH(errctx, akgl_heap_next_character(&testcharacter));
@@ -158,7 +158,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
testsprite2, testsprite2,
AKERR_KEY, AKERR_KEY,
"Sprite 2 loaded from json but not in registry"); "Sprite 2 loaded from json but not in registry");
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json")); CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL); testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
@@ -195,12 +195,13 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) { if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }

View File

@@ -19,14 +19,14 @@
int numsprites = 8; int numsprites = 8;
char *spritepaths[] = { char *spritepaths[] = {
"assets/sprites/little_guy_walking_left.json", "assets/sprites/little_guy_walking_left.json",
"assets/sprites/little_guy_walking_right.json", "assets/sprites/little_guy_walking_right.json",
"assets/sprites/little_guy_walking_up.json", "assets/sprites/little_guy_walking_up.json",
"assets/sprites/little_guy_walking_down.json", "assets/sprites/little_guy_walking_down.json",
"assets/sprites/little_guy_facing_left.json", "assets/sprites/little_guy_facing_left.json",
"assets/sprites/little_guy_facing_right.json", "assets/sprites/little_guy_facing_right.json",
"assets/sprites/little_guy_facing_up.json", "assets/sprites/little_guy_facing_up.json",
"assets/sprites/little_guy_facing_down.json" "assets/sprites/little_guy_facing_down.json"
}; };
int main(void) int main(void)
@@ -34,15 +34,15 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
SDL3GControlMap *controlmap; SDL3GControlMap *controlmap;
actor *actorptr = NULL; actor *actorptr = NULL;
ATTEMPT { ATTEMPT {
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) { if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }
@@ -55,7 +55,7 @@ int main(void)
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer"); strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
game.screenwidth = 640; game.screenwidth = 640;
game.screenheight = 480; game.screenheight = 480;
CATCH(errctx, akgl_GAME_init()); CATCH(errctx, akgl_GAME_init());
for ( int i = 0; i < numsprites ; i++) { for ( int i = 0; i < numsprites ; i++) {
@@ -96,7 +96,7 @@ int main(void)
controlmap->controls[1].target_del_state_off = AKGL_ACTOR_STATE_MOVING_UP; controlmap->controls[1].target_del_state_off = AKGL_ACTOR_STATE_MOVING_UP;
controlmap->controls[1].event_on = SDL_EVENT_KEY_DOWN; controlmap->controls[1].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[1].event_off = SDL_EVENT_KEY_UP; controlmap->controls[1].event_off = SDL_EVENT_KEY_UP;
// Move left // Move left
controlmap->controls[2].key = SDLK_LEFT; controlmap->controls[2].key = SDLK_LEFT;
//controlmap->controls[2].target_state_gate = AKGL_ACTOR_STATE_MOVING_LEFT; //controlmap->controls[2].target_state_gate = AKGL_ACTOR_STATE_MOVING_LEFT;
@@ -105,13 +105,13 @@ int main(void)
controlmap->controls[2].target_del_state_off = AKGL_ACTOR_STATE_MOVING_LEFT; controlmap->controls[2].target_del_state_off = AKGL_ACTOR_STATE_MOVING_LEFT;
controlmap->controls[2].event_on = SDL_EVENT_KEY_DOWN; controlmap->controls[2].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[2].event_off = SDL_EVENT_KEY_UP; controlmap->controls[2].event_off = SDL_EVENT_KEY_UP;
// Move right // Move right
controlmap->controls[3].key = SDLK_RIGHT; controlmap->controls[3].key = SDLK_RIGHT;
//controlmap->controls[3].target_state_gate = AKGL_ACTOR_STATE_MOVING_RIGHT; //controlmap->controls[3].target_state_gate = AKGL_ACTOR_STATE_MOVING_RIGHT;
controlmap->controls[3].target_add_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT; controlmap->controls[3].target_add_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT;
controlmap->controls[3].target_del_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_ALL; controlmap->controls[3].target_del_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_ALL;
controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT; controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT;
controlmap->controls[3].event_on = SDL_EVENT_KEY_DOWN; controlmap->controls[3].event_on = SDL_EVENT_KEY_DOWN;
controlmap->controls[3].event_off = SDL_EVENT_KEY_UP; controlmap->controls[3].event_off = SDL_EVENT_KEY_UP;
} CLEANUP { } CLEANUP {
@@ -120,6 +120,6 @@ int main(void)
LOG_ERROR(errctx); LOG_ERROR(errctx);
return 1; return 1;
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
return 0; return 0;
} }

View File

@@ -524,6 +524,164 @@ akerr_ErrorContext *test_controller_device_enumeration(void)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *test_controller_poll_key(void)
{
PREPARE_ERROR(e);
SDL_Event event;
int keycode = -1;
bool available = true;
ATTEMPT {
reset_control_maps();
CATCH(e, make_player());
CATCH(e, akgl_controller_flush_keys());
// An empty buffer answers "no key waiting" and succeeds.
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling an empty key buffer");
TEST_ASSERT(e, available == false,
"polling an empty buffer reported a key was available");
TEST_ASSERT(e, keycode == 0,
"polling an empty buffer left keycode at %d, expected 0", keycode);
// A key press pumped through the handler is drained by the poller,
// exactly once.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key press with no control maps installed");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key");
TEST_ASSERT(e, available == true, "the pressed key was not available to the poller");
TEST_ASSERT(e, keycode == SDLK_A,
"the poller returned keycode %d, expected %d", keycode, (int)SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again");
TEST_ASSERT(e, available == false, "the same keystroke was delivered twice");
// Key releases are not keystrokes.
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key release");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release");
TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke");
// Keys come back in the order they were pressed.
CATCH(e, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key");
TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d",
keycode, (int)SDLK_1);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key");
TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d",
keycode, (int)SDLK_2);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key");
TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d",
keycode, (int)SDLK_3);
// A key that a control map also acts on still reaches the poller, so a
// game and an embedded interpreter can share one keyboard.
CATCH(e, akgl_controller_flush_keys());
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
player->state = 0;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a bound key press");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"the bound key stopped driving the actor (state %d)", player->state);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling for a key a control map claimed");
TEST_ASSERT(e, available == true, "a bound key never reached the key buffer");
TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d",
keycode, (int)SDLK_LEFT);
// Flushing discards the backlog.
CATCH(e, akgl_controller_flush_keys());
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush");
TEST_ASSERT(e, available == false, "a flush left a keystroke behind");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available),
"polling into a NULL keycode");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL),
"polling into a NULL availability flag");
} CLEANUP {
reset_control_maps();
IGNORE(akgl_controller_flush_keys());
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_controller_poll_key_overflow(void)
{
PREPARE_ERROR(e);
SDL_Event event;
int keycode = -1;
bool available = true;
bool ordered = true;
bool pumped = true;
int i = 0;
ATTEMPT {
reset_control_maps();
CATCH(e, akgl_controller_flush_keys());
// Fill the buffer exactly, then press one more. The overflowing key is
// the one that is dropped -- what was typed first survives.
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) {
akerr_ErrorContext *pumpresult = NULL;
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i);
pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event);
if ( pumpresult != NULL ) {
pumpresult->handled = true;
pumpresult = akerr_release_error(pumpresult);
pumped = false;
}
}
TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed");
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) {
akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available);
if ( pollresult != NULL ) {
pollresult->handled = true;
pollresult = akerr_release_error(pollresult);
ordered = false;
}
TEST_ASSERT_FLAG(ordered, available == true);
TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i));
}
TEST_ASSERT(e, ordered == true,
"a full buffer did not return the first %d keys in order",
AKGL_CONTROLLER_KEY_BUFFER);
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling after draining a full buffer");
TEST_ASSERT(e, available == false,
"the key pressed past capacity was buffered anyway (keycode %d)", keycode);
// The buffer is reusable after an overflow rather than wedged.
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z);
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
"dispatching a key after an overflow");
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
"polling after an overflow");
TEST_ASSERT(e, available == true, "the buffer stayed full after being drained");
TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d",
keycode, (int)SDLK_Z);
} CLEANUP {
reset_control_maps();
IGNORE(akgl_controller_flush_keys());
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -532,6 +690,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD), SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
@@ -550,6 +709,8 @@ int main(void)
CATCH(errctx, test_controller_gamepad_button_handlers()); CATCH(errctx, test_controller_gamepad_button_handlers());
CATCH(errctx, test_controller_device_events()); CATCH(errctx, test_controller_device_events());
CATCH(errctx, test_controller_device_enumeration()); CATCH(errctx, test_controller_device_enumeration());
CATCH(errctx, test_controller_poll_key());
CATCH(errctx, test_controller_poll_key_overflow());
} CLEANUP { } CLEANUP {
SDL_Quit(); SDL_Quit();
} PROCESS(errctx) { } PROCESS(errctx) {

600
tests/draw.c Normal file
View File

@@ -0,0 +1,600 @@
/**
* @file draw.c
* @brief Unit tests for the immediate-mode drawing primitives.
*
* Everything here draws into a small software renderer under the dummy video
* driver and then reads the target back with SDL_RenderReadPixels, so the
* assertions are about pixels that actually changed rather than about SDL
* having been called. No window is shown and no display is required.
*
* The target is deliberately tiny: at 64x64 a full readback is 16 KB, which
* makes it cheap to read the whole thing back after every operation.
*/
#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/draw.h>
#include <akgl/renderer.h>
#include "testutil.h"
/** @brief Width and height of the offscreen target every test draws into. */
#define TEST_TARGET_SIZE 64
/** @brief Opaque black, what each test clears the target to. */
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
/** @brief The color most tests draw with. */
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
/** @brief A second color, for tests that need to tell two marks apart. */
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };
/** @brief Clear the whole target to opaque black. */
static akerr_ErrorContext *clear_target(void)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(
errctx,
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
FAIL_ZERO_RETURN(
errctx,
SDL_RenderClear(renderer->sdl_renderer),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
SUCCEED_RETURN(errctx);
}
/**
* @brief Report whether one pixel of @p shot carries @p color.
*
* Alpha is not compared: the render target's own alpha depends on the format
* SDL picked for it, and none of these tests draw translucently.
*/
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
{
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 0;
if ( shot == NULL ) {
return false;
}
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
return false;
}
return ((r == color.r) && (g == color.g) && (b == color.b));
}
akerr_ErrorContext *test_draw_point(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_point(renderer, 10.0f, 20.0f, testred),
"plotting one pixel");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
"the plotted pixel at 10,20 is not the color it was drawn with");
TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack),
"plotting one pixel also changed its neighbour at 11,20");
TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack),
"plotting one pixel also changed the pixel below it");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_point(NULL, 0.0f, 0.0f, testred),
"plotting through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_line(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
bool onthe_line = true;
int i = 0;
ATTEMPT {
CATCH(errctx, clear_target());
// A vertical line, so every pixel of it is known without reasoning
// about how SDL rasterises a diagonal.
TEST_EXPECT_OK(errctx, akgl_draw_line(renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
"drawing a vertical line");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
for ( i = 4; i <= 12; i++ ) {
TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
}
TEST_ASSERT(errctx, onthe_line == true,
"the vertical line from 5,4 to 5,12 has a gap in it");
TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack),
"the line ran past its first endpoint");
TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack),
"the line ran past its second endpoint");
TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack),
"the line is wider than one pixel");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred),
"drawing a line through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_rects(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_FRect box;
ATTEMPT {
box.x = 8.0f;
box.y = 8.0f;
box.w = 16.0f;
box.h = 16.0f;
// The outline touches the border and leaves the middle alone.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_rect(renderer, &box, testred), "outlining a rectangle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
"the outline is missing its bottom right corner");
TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge");
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior");
SDL_DestroySurface(shot);
shot = NULL;
// The filled form covers the interior as well.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(renderer, &box, testred), "filling a rectangle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack),
"the fill ran one pixel past its bottom right corner");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
"outlining through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(renderer, NULL, testred),
"outlining a NULL rectangle");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
"filling through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(renderer, NULL, testred),
"filling a NULL rectangle");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_circle(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
bool symmetric = true;
int x = 0;
int y = 0;
ATTEMPT {
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 32.0f, 32.0f, 10.0f, testred),
"drawing a circle");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
// The four axis points are exact for any correct midpoint circle.
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel");
TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel");
TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel");
// It is an outline, not a disc.
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center");
// ...and nothing lands outside the radius.
TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false,
"the circle drew a pixel one past its radius");
// Every plotted pixel has a mirror in the other three quadrants. The
// circle is drawn one octant at a time and reflected seven ways, so a
// sign error in any single reflection breaks this and nothing else --
// the four axis points above stay put either way.
for ( y = 22; y <= 42; y++ ) {
for ( x = 22; x <= 42; x++ ) {
if ( !pixel_is(shot, x, y, testred) ) {
continue;
}
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred));
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred));
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred));
}
}
TEST_ASSERT(errctx, symmetric == true,
"the circle is not symmetric about its center; an octant is reflected wrong");
SDL_DestroySurface(shot);
shot = NULL;
// A zero radius is the degenerate case, not an error: one pixel.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 5.0f, 5.0f, 0.0f, testred),
"drawing a circle of radius zero");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
"a circle of radius zero did not plot its center");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_circle(renderer, 5.0f, 5.0f, -1.0f, testred),
"drawing a circle of negative radius");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
"drawing a circle through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_flood_fill(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_FRect box;
ATTEMPT {
// A red box outline on black. Filling inside it must stay inside it,
// which is the whole contract of PAINT.
box.x = 10.0f;
box.y = 10.0f;
box.w = 20.0f;
box.h = 20.0f;
CATCH(errctx, clear_target());
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
"flooding the inside of a box");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
"the fill did not reach the top left of the interior");
TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen),
"the fill did not reach the bottom right of the interior");
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary");
TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge");
TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box");
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
"the fill leaked into the rest of the target");
SDL_DestroySurface(shot);
shot = NULL;
// Filling a region that is already the requested color changes nothing
// and is not an error.
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
"flooding a region that is already that color");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
"refilling a region disturbed it");
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
"refilling a region disturbed its boundary");
SDL_DestroySurface(shot);
shot = NULL;
// Flooding the outside reaches every pixel that is not the box.
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 0, 0, testgreen),
"flooding the area around a box");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
"the fill did not reach the far corner of the target");
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
"the fill from outside overwrote the boundary");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_flood_fill(renderer, -1, 0, testred),
"flooding from a seed left of the target");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_flood_fill(renderer, 0, TEST_TARGET_SIZE, testred),
"flooding from a seed below the target");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_flood_fill(NULL, 0, 0, testred),
"flooding through a NULL backend");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_copy_and_paste_region(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
SDL_Surface *saved = NULL;
SDL_Surface *reused = NULL;
SDL_Rect region;
SDL_FRect box;
ATTEMPT {
// Put something recognisable in the top left corner and save it.
box.x = 0.0f;
box.y = 0.0f;
box.w = 8.0f;
box.h = 8.0f;
CATCH(errctx, clear_target());
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
region.x = 0;
region.y = 0;
region.w = 8;
region.h = 8;
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, &region, &saved),
"saving a region of the target");
TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface");
TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8,
"the saved surface is %dx%d, expected 8x8", saved->w, saved->h);
// Wipe the screen and put it back somewhere else.
CATCH(errctx, clear_target());
TEST_EXPECT_OK(errctx, akgl_draw_paste_region(renderer, saved, 32.0f, 32.0f),
"pasting a saved region");
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred),
"the pasted region did not land at its destination");
TEST_ASSERT(errctx, pixel_is(shot, 39, 39, testred),
"the pasted region is smaller than what was saved");
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
"the pasted region is larger than what was saved");
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
"pasting also redrew the region at its original position");
SDL_DestroySurface(shot);
shot = NULL;
// A surface the caller already owns is reused rather than replaced, so
// saving the same region repeatedly does not churn allocations.
reused = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError());
region.x = 32;
region.y = 32;
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, &region, &reused),
"saving into a caller-owned surface");
TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred),
"the caller-owned surface did not receive the region");
// Wrong-sized destinations and regions off the edge of the target are
// refused rather than silently clipped.
region.w = 4;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
"saving into a destination of the wrong size");
region.x = TEST_TARGET_SIZE - 4;
region.y = 0;
region.w = 8;
region.h = 8;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
"saving a region that runs off the right edge");
region.x = 0;
region.w = 0;
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_draw_copy_region(renderer, &region, &reused),
"saving a region with no area");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(NULL, &region, &reused),
"saving through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(renderer, NULL, &reused),
"saving a NULL region");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(renderer, &region, NULL),
"saving into a NULL destination");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f),
"pasting through a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_paste_region(renderer, NULL, 0.0f, 0.0f),
"pasting a NULL surface");
} CLEANUP {
if ( shot != NULL ) {
SDL_DestroySurface(shot);
}
if ( saved != NULL ) {
SDL_DestroySurface(saved);
}
if ( reused != NULL ) {
SDL_DestroySurface(reused);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_preserves_render_draw_color(void)
{
PREPARE_ERROR(errctx);
SDL_FRect box;
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 0;
ATTEMPT {
box.x = 0.0f;
box.y = 0.0f;
box.w = 4.0f;
box.h = 4.0f;
// Drawing must not leave the renderer's color set to whatever it drew
// with, or the host's next SDL_RenderClear() paints the wrong color.
FAIL_ZERO_BREAK(
errctx,
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
CATCH(errctx, akgl_draw_point(renderer, 1.0f, 1.0f, testred));
CATCH(errctx, akgl_draw_line(renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
CATCH(errctx, akgl_draw_circle(renderer, 20.0f, 20.0f, 4.0f, testred));
FAIL_ZERO_BREAK(
errctx,
SDL_GetRenderDrawColor(renderer->sdl_renderer, &r, &g, &b, &a),
AKGL_ERR_SDL,
"%s",
SDL_GetError());
TEST_ASSERT(errctx, (r == 0x11) && (g == 0x22) && (b == 0x33) && (a == 0x44),
"drawing left the render draw color at %02x%02x%02x%02x, expected 11223344",
r, g, b, a);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_draw_backend_without_a_renderer(void)
{
PREPARE_ERROR(errctx);
akgl_RenderBackend empty;
SDL_Surface *saved = NULL;
SDL_Surface *scratch = NULL;
SDL_FRect box;
SDL_Rect region;
ATTEMPT {
// A backend that exists but was never given an SDL_Renderer. Every
// entry point has to say so rather than dereference it -- this is the
// state a host is in between allocating a backend and initializing it.
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
box.x = 0.0f;
box.y = 0.0f;
box.w = 4.0f;
box.h = 4.0f;
region.x = 0;
region.y = 0;
region.w = 4;
region.h = 4;
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_point(&empty, 0.0f, 0.0f, testred),
"plotting through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_line(&empty, 0.0f, 0.0f, 1.0f, 1.0f, testred),
"drawing a line through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_rect(&empty, &box, testred),
"outlining through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_filled_rect(&empty, &box, testred),
"filling through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_circle(&empty, 4.0f, 4.0f, 2.0f, testred),
"drawing a circle through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_flood_fill(&empty, 0, 0, testred),
"flooding through an uninitialized backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_copy_region(&empty, &region, &saved),
"saving through an uninitialized backend");
TEST_ASSERT(errctx, saved == NULL,
"a refused save still wrote something to the destination");
scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32);
FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f),
"pasting through an uninitialized backend");
} CLEANUP {
if ( saved != NULL ) {
SDL_DestroySurface(saved);
}
if ( scratch != NULL ) {
SDL_DestroySurface(scratch);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer;
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_VIDEO),
AKGL_ERR_SDL,
"Couldn't initialize SDL: %s",
SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
SDL_CreateWindowAndRenderer(
"net/aklabs/libakgl/test_draw",
TEST_TARGET_SIZE,
TEST_TARGET_SIZE,
0,
&window,
&renderer->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s",
SDL_GetError());
CATCH(errctx, test_draw_point());
CATCH(errctx, test_draw_line());
CATCH(errctx, test_draw_rects());
CATCH(errctx, test_draw_circle());
CATCH(errctx, test_draw_flood_fill());
CATCH(errctx, test_draw_copy_and_paste_region());
CATCH(errctx, test_draw_preserves_render_draw_color());
CATCH(errctx, test_draw_backend_without_a_renderer());
} CLEANUP {
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

102
tests/error.c Normal file
View File

@@ -0,0 +1,102 @@
/**
* @file error.c
* @brief Unit tests for the libakgl status band: reservation, ownership and names.
*
* The libakerror registry is process-global, so these tests assert against
* whatever akgl_error_init() left behind rather than building their own state.
*/
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include "testutil.h"
/**
* @brief akgl_error_init() must own the libakgl status band and name every code in it.
*
* A code whose name never registered degrades to "Unknown Error" in every stack
* trace that carries it, and a band we never reserved is one another component
* can name out from under us. Both stay silent until something has already gone
* wrong, so assert them directly rather than waiting to read a useless trace.
*/
akerr_ErrorContext *test_error_init_owns_the_status_band(void)
{
PREPARE_ERROR(e);
static const struct {
int status;
const char *name;
} expected[] = {
{ AKGL_ERR_SDL, "SDL Error" },
{ AKGL_ERR_REGISTRY, "Registry Error" },
{ AKGL_ERR_HEAP, "Heap Error" },
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" }
};
bool named = true;
int i = 0;
ATTEMPT {
CATCH(e, akgl_error_init());
TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT,
"the libakgl status band holds %d codes but %d are named here",
AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0])));
for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) {
TEST_ASSERT_FLAG(named,
strcmp(akerr_name_for_status(expected[i].status, NULL),
expected[i].name) == 0);
}
TEST_ASSERT(e, named,
"akgl_error_init did not register the expected name for every AKGL_ERR_* code");
// The reservation is what makes those names ours. Without it the
// registrations above would still succeed for anyone who asked.
TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN,
akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"),
"a foreign owner was allowed to rename a libakgl status");
TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP,
akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"),
"a foreign owner was allowed to reserve the libakgl status band");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/**
* @brief Calling akgl_error_init() twice must be a no-op, not a self-collision.
*
* Nothing in libakgl orders initialization for an embedding program, so a second
* call has to be harmless. libakerror only treats an *identical* reservation as
* a repeat -- a subset or superset raises -- which makes this a real constraint
* on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality.
*/
akerr_ErrorContext *test_error_init_is_idempotent(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed");
TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed");
TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0,
"re-running akgl_error_init lost the name for AKGL_ERR_SDL");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, test_error_init_owns_the_status_band());
CATCH(errctx, test_error_init_is_idempotent());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -402,6 +402,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());

View File

@@ -361,6 +361,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());

View File

@@ -347,6 +347,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture()); CATCH(errctx, load_fixture());

View File

@@ -722,6 +722,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties()); CATCH(errctx, akgl_registry_init_properties());

View File

@@ -17,7 +17,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
SDL_calloc_func calloc_func; SDL_calloc_func calloc_func;
SDL_realloc_func realloc_func; SDL_realloc_func realloc_func;
SDL_free_func free_func; SDL_free_func free_func;
SDL_GetMemoryFunctions( SDL_GetMemoryFunctions(
&malloc_func, &malloc_func,
&calloc_func, &calloc_func,
@@ -54,7 +54,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
@@ -62,7 +62,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
@@ -70,7 +70,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
@@ -78,7 +78,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Sucess\n"); printf("Sucess\n");
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -87,6 +87,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_registry_init_creation_failures()); CATCH(errctx, test_akgl_registry_init_creation_failures());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {

View File

@@ -16,53 +16,53 @@
akerr_ErrorContext *test_akgl_spritesheet_initialize(void) akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akgl_SpriteSheet *sheet = NULL; akgl_SpriteSheet *sheet = NULL;
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
// Does the image file get loaded? // Does the image file get loaded?
// Is the image file loaded correctly? (Surface comparison) // Is the image file loaded correctly? (Surface comparison)
// Is the spritesheet in the registry? // Is the spritesheet in the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_spritesheet(&sheet)); CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
CATCH(errctx, akgl_heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
FAIL_NONZERO_BREAK(
errctx,
((sheet->texture->w != 576) || (sheet->texture->h != 384)),
AKERR_VALUE,
"Loaded texture was not the correct size");
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH( snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
errctx, CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
akgl_render_and_compare( FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
sheet->texture, FAIL_NONZERO_BREAK(
image, errctx,
0, 0, 576, 384, ((sheet->texture->w != 576) || (sheet->texture->h != 384)),
"test_spritesheet_loaded_image.png") AKERR_VALUE,
); "Loaded texture was not the correct size");
FAIL_ZERO_BREAK( snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
errctx, image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL), FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
AKERR_KEY,
"Spritesheet was not placed in the registry"); CATCH(
errctx,
} CLEANUP { akgl_render_and_compare(
IGNORE(akgl_heap_release_string(tmpstr)); sheet->texture,
IGNORE(akgl_heap_release_spritesheet(sheet)); image,
if ( image != NULL ) 0, 0, 576, 384,
SDL_DestroyTexture(image); "test_spritesheet_loaded_image.png")
} PROCESS(errctx) { );
} FINISH(errctx, true);
SUCCEED_RETURN(errctx); FAIL_ZERO_BREAK(
errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
AKERR_KEY,
"Spritesheet was not placed in the registry");
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_spritesheet(sheet));
if ( image != NULL )
SDL_DestroyTexture(image);
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *test_akgl_sprite_initialize(void) akerr_ErrorContext *test_akgl_sprite_initialize(void)
@@ -71,28 +71,28 @@ akerr_ErrorContext *test_akgl_sprite_initialize(void)
akgl_SpriteSheet *testsheet = NULL; akgl_SpriteSheet *testsheet = NULL;
akgl_Sprite *testsprite = NULL; akgl_Sprite *testsprite = NULL;
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
// Does the sprite get loaded? // Does the sprite get loaded?
// Do all frames of the sprite get loaded? // Do all frames of the sprite get loaded?
// Are all the frames of the sprite what we expect? (Surface comparison) // Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry? // Is the sprite added to the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_spritesheet(&testsheet)); CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
CATCH(errctx, akgl_heap_next_sprite(&testsprite)); CATCH(errctx, akgl_heap_next_sprite(&testsprite));
CATCH(errctx, akgl_heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png")); CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed"); FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");
CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
FAIL_ZERO_BREAK(
errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
AKERR_KEY,
"Sprite was not placed in the registry");
CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
FAIL_ZERO_BREAK(
errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
AKERR_KEY,
"Sprite was not placed in the registry");
} CLEANUP { } CLEANUP {
IGNORE(akgl_heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
IGNORE(akgl_heap_release_string(tmpstr)); IGNORE(akgl_heap_release_string(tmpstr));
@@ -108,65 +108,65 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
akgl_Sprite *testsprite2 = NULL; akgl_Sprite *testsprite2 = NULL;
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
// Does the sprite get loaded? // Does the sprite get loaded?
// Do all frames of the sprite get loaded? // Do all frames of the sprite get loaded?
// Are all the frames of the sprite what we expect? (Surface comparison) // Are all the frames of the sprite what we expect? (Surface comparison)
// Is the sprite added to the registry? // Is the sprite added to the registry?
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr)); CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK(
errctx,
testsprite,
AKERR_KEY,
"akgl_sprite_load_json succeeds but sprite is not placed in the registry");
FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width); CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height); testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed); FAIL_ZERO_BREAK(
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop); errctx,
FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse); testsprite,
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames); AKERR_KEY,
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]); "akgl_sprite_load_json succeeds but sprite is not placed in the registry");
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
// Is it using the right spritesheet? FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png"); FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data); FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image"); FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
CATCH( // Is it using the right spritesheet?
errctx,
akgl_render_and_compare(
testsprite->sheet->texture,
image,
0, 0, 576, 384,
"test_sprite_loaded_from_json_sheet.png"
)
);
// If we load a second sprite using the same sheet name, do they use the same sheet in memory? snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json"); image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json")); FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK( CATCH(
errctx, errctx,
testsprite, akgl_render_and_compare(
AKERR_KEY, testsprite->sheet->texture,
"akgl_sprite_load_json succeeds but second sprite is not placed in the registry"); image,
0, 0, 576, 384,
"test_sprite_loaded_from_json_sheet.png"
)
);
// If we load a second sprite using the same sheet name, do they use the same sheet in memory?
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
FAIL_ZERO_BREAK(
errctx,
testsprite,
AKERR_KEY,
"akgl_sprite_load_json succeeds but second sprite is not placed in the registry");
FAIL_ZERO_BREAK(
errctx,
(testsprite->sheet == testsprite2->sheet),
AKERR_VALUE,
"Previously loaded spritesheets are not reused");
FAIL_ZERO_BREAK(
errctx,
(testsprite->sheet == testsprite2->sheet),
AKERR_VALUE,
"Previously loaded spritesheets are not reused");
} CLEANUP { } CLEANUP {
if ( testsprite != NULL ) { if ( testsprite != NULL ) {
IGNORE(akgl_heap_release_sprite(testsprite)); IGNORE(akgl_heap_release_sprite(testsprite));
@@ -185,16 +185,17 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) { if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }

View File

@@ -8,19 +8,19 @@ void reset_string_heap(void);
akerr_ErrorContext *test_fresh_heap_gives_strings(void) akerr_ErrorContext *test_fresh_heap_gives_strings(void)
{ {
akgl_String *ptr = NULL; akgl_String *ptr = NULL;
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP {
reset_string_heap();
} PROCESS(errctx) {
} FINISH(errctx, true);
}
return 0; PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP {
reset_string_heap();
} PROCESS(errctx) {
} FINISH(errctx, true);
}
return 0;
} }
akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void) akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
@@ -95,7 +95,7 @@ akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *test_akgl_string_initialize(void) akerr_ErrorContext *test_akgl_string_initialize(void)
{ {
@@ -111,7 +111,7 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
CATCH(errctx, akgl_string_initialize(ptr, "Test value")); CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data); FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
CATCH(errctx, akgl_heap_release_string(NULL)); CATCH(errctx, akgl_heap_release_string(NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
@@ -130,9 +130,10 @@ void reset_string_heap(void)
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
printf("test_fresh_heap_gives_string ....\n"); printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings(); test_fresh_heap_gives_strings();
reset_string_heap(); reset_string_heap();
@@ -150,6 +151,6 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
return 0; return 0;
} }

View File

@@ -21,9 +21,9 @@
#include <akgl/error.h> #include <akgl/error.h>
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */ /** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
#define TEST_ASSERT(e, cond, ...) \ #define TEST_ASSERT(e, cond, ...) \
if ( ! (cond) ) { \ if ( ! (cond) ) { \
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \ FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
} }
/** /**
@@ -33,25 +33,25 @@
* paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to * paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to
* require success. * require success.
*/ */
#define TEST_EXPECT_STATUS(e, expected, stmt, desc) \ #define TEST_EXPECT_STATUS(e, expected, stmt, desc) \
{ \ { \
akerr_ErrorContext *__tec = (stmt); \ akerr_ErrorContext *__tec = (stmt); \
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \ int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
if ( __tec != NULL ) { \ if ( __tec != NULL ) { \
__tec->handled = true; \ __tec->handled = true; \
__tec = akerr_release_error(__tec); \ __tec = akerr_release_error(__tec); \
} \ } \
if ( __tst != (expected) ) { \ if ( __tst != (expected) ) { \
FAIL_BREAK( \ FAIL_BREAK( \
e, \ e, \
AKGL_ERR_BEHAVIOR, \ AKGL_ERR_BEHAVIOR, \
"%s: expected status %d (%s), got %d (%s)", \ "%s: expected status %d (%s), got %d (%s)", \
desc, \ desc, \
(int)(expected), \ (int)(expected), \
akerr_name_for_status((int)(expected), NULL), \ akerr_name_for_status((int)(expected), NULL), \
__tst, \ __tst, \
akerr_name_for_status(__tst, NULL)); \ akerr_name_for_status(__tst, NULL)); \
} \ } \
} }
/** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */ /** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */

259
tests/text.c Normal file
View File

@@ -0,0 +1,259 @@
/**
* @file text.c
* @brief Unit tests for font loading and text measurement.
*
* Measurement needs a font but no renderer, so this suite runs without a window
* and without the offscreen harness the drawing half of src/text.c is waiting
* on. akgl_text_rendertextat() is therefore not covered here.
*
* The fixture font is monospaced on purpose: the width of an N-character string
* is exactly N times the width of one character, so every assertion below is a
* relationship between measurements rather than a hardcoded pixel count that
* would break when FreeType changes its rounding.
*/
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/registry.h>
#include <akgl/text.h>
#include "testutil.h"
/** @brief The monospaced ASCII subset described in assets/akgl_test_mono.LICENSE.txt. */
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
/** @brief Point size every test in this file opens the fixture font at. */
#define TEST_FONT_SIZE 16
/** @brief The fixture font, opened once by main() and shared by every test. */
static TTF_Font *testfont = NULL;
akerr_ErrorContext *test_text_loadfont(void)
{
PREPARE_ERROR(errctx);
TTF_Font *registered = NULL;
ATTEMPT {
CATCH(errctx, akgl_registry_init_font());
TEST_EXPECT_OK(
errctx,
akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE),
"loading the fixture font");
registered = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL);
TEST_ASSERT(errctx, registered != NULL,
"akgl_text_loadfont did not place the font in AKGL_REGISTRY_FONT");
TEST_ASSERT(errctx, TTF_GetFontHeight(registered) > 0,
"the registered font reports height %d, expected a positive height",
TTF_GetFontHeight(registered));
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_loadfont(NULL, TEST_FONT_PATH, TEST_FONT_SIZE),
"loading a font under a NULL name");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_loadfont("nullpath", NULL, TEST_FONT_SIZE),
"loading a font from a NULL path");
TEST_EXPECT_STATUS(errctx, AKGL_ERR_SDL,
akgl_text_loadfont("missing", "assets/no_such_font.ttf", TEST_FONT_SIZE),
"loading a font that does not exist");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_text_measure(void)
{
PREPARE_ERROR(errctx);
int w = 0;
int h = 0;
int onecharw = 0;
int onecharh = 0;
int emptyw = 0;
int emptyh = 0;
ATTEMPT {
// One cell. This is the measurement a character grid is built from.
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "A", &onecharw, &onecharh),
"measuring a single character");
TEST_ASSERT(errctx, onecharw > 0,
"one character measured %d wide, expected a positive width", onecharw);
TEST_ASSERT(errctx, onecharh == TTF_GetFontHeight(testfont),
"one character measured %d high, expected the font height %d",
onecharh, TTF_GetFontHeight(testfont));
// The font is monospaced, so four cells are exactly four times one.
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "AAAA", &w, &h),
"measuring four characters");
TEST_ASSERT(errctx, w == (onecharw * 4),
"four characters measured %d wide, expected %d", w, onecharw * 4);
TEST_ASSERT(errctx, h == onecharh,
"four characters on one line measured %d high, expected %d", h, onecharh);
// ...and every character advances by the same amount, which is what
// makes a fixed grid legitimate in the first place.
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "W", &w, &h), "measuring a wide glyph");
TEST_ASSERT(errctx, w == onecharw,
"'W' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "i", &w, &h), "measuring a narrow glyph");
TEST_ASSERT(errctx, w == onecharw,
"'i' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
// The empty string is zero wide and still one line high, so a cursor
// sitting on an empty line has somewhere to be.
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "", &emptyw, &emptyh),
"measuring the empty string");
TEST_ASSERT(errctx, emptyw == 0,
"the empty string measured %d wide, expected 0", emptyw);
TEST_ASSERT(errctx, emptyh == onecharh,
"the empty string measured %d high, expected the font height %d",
emptyh, onecharh);
// Measuring does not disturb the destinations it was not asked about.
w = -1;
h = -1;
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "hello", &w, &h),
"measuring a word");
TEST_ASSERT(errctx, w == (onecharw * 5),
"\"hello\" measured %d wide, expected %d", w, onecharw * 5);
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(NULL, "A", &w, &h),
"measuring with a NULL font");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, NULL, &w, &h),
"measuring a NULL string");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", NULL, &h),
"measuring into a NULL width");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", &w, NULL),
"measuring into a NULL height");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_text_measure_wrapped(void)
{
PREPARE_ERROR(errctx);
int w = 0;
int h = 0;
int onecharw = 0;
int flatw = 0;
int flath = 0;
int lineskip = 0;
ATTEMPT {
CATCH(errctx, akgl_text_measure(testfont, "A", &onecharw, &flath));
lineskip = TTF_GetFontLineSkip(testfont);
TEST_ASSERT(errctx, lineskip > 0, "the font reports a line skip of %d", lineskip);
// A wrap length wide enough for the whole string measures the same as
// the unwrapped call.
CATCH(errctx, akgl_text_measure(testfont, "one two", &flatw, &flath));
TEST_EXPECT_OK(errctx,
akgl_text_measure_wrapped(testfont, "one two", flatw + onecharw, &w, &h),
"measuring a string that fits inside the wrap length");
TEST_ASSERT(errctx, w == flatw,
"an unwrapped measurement gave %d wide, expected %d", w, flatw);
TEST_ASSERT(errctx, h == flath,
"an unwrapped measurement gave %d high, expected %d", h, flath);
// Narrow enough to force a break at the space: two lines, and nothing
// wider than the wrap length.
TEST_EXPECT_OK(errctx,
akgl_text_measure_wrapped(testfont, "one two", onecharw * 4, &w, &h),
"measuring a string that has to wrap");
TEST_ASSERT(errctx, w <= (onecharw * 4),
"a wrapped measurement gave %d wide, past the %d wrap length",
w, onecharw * 4);
TEST_ASSERT(errctx, h >= (lineskip * 2),
"a wrapped measurement gave %d high, expected at least two lines (%d)",
h, lineskip * 2);
// Zero wraps on newlines only, so an embedded newline still costs a line
// and a long unbroken string does not.
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one\ntwo", 0, &w, &h),
"measuring a string with an embedded newline");
TEST_ASSERT(errctx, h >= (lineskip * 2),
"an embedded newline measured %d high, expected at least two lines (%d)",
h, lineskip * 2);
TEST_ASSERT(errctx, w == (onecharw * 3),
"the longer of two three-character lines measured %d wide, expected %d",
w, onecharw * 3);
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", 0, &w, &h),
"measuring a string with no newline at wrap length zero");
TEST_ASSERT(errctx, h == flath,
"a string with no newline measured %d high at wrap length 0, expected %d",
h, flath);
// A negative wrap length is refused rather than silently treated as
// "never wrap", which is what SDL_ttf does with it.
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_text_measure_wrapped(testfont, "one two", -1, &w, &h),
"measuring at a negative wrap length");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_measure_wrapped(NULL, "A", 100, &w, &h),
"measuring wrapped with a NULL font");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_measure_wrapped(testfont, NULL, 100, &w, &h),
"measuring a NULL string wrapped");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_measure_wrapped(testfont, "A", 100, NULL, &h),
"measuring wrapped into a NULL width");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
akgl_text_measure_wrapped(testfont, "A", 100, &w, NULL),
"measuring wrapped into a NULL height");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_error_init());
FAIL_ZERO_BREAK(
errctx,
SDL_Init(0),
AKGL_ERR_SDL,
"Couldn't initialize SDL: %s",
SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
TTF_Init(),
AKGL_ERR_SDL,
"Couldn't initialize the font engine: %s",
SDL_GetError());
testfont = TTF_OpenFont(TEST_FONT_PATH, TEST_FONT_SIZE);
FAIL_ZERO_BREAK(
errctx,
testfont,
AKGL_ERR_SDL,
"Couldn't open %s: %s",
TEST_FONT_PATH,
SDL_GetError());
CATCH(errctx, test_text_loadfont());
CATCH(errctx, test_text_measure());
CATCH(errctx, test_text_measure_wrapped());
} CLEANUP {
if ( testfont != NULL ) {
TTF_CloseFont(testfont);
}
TTF_Quit();
SDL_Quit();
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -18,7 +18,7 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
json_error_t jsonerr; json_error_t jsonerr;
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
int propnum; int propnum;
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -97,7 +97,7 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y &gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
}; };
int i = 0; int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
gamemap->tilesets[0].tilecount = 4; gamemap->tilesets[0].tilecount = 4;
gamemap->tilesets[0].columns = 2; gamemap->tilesets[0].columns = 2;
@@ -194,7 +194,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
json_error_t errdata; json_error_t errdata;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -212,7 +212,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
(gamemap->layers[0].data[2] != 3) || (gamemap->layers[0].data[2] != 3) ||
(gamemap->layers[0].data[3] != 4) ) { (gamemap->layers[0].data[3] != 4) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)"); FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
} }
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
@@ -234,7 +234,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
int i = 0; int i = 0;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -267,13 +267,13 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)"); FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
} }
// Layer 1 and 3 should have no objects // Layer 1 and 3 should have no objects
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) { for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[0].objects[i].id != 0 ) { if ( gamemap->layers[0].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects"); FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup; goto _test_akgl_tilemap_load_layers_cleanup;
} }
} }
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) { for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( gamemap->layers[2].objects[i].id != 0 ) { if ( gamemap->layers[2].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects"); FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup; goto _test_akgl_tilemap_load_layers_cleanup;
@@ -291,7 +291,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
) { ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect"); FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
} }
_test_akgl_tilemap_load_layers_cleanup: _test_akgl_tilemap_load_layers_cleanup:
} CLEANUP { } CLEANUP {
if ( pathstr != NULL ) { if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr)); IGNORE(akgl_heap_release_string(pathstr));
@@ -313,7 +313,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
SDL_Texture *image = NULL; SDL_Texture *image = NULL;
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap)); memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -385,7 +385,7 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
akerr_ErrorContext *test_akgl_tilemap_draw(void) akerr_ErrorContext *test_akgl_tilemap_draw(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -399,7 +399,7 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void) akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
@@ -412,16 +412,17 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
} }
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) { if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
} }
@@ -445,6 +446,5 @@ int main(void)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
}
}

View File

@@ -26,7 +26,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
} HANDLE(errctx, AKERR_NULLPOINTER) { } HANDLE(errctx, AKERR_NULLPOINTER) {
// noop // noop
} FINISH(errctx, true); } FINISH(errctx, true);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, NULL)); CATCH(errctx, akgl_rectangle_points(&points, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
@@ -50,7 +50,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
RectanglePoints points; RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
memset((void *)&points, 0x00, sizeof(RectanglePoints)); memset((void *)&points, 0x00, sizeof(RectanglePoints));
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect)); CATCH(errctx, akgl_rectangle_points(&points, &testrect));
@@ -83,7 +83,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
point testpoint; point testpoint;
RectanglePoints testrectpoints; RectanglePoints testrectpoints;
bool testcollide; bool testcollide;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
@@ -138,7 +138,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
RectanglePoints testrectpoints; RectanglePoints testrectpoints;
bool testcollide = false; bool testcollide = false;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect)); CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
@@ -163,7 +163,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
SDL_FRect testrect1; SDL_FRect testrect1;
SDL_FRect testrect2; SDL_FRect testrect2;
bool testcollide; bool testcollide;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
@@ -216,9 +216,9 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40}; SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
bool testcollide = false; bool testcollide = false;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
// Collision overlapping on the top left // Collision overlapping on the top left
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
@@ -298,7 +298,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
if ( testcollide == true ) { if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported"); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
} }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
@@ -310,6 +310,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_rectangle_points_nullpointers()); CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math()); CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers()); CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());

108
tests/version.c Normal file
View File

@@ -0,0 +1,108 @@
/**
* @file version.c
* @brief Unit tests for the version macros and the linked-library accessor.
*
* These assert that the several places the version appears cannot drift: the
* string, the numeric components, and what the shared library reports at
* runtime all come from one project() call, and this is what proves it.
*/
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/version.h>
#include "testutil.h"
/**
* @brief The version string and the numeric components must describe one version.
*
* They are separate substitutions in version.h.in, so a mangled template can
* leave them disagreeing and nothing else would notice.
*/
akerr_ErrorContext *test_version_string_matches_components(void)
{
PREPARE_ERROR(e);
char assembled[64];
ATTEMPT {
snprintf(assembled, sizeof(assembled), "%d.%d.%d",
AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH);
TEST_ASSERT(e, strcmp(assembled, AKGL_VERSION) == 0,
"AKGL_VERSION is \"%s\" but the components assemble to \"%s\"",
AKGL_VERSION, assembled);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/**
* @brief The linked library must report the version its headers were generated from.
*
* In this build tree they are the same tree, so this can only fail if the test
* picked up an installed libakgl off LD_LIBRARY_PATH instead of the one just
* built -- which is precisely the mispairing the accessor exists to catch.
*/
akerr_ErrorContext *test_version_linked_matches_compiled(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_ASSERT(e, akgl_version() != NULL, "akgl_version returned NULL");
TEST_ASSERT(e, strcmp(akgl_version(), AKGL_VERSION) == 0,
"linked libakgl reports \"%s\" but the headers say \"%s\"",
akgl_version(), AKGL_VERSION);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/**
* @brief AKGL_VERSION_AT_LEAST must order versions correctly at the boundaries.
*
* Checked against the current version rather than fixed literals, so the test
* does not have to be rewritten every time the version is bumped.
*/
akerr_ErrorContext *test_version_at_least_boundaries(void)
{
PREPARE_ERROR(e);
ATTEMPT {
TEST_ASSERT(e,
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH),
"AKGL_VERSION_AT_LEAST rejected the current version");
TEST_ASSERT(e,
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH + 1),
"AKGL_VERSION_AT_LEAST accepted a later patch");
TEST_ASSERT(e,
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR + 1, 0),
"AKGL_VERSION_AT_LEAST accepted a later minor");
TEST_ASSERT(e,
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR + 1, 0, 0),
"AKGL_VERSION_AT_LEAST accepted a later major");
TEST_ASSERT(e,
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, 0),
"AKGL_VERSION_AT_LEAST rejected an earlier patch");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_version_string_matches_components());
CATCH(errctx, test_version_linked_matches_compiled());
CATCH(errctx, test_version_at_least_boundaries());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}

View File

@@ -29,7 +29,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
char *characterjson = NULL; char *characterjson = NULL;
char pathbuf[4096]; char pathbuf[4096];
char cwdbuf[1024]; char cwdbuf[1024];
if ( argc < 3 ) { if ( argc < 3 ) {
SDL_Log("charviewer [CHARACTER_FILE] [SPRITE ...]"); SDL_Log("charviewer [CHARACTER_FILE] [SPRITE ...]");
return SDL_APP_FAILURE; return SDL_APP_FAILURE;
@@ -39,9 +39,9 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
characterjson = argv[1]; characterjson = argv[1];
memset((char *)&pathbuf, 0x00, 4096); memset((char *)&pathbuf, 0x00, 4096);
memset((char *)&cwdbuf, 0x00, 1024); memset((char *)&cwdbuf, 0x00, 1024);
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer"); FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
FAIL_ZERO_BREAK(errctx, getcwd((char *)&cwdbuf, 1024), AKERR_NULLPOINTER, "Couldn't get current working directory"); FAIL_ZERO_BREAK(errctx, getcwd((char *)&cwdbuf, 1024), AKERR_NULLPOINTER, "Couldn't get current working directory");
@@ -79,7 +79,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
memset((char *)&pathbuf, 0x00, 4096); memset((char *)&pathbuf, 0x00, 4096);
} }
ATTEMPT { ATTEMPT {
if ( characterjson[0] != '/' ) { if ( characterjson[0] != '/' ) {
sprintf((char *)&pathbuf, "%s/%s", (char *)&cwdbuf, characterjson); sprintf((char *)&pathbuf, "%s/%s", (char *)&cwdbuf, characterjson);
} else { } else {
@@ -105,7 +105,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
SDL_Log("Opening gamepad %d", gamepadids[0]); SDL_Log("Opening gamepad %d", gamepadids[0]);
FAIL_ZERO_BREAK(errctx, SDL_OpenGamepad(gamepadids[0]), AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, SDL_OpenGamepad(gamepadids[0]), AKGL_ERR_SDL, "%s", SDL_GetError());
CATCH(errctx, akgl_controller_default(0, "player", 0, gamepadids[0])); CATCH(errctx, akgl_controller_default(0, "player", 0, gamepadids[0]));
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
@@ -123,7 +123,7 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer"); FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
FAIL_ZERO_BREAK(errctx, event, AKERR_NULLPOINTER, "NULL event pointer"); FAIL_ZERO_BREAK(errctx, event, AKERR_NULLPOINTER, "NULL event pointer");
CATCH(errctx, akgl_controller_handle_event(appstate, event)); CATCH(errctx, akgl_controller_handle_event(appstate, event));
if (event->type == SDL_EVENT_QUIT) { if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */ return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */
@@ -140,7 +140,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
akgl_Iterator opflags; akgl_Iterator opflags;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_BITMASK_CLEAR(opflags.flags); AKGL_BITMASK_CLEAR(opflags.flags);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE); AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER); AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);